移动应用篡改和逆向工程¶
长期以来,逆向工程和篡改技术一直属于破解者、修改者、恶意软件分析师等人的领域。对于“传统”的安全测试人员和研究人员来说,逆向工程更多的是一种补充技能。但潮流正在转变:移动应用黑盒测试越来越需要反汇编编译后的应用、应用补丁以及篡改二进制代码甚至实时进程。许多移动应用实现了针对不受欢迎的篡改的防御,这一事实并没有让安全测试人员的工作变得更容易。
移动应用逆向工程是分析编译后的应用以提取有关其源代码信息的过程。逆向工程的目标是理解代码。
篡改是更改移动应用(编译后的应用或运行进程)或其环境以影响其行为的过程。例如,某个应用可能拒绝在您已 Root 的测试设备上运行,从而使您无法运行某些测试。在这种情况下,您需要更改应用的行为。
理解基本的逆向工程概念对移动安全测试人员非常有益。他们还应该深入了解移动设备和操作系统:处理器架构、可执行文件格式、编程语言的复杂性等等。
逆向工程是一门艺术,描述它的每一个方面都会填满整个图书馆。技术和专业化的范围之广令人叹为观止:一个人可以花费数年时间研究一个非常具体和孤立的子问题,例如自动化恶意软件分析或开发新型反混淆方法。安全测试人员是通才;为了成为有效的逆向工程师,他们必须过滤掉大量相关信息。
没有总是有效的通用逆向工程流程。也就是说,我们将在本指南后面描述常用的方法和工具,并给出解决最常见防御方法的示例。
为何需要了解¶
出于以下几个原因,移动安全测试至少需要基本的逆向工程技能
1. 启用移动应用的黑盒测试。现代应用通常包含会阻碍动态分析的控件。SSL pinning 和端到端 (E2E) 加密有时会阻止您使用代理拦截或操作流量。Root 检测可能会阻止应用在已 Root 的设备上运行,从而阻止您使用高级测试工具。您必须能够停用这些防御措施。
2. 增强黑盒安全测试中的静态分析。在黑盒测试中,应用字节码或二进制代码的静态分析可帮助您了解应用的内部逻辑。它还允许您识别硬编码凭据等缺陷。
3. 评估抵御逆向工程的能力。实现移动应用安全验证标准防逆向工程控件 (MASVS-R) 中列出的软件保护措施的应用应在一定程度上抵御逆向工程。为了验证此类控件的有效性,测试人员可以执行弹性评估作为一般安全测试的一部分。对于弹性评估,测试人员承担逆向工程师的角色并尝试绕过防御措施。
在我们深入研究移动应用逆向的世界之前,我们有一些好消息和一些坏消息。让我们从好消息开始
最终,逆向工程师总是赢家。
这在移动行业尤其如此,因为逆向工程师具有天然优势:移动应用的部署和沙盒化方式在设计上比传统桌面应用的部署和沙盒化方式更严格,因此包含 Windows 软件中常见的 Rootkit 式防御机制(例如 DRM 系统)根本不可行。Android 的开放性允许逆向工程师对操作系统进行有利的更改,从而帮助逆向工程过程。iOS 为逆向工程师提供了较少的控制权,但防御选项也更加有限。
坏消息是,处理多线程反调试控件、加密白盒、隐秘的反篡改功能和高度复杂的控制流转换不适合胆小的人。最有效的软件保护方案是专有的,无法通过标准调整和技巧来击败。战胜它们需要繁琐的手动分析、编码、挫败感,并且取决于您的个性,还需要不眠之夜和紧张的人际关系。
初学者很容易被逆向工程的巨大范围所淹没。最好的入门方法是设置一些基本工具(请参阅 Android 和 iOS 逆向章节中的相关部分),然后从简单的逆向任务和 crackme 开始。您需要了解汇编器/字节码语言、操作系统、遇到的混淆等等。从简单的任务开始,逐步升级到更困难的任务。
在以下部分中,我们将概述移动应用安全测试中最常用的技术。在后面的章节中,我们将深入研究 Android 和 iOS 的特定于操作系统的详细信息。
基本篡改技术¶
二进制补丁¶
补丁是更改编译后的应用的过程,例如,更改二进制可执行文件中的代码、修改 Java 字节码或篡改资源。此过程在移动游戏黑客场景中称为模组。补丁可以通过多种方式应用,包括在十六进制编辑器中编辑二进制文件以及反编译、编辑和重新组装应用。我们将在后面的章节中给出有用的补丁的详细示例。
请记住,现代移动操作系统严格执行代码签名,因此运行修改后的应用不像过去在桌面环境中那么简单。安全专家在 90 年代的生活要容易得多!幸运的是,如果您在自己的设备上工作,打补丁并不是很困难。您只需重新签署应用或禁用默认的代码签名验证工具即可运行修改后的代码。
代码注入¶
代码注入是一种非常强大的技术,可让您在运行时探索和修改进程。注入可以通过多种方式实现,但是您无需了解所有详细信息,这要归功于可以自动执行该过程的免费提供的、有据可查的工具。这些工具使您可以直接访问进程内存和重要结构,例如应用实例化的实时对象。它们带有许多实用功能,这些功能对于解析加载的库、挂钩方法和本机函数等很有用。进程内存篡改比文件补丁更难检测,因此在大多数情况下是首选方法。
ElleKit、 Frida和 Xposed是移动行业中使用最广泛的挂钩和代码注入框架。这三个框架在设计理念和实现细节上有所不同:ElleKit 和 Xposed 侧重于代码注入和/或挂钩,而 Frida 旨在成为一个成熟的“动态检测框架”,包括代码注入、语言绑定和一个可注入的 JavaScript VM 和控制台。
我们将包含所有三个框架的示例。我们建议从 Frida 开始,因为它是这三个框架中最通用的(因此,我们还将包含更多 Frida 详细信息和示例)。值得注意的是,Frida 可以在 Android 和 iOS 上将 JavaScript VM 注入到进程中,而 ElleKit 的注入仅在 iOS 上有效,而 Xposed 仅在 Android 上有效。但是,最终,您当然可以通过任一框架实现许多相同的目标。
静态和动态二进制分析¶
逆向工程是重建编译程序的源代码语义的过程。换句话说,您将程序拆开、运行、模拟其部分内容并对其进行其他难以形容的事情,以了解其作用和方式。
使用反汇编器和反编译器¶
反汇编器和反编译器允许您将应用的二进制代码或字节码转换回或多或少可以理解的格式。通过在原生二进制文件上使用这些工具,您可以获得与应用编译时所针对的架构相匹配的汇编程序代码。反汇编器将机器代码转换为汇编代码,而反编译器又使用汇编代码来生成等效的高级语言代码。Android Java 应用可以反汇编为 smali,smali 是一种用于 Dalvik(Android 的 Java VM)使用的 DEX 格式的汇编语言。Smali 汇编程序也可以很容易地反编译回等效的 Java 代码。
从理论上讲,汇编程序和机器代码之间的映射应该是一对一的,因此,这可能会给人一种反汇编是一项简单任务的印象。但实际上,存在多个陷阱,例如
- 可靠地区分代码和数据。
- 可变指令大小。
- 间接分支指令。
- 可执行文件的代码段中没有显式 CALL 指令的函数。
- 位置无关代码 (PIC) 序列。
- 手工制作的汇编代码。
同样,反编译是一个非常复杂的过程,涉及许多确定性和基于启发式的方法。因此,反编译通常不是很准确,但对于快速了解正在分析的函数仍然非常有用。反编译的准确性取决于代码中可用的信息量以及反编译器的复杂程度。此外,许多编译和编译后工具会在编译后的代码中引入额外的复杂性,以增加理解和/或反编译本身的难度。此类代码称为混淆代码。
在过去的几十年中,许多工具已经完善了反汇编和反编译的过程,从而产生了高保真度的输出。任何可用工具的高级使用说明通常可以轻松地填满一本书。最好的入门方法是简单地选择一个适合您的需求和预算的工具,并获得一份经过良好审查的用户指南。在本节中,我们将介绍其中的一些工具,并在随后的“逆向工程和篡改”Android 和 iOS 章节中,我们将重点介绍技术本身,尤其是特定于手头平台的那些技术。
代码混淆¶
混淆是转换代码和数据的过程,以使其更难以理解(有时甚至难以反汇编)。它通常是软件保护方案的组成部分。混淆并不是可以简单地打开或关闭的东西,程序可以通过多种方式在整体或部分上变得难以理解,并且程度不同。
注意:以下所有介绍的技术都无法阻止有足够时间和预算的人对您的应用进行逆向工程。但是,结合使用这些技术将使他们的工作变得更加困难。因此,目的是阻止逆向工程师执行进一步的分析,而不是使其不值得付出努力。
以下技术可用于混淆应用程序
- 名称混淆
- 指令替换
- 控制流扁平化
- 死代码注入
- 字符串加密
- 加壳
名称混淆¶
标准编译器基于源代码中的类名和函数名生成二进制符号。因此,如果不应用混淆,符号名称将保持有意义,并且可以很容易地从应用二进制文件中提取。例如,可以通过搜索相关关键字(例如“jailbreak”)来找到检测越狱的函数。下面的列表显示了来自 DVIA-v2的反汇编函数 JailbreakDetectionViewController.jailbreakTest4Tapped
。
__T07DVIA_v232JailbreakDetectionViewControllerC20jailbreakTest4TappedyypF:
stp x22, x21, [sp, #-0x30]!
mov rbp, rsp
在混淆之后,我们可以观察到符号名称不再有意义,如下面的列表所示。
__T07DVIA_v232zNNtWKQptikYUBNBgfFVMjSkvRdhhnbyyFySbyypF:
stp x22, x21, [sp, #-0x30]!
mov rbp, rsp
但是,这仅适用于函数、类和字段的名称。实际代码保持不变,因此攻击者仍然可以读取函数的反汇编版本并尝试了解其用途(例如,检索安全算法的逻辑)。
指令替换¶
此技术用更复杂的表示形式替换标准二进制运算符(如加法或减法)。例如,加法 x = a + b
可以表示为 x = -(-a) - (-b)
。但是,使用相同的替换表示形式可以很容易地反转,因此建议为单个案例添加多个替换技术并引入一个随机因子。此技术可以在反编译期间反转,但根据替换的复杂性和深度,反转它仍然可能很耗时。
控制流扁平化¶
控制流扁平化用更复杂的表示形式替换原始代码。转换将函数体分解为基本块,并将它们全部放入一个带有开关语句的无限循环中,该语句控制程序流。这使得程序流难以跟踪,因为它删除了通常使代码更易于阅读的自然条件构造。
该图显示了控制流扁平化如何更改代码。有关更多信息,请参见“通过控制流扁平化混淆 C++ 程序”。
死代码注入¶
此技术通过将死代码注入程序来使程序的控制流更加复杂。死代码是一段不影响原始程序行为的代码存根,但会增加逆向工程过程的开销。
字符串加密¶
应用程序通常使用硬编码的密钥、许可证、令牌和端点 URL 进行编译。默认情况下,所有这些都以纯文本形式存储在应用程序二进制文件的数据部分中。此技术加密这些值并将代码存根注入到程序中,这些存根将在程序使用该数据之前对其进行解密。
加壳¶
加壳是一种动态重写混淆技术,它将原始可执行文件压缩或加密为数据,并在执行过程中动态恢复它。打包可执行文件会更改文件签名,以尝试避免基于签名的检测。
调试和追踪¶
从传统意义上讲,调试是作为软件开发生命周期的一部分识别和隔离程序中问题的过程。即使识别错误不是主要目标,用于调试的相同工具对逆向工程师也很有价值。调试器能够在运行时暂停程序的任何位置、检查进程的内部状态,甚至修改寄存器和内存。这些能力简化了程序检查。
调试通常意味着交互式调试会话,其中调试器附加到正在运行的进程。相比之下,跟踪是指应用程序执行信息的被动日志记录(例如 API 调用)。可以使用多种方式进行跟踪,包括调试 API、函数挂钩和内核跟踪工具。同样,我们将在特定于操作系统的“逆向工程和篡改”章节中介绍其中的许多技术。
高级技术¶
对于更复杂的任务,例如反混淆高度混淆的二进制文件,如果您不自动化分析的某些部分,您将不会取得进展。例如,基于反汇编程序中的手动分析来理解和简化复杂的控制流程图将花费您数年时间(并且很可能会在您完成之前让您发疯)。相反,您可以使用自定义工具来增强您的工作流程。幸运的是,现代反汇编程序带有脚本和扩展 API,并且许多有用的扩展可用于流行的反汇编程序。还有开源反汇编引擎和二进制分析框架。
与以往的黑客攻击一样,适用于任何规则:只需使用最有效的工具即可。每个二进制文件都是不同的,所有逆向工程师都有自己的风格。通常,实现目标的最佳方法是将方法结合起来(例如基于模拟器的跟踪和符号执行)。要开始使用,请选择一个好的反汇编程序和/或逆向工程框架,然后熟悉它们的特定功能和扩展 API。最终,提高的最佳方法是获得实践经验。
动态二进制插桩¶
对于本机二进制文件,另一种有用的方法是动态二进制检测 (DBI)。Valgrind 和 PIN 等检测框架支持对单个进程进行细粒度的指令级跟踪。这是通过在运行时插入动态生成的代码来实现的。Valgrind 在 Android 上编译得很好,并且预构建的二进制文件可供下载。
Valgrind README 包含 Android 的特定编译说明。
基于模拟的动态分析¶
模拟是在不同平台或另一个程序中执行的特定计算机平台或程序的模仿。执行此模仿的软件或硬件称为模拟器。模拟器为实际设备提供了一种更便宜的替代方案,用户可以在其中操作它,而不必担心损坏设备。有多个模拟器可用于 Android,但对于 iOS,实际上没有可用的可行模拟器。iOS 只有模拟器,在 Xcode 中提供。
模拟器和模拟器之间的差异经常引起混淆,并导致互换使用这两个术语,但实际上它们是不同的,特别是对于 iOS 用例。模拟器模仿目标平台的软件和硬件环境。另一方面,模拟器仅模仿软件环境。
基于 QEMU 的 Android 模拟器在运行应用程序时会考虑 RAM、CPU、电池性能等(硬件组件),但在 iOS 模拟器中根本不考虑此硬件组件的行为。iOS 模拟器甚至缺少 iOS 内核的实现,因此,如果应用程序使用 syscall,则无法在此模拟器中执行。
简而言之,模拟器是对目标平台的更接近的模仿,而模拟器仅模仿其中的一部分。
在模拟器中运行应用程序为您提供了强大的方式来监控和操控其环境。对于某些逆向工程任务,尤其是那些需要低级指令跟踪的任务,模拟是最佳(或唯一)选择。不幸的是,这种类型的分析仅适用于 Android,因为没有免费或开源的 iOS 模拟器(iOS 模拟器不是模拟器,并且为 iOS 设备编译的应用程序无法在其上运行)。唯一可用的 iOS 模拟器是商业 SaaS 解决方案 - Corellium。
使用逆向工程框架的自定义工具¶
即使大多数专业的基于 GUI 的反汇编器都具有脚本功能和可扩展性,但它们根本不适合解决特定问题。逆向工程框架允许您执行和自动化任何类型的逆向任务,而无需依赖重量级的 GUI。值得注意的是,大多数逆向框架都是开源和/或免费提供的。流行的框架包括支持移动架构的 radare2 for iOS 和 Angr。
示例:使用符号/混合执行进行程序分析¶
在 2000 年代后期,基于符号执行的测试已成为识别安全漏洞的一种流行方法。符号“执行”实际上是指将程序中可能的路径表示为一阶逻辑中的公式的过程。“可满足性模理论”(SMT)求解器用于检查这些公式的可满足性并提供解决方案,包括到达与已求解公式对应的路径上的某个执行点所需的变量的具体值。
简而言之,符号执行是在不执行程序的情况下对其进行数学分析。在分析期间,每个未知输入都表示为一个数学变量(一个符号值),因此对这些变量执行的所有操作都记录为操作树(也称为 AST(抽象语法树),来自编译器理论) 。这些 AST 可以转换为所谓的约束,这些约束将由 SMT 求解器解释。在此分析结束时,获得最终的数学方程式,其中变量是其值未知的输入。 SMT 求解器是特殊的程序,它们求解这些方程式,以在给定最终状态的情况下给出输入变量的可能值。
为了说明这一点,假设有一个函数接受一个输入 (x
) 并将其乘以第二个输入 (y
) 的值。最后,有一个if条件检查计算出的值是否大于外部变量 (z
) 的值,如果为真则返回“成功”,否则返回“失败”。此操作的方程式为(x * y) > z
。
如果我们希望该函数始终返回“成功”(最终状态),我们可以告诉 SMT 求解器计算满足相应方程式的x
和y
(输入变量)的值。与全局变量一样,它们的值可以从此函数外部更改,这可能会导致每次执行此函数时产生不同的输出。这增加了确定正确解决方案的额外复杂性。
在内部,SMT 求解器使用各种方程式求解技术来为此类方程式生成解决方案。其中一些技术非常先进,对它们的讨论超出了本书的范围。
在实际情况下,函数比上面的示例复杂得多。函数复杂性的增加可能会给经典的符号执行带来重大挑战。下面总结了一些挑战
- 程序中的循环和递归可能导致无限执行树。
- 多个条件分支或嵌套条件可能导致路径爆炸。
- 符号执行生成的复杂方程式可能因其限制而无法被 SMT 求解器求解。
- 程序正在使用符号执行无法处理的系统调用、库调用或网络事件。
为了克服这些挑战,通常将符号执行与其他技术(例如动态执行(也称为具体执行))相结合,以缓解经典符号执行特有的路径爆炸问题。混凝土(实际)执行和符号执行的这种组合称为混合执行(混合执行的名称源自 concrete 和 symbolic),有时也称为动态符号执行。
为了可视化这一点,在上面的示例中,我们可以通过执行进一步的逆向工程或通过动态执行程序并将此信息输入到我们的符号执行分析中来获得外部变量的值。此额外信息将降低我们方程式的复杂性,并可能产生更准确的分析结果。与改进的 SMT 求解器和当前硬件速度一起,混合执行允许探索中等大小的软件模块中的路径(即,大约 10 KLOC)。
此外,符号执行还有助于支持反混淆任务,例如简化控制流图。例如,Jonathan Salwan 和 Romain Thomas 展示了如何使用动态符号执行来逆向工程基于 VM 的软件保护 [#salwan](即,使用实际执行跟踪、模拟和符号执行的混合)。
在 Android 部分,您将找到一个演练,介绍如何使用符号执行来破解 Android 应用程序中的简单许可检查。
参考资料¶
- [#vadla] Ole André Vadla Ravnås,代码跟踪器的解剖 - https://medium.com/@oleavr/anatomy-of-a-code-tracer-b081aadb0df8
- [#salwan] Jonathan Salwan 和 Romain Thomas,Triton 如何帮助逆向基于虚拟机的软件保护 - https://drive.google.com/file/d/1EzuddBA61jEMy8XbjQKFF3jyoKwW7tLq/view?usp=sharing