Fastbin浅析——2015RCTF的沙县小吃
emmm这一篇既是开始,也是一个小小的总结。
Q1:为什么是ptmalloc呢?
A:内存的分配释放都很频繁,ptmalloc使用缓存来提高性能:(引用pwn之堆内存管理)
1. Bins
为了避免每次触发系统调用, 首先想到的解决方法就是释放的内存暂时不归还给系统, 标记为空闲, 等下一次再需要相同大小时, 直接使用这块空闲内存即可。(存储结构是双向环链表)之前的unlink就是在bins里面捣鼓的。
2. Top
另一个应该想到的就是, 可以先利用系统调用 brk() 分配一块比较大的内存作为缓存, 之后即使没有在 Bins 中也找不到, 也不需要每次触发系统调用, 直接切割这块大的内存即可.
3. Fastbins
Bins 和 Top 缓存是最基本的, 如果想要做进一步的优化, 其实就是更细分的缓存, 也就是更准确的命中缓存, 这里 Fastbins 存在的更具体的原因是 避免 chunk 重复切割合并。
Ok, 再回到 Fastbins 的讨论, 对于长度很小的 chunk 在释放后不会放到 Bins, 也不会标记为空闲, 这就避免了合并, 下次分配内存时首先查找 Fastbins, 这就避免了切割。
4. Unsorted bin
Unsorted 是更细粒度的缓存, 属于 '刚刚释放的内存'与 Bins 之间的缓存.
在 1. Bins 中涉及一个问题, 刚刚释放的内存什么时候加到 Bins ? 这其实就与 Unsorted 有关, 刚刚释放的内存会先放到 Unsorted 缓存, 在下一次内存分配时, 会优先于 Bins 查找, 如果能命中 Unsorted 缓冲最好, 否则:
1)若申请的大小大于unsorted bin中堆块的大小,则把unsorted bin中的堆块放入bins,并再去bins中寻找;
2)若申请的大小小于unsorted bin中堆块的大小,则从该堆块中切割出对应大小分配给用户,剩余部分仍然放在unsorted bin中(用main_arena泄露libc基址就和unsorted bin的机制有关)
0x00 About Fastbin
fastbin所包含chunk的大小为16 Bytes, 24 Bytes, 32 Bytes, … , 80 Bytes。当分配一块较小的内存(mem<=64 Bytes)时,会首先检查对应大小的fastbin中是否包含未被使用的chunk,如果存在则直接将其从fastbin中移除并返回;否则通过其他方式(剪切top chunk)得到一块符合大小要求的chunk并返回。
而当free一块chunk时,也会首先检查其大小是否落在fastbin的范围中。如果是,则将其插入对应的bin中。顾名思义,fastbin为了快速分配回收这些较小size的chunk,并没对之前提到的bk进行操作,即仅仅通过fd组成了单链表而非双向链表,而且其遵循后进先出(LIFO)的原则。
相比在bins中的双链表结构,fastbin要简单些,仅使用单链表结构,即只用到offset + 8 的fd指针,bk指针置NULL
buff0 = malloc(malloc_size)
buff1 = malloc(malloc_size)
buff2 = malloc(malloc_size)
free(buff0)
free(buff1)
free(buff2)
此后fastbin的链表结构大致如下:
fastbin中只记录最近一次释放掉的堆块地址(即链表的尾项)。如果再使用malloc申请malloc_size大小的堆块,则会先在fastbin中查询是否有符合条件的表项,若有,还要进一步检查目标堆块的大小(size位),然后把该堆块作为malloc的返回值返回给用户,并将该堆块的fd项填回fastbin。
0x01 Exploit Fastbin
考虑这样的分配释放次序:
1. buf0=malloc(32)
2. buf1=malloc(32)
3. free(buf1)
4. free(buf0)
5. buf0=malloc(32)
6. read(buf0) //overflow to next chunk
7. buf1=malloc(32)
8. buf2=malloc(32)
9. read(buf2)
第四行执行完后的状态:
第五行:malloc(32)的时候在fastbin (size=0x28)中查找,找到了chunk0的地址,验证chunk0的size位也ok,则把chunk0从fastbin中拿出作为返回值赋值给buf0,并将chunk0的fd填入fastbin中。(malloc返回chunk+8,后文不再特别说明)
第六行向buff0中写入数据,假定输入足够长可以覆盖到其后的chunk1(chunk1.presize=buff0+0x28 ; chunk1.size=buff0+0x30 ; chunk1.fd=buff0+0x38)
第七行malloc仍然先在fastbin中查找,检查chunk1的size位后,将chunk1作为malloc的返回值,并将chunk1的fd填入fastbin对应位置。
第八行malloc仍然现在fastbin中查找,检查AAAA指向的堆块的结构后,将该堆块返回,即返回AAAA+8。
显然,如果我们将AAAA替换为any_address-8,并在any_address-8处伪造一个presize=0,size=0x29的堆块,则将能够控制malloc的返回值(malloc将返回any_address),结合后面的read将实现任意地址写。
同时注意到从fastbin中取出any_address-8后,会继续将该堆块的fd指针填入fastbin,如果我们将fake chunk伪造为:pre_size=0,size=0x29,fd=any_address-8,那么之后的每一次malloc都会返回固定的值any_address!
参考:浅析Linux堆溢出之fastbin --FreeBuf
0x02 2015 RCTF shaxian
基本的程序逆向分析就不多说。
思路:
1.程序本身也使用了单向链表结构来存储菜单(这个是真真儿的菜单啊),并在bss段用四字节的指针记录链表的尾项(记为ptr),无形中呼应fastbin。每个菜单项malloc(0x28)的堆块使用,菜单项结构体大致如下:
offset+0 num #how many?
offset+4 buff #what you want?
offset+36 fd #point to front node
向buff中读60个字节,显然可以覆盖到fd指针,首先想到的就是结合review打印来实现任意地址读,从而泄露libc。
尝试了下泄露puts、atoi之类的地址没问题,然而扔进libc database一搜,没有?这可是本地啊,没办法,上DynELF,接着问题就出来了,函数review进行一轮打印后,会把fd指针填入ptr再开始新一轮打印,显然很容易发生访问无效地址导致程序崩溃,此路不通,打出GG。(由于pwndbg对x86似乎不太支持,无法正确显示fastbin堆结构,干脆拿x64来做,这下泄露出puts或者atoi,libc db是可以查出来了,但是不甘心啊,而且这还只是在本地,必须想个办法实现无限次任意地址读。
2.再看submit也有打印的功能,流程相比review无非多了一个free的操作,如果用fastbin把free的got表中填入main函数开始的地址,也就是说每次打印的时候只打印一次(即ptr -- > buff和ptr -- > num),然后强行跳转到main函数开始的地方,不就防止程序崩溃了吗?再用fastbin把ptr改成任意值,应该就能实现DynELF要的任意地址读了。
3.然后就是fastbin的套路。考虑程序的流程,先buff=malloc(40),然后往buff+8里读,最后再往[ptr]中读入一个数字。关于堆的题目一般都是把用来索引的目录地址改了,然后为所欲为所欲为所欲为,这里也差不多,所以我们想要malloc返回的地址就瞄准了ptr:0x804b1c0,更好的方案是再前面一点,例如0x804b1b0,这样可以在输入buff的时候覆盖掉ptr的值,然后读入数字的时候任意地址写。
首先是输入住址和电话,然后开始点菜!注意到ptr(0x804b1c0)的前面就是我们输入的电话,可以在其中布置伪造的堆块:
init('AAAA','B'*240+p32(0)+p32(0x31)+p32(0x804b1b0))
住址没什么用,随便填上AAAA就行,电话加入适当填充,然后在0x804b1b0的位置伪造堆块。
先点两个小菜,然后释放掉:
diancai('CCCC','1')
diancai('DDDD','2')
submit()
紧接着连申请带输入顺便覆盖了同在fastbin里面隔壁的兄弟。
diancai('E'*36+p32(0)+p32(0x31)+p32(0x804b1b0),'3')
diancai('FFFF','4')
diancai('AAAA'+p32(e.got['free']),str(0x8048b55))
到这一步可以gdb attach一下,断在malloc的位置,如果返回值是0x804b1b8那就成了。然后先把free给弄掉。
4.记得吧前面还有3个序号呢,4不是突兀的出现的。程序会问你how many,读取输入然后用atoi得到数值,之前对atoi一知半解,直接往里输str(num),这一步num=0x48048b55是没问题,后面输num>0x7fffffff的就会出问题,不是atoi本身对范围有限制,而是atoi读取的是signed int,大于0x7ffffff的数字必须用负数输进去。python里无符号转有符号的姿势是用ctypes:
import ctypes
signed_addr = ctypes.c_int32(unsigned_addr).value
unsigned_addr = ctypes.c_uint32(signed_addr).value
然后就是leak函数:
输入buff的时候把ptr修改为0x804b1c0,然后用num的数值再去修改,实现任意地址读。
最后一步,既然已经拿到system地址,水到渠成上fastbin:
diancai('AAAA'+p32(e.got['atoi']),str(ctypes.c_int32(system_addr).value))
然后等到提示choose:的时候输入“/bin/sh”,atoi("/bin/sh")就相当于system("/bin/sh")
getshell!