查看原文
其他

HITB CTF 2018 gundam分析

dolphindiv 看雪学苑 2022-07-01

本文为看雪论坛优秀文章
看雪论坛作者ID:dolphindiv

这道题主要考察tcache poisoning技术(修改tcache 中chunk的next指针),涉及到内存地址泄露、double free等技术,题目难度不大,但是对于入门选手来说,要跨过各种坑,成功利用漏洞获得系统控制权也不是简单容易的事。

1


gundam结构分析


(一) 基本结构:

通过逆向分析,可以知道,gundam结构如下:
struct gundam{ uint32_t flag; char *name; char type[24];}gundam;struct gundam *factory[9]

包含一个结构体,命名为gundam,一个指向该结构体的指针factory。实际上在建立gundam的过程中,有两次malloc过程:
s = malloc(0x28) #1buf = malloc(0x100uLL); #2

语句#1申请0x28字节的堆内存,用于创建结构体gundam(factory指针指向该结构体)。

语句#2申请0x100uLL字节的内存,用于保存gundam的name,返回地址buf指向该chunk的用户数据部分

使用命令gdb gundam启动pwndbg,创建8个gundam之后,使用heap命令查看chunk的状态如下:

每个gundam包含两个chunk,一个大小为0x31的factory,另一个大小为0x111的name。

创建8个gundam,再释放,可以看到前7个进入tcache,最后一个进入unsortedbin:

(二)gundam中的关键函数

__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;}

该函数在读入用户输入的gundam name时,没有对字符串末尾进行处理(加上'\x00')。
__int64 sub_D32(){ unsigned int v1; // [rsp+4h] [rbp-Ch] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h] v2 = __readfsqword(0x28u); if ( dword_20208C ) //gundam数目 { 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;}

destroy函数删除gundam,首先将gundam->flag置0,然后释放gundam->name。而在释放name后,并没有将name指针置空。
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_2020A0[i]&&!*(_DWORD *)qword_2020A0[i] ) { free((void *)qword_2020A0[i]); qword_2020A0[i] = 0LL; --dword_20208C; } } puts("Done!"); return __readfsqword(0x28u) ^ v2;}

blow_up函数将已经flag置0的factroy释放,并将factroy指针置空,减少gundam的数量,但是仍然没有将gundam->name指针置空,存在double free漏洞。
 

2


内存泄露


由前面分析得知,由于tcache机制本身的限制,当释放的chunk数量超过7个,会充满tcache bin ,超出的chunk根据大小放到unsorted bin。当chunk被free放入tcache bin时,会在该chunk对应的用户数据区(fd区域)写入tcache_entry, 指向下一个位于tcache bin 中的chunk的用户数据,从而构成单链表。

前面分析,函数构造gundam时,对于用户的输入字符串没有进行处理,即末尾增加截断字符“\x00”,而申请的堆空间有0x100字节,没有初始化,导致存在泄露信息的可能。

通过gdb.attach()动态跟进调试gundam,在构造8个gundam处设置断点,前7个的name 是‘AAAAAAA’,最后一个的name是'BBBBBBB',查看最后一个gundam->name的内存,可以看到0x7fe02bafbc78这个地址,而该地址是main_arena+88地址:
我们知道,tcache结构位于heap的最前端,也是一个堆块,其中包含数组entries,用于放置64个bins的地址,数组counts存放每个bins中chunk的数量。

通过vmmap命令,可以看到heap的开始位置为0x55e22cd98000:
使用x/26gx 0x55e22cd98000+0x10继续查看该地址处的值:
可以看到,tcache中已经存放了7个chunk,所以第8个chunk放入unsorted bin。

认真观察可以发现,地址0x55e22cd980c8处的指针0x000055e22cd98a10正好就是tcache bin头结点指向的第一个chunk,也就是最后被释放加入tcache的chunk(第7个chunk):
我们知道,一个gundam包含两个chunk,大小为0x30,另一个为0x110。我们由此出发,寻找第8个chunk。

通过连续申请创建8个gundam,第7个gundam的地址加上2个chunk的大小,就是第8个chunk的地址。

使用x/26gx 0x000055e22cd98a10+0x30+0x110-0x10,可以看到第8个chunk的地址为0x55e22cd98b40,其bk和fd指针都指向0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址):
思考为什么会泄露地址0x7f566befac78(main_arena+88)?因为在反复的gundam创建和释放过程中,伴随着0x55e22cd98b40+0x10(基址会变,偏移不变)处数据的变化。

在创建第8个chunk时,地址0x55e22cd98b40+0x10(基址会变,偏移不变)存放gundam->name,由于我们输入name字符串为'BBBBBBB',只有7个字节(加上末尾的0x0a正好8个字节),0x55e22cd98b40+0x18后面内存地址处的值没有专门处理。

在释放第8个chunk时,通过前面的分析可以知道第8个chunk的地址0x55e22cd98b40处,其bk和fd指针都指向0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)。

而再次创建第8个chunk时,由于gundam构造函数没有对用户输入的字符串末尾进行处理,也没有对其申请的chunk进行过初始化,导致上次释放时写入的bk指针的值0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)仍然存在。

利用者就可以通过查找'BBBBBBB'字符串进而找到泄露的地址0x7f566befac78(unsortedbin头结点,是main_arena+88,也是泄露的地址)。前提是,必须至少有两次创建和释放8个以上gundam的过程,才能使第八个chunk地址的偏移0x18处存在泄露地址。

知道了泄露地址,我们再查找libc基地址,紧挨着heap的libc-2.26.so对应的地址0x7f566bb4f000就是libc基地址:
 泄露地址0x7f566befac78-libc基地址0x7f566bb4f000=偏移0x3ac78是一个固定值由于每次运行时程序加载到地址空间的基址会随机变化,可以用泄露地址减去该偏移得到对应的libc的基地址,进而计算出free_hook_addr和system_addr地址。
 

3


双重释放漏洞


libc-2.26没有对tcache 中的double free进行安全性检查,直到libc-2.28,才增加了对tcache 中的double free的安全性检查。

前面我们在分析gundam的关键函数时知道,destroy函数和blow_up函数都没有将gundam->name置空,存在着双重释放的漏洞。

对于前面我们创建的8个gundam,我们先依次destroy序号为2、1、0的gundam,可以看到有3个空闲的chunk进入tcache:

使用heap命令观察chunk状态:


0x5627336a1a00地址处chunk为0号gundam的name对应的chunk,最后进入tcache,位于tcache指向的第一个位置。此时factroy还没有释放,大小为0x31的chunk还没有free。

再次destroy序号为0的gundam,可以看到:


此时,tcache中只有一个chunk,且该chunk的fd指针指向自身。为什么重复destroy了0号gundam,反而造成tcache中空闲chunk减少2个?

如上图所示,1表示destroy3个gundam后,tcache bin的状态。
当free一个chunk时,该chunk的fd指针会指向tcache bin中的第一个chunk,然后tcache bin的头结点指向刚free掉的chunk,依次类推,保证tcache bin的头结点指向的chunk永远是最后freed的。

当再次destroy 0号gundam时,对应的chunk(已在tcache中)会再次被free,并修改fd指针,从而使chunk 0的fd指向自身,从而完成double free。

通过前面计算出的泄露地址与libc基地址的偏移,可以在获得泄露地址的前提
下,动态算出libc基地址的,进而计算出free_hook_addr和system_addr地址。

再次构造gundam,分别以free_hook_addr、'/bin/sh\x00'字符串和system_addr作为name参数:

1、构造第1个gundam时,参数为free_hook_addr的地址,glibc会从tcache bin 中找到空闲的chunk,此时为chunk0,tcache_get 操作会将tcache->entries[tc_idx]指向的第一个chunk返回,并使entries[tc_idx]指向下一个chunk。

由于chunk0的fd指向自身,tcache->entries[tc_idx]仍然指向chunk0,同时chunk0的fd被改写成free_hook_addr;
从上图可以看到,chunk0的fd指向_free_hook地址。

2、构造第2个gundam时,参数为'/bin/sh\x00'字符串,glibc会从tcache bin 中找到空闲的chunk,此时仍然为chunk0,tcache_get 操作会使chunk0返回,使tcache->entries[tc_idx]指向前面chunk0的fd,即free_hook_addr,同时使chunk0的fd改写为'/bin/sh\x00'字符串;
从上图可以看到,tcache->entries[tc_idx]已经指向_free_hook地址。
从上图可以看到,chunk0的fd处为'/bin/sh\x00'字符串。

3、构造第3个gundam时,参数为system_addr地址,glibc会从tcache bin 中找到空闲的chunk,因为此时tcache->entries[tc_idx]指向free_hook_addr,就在free_hook_addr处写上system_addr。



从上图可以看到,free_hook_addr处已经写上system_addr。
通过以上3步操作,成功使system_addr与free_hook绑定,再执行1个destroy操作,即可启动system。

因此,选择destroy含有'/bin/sh\x00'字符串的1号gundam,成功执行shell:



4


总结遇到的各种坑


调试过程遇到不少问题,首先是如何在本地系统上调试针对libc-2.26.so的程序gundam。

通过在网上查找资料,将libc.so.6分别作为LD_PRELOAD环境变量和ELF()的参数进行设置,根据libc.so.6在内存地址空间的基地址和泄露地址的偏移量计算出偏移,exploit脚本能够执行到最后,但是无法执行用户输入命令,输入命令就会报出segment fault。vmmap显示的内存中的链接器仍然是本地系统glibc-2.27的链接器ld-2.27.so。

同时,使用gdb直接加载gundam,在提示符下通过语句设置环境变量的方式,根本无法启动调试。最后想到是libc版本的问题,从服务器下载glibc的源码,进行编译,生成libc-2.26.so,解决了编译的问题。再就是动态跟进调试的问题。使用gdb直接加载gundam,并逐句调试的方式效率太低,最后想到在python脚本中使用gdb.attach()和pause()语句,可以动态跟进调试,效率提高不少。

最后就是为什么会出现泄漏地址的问题?而这恰恰是解决问题的关键。查了网上很多资料,只讲了可以找到该泄漏地址,但没有讲这个问题的原因,给利用造成了很多不便。

经过反复调试,弄清了造成地址泄露的原因是:两次以上构造和销毁gundam造成第8个chunk的bk处保留了原来指向unsortedbin的指针,导致再次构造gundam时,关键地址被泄露。


 


看雪ID:dolphindiv

https://bbs.pediy.com/user-home-717768.htm

*本文由看雪论坛 dolphindiv 原创,转载请注明来自看雪社区






# 往期推荐

1.CVE-2010-2883漏洞分析与复现

2. D-Link DIR-645路由器溢出分析

3.API 钩取:逆向分析之“花”

4. 极为详细:双重释放漏洞调试分析

5. 新人PWN入坑总结

6. 新人PWN堆Heap总结



公众号ID:ikanxue
官方微博:看雪安全
商务合作:wsc@kanxue.com



球分享

球点赞

球在看



点击“阅读原文”,了解更多!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存