0x00 简介
ARM 属于 CPU 架构的一种,主要应用于嵌入式设备、智能手机、平板电脑、智能穿戴、物联网设备等。 ARM64 指使用64位ARM指令集,64位指指数据处理能力即一条指令处理的数据宽度,指令编码使用定长32比特编码。
0x01 ARM
1.1 字节序
字节序分为大端(BE)和小端(LE):
-
大端序(Big-Endian)将数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。这种排列方式与数据用字节表示时的书写顺序一致,符合人类的阅读习惯。
-
小端序(Little-Endian)将一个多位数的低位放在较小的地址处,高位放在较大的地址处。小端序与人类的阅读习惯相反,但更符合计算机读取内存的方式,因为CPU读取内存中的数据时,是从低地址向高地址方向进行读取的。 在v3之前,ARM体系结构为little-endian字节序,此后,ARM处理器成为BI-endian,并具允许可切换字节序。
1.2 寄存器
1.2.1 32位寄存器
R0-R12:正常操作期间存储临时值、指针。 其中:
-
R0用于存储先前调用的函数的结果
-
R7用于存储系统调用号
-
R11跟踪用作帧指针的堆栈的边界,函数调用约定指定函数的前四个参数存储在寄存器 R0-R3 中
-
R13也称为SP,作为堆栈指针,指向堆栈的顶部
-
R14也被称为 LR,作为链接寄存器,进行功能调用时,链接寄存器将使用一个内存地址进行更新,该内存地址引用了从其开始该功能的下一条指令,即保存子程序保存的地址
-
R15也称为PC,即程序计数器,程序计数器自动增加执行指令的大小。 当参数少于4个时,子程序间通过寄存器R0~R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值。 在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存。 子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位。
1.2.2 64位寄存器
1.3 指令
ARM 指令模版: MNEMONIC{S} {condition} {Rd}, Operand1, Operand2
[!NOTE] MNEMONIC:指令简称 {S}:可选后缀,如果指定了S,即可根据结果更新条件标志 {condition}:执行指令需满足的条件 {Rd}:用于存储指令结果的寄存器 Operand1:第一个操作数,寄存器或立即数 Operand2:可选,可以是立即数或者带可移位的寄存器
1.4 栈帧
栈帧是一个函数所使用的那部分栈,所有的函数的栈帧串起来就是一个完整的栈。栈帧的边界分别由 fp 和 sp 来限定。 FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。 如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。从main函数进入到func1函数,main函数的上边界和下边界保存在被它调用的栈帧里面。
1.5 叶子函数和非叶子函数
叶子函数是指本身不会调用其他函数,非叶子函数相反。非叶子函数在调用时 LR 会被修改,因此需要保留该寄存器的值。在栈溢出的场景下,非叶子函数的利用会比较简单。
0x02 固件模拟
ARM架构的二进制程序,要进行固件模拟才可以运行和调试。 目前比较主流的方法还是使用QEMU,可以参考路由器固件模拟环境搭建,也可以用一些仿真工具,比如 Firmadyne 和 firmAE。 如果想依靠固件模拟自建一些工具,可以考虑使用 Qiling。
0x03 漏洞
3.1 exploit_me
下载地址 运行bin目录下的 expliot64开始进行挑战:
Level 1: Integer overflow
漏洞触发代码如下: a1 是传入的第二个参数,atoi 函数可以将传入的参数字符串转为整数,然后进行判断,第一个判断是判断转换后的v2是否为0或负数,若不是,则进入第二个判断,判断 v2 的低16位(WORD)是否不为0,即低16位是否为0,若为0则跳过判断。 atoi 函数存在整数溢出,当输入 -65536,会转换为0xffff0000: 此数的低16位是0,因此可以绕过判断。
Level 2: Stack overflow
漏洞触发代码如下:
verify_user 函数会验证输入的第一个参数是否为 admin,验证完成后,会继续验证第二个参数是否是 funny,看起来似乎很简单,但是可以看到即使两个参数都按照要求输入,也不会得到 level3 password: 查看第三关的入口,可以看到第三关的password参数为 aVelvet: 通过查找引用可以得到 level3password 这个函数才是输出 aVelvet 的关键函数,但是此函数并未被引用。然而 strcpy(&v3, a1) 这一行伪代码暴露出存在栈溢出,因此可以通过覆盖栈上存储的返回地址,来跳转到 level3password 函数中。 首先通过 pwndbg cyclic 生成一个长度为200的序列,当作第一个参数输入: 得到偏移为16。 IDA 查看 level3password 地址为 0x401178,因此构造PoC:
1 |
|
得到 password “Velvet”
Level 3: Array overflow
抽出核心代码为:
1 |
|
这里有很明显的数组越界写入的问题。用gdb 进行调试:gdb --args ./exploit64 Velvet 33 1
,在 0x401310(str w2, [x1, x0] 数组赋值) 处下断点,查看寄存器:
可以看到,当执行到赋值语句时,值传递是在栈上进行的,因此此处可以实现栈上的覆盖。
gdbserver配合IDA pro 尝试:
当传入参数为34,00000000 时,可以看到此刻执行STR W2, [X1,X0]
是将00000000 传到 0x000FFFFFFFFEBC0 + 0x84 = 0x000FFFFFFFFEC44 中:
0x000FFFFFFFFEC48 存储的就是 array_overflow 函数在栈上存储的返回地址。
因此只要覆盖此位置就可以劫持程序执行流。与 level2 同理,找到password 函数地址为 0x00000000004012C8
因此构造PoC:
1 |
|
Level 4: Off by one
核心代码如下: 传入的参数长度不能大于 0x100,复制到v3[256] 后,要使 v4 = 0。 字符串在程序中表示时,其实是会多出一个 “0x00” 来作为字符串到结束符号。因此PoC为:
1 |
|
Level 5: Stack cookie
Stack Cookie 是为了应对栈溢出采取的防护手段之一,目的是在栈上放入一个检验值,若栈溢出Payload在覆盖栈空间时,也覆盖了 Stack Cookie,则校验 Cookie 时会退出。 这里的Stack Cookie 是 secret = 0x1337。 通过汇编可以看出,v2 存储在 sp+0x58 – sp+ 0x28 中,所以当 strcpy 没限制长度时,可以覆盖栈空间,secret 存储在 sp+0x6C 中,v3 存储在 sp+0x68中,因此只要覆盖这两个位置为判断值,就可以完成攻击。
Level 6: Format string
[!NOTE] 格式化字符串漏洞 格式化字符串漏洞是比较经典的漏洞之一。 printf 函数的第一个参数是字符串,开发者可以使用占位符,将实际要输出的内容,也就是第二个参数通过占位符标识的格式输出。 例如: 当第二个参数为空时,程序也可以输出: 这是因为,printf从栈上取参时,当第一个参数中存在占位符,它会认为第二个参数也已经压入栈,因此通过这种方式可以读到栈上存储的值。 格式化字符串漏洞的典型利用方法有三个:
- 栈上数据,如上所述
- 任意地址读: 当占位符为%s时,读取的第二个参数会被当作目标字符串的地址,因此如果可以在栈上写入目标地址,然后作为第二个参数传递给printf,就可以实现任意地址读
- 任意地址写: 当占位符为%n时,其含义是将占位符之前成功输出的字节数写入到目标地址中,因此可以实现任意地址写,如: 一般配合%c使用,%c可以输出一个字符,在利用时通过会使用
程序逻辑为: 程序逻辑为输入 password,打印 password,判断 v1 是否为 89,是则输出密码。 这里 printf(Password),Password 完全可控,这里存在格式化字符串漏洞。 通过汇编可以看到: Arm64 函数调用时前八个参数会存入 x0-x7 寄存器,因此第8个占位符会从栈上开始取参,sp+0x18 是我们需要修改的地址,因此偏移为 7+3=10。 PoC为:
1 |
|
Level 7: Heap overflow
v7 和 v8 都是创建的堆空间,且在 strcpy 时未规定长度,因此可以产生堆溢出,从而覆盖到v7的堆空间,完成判断。 通过 pwngdb 生成测试字符串,在 printf处下断点,查看 v7 是否被覆盖和偏移: 通过 x1 寄存器可知,偏移为 48。 因此 PoC为:
1 |
|
Level 8: Structure redirection / Type confusion
一直跟进Msg:Msg: 而在strcpy 下面一行有用 v12 作为函数指针执行,v12 = v2 = &Run::Run(v2),
因此需要通过strcpy覆盖栈空间上存储的v12,来控制函数执行流。且覆盖为0x4c55a0,从而在取出 v12 当作函数指针执行时可以指向Msg::msg:
根据汇编,v12 存储在 sp+0x90-0x10 = sp+0x80 处,strcpy 从 sp+0x90-0x68 = sp+0x28 处开始,偏移为 0x80-0x28 - 0x8 = 0x50。
因此PoC为:./exploit64 Exploiter $(python -c 'import sys;sys.stdout.buffer.write(b"A"*(20*4)+b"\xa0\x55\x4c\x00\x00\x00\x00\x00")')
Level 9: Zero pointers
先使用 gdb 调试, gdb --args ./exploit64 Gimme a 1
,程序报错:
第二个参数应该填写的是地址:
在程序执行过程中,v4指向的值会被置0,v4指向的是a1的地址,因此a1地址指向的值会变为0。
因此要想完成 v3 的判断,只需要将 v3 地址传入即可。
PoC为./exploit64 Gimme 0xffffffffec9c 1
:
Level 10: Command injection
关键点 v9 = "man" + *(a2+16)
,v9中要包含 “;” 才可以完成判断。
因此 PoC 为 ./exploit64 Fun "a;whoami"
:
Level 11: Path Traversal
PoC为./exploit64 Violet dir1/dir2/../..
Level 12: Return oriented programming (ROP)
scanf 并未限制长度,因此此处存在溢出,通过pwndbg 的 cyclic 得到偏移为72。
现在目的是跳到 comp 函数中,且参数要为 0x5678:
因此需要构造 Rop Gadget,构造的 Gadget 应该具备以下功能,将comp 判断的地址 0x400784加入lr寄存器(x30),并能执行 ret,这样就可以在溢出时,将程序执行流劫持到 Rop Gadget 的地址。
这样在跳转时这个地址会被压栈作为 Rop 的 ret 的返回地址。
程序给了一个叫 ropgadgetstack 的 rop Gadget:
通过调试可知,x0 和 x1 在 0x400744 执行后都为当前sp寄存器存储的值,因此 0x400784的比较可以正常完成;lr寄存器,也就是x30 = (0x400744 时)sp+0x10 + 0x8。
输入 payload = b'A'*72 + p64(0x400744) + b'BBBBCCCCDDDDEEEEFFFFGGGGHHHHJJJJKKKKLLLLMMMMNNNNOOOOPPPP'
进行调试:
可知,paylad 从 0x400744后,再填充32个字符开始,会覆盖到当前$sp,因此只要再覆盖24个字符后,覆盖为 0x400784 就可以在 ret 执行后跳到 comp 的比较处。payload为 payload = b'A'*72 + p64(0x400744) + b'B'*32 + b'C'*16 + b'D'*8 + p64(0x400784)
Level 13: Use-after-free
根据代码逻辑可看出,输入的第二个参数决定了执行 switch 几次以及执行那一个选项,0是 malloc,mappingstr就指向堆块;1是 free;2 是函数指针执行;3是 malloc,且malloc 申请地址后存储的值就是 command。
很明显这是一个Use After Free,通过0创建一个堆块,mappingstr不为空此时再 free mappingstr,再malloc一个相同大小的堆块,就可以申请到 mappingstr 的堆块,复制 command 到堆块后,再通过函数指针执行。level13password函数地址是 0x4008c4。
因此 payload 为:payload = b'a' * 64 + p64(0x4008c4)
执行参数为 0312:
Level 14: Jump oriented programming (JOP)
与ROP不同,JOP 是使用程序间接接跳转和间接调用指令来改变程序的控制流,当程序在执行间接跳转或者是间接调用指令时,程序将从指定寄存器中获得其跳转的目的地址,由于这些跳转目的地址被保存在寄存器中,而攻击者又能通过修改栈中的内容来修改寄存器内容,这使得程序中间接跳转和间接调用的目的地址能被攻击者篡改,从而劫持了程序的执行流。
关键点在两个 fread 上,第一个 fread 将文件中的4个字节读到 v2 中,第二个read 又将 v2 作为读取字节数,从 v5 中读取到 v3,v3 分配在栈空间上,因此这里会出现栈溢出。
看汇编会更明显:
通过 gdb 测试,得到偏移为 52。
往一个文件里写入 data = b'A'*52 + p64(0x400898)
执行:
3.2 CVE 案例
CVE-2018-5767
具体分析过程可见:CVE-2018-5767
漏洞触发点在:
此处存在一个未经验证的 sscanf,它会将 v40 中存储的值按照 "%*[^=]=%[^;];*"
格式化输入到 v33,但是并未验证字符串长度,因此此处会产生一个栈溢出。
PoC 如下:
1 |
|
通过远程调试可观察到,在 PoC 执行后: 由于栈溢出,PC 寄存器被覆盖,导致程序执行到错误的内存地址而报错。偏移量为444。
0x04 总结
ARM的分析与x86类似,只是在函数调用、寄存器等方面存在差异,相对来说保护机制也较弱,且应用场景更为广泛,如IOT设备、车联网等,如果能顺利完成固件提取,可玩性相对较高。