案例研究
在这个案例研究中, 我们将检查一个 Nexus 银行木马恶意样本(文件 MD5: d87e04db4f4a36df263ecbfe8a8605bd)。Nexus 是在地下论坛上出售的一个框架,它能够从安卓手机上的许多银行应用程序中窃取资金。Cyble 发布的一份报告提供了有关该框架的更多详细信息以及对样本的彻底分析。
使用 jadx 对样本进行分析,应用程序中的 AndroidManifest.xml 文件(d87...)显示它请求访问设备的短信、联系人、电话通话等敏感信息。AndroidManifest.xml 中的主要活动在应用程序最初时不会出现,因为它稍后会被解压,但另一个类被提及为 "com.toss.soda.RWzFxGbGeHaKi" 并且扩展了 Application 类,这意味着它将是应用程序中首个运行的类:
图片
在 Application 子类 "com.toss.soda.RWzFxGbGeHaKi" 中的 onCreate() 回调引用了两个额外的方法:melodynight() 和 justclinic(),而后者调用了另一个方法:bleakperfect()。
图片
bleakperfect() 方法以及应用程序中的其他几个方法包含大量的死代码,涉及将值分配给变量并使用多个循环对它们进行算术运算,但最终这些变量从未被使用。
此外,该方法用于解码在代码其他位置引用的字符串。这是通过将一个字节数组(编码字符串)与另一个字节数组(XOR 密钥)进行异或操作,并将结果存储在第三个字节数组中,然后将其转换为字符串。
图片
诸如此类的修补方法可以删除冗余代码并用字符串返回替换冗长的 XOR 操作,可以使应用程序的分析变得更加容易且更高效。为此,我们必须了解此代码在DEX文件中的呈现方式。
DEX概述
Android应用程序主要是用Java编写的。为了在Android设备上运行,Java代码被编译成Java字节码,然后被转换成Dalvik字节码。Dalvik字节码可以在APK的DEX(Dalvik可执行)文件中找到。APK(安卓包文件)本质上是一个包含应用程序代码和所需资源的ZIP文件。可以通过提取APK的内容来检查DEX文件。
DEX文件分为几个部分,包括头部、字符串表、类定义、方法代码和其他数据。大多数部分被划分为大小相等的块,这些块中包含多个值来定义部分中的项目。为了展示在DEX文件中如何翻译Java中的常见概念,例如类或字符串,我们将使用class_defs部分作为示例。
图片
关于类
class_defs部分由class_def_items组成,每个类在应用程序中都是32字节长的。类的名称以以下方式存储:class_def_item包含对type_ids部分中的项目的索引(class_idx),而type_ids部分又包含对string_ids中的另一个项目的索引(descriptor_idx)。
string_id_item下的值是从文件开头的偏移量,它指向包含实际类名字符串(data)的string_data_item的开头,该字符串前面有其长度(utf16_size)。
图片
class_def_item还有另一个成员(class_data_off),它是指向一个class_data_item的偏移量,该项代表与类相关联的数据。它包含了有关类的静态和虚拟方法、静态和实例字段的信息,以及每个方法和字段的匹配的encoded_method和encoded_field项。
关于方法
direct_methods和virtual_methods包含一系列encoded_method项目。在每个方法类型的第一个encoded_method项目中,method_idx_diff值持有在method_ids部分中匹配项目的索引。
然而,在后续项目中,这个值是相对于前一个项目的差异,并且要计算method_ids索引,必须将差异增加到前一个method_idx_diff值。
图片
最后,method_id_item中的方法名称存储在name_idx下,类似于type_id_item中的类名称,并且使用string_id_item索引检索方法名称的字符串值。
图片
在Android应用程序中,每个方法都有一个前言(或者称为code_item),它指定了有关方法大小、输入和输出参数以及异常处理数据的信息。这个前言在DEX文件中的偏移量存储在前面提到的encoded_method项的code_off值中。
前言的前两个字节表示寄存器大小,即字节码使用了多少个寄存器,接着是输入和输出参数的字大小,而最后四个字节是字节码大小(或insns_size)。
字节码大小以16位指令单元计算,这意味着要计算字节码中总字节数(8位单位),必须将这个值乘以二。方法的Dalvik字节码直接在前言之后开始。
图片
关于字符串
到目前为止,我们已经看到了两个例子中的string_id_items用于从DEX文件中的字符串表中提取类名和方法名。但是,在Dalvik字节码中,string_id_item也非常重要,当在应用程序代码中使用字符串值时,它会被引用。
例如,以下字节码序列返回"sampleValue"字符串,其中"0xABCD"是在string_ids部分中的"sampleValue"的string_id_item的索引
1A 00 CD AB # const-string v0, "sampleValue" [string@ABCD]
11 00 # return-object v0
这意味着,在对恶意样本的字节码进行修补时,一个障碍是,解码后应该返回的解密字符串并不存在于DEX文件的字符串表中。相反,它们必须在解码后添加到文件中,以便具有匹配的string_data_item和可以被代码引用的string_id_item索引。
自然地,添加这些字符串会导致文件的部分大小、索引和偏移量发生变化。这会产生另一个障碍,因为在先前显示的DEX文件中,不同项之间存在多个依赖关系,改变它们引用的索引或偏移量将导致这些项被错误地解析或具有不正确的成员值。这就是为什么在对方法进行修补时,必须确保DEX文件的其余部分保持完整。
关于补丁
为了实现这一点,我们创建了dexmod,这是一个Python辅助工具,根据用户指定的反混淆逻辑来修补DEX文件。除了修补之外,该工具还支持诸如使用字节码模式进行方法查找或添加字符串等操作。dexmod下载地址:https://github.com/google/dexmod/
对于Nexus样本中的混淆方法来说,要使其返回解密后的字符串,必须使用dexmod解码并将字符串添加到文件中。然后,将在DEX文件中看到的返回字符串的字节码序列放置在每个混淆方法的字节码开头,并与相应的string_id_item索引配对。方法中的任何剩余字节都可以用0x00(NOP)替换,以进行额外的代码清理,但这并非必要。
还需要更新每个方法的前言以反映这些更改;寄存器大小减小到1,因为只使用了一个寄存器(v0),而字节码大小更新为3,因为现在它只包含3个16位指令(6字节)。前言中的其他值可以保持不变,因为它们表示的项没有受到影响。
图片
在DEX文件的头部中,校验和和SHA-1签名值也必须更新;否则,文件内容的验证将失败。在使用dexmod实施了这些步骤之后,可以使用jadx重新检查DEX文件,一旦混淆的函数现在将会移除所有死代码并返回解码后的字符串:
图片
由于Nexus样本中的混淆方法是由另一个方法调用而不是直接调用的,另一种可能性是修补调用者方法并返回一个字符串,从而完全跳过混淆方法。这样做可以节省研究人员在分析过程中重复跳转方法的时间。
总结
本案例研究展示了Dalvik字节码修补对研究人员的用处,以及如何使用免费的开源工具来实现。与其他反混淆解决方案面临的问题类似,打包器和混淆技术经常更新,不幸的是很难找到一个能够长时间内适用于大量应用程序的修补解决方案。此外,虽然搜索应用程序的字节码可以高效地识别代码模式,但尝试修改DEX文件而不损坏其中某些部分可能是一项挑战。
附录(DexMod)
dexmod工具包含以下脚本:
- dexmod.py 主模块: 接受DEX文件名作为参数,并调用editBytecode.py中的方法来修补文件
- getMethodObjects.py:
创建具有以下属性的方法对象:
- methodIdx:method_idx值,在Dalvik字节码中用于调用方法
- offset:方法字节码的文件偏移量
- name:方法的名称
- bytecode:方法的字节码
- searchBytecode.py:在DEX文件中查找字节码模式并返回匹配的方法对象
- editStrings.py:向DEX文件添加字符串
- editBytecode.py:用于实现自定义修补逻辑,包含空方法
- example/editBytecodeCustom.py :实现了文章中案例研究的修补逻辑
dexmod 工具利用 dexterity(一个解析DEX文件的开源库),并协助将字符串添加到 DEX文件,同时修复对受影响字符串 ID 和其他部分偏移量的引用。dexterity库有一些局限性,它不会一次修复字节码中引用的字符串索引,并且在本案例研究期间对其代码进行了一些更改以正确添加字符串。
dexterity开源库地址:https://github.com/rchiossi/dexterity