ARM固件基址自动识别

IoT 1个月前 admin
21 0 0

推测固件基址是 IoT 安全的经典问题。目前有基于字符串引用,跳转表等固件基址推测方法,但是当固件字符串较少或者被加密时,前者就会失效;当跳转表中不存在绝对地址时,后者就会失效。本文提出了一种基于函数引用的新颖的固件基址推测方法,作为对已有方法的补充有非常好的效果。同时,本文开发了一个 IDA 插件,集成了以上提到的三种固件基址推测方法

引言

固件基址推测问题的解决方案可以分为两类,一类是启发式方法,基于字符串引用,跳转表等等推测搜索固件基地,一类是特殊处理方法,针对某种类型的固件做一些特殊处理。本文只讨论启发式方法。

已有方法

基于字符串引用

ARM 汇编中有一条伪指令 LDR Rt, =Label,它与 X86 汇编的 LEA 指令有一点点相像,和 LEA Rt, Label 指令一样,这条指令也是将 Label 的地址赋值给 Rt 寄存器,但是具体的实现方式不同。LDR 这条伪指令是通过常量池实现的,因为 ARM 汇编中的指令是定长 4 字节的,所以不能像 X86 一样直接将 32 位立即数编码进入指令。在 LDR 伪指令附近存在一个常量池,常量池中放置有 32 位立即数,LDR 伪指令的实际工作流程是先基于 PC 寄存器定位到常量池,然后从常量池中装载 32 位立即数进入寄存器。

在 ARM 汇编中,访问字符串也可以通过 LDR 伪指令实现,通过将字符串地址硬编码在常量池中。这就是基于字符串引用推测固件基址的核心,假设我们现在不知道固件基址,在 IDA 中将固件映射在 0x0 处,我们通过推测知道某条 LDR 伪指令引用的字符串位于 0x1111 处,同时 LDR 伪指令装载的立即数是 0x4111,那么固件的基址就应该是 0x4111 – 0x1111 = 0x3000。

现在我们提取程序中的所有 LDR 伪指令装载的立即数形成集合 R,提取程序中的所有字符串的起始地址形成集合 V,设固件真实基址为 B,有 f(x) = B + x 函数,那么固件基址推测过程其实就是寻找到合适的参数 B,使得|f(R) ∩ V|最大。值得注意的是,这是一个启发式策略,即使参数 B 满足|f(R) ∩ V|最大,也不能说它就一定是正确基址。同时注意到固件基址的一般对齐 4096,这能极大加快我们寻找参数 B 的速度。该方法的核心代码如下

# LDR伪指令装载的立即数集合
refs = set(refs)
# 字符串起始地址集合
strs = set([x.ea for x in idautils.Strings()])

# 根据基址对齐4096统计所有可能的基址
occurs = [base for s, r in itertools.product(strs, refs) if (base := r - s) >= 0 and base % 4096 == 0]
bases = {}

for o in occurs:
    bases[o] = bases.get(o, 0) + 1
# 将基址按照出现次数排序,寻找出现次数最多的基址,即|f(R) ∩ V|最大
bases = dict(sorted(bases.items(), key=lambda x: x[1]))

基于跳转表

基于跳转表的方法限制很大,它适用的前提条件是跳转表中必须存在绝对地址,而不是相对地址,如下图所示,跳转表中存放的都是绝对地址,只有这种情况才可以使用跳转表推测固件基址。图中有 case 块的绝对地址是 0x1629C,固件加载基址一般对齐 4096,因此 case 块的尾部地址 29C 应该是保持不变的,前往 0xE29C,将它转换为代码,可知确实是 case 块,因此固件装载地址是 0x1629C – 0xE29C = 0x8000。

ARM固件基址自动识别

ARM固件基址自动识别

本文方法

前文提到了在 ARM 汇编中对字符串的访问可以使用 LDR 伪指令完成,在 ARM 汇编中,取函数指针也可以使用 LDR 指令完成。相比于字符串引用关系,为什么不使用函数引用关系推测固件基址呢?取程序中的所有 LDR 伪指令装载的立即数形成集合 R,取程序中的所有识别出的函数的起始地址形成集合 V,设固件真实基址为 B,有 f(x) = B + x 函数,g(x) = B + x + 1,那么固件基址推测过程其实就是寻找到合适的参数 B,使得|(f(R) ∪ g(R))∩ V|最大。注意这里引入了 g(x) = B + x + 1,因为有的函数是 Thumb 模式的指令,调用这种函数时需要通过跳转到函数地址 + 1 的地方使得 CPU 切换到 Thumb 模式,因为 Thumb 函数首地址肯定对齐 2,因此低位就被用来指示 CPU 切换 Thumb 模式了。该方法的核心代码如下。

refs = set(refs)
funcs = set([x for x in idautils.Functions()])

occurs = [base if base % 4096 == 0 else base - 1 for s, r in itertools.product(funcs, refs) if (base := r - s) and base >= 0 and base % 4096 <= 1]
bases = {}
for o in occurs:
    bases[o] = bases.get(o, 0) + 1

bases = dict(sorted(bases.items(), key=lambda x: x[1]))
return bases

IDA 插件集成

注意,必须要使用 IDA 8.0 版本以上,因为 IDA 8.0 才引入了 Find functions 插件,它通过搜索函数序言指令自动寻找 BIN 中的函数开始反汇编,8.0 以下的 IDA 在加载 BIN 后不会自动反汇编,也就无法识别出函数等。

在反汇编窗口右键,进入 ArmBaseFinder(ARM 固件基址寻找)菜单,Find By Str Ref 就是通过字符串引用推测固件基址,Find By Func Ref 就是通过函数引用推测固件基址,Find By Switch Jump Table 就是通过跳转表推测固件基址。

ARM固件基址自动识别

以 opkg 固件为例,该固件中字符串较少,因此只能通过跳转表与函数引用推测出固件基址是 0x8000,通过字符串引用是无法推测出正确的固件基址的。

ARM固件基址自动识别

准备过几天用 C 重写一遍插件,Python 跑双重循环真的是非常非常慢,时间开销可能是 C/C 的百倍以上吧。。。

插件 GitHub 地址: https://github.com/ddddhm1234/arm_base_rec


原文始发于微信公众号(网络空间威胁观察):ARM固件基址自动识别

版权声明:admin 发表于 2024年4月11日 下午1:01。
转载请注明:ARM固件基址自动识别 | CTF导航

相关文章