调查一个未知档案

搬迁。 新城市。 求职。 即使对于IT专业人员,这也可能需要很长时间。 通常,一系列采访非常相似。 通常,当您找到工作后,便会宣布一个有趣的办公室。

很难理解她在做什么,但是,她感兴趣的领域是对其他人的软件的研究。 这听起来很吸引人,尽管当您意识到这似乎不是发行用于网络安全的软件的供应商时,您停了一秒钟然后开始抓萝卜。



简而言之:他们放弃了档案,并提出将其作为测试任务进行检查,并尝试根据显示的输入数据来计算某个签名。 值得注意的是,我在此类活动上的经验很少,这也许就是为什么在解决方案的第一次迭代中我只有几个小时的原因-这样做的动力还在持续。 是的,当然,我尝试在电话/仿真器上运行它的第一件事-此应用程序无效。

我们所拥有的:扩展名为“ .apk” 存档。 我将任务本身放在破坏者的下面,这样它就不会被搜索引擎索引:如果家伙们不喜欢它,该将解决方案放在Habr上怎么办?

任务本身
APK包含用于为关联数组生成签名的功能。
尝试获取以下数据集的签名:

{ "user" : "LeetD3vM4st3R", "password": "__s33cr$$tV4lu3__", "hash": "34765983265937875692356935636464" } 


卷起袖子


据说存档包含对关联数组进行签名的功能。 通过文件扩展名,我们立即了解到我们正在处理为Android编写的应用程序。 首先,我们解压缩档案。 实际上,这是一个常规的ZIP存档,任何存档者都可以轻松应对。 我使用了apktool实用程序,事实证明,它无意间绕过了几次耙。 是的,它发生了(通常相反,是吗?)。 咒语很简单:

 apktool d task.zip 

事实证明,apk文件中的代码和资源也打包存储在单独的二进制文件中,并且需要其他软件来提取它们。 apktool隐式提取类字节,资源,并将其全部分解为自然的文件层次结构。 您可以继续。

 ├── AndroidManifest.xml ├── apktool.yml ├── lib │   └── arm64-v8a ├── original │   ├── AndroidManifest.xml │   └── META-INF ├── res │   ├── anim │   ├── color │   ├── drawable │   ├── layout │   ├── layout-watch-v20 │   ├── mipmap-anydpi-v26 │   ├── values │   └── values-af ├── smali │   ├── android │   ├── butterknife │   ├── com │   ├── net │   └── org └── unknown   └── org 

我们看到了类似的层次结构(简化后的版本),并试图找出从哪里开始。 值得注意的是,我曾经为Android编写了两个小型应用程序,因此我对目录的一部分本质以及Android应用程序的设备原理几乎一清二楚。

首先,我决定只是“遍历”文件。 我打开AndroidManifest.xml并开始有意义地阅读。 我的注意力被一个奇怪的属性所吸引

 android:supportsRtl="true" 

事实证明,他负责在应用程序中支持字母为“从右到左”的语言。 我们开始紧张。 不好

此外,我的目光紧贴着未知的文件夹。 它的下面是层次结构: org.apache.commons.codec.language.bm和大量文本内容模糊的文本文件。 谷歌搜索包的全名,然后发现存储在这里的东西,这与单词的搜索算法有关,在语音上类似于给定的单词。 坦白说,我在这里开始更加努力。 经过一番探索,我发现了代码本身,然后开始了乐趣。 我曾经遇到过的Java字节码不是我遇到的,而是其他东西。 非常相似,但不同。

事实证明,Android有自己的虚拟机-Dalvik。 而且,像每个受人尊敬的虚拟机一样,它具有自己的字节码。 似乎是在第一次尝试解决这个问题的时候,正是在这个悲伤的音符上,我宣布中场休息,鞠躬,放下窗帘,将其全部扔了4个月,直到我的好奇心完全消灭了我。

卷起袖子[2]


“但是,不是所有事情都变得容易吗?” -这是我第二次开始任务时问自己的问题。 我开始在互联网上搜索从smali到Java的反编译器。 我只看到不可能明确地执行此过程。 他皱着眉头,去了Github,把几个关键短语带入了搜索行。 第一个是smali2java

 git clone gradle build java -jar smali2java.jar .. 

失误 我在终端的几页上看到了巨大的堆栈跟踪和错误。 在稍微了解了内容的本质(并抑制了堆栈轨迹的大小而产生的情绪)之后,我发现该工具基于描述的某种语法运行,而她遇到的字节码显然与之不符。 我打开smali字节码,并在其中看到注释,合成方法和其他奇怪的构造。 Java字节码中没有这样的东西! 多久 删除!

更多细节
事实证明,Dalvik虚拟机(以及JVM)并不了解诸如内部/外部类(读取嵌套类)之类的概念,并且编译器会生成所谓的“合成”方法以提供从嵌套类到例如外部字段。

例如:


如果外部类(OuterClass)有一个字段

 public class OuterClass { List a; ... } 

为了使私有类可以访问外部类的字段,编译器将隐式生成以下方法:

 static synthetic java.util.List getList(OuterClass p1) { p1 = p1.a; return p1; } 

而且,由于有了这个“引擎室”厨房,该语言提供的其他一些机制也得以实现。

您可以从这里开始更详细地研究这个问题。

没有帮助。 他甚至发誓看似没有可疑的字节码。 我打开了反编译器的源代码,阅读并看到了一些非常奇怪的东西:即使是印度教程序员(在所有应有的尊重下)也不会编写此代码。 想法浮出水面:并非真正生成的代码。 我拒绝这个想法大约30分钟,试图了解错误所在。 复杂。 我再次打开Github-实际上是语法生成的解析器。 这是发电机本身。 收拾一切,试图从另一侧接近。

值得注意的是,过一会儿我仍然尝试更改语法,甚至更改字节码本身,以便反编译器仍能消化它。 但是,即使就反编译器语法而言字节码变得有效,该程序也没有向我返回任何内容。 开源...

我翻阅了字节码,偶然发现了我不知道的常量。 谷歌搜索,我在关于反向Android应用程序的书中也遇到了同样的问题。 我记得这只是由编译器预处理程序分配的ID,它已分配给Android应用程序的资源(代码编写时间常数为R. *)。 在接下来的半小时内,我将简要检查哪些寄存器负责什么内容,以什么顺序传递参数,并且通常会深入研究语法。

看起来像什么?


我找到了主应用程序窗口的布局,从中我已经了解了应用程序中正在发生的事情:在主屏幕(活动)上有一个带有输入字段的RecyclerView(有条件的视图可以重用当前未显示的UI对象用于内存利用)键/值对,负责将新的键/值对添加到某个抽象容器的几个按钮,以及一个为该容器生成签名(签名)的按钮。

查看批注并观察到一定数量的可疑代码,这些代码与生成的代码类似,我开始使用google。 该项目使用ButterKnife库,该库允许使用批注自动膨胀()-> bind() UI元素。 如果该类中有注释,则ButterKnife注释处理器将隐式创建另一个形式为<original_class> __ViewBinding的活页夹类,该活页夹将在后台进行所有脏活。 实际上,在我从一个MainActivity文件手动重新创建了Java源代码的相似性之后,我才从所有MainActivity文件中获得了所有这些信息。 半个小时后,我意识到该库的注释还可以设置按钮操作的回调,并找到了实际上负责将键/值对添加到容器并生成签名的那些键函数。

当然,在研究期间,我不得不进入各种库和插件的“内脏”,因为即使是带有cookie的漂亮的Landos也无法涵盖所有​​用例和细节,对于任何“反向”,我认为这都是常见的做法。

懒惰是程序员的朋友


在第二个来源上花了更多时间后,我完全疲倦了,意识到无法煮稀饭。 我再次爬上Github,这次我更加仔细地看。 我找到了Smali2PsuedoJava项目-“伪Java代码”中的反编译器。 即使使用此实用程序,至少也可以导致人的外观,但对我而言,作者还是一杯他最喜欢的啤酒(好吧,或者至少在Github上加一个星号,以供初学者使用)。

它确实有效! 对脸的影响:



认识Cipher.so


稍后,研究了该项目的Java伪代码并将其与smali字节码进行了难以置信的比较,我在代码中找到了一个奇怪的库-Cipher.so。 谷歌搜索,我发现这是APK归档中一组编译时间值的加密。 当应用程序使用以下形式的常量时,通常这是必需的:IP地址,用于外部数据库的凭据,用于授权的令牌等。 -可以通过应用程序的逆向工程获得什么。 确实,作者清楚地写道,他们说这个项目被放弃了。 这变得越来越有趣。

该库通过Java库提供对值的访问,其中特定的方法是我们关注的关键。 这只会激发我的兴趣,我开始更深入。

简而言之,Cipher.so是做什么的,它是如何工作的:

  • 在我们项目的Gradle文件中,注册了密钥和相应的值
  • 所有键值将自动打包到一个单独的动态库(.so)中,该库将在编译时生成。 是-是,将生成。
  • 然后可以从Cipher.so生成的Java方法获得这些密钥
  • 创建APK后,密钥名称将由MD5进行哈希处理(当然,这是为了提高安全性)

在存档文件夹中找到了我需要的动态库之后,我继续进行选择。 首先,作为一个经验丰富的反向练习(否),我尝试从一个简单的反向练习开始-我决定在具有ELF的类比二进制空间中,以常数和有趣的线条作为参考。 不幸的是,开箱即用的Mac readelf用户丢失了,在开始之前,我们要说一句珍惜的东西:

 brew install binuitls 

并且不要忘记在PATH中将路径写入/ usr / local ,因为brew以一种绅士的方式保护您免受一切侵害...

 greadelf -p .rodata lib/arm64-v8a/libcipher-lib.so | head -n 15 

我们将输出限制为前15行,否则可能会给准备不足的工程师带来震惊。



在较低的地址中,我们注意到可疑的行。 正如我发现的那样,在研究Cipher.so的来源时,将密钥和值放在通常的std ::映射中:这提供的信息很少,但是我们知道在Binar本身以及加密的密码中也存在混淆的密钥。

值加密如何? 在研究源代码后,我发现加密是使用AES(标准对称加密系统)进行的。 因此,如果存在加密的值,则该密钥应该在附近...经过一段时间的研究,我在同一个项目中遇到了一个带有挑衅性标题“不安全的密钥存储:秘密非常容易获取”的问题 。 实际上,我发现其中的密钥以明文形式存储在二进制文件中,并找到了解密算法。 在示例中,密钥位于零地址,尽管我理解编译器可以将其放在二进制文件的.rodata节中的其他位置,但我还是认为位于零地址的可疑单元是密钥。

尝试#1:我继续解密这些值,并认为加密密钥是相同的。 错误。 OpenSSL提示某些错误。 在稍微阅读了Cipher.so的源代码之后,我了解到,如果用户在组装期间未指定键,则使用默认键-Cipher.so@DEFAULT

尝试#2:再次错误。 嗯...这个常数真的重新定义了吗? 弄错很简单:用Gradle格式混淆以Gradle编写的代码。 我再检查一次。 一切似乎都是如此。

它们的MD5哈希值不是键,而是键,然后我尝试尝试自己的运气并使用Rainbow表打开服务。 Voila-关键之一就是单词“ password”。 没有第二个。 当然,它给了我们很多。 这两个密钥分别位于地址240和2a2。 原则上,立即识别它们很容易-32个字符(MD5)。

我再次检查了所有内容,并尝试使用所有其他行(位于低位地址中)作为解密密钥进行解密-一切都是徒劳的。
因此,还有其他一些秘密密钥,动作算法似乎是正确的。 我把这项任务抛在一边,尽量不要埋葬自己。

在容器签名算法中稍作改动后,我仍然看到对Cipher.so库的调用和也使用Java库的加密功能的代码。

一个谜语(我从未解决过)


在负责加密的功能中,一开始会检查容器中的密钥。

 public byte[] a(java/util/Map p1) { v0 = p1.size() v1 = 0x0; if (v0 != 0) goto :cond_0 p1 = new byte[v1]; return p1; :cond_0 v0 = "user"; v0 = p1.containsKey(v0) if (v0 == 0) goto :cond_1 p1 = new byte[v1]; return p1; ... 

从字面上看:如果有一个“用户”密钥,则此容器未签名(返回零签名)。 一种奇怪的感觉:问题似乎已经解决,但似乎有点可疑。 那为什么还要发明其他东西呢? 误入歧途? 那为什么以前我没流利地研究过这个代码呢? 嗯...

不,不是真的。 我用一个蓝色的Messenger指定了某个用户的答案,在给他分配任务时会提供给我的联系人。 进一步挖掘。 也许输入键/值集在添加到容器后会有所变化? 我仔细阅读了代码。

请注意,反编译器已从smali代码中删除了注释。 如果他删除了重要的内容该怎么办? 我检查了主文件-似乎没什么大不了的。 一切都很重要,但含义没有丢失。 我检查了负责将键/值对从条件TextBox写入内部容器的回调函数。 我没有发现任何犯罪。

我对每一行代码都持怀疑态度-我再也不能信任任何人了。

简单的解决方案2:我注意到,签名过程首先检查与应用程序签署的证书的签名中是否存在某些值(字符串中的子字符串)。

 @OnClick //   protected void huvot324yo873yvo837yvo() { String signature = "no data"; boolean result = some_packages.isKeyInSignature(this); if result { Map map = new HashMap(); ... 

当然,含义本身就是在那个不幸的二进制文件中加密的。 实际上,如果此值不在签名中,则该算法将不会对任何内容进行签名,而只是返回字符串“ no data”作为签名。同样,我们将密码用于...

密钥解密最后一战


为了了解这场悲剧的严重程度,我感到困惑,例如:

我在本节进行了十六进制转储,并凝视了前两行,从一开始就没有消除对此的怀疑。



请注意,此处分隔行的字符为“ 0x00”。 标准C库在字符串函数中也经常使用它。 因此,第一行的中间是什么样的空格字符也同样有趣。 接下来,开始疯狂尝试,关键是:

  • 整个第一行
  • 空格前的第一行
  • 从太空到终点的第一行
  • ...

偏执程度已经可以估计。 当您不了解任务的难度和难度时,便会开始努力。 然而,事实并非如此。 然后我想到了这个想法:“算法从我机器上的问题起是否可以正常工作?”。 总的来说,那里的动作顺序是合乎逻辑的,没有引起任何问题,但是问题是:我机器上的命令是否可以满足他们的要求? 那你觉得呢?

手动检查了所有步骤之后,结果发现

 echo "some_base64_input" | openssl base64 -d 

在某些输入参数上,它突然返回一个空字符串。 嗯

将其替换为计算机上的第一个base64解码器,然后对主要候选项进行分类,立即找到合适的密钥,并对该密钥进行相应的解密。

从证书中检索签名


 class a { public static boolean isKeyInSignature(android.content.Context p1) { v0 = 0x0; try TRY_0{ v1 = p1.getPackageManager() p0 = p1.getPackageName() v2 = 0x40; // GET_SIGNATURES PackageInfo p0 = v1.getPackageInfo(p0, v2) android.content.pm.Signature[] p0 = p0.signatures; // Order are not guaranteed v1 = p0.length; v2 = 0x0; :goto_0 if (v2 >= v1) goto :cond_1 v3 = p0[v2]; String v3 = v3.toCharsString() String v4 = net.idik.lib.cipher.so.CipherClient.a() v3 = v3.contains(v4) }TRY_0 catch TRY_0 (android/content/pm/PackageManager$NameNotFoundException) goto :catch_0; if (v3 == 0) goto :cond_0 p1 = 0x1; return p1; :cond_0 v2 = v2 + 0x1; goto :goto_0 :catch_0 p0 = Thrown Exception p1.printStackTrace() :cond_1 return v0; } 

这是我的次要编辑后生成的伪代码的样子。 混淆了两件事:

  • 对密码学和设备证书的“厨房”了解不足
  • 根据文档,此方法不能保证返回集合中证书的顺序,因此,无法以相同顺序循环-如果应用程序使用多个证书签名怎么办?
  • 由于尚不清楚在这种情况下Android Runtime的功能,因此缺乏有关如何从APK中提取证书的知识

我不得不深入研究所有这些问题,结果如下:

  • 证书本身位于原始目录/ META-INF / CERT.RSA中

    在此目录中,只有一个具有此扩展名的文件-这意味着应用程序仅使用一个证书进行了签名
  • 在有关Android应用程序研究工程的网站上,发现了一个列表,可以像Android一样提取我们需要的签名。 据作者说,至少。

通过运行此代码,我可以弄清楚签名,实际上,我们需要的密钥是一个子字符串。 来吧 2号简单解决方案已被淘汰。

的确,密钥在证书中,它仅是用来了解下一步的内容,因为如果我们拥有“用户”密钥,我们所有人也都将获得零签名,并且正如我们从上文中学到的,这是错误的答案。

仔细编写文档!


由于缺乏证据,因此放弃了对从文本字段输入的数据已更改的事实的进一步研究。 偏执狂以新的活力出现:也许从证书中提取签名的代码是不正确的,还是旧的Android版本的代码实现? 我再次打开文档,然后看到以下内容:( https://developer.android.com/reference/android/content/pm/Signature.html#toChars() ):



注意:该函数将签名编码为ASCII文本。 我上面收到的输出是数据的十六进制表示。 这个API对我来说似乎很奇怪,但是如果您相信文档,原来我就再度陷入僵局,并且加密密钥不是签名的子字符串。 在沉思了一段时间之后,我忍不住打开了该类的源代码。 https://android.googlesource.com/platform/frameworks/base/+/e639da7/core/java/android/content/pm/Signature.java

答案很快就到了。 实际上,在代码本身中是一幅油画:输出格式是普通的十六进制字符串。 现在想想:要么我听不懂,要么文档写得“略”错误。 毫无疑问,我开始再次工作。

总结


接下来的n小时过去了:

  • 使用RecyclerView检查代码中工作的正确性,并通过源代码确定其行为,因为 再次,并非所有点都在扩展坞中甚至在Stackoverflow上都有详细介绍
  • 手动反编译负责将集合签名到已编译Java中的代码片段。我假设我仍然错过了一些东西,并且容器中的第一个键(“用户”)被隐式地从集合中删除。我决定将其余数据设置在代码上。

通常,此代码甚至拒绝对其余的参数进行签名(在使用密码学的代码中,这些参数进一步隐式地将我甩开了)。

不行原来,您无法对此输入进行签名。不幸的是,我将无法通过这项工作并确定是否确实如此。真可惜有一段时间,它充满了我的思想,但我向自己保证,我已经尽了一切可能。

实际上,我在此任务上花了很多时间,同时又花了很多时间来弥补知识上的空白。真的很有帮助。您可以一路追踪,并注意一开始我是如何牢牢抓住非决定性部分的。也许这将有助于某人了解初学者如何解决此类问题,因为我们通常阅读“成功案例”,其中所有步骤都是合乎逻辑的,一致的,并能得出正确的结果。

如果有人想尝试进一步研究该任务或提出问题,请在蓝色的arturbrsg信使中写信给我

请继续关注。

Source: https://habr.com/ru/post/zh-CN450394/


All Articles