MASTG-TECH-0077: 审查反汇编的原生代码

分析反汇编后的原生代码需要很好地理解底层平台使用的调用约定和指令。在本节中,我们将查看原生代码的 ARM64 反汇编。学习 ARM 架构的一个好的起点是 Azeria Labs Tutorials 的ARM 汇编基础入门。这是我们将在本节中使用的一些内容的快速总结

  • 在 ARM64 中,寄存器大小为 64 位,称为 Xn,其中 n 是 0 到 31 的数字。如果使用寄存器的较低 (LSB) 32 位,则称为 Wn。
  • 函数的输入参数通过 X0-X7 寄存器传递。
  • 函数的返回值通过 X0 寄存器传递。
  • 加载 (LDR) 和存储 (STR) 指令用于从寄存器读取或写入内存。
  • B、BL、BLX 是用于调用函数的分支指令。

如上所述,Objective-C 代码也会被编译成原生二进制代码,但分析 C/C++ 原生代码可能更具挑战性。对于 Objective-C,存在各种符号(尤其是函数名称),这简化了代码的理解。在上面的部分中,我们了解到像 setTextisEqualStrings 这样的函数名称的存在可以帮助我们快速理解代码的语义。对于 C/C++ 原生代码,如果所有二进制文件都被剥离,可能只有很少或没有符号来帮助我们进行分析。

反编译器可以帮助我们分析原生代码,但应谨慎使用。现代反编译器非常复杂,它们用于反编译代码的许多技术中,其中一些是基于启发式的。基于启发式的技术可能并不总是给出正确的结果,其中一种情况是确定给定原生函数的输入参数的数量。掌握分析反汇编代码的知识,并辅以反编译器,可以减少分析原生代码时的错误。

我们将分析前面部分中 viewDidLoad 函数中标识的原生函数。该函数位于偏移量 0x1000080d4 处。该函数的返回值在标签的 setText 函数调用中使用。此文本用于与用户输入进行比较。因此,我们可以确定该函数将返回字符串或等效内容。

我们在函数反汇编中首先看到的是,该函数没有输入。在整个函数中,没有读取 X0-X7 寄存器。此外,还有对其他函数的多次调用,例如 0x100008158、0x10000dbf0 等处的函数。

与此类函数调用相对应的指令如下所示。分支指令 bl 用于调用 0x100008158 处的函数。

1000080f0 1a 00 00 94     bl         FUN_100008158
1000080f4 60 02 00 39     strb       w0,[x19]=>DAT_10000dbf0

函数的返回值(在 W0 中找到)存储在寄存器 X19 中的地址中(strb 将一个字节存储到寄存器中的地址)。我们可以看到其他函数调用的相同模式,返回的值存储在 X19 寄存器中,并且每次偏移量都比前一个函数调用多 1。此行为可能与一次填充字符串数组的每个索引相关联。每个返回值都写入此字符串数组的索引。有 11 个这样的调用,从当前的证据来看,我们可以做出一个聪明的猜测,即隐藏标志的长度为 11。在反汇编的末尾,该函数返回此字符串数组的地址。

100008148 e0 03 13 aa     mov        x0=>DAT_10000dbf0,x19

为了确定隐藏标志的值,我们需要知道上面标识的每个后续函数调用的返回值。在分析函数 0x100006fb4 时,我们可以观察到该函数比我们之前分析的函数更大更复杂。在分析复杂函数时,函数图非常有用,因为它可以帮助更好地理解函数的控制流。可以通过单击子菜单中的“显示函数图”图标在 Ghidra 中获得函数图。

手动完全分析所有原生函数将非常耗时,并且可能不是最明智的方法。在这种情况下,强烈建议使用动态分析方法(请参阅iOS 上的动态分析)。例如,通过使用像 Hook 这样的技术或简单地调试应用程序,我们可以轻松地确定返回的值。通常,最好使用动态分析方法,然后回退到手动分析反馈循环中的函数。这样,您可以同时受益于两种方法,同时节省时间和减少精力。