如何克服 ARM 固件中的可执行代码和文字池混合导致的反汇编问题?

逆向工程 拆卸 二元分析 固件 手臂 反汇编者
2021-06-27 23:03:49

背景和背景信息

第 1 部分:ARM 固件与其他架构固件的比较

我在这两个问题中查看了ARM固件:

并且对代码与数据的混合感到困惑,因为我查看了其他架构的固件,如 Ubicom、MIPSEL 和 MIPS,但没有观察到与 ARM 固件中相同类型的代码和数据混合。

以下是上下文的各种二进制文件的一些可视化:

来自分析 eCos 图像的MIPSEL eCos 固件

eCos 固件熵图(裁剪图) 熵可视化 细节可视化 <------------------------ code -------------------------><---- images, ASCII---->

未知指令集,来自lzma 的uC/OS 固件:无法识别文件格式 [附上详细信息]

CleverDOg 相机固件熵图(裁剪图) 熵可视化 细节可视化 <-------------------------- code??? ------------------------><-- ASCII -->

Ubicom32 SlingBox 固件需要帮助从固件 .bin 文件中提取 YAFFS

SlingBox 固件熵图(裁剪图) 熵可视化 细节可视化 <--------------------------- code -----------------------> <- data ->< code >

一种模式似乎出现了,不是吗?此处显示的固件映像中的可执行代码与数据并不难区分,因为它主要位于 1 或 2 个大的连续块中。

然而,对于 ARM 固件,事情并没有那么简单:

运行由 binwalk --disasm 识别为 ARM 可执行文件的二进制文件中的32 位 ARM 固件

固件熵图(裁剪) 熵可视化 细节可视化

代码在哪里?数据在哪里?没有像其他固件二进制文件那样容易辨别的分离。

Thumb-2(16 位 + 32 位)ARM 固件来自

电子烟熵图(裁剪图) 熵可视化 细节可视化

这里的情况类似。

当可视化并列时,差异变得非常明显:

MIPSEL、eCos 未知 Ubicom32,SlingBox ARM 32 位松下相框 ARM Thumb-2,SMOK CUBE 东西

本示例中的非 ARM 固件二进制文件具有相当清晰的代码和数据分离,这对于我们要反汇编代码很有帮助。然而,在 ARM 固件二进制文件的情况下,代码和数据的识别看起来很成问题。

第 2 部分:ARM 文字池

那么为什么 ARM 固件二进制文件看起来与其他文件如此不同呢?为什么没有清晰可辨的代码和数据分离?答案似乎涉及 ARM 文字池。

我在这里包含有关 ARM 文字池的信息,因为它看起来很相关,而且因为我自己花了很长时间才找到它们,所以希望这可以为其他人节省一些时间。

根据伊恩库克(强调我的),

...代码和数据的混合实际上在 ARM 代码中很常见。

ARM 指令的大小是固定的(ARM 上为 4 个字节,THUMB 为 2 个字节)并且没有足够的空间来编码 32 位立即数。相反,ARM 编译器通常会做两件事——

  • 他们在需要常量的函数指令之后直接放置 32 位立即数,并且

  • 它们在函数中使用与 PC 相关的加载指令将这些常量放入寄存器中。

这类似于我在文字池的麻烦一文中找到的信息

那么,什么是文字池?我很高兴你问。文字池是一块内存区域(在文本段中),用于存储常量。这些常量可以是普通的数字常量,但它们的主要用途是存储系统中变量的地址。这些地址是必需的,因为 ARM 指令没有任何直接在内存中加载(或存储)地址的指令。相反 ldr 和 str 只能存储在寄存器的 ±12 位偏移处。现在有很多方法可以生成具有此限制的代码,例如,您可以确保数据部分的大小小于 8KiB,并保留一个寄存器以用作所有数据查找的基础。但这种方法仅适用于数据节大小有限的情况。所采用的标准方法是,当使用变量时,将其地址写出到文字池中。编译器然后生成两条指令,第一条是从这个文字池中读取地址,第二条是访问变量的指令。

那么,这个文字池究竟是如何工作的呢?好吧,为了不需要指向文字池的特殊寄存器,编译器使用程序计数器 (PC) 寄存器作为基址寄存器。生成的代码看起来类似:ldr r3, [pc, #28]该代码将与 PC 的当前值相距 28 字节偏移处的值加载到寄存器 r3 中。r3 然后包含我们要访问的变量的地址,可以像这样使用:ldr r1, [r3, #0],它将变量的值(而不是地址)加载到 r1 中。现在,由于 PC 被用作文字池访问的基础,因此应该清楚文字池的存储位置足够接近需要使用它的代码。

为了确保文字池与使用它的代码足够接近,编译器在每个函数的末尾存储了一个文字池这种方法效果很好(除非你有一个 4KiB+ 的函数,这无论如何都是愚蠢的),但可能有点浪费。

但是,在文字池ARM 文档中,指出

汇编器使用文字池来保存要加载到寄存器中的某些常量值。汇编器在每个部分的末尾放置一个文字池节的结尾由程序集末尾的 END 指令或下节开头的 AREA 指令定义。包含文件末尾的 END 指令不表示节的结束。

我不了解 ARM 的 jack,所以这很令人困惑。我可以把它作为一个单独的问题。无论如何,关键是文字池似乎是代码和数据在前面可视化的 ARM 固件中混合在一起的原因。还有一种叫做scatterloading 的东西,但我不知道这是否与这里描述的问题有关。

问题

如何克服 ARM 固件中混合可执行代码和文字池所带来的反汇编问题?

根据伊恩库克(来自与之前相同的答案)(强调我的),

通常会按照访问它们的函数的地址顺序查看存储在函数之后的立即数。因此,在这种情况下,函数以 0x00000F5C 处的指令结束,并且立即数似乎跨越地址范围 0x00000F60 到 0x00000FFF,我通常希望以下函数从地址 0x00001000 开始。

知道了这一点,反汇编程序就可以识别这种模式并自动跳过相关数据。

是的,怎么样?我尝试反汇编 ARM 32 位松下相框固件和 ARM Thumb-2 SMOK-X Cube 装置固件,radare2但似乎数据与代码一起被反汇编。

由于函数后文字池的位置似乎是 ARM 二进制文件的一个共同特征,即使二进制文件中没有任何符号,现有的反汇编程序是否能够区分函数中的代码和相邻文字池中的数据?据我所知,Capstone 的情况并非如此。

1个回答

这确实是线性扫描反汇编器的问题;递归下降的大多数不受影响,因为如果没有代码流入,它们不会尝试反汇编数据。但是,可能有一些启发式/便宜的技巧甚至可能对线性扫描反汇编器有帮助。想到的一些:

  • 反汇编指令时,寻找与PC相关的操作数(以及ADR指令)的LDR并记住目标地址;到达这些地址时停止反汇编。

  • 在 ARM 模式下,当解码的指令有无意义的情况时停止反汇编,特别是当没有前面的标志设置指令时。

  • 从具有符号的二进制文件中收集一些统计信息(例如 n-gram 频率),并使用它们来区分数据中的代码。

    最后,我认为这个问题没有很好的通用解决方案,因此针对每个特定目标调整方法可能会更容易。