-
-
[原创]HITB CTF 2018 gundam分析
-
2021-8-22 23:18 16204
-
这道题主要考察tcache poisoning技术(修改tcache 中chunk的next指针),涉及到内存地址泄露、double free等技术,题目难度不大,但是对于入门选手来说,要跨过各种坑,成功利用漏洞获得系统控制权也不是简单容易的事。
一、gundam结构分析
二、内存泄露
三、双重释放漏洞(double free)
四、总结遇到的各种坑
一、gundam结构分析
(一) 基本结构:
通过逆向分析,可以知道,gundam结构如下:
1 2 3 4 5 6 | struct gundam{ uint32_t flag; char * name; char type [ 24 ]; }gundam; struct gundam * factory[ 9 ] |
包含一个结构体,命名为gundam,一个指向该结构体的指针factory。实际上在建立gundam的过程中,有两次malloc过程:
1 2 | s = malloc( 0x28 ) #1 buf = 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中的关键函数
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 ; } |
该函数在读入用户输入的gundam name时,没有对字符串末尾进行处理(加上'\x00')。
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 ) / / 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指针置空。
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_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漏洞。
二、内存泄露
由前面分析得知,由于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地址。
三、双重释放漏洞
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:
四、总结遇到的各种坑
调试过程遇到不少问题,首先是如何在本地系统上调试针对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时,关键地址被泄露。
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。