pwnable.tw->Tcache Tear
前言
花了两天,libc的泄露一直没想到思路,遂看了别人的writeup。终于在中午两点的时候成功了,纪念差点儿低血糖晕倒的清明假期。
程序逻辑
函数主要有三个功能,Malloc、Free和Info。Malloc里可以malloc一个大小小于0xff的堆块,注意这里的get_input函数获取的输入大小为size-16,如果size为小于16的正数,得到的结果被转换为无符号整数参数就会产生堆溢出。此外这里存储malloc_chunk的地方只有一个,每当Malloc被调用,0x602088就会写入malloc_chunk_addr。
get_input函数没有什么特别的,这里注意一下使用的是__read_chk进行读取,这个函数跟read不一样的地方在于其参数有一个buf,用来标识缓存区大小,避免溢出。
free部分一是没有检验ptr是不是为释放过的指针,造成double free,二是没有将ptr的内容置为空,存在被泄露的风险。此外这里限制了free的次数,最多可以free 8次,目前看来也没什么好的办法把这个栈上的free_num修改掉
漏洞利用
这里最重要的两个漏洞就是double free和堆溢出。如果是Glibc-2.23可以构造unlink或者double free。根据Libc.so的版本2.27(给执行权限直接执行即可看到版本)以及题目名,可以想到这里主要考察的是tcache漏洞的利用。关于tcache,p4nda师傅有一篇非常详尽的分析
p4nda
其利用方法也比较多,这里由于有double free,我们可以通过tcache_dup去做,fastbin的检查机制是刚释放的堆块和之前的不同
而tcache没有类似的检查,直接释放即可。下面是具体的漏洞利用
泄露Libc地址
Malloc(0x60)一个堆块,Free()之后它被放入tcache_entry里,此时继续Free(),tcache_entry[i]有两个成员,均为第一个chunk的地址,此时Malloc(0x60,p64(addr))会将tcache_entry[i]的chunk分配出来,且fd指向的addr也进入了这个tcache_entry[i],调用一次Malloc(0x60)得到第一个堆块,再调用一次Malloc(0x60)即可分配得到目标地址所在的堆块(注意在entry中存储的地址是chunk_addr+0x10,因此addr填的应当是希望分配到的chunk的地址+0x10)
根据上述原理,我们基本思路是在Name所在的0x20区域内构造一个unsortebin(其size>0x408),Free(unsoretd_bin)会使得其fd和bk通过Info泄露出来,fd = main_arena_add + 96,即可反推出libc_base。这里我们先Malloc(0x60)到name附近的一个fake_chunk,这个fake_chunk的地址是0x60203d(通过pwndbg的find_fake_fast)找到,之后在其中构造另一个fake chunk,其结构如下:
0x602050:prev_size,chunk_size(0x421)
0x602060:paddings
0x602070:paddings
0x602080:paddings,p64(0x602050)
如此,Free()的时候即可往0x602060处写入main_arena+96。但是注意这里__libc_free的时候对于chunk有检查,很重要的一点是这个chunk的next_chunk的prev_in_use位要置为1,因此在构造这个chunk之前,我们要先在0x602050+0x420处构造另一个fake_next_chunk以逃避检查(bss段页对齐因此肯定不会越界),这个chunk的结构如下:
0x602470:0,size(0x21)
0x602480:padding
0x602090:0x20,0x21
注意后面Next_chunk的后面也不能省略,因为free的时候会看这个chunk来确定next_chunk能不能unlink。
按照上述操作先后malloc(next_fake_chunk)和malloc(fake_unsorted_chunk)再Info()可以获得Libc_base
get shell
依然是相似的方法,用One_gadgets覆盖掉free_hook或者malloc_hook,这里我开始用malloc_hook发现one_gadgets的三个gadget条件均不满足,换成free_hook即可成功拿到shell
待解疑惑
第一次double free后通过Malloc(0x60)成功写入next_chunk,之后准备第二次double free的时候如果还是Malloc(0x60)再Free(),会发现此时的chunk没有进入tcache_entry,而是进了fastbin,这直接导致double free失败,若是Malloc(other_size_but_0x60)即可重新进tcache_entry,看了会代码也没想明白,打算明天问下p4nda学长
后记
调试了很久终于破案了。是这样的,malloc的时候会先执行tcache_get()函数,这函数是这样的。借用ctf-wiki的图,在堆初始化的时候会分配一块内存用来存储这样一个数据结构,本地调试的地址为0x603000,那么0x603010+0x5代表的就是counts[5]即0x60大小的堆块在tcache中的个数。问题就出在这个地方,在double free又malloc的时候,counts[5]已经被置为0,而malloc的时候并不会检查这个地方是不是0,直接-1变成了0xff,而tcache_put()之前会比较counts[5]和0x7,由于都是无符号整数,这个if进不去导致堆管理认为tcache已经满了,因此把它放进了fastbin,造成了后来double free的失败
ps:我调试的时候是比对着正常malloc、free过程看的汇编,p4nda师傅给了一个directory命令用来添加libc用来看C,感觉解锁了新世界大门2333
1 | /* Caller must ensure that we know tc_idx is valid and there's |
exp.py
1 | #coding=utf-8 |