House of pig复现

House of pig 复现

鸽了n久的复现,主要是学习下现在libc的利用思路
https://www.anquanke.com/post/id/242640 House of pig的原文
House of pig有两个前置的攻击需求:largebin attack和tcache_stashing_unlink_attack

glibc源码调试

因为House of pig相对来说涉及的利用方式比较复杂,因此最好设置下源码调试来看
https://xuanxuanblingbling.github.io/ctf/tools/2020/03/20/gdb/
按上面这个博客做的话,因为原本libc不是debug版本编译的,所以调试的时候还是可能跳行,因为代码经过优化了,要想不跳行,还是得编译个Debug版本的libc
https://warm-winter.github.io/2021/02/18/%E7%BC%96%E8%AF%91debug%E7%89%88%E6%9C%AC%E7%9A%84glibc/

build
1
2
3
4
cd build
../configure --prefix=/usr/local/glibc-2.27 --enable-debug=yes
make -j4
sudo make install

另外编译的时候最好是你要在哪替换libc就在哪编译,因为libc的编译可能会涉及到系统适配
比如要是在docker中调代码,就在docker里面编译
编译完成后,最简单的替换libc的方法
最简单的换libc的方法就是

1
2
3
root@ctf:/ctf/work# cp /usr/lib/x86_64-linux-gnu/libc-2.31.so /usr/lib/x86_64-linux-gnu/libc-2.31.so.bak

root@ctf:/ctf/work# cp /ctf/work/glibc-release-2.31-master/build/libc.so /usr/lib/x86_64-linux-gnu/libc-2.31.so

这样两句话,别的都或多或少有点问题,因为必须保证当前有个能用的libc,要不然系统会崩掉

这样替换完成后,bash和tmux都会报一个语言错误
解决方案参考这里
https://blog.csdn.net/guitar___/article/details/77651983
是因为新编译的libc里面缺少locale

直接将/usr/bin/locale这个locale命令使用的locale-archive文件copy到/opt/glibc-2.14/lib/locale/locale-archive即可

Larginbin Attack

https://www.anquanke.com/post/id/189848
先回顾下unsortedbin Attack
https://github.com/shellphish/how2heap/blob/master/glibc_2.27/unsorted_bin_attack.c
Unsortedbin是FIFO先进先出,因此每次从中取出chunk的时候都是从链表尾部去取,其attack原理如下

以howtoheap中的代码为例

就是先free,然后修改其bk为想要修改的地址 - 2size_t,也就是把目标地址视为一个假chunk,那么其+2size_t的位置就正好是fd,下次malloc时,unsorted断链走到上面的流程,就会在bck->fd写入unsortedbin这一个大整数

在libc2.29版本之后,unsortedbin attack被限制了,所以得用largebin attack来写入大整数
Largebin的构造图

上图是large bin的内部结构,large bin与其他bin的主要区别在于它使用fd_nextsize指针和bk_nextsize指针链接大小不同的链表,每条链表中大小相同的chunk用fd指针和bk指针相连,large bin使用这种结构可以更好的整理内存空间。
而large bin中每个bin中存储的chunk size大小不固定,每个bin中链接了不同的链表(如上图的chunk1-1和chunk1-2是一个链表),每个链表中的chunk size大小相同,取出chunk时为了能够循环查找合适的chunk需要使用fd_nextsize和bk_nextsize循环bin中的不同链表,fd_nextsize指向比自己小的链表中最大的链表的链头,bk_nextsize指向比自己大的链表中最小的链表的链头,fd_nextsize和bk_nextsize在每条链表的链头才有用,因为链表中的chunk大小固定使用fd指针和bk指针链接,large bin使用FIFO分配策略,将先进入的chunk设置为链头,后面插入chunk在链头的后面,取出时取出链头的下一个chunk。
在2.29以下的版本中,根据插入chunk size不同,会有不同的流程:

小于链表中的最小chunk就是插入到链表尾,否则就会插入到链头或者链表中间
attack方法以howtoheap为例
https://github.com/shellphish/how2heap/blob/master/glibc_2.31/large_bin_attack.c

这里先申请一个large chunk,然后分配一个小的chunk防止topchunk合并

分配第二个large chunk,这个chunk应该小于上一个large chunk,但是应该在一个bin里面

释放掉第一个largebin chunk,此时其应该会先放进unsorted bin里面
所以这里先malloc一个比第一个chunk还大的chunk,这样就会使得unsortedbin里的largechunk进到自己的largebin里面

然后free p2

此时这个p2也是会先放到unsortedbin里面

修改p1的bk_nextsize到target - 4,最终

再次申请一个最大的chunk,让p2插入到largebin,触发插入代码

因为前面已经修改p1->nextsize为target上的fake chunk了
这里倒数第二句victim->bk_nextsize = fwd->fd->bk_nextsize就把victim->bk_nextsize设置成fake chunk了
最终在最后一句,victim->bk_nextsize->fd_nextsize = victim就成功写入target数值了

https://www.anquanke.com/post/id/198173#h2-0
回顾下前身house of Lore

其涉及到的代码是malloc里面的申请small的chunk的时候,这里victim是倒数第一个chunk,也就是准备被分配出去的chunk,bck=victim->bk,bck就等于倒数第二个chunk ,后面就是取出victim,如果这里我们能修改其bk,让其bk指向我们的target,但是后面必须得保证bck->fd ==victim,也就是必须得有一定的target的修改能力,要不然过不了中间的check,都修改完之后bin->bk = bck就会使得我们的target被插入到bin里面,进而就可以在任意地址分配出一个chunk

引入tcache之后

以how to heap中的代码为例
https://github.com/shellphish/how2heap/blob/master/glibc_2.31/tcache_stashing_unlink_attack.c
这里stack_var就是我们的目标fakechunk

先给stack_var[3]也就是fakechunk->bk 赋值一个可以写的地址,后面要用到这个特性

申请9个smallchunk,释放掉其中的3-8,等于释放了其中的6个chunk到Tcache里
再释放chunk1,也会进入tcache里面,此时tcache就满了

再次释放chunk0和chunk2,这俩就会放进unsortedbin里面
分配一个稍微大点的chunk,chunk0和chunk2就被重新分配到smallbin里面

从tcache里面取出两个,此时tcache里面只有5个块

修改chunk2的bk为stack_var chunk2为chunk0的bck,victim是chunk0

calloc跳过tcache直接申请smallbin,触发攻击

整个攻击涉及到的代码在这,在开启了tcache的情况下,会先取出smallbin中的一个chunk,将其断链,准备分配出去,然后如果tcache还没满的情况下,就把当前smallbin中剩下的chunk全部填到tcache中去,在howtoheap的流程中就是取出chunk0分配出去,然后准备把chunk2放到tcache中去,


第一次:
tc_victim=last(bin) = chunk2
bck = tc_victim->bk = stack_var
bin->bk = bck = stack_var
bck->fd = bin ==> stack_var[2] = bin
tcache_put(chunk2)
第二次
tc_victim = last(bin) = stack_var
bck = tc_victim->bk = stack_var[3]
bin->bk = bck = stack_var[3]
bck->fd = bin ==> stack_var[3]->fd = bin
因为前面设置了stack_var[3] 为&stack_var[2]了,所以这里会把bin的值写到stack_var[4],这里就回答了前面为什么要设置成一个可写的地址的原因
tcache_put(stack_var),成功填入一个fake chunk
之后不会有第三次了,因为tcahce已经满了!!(原先我还笨比的分析第三次循环)

IO_FILE相关

House of pig主要修改和利用的地方涉及到_IO_FILE相关的知识,因此这里也列一下,主要参考的是angelboy的PPT

_IO_List_all是libc里面的全局变量,其里面串着全部的_IO_FILE结构体
最前面的三个是stdin,stdout,stderr

当程序退出时:glibc abort,exit,main return
会调用_IO_Flush_all_lockp
_IO_Flush_all_lockp会对IO_list_all上面的所有IO_FILE都调用_IO_OVERLOW ,_IO_OVERFLOW是每个_IO_FILE的跳表里的函数,我们可以修改它,让它执行我们想让它执行的Overflow函数


Houseofpig就是修改_IO_OVERFLOW取执行IO_str_overflow函数,这个函数会连续执行malloc、memcpy、free等函数,并且三个函数的参数都可以由FILE结构体内的数据来控制,而FILE结构我们也可以伪造。

程序分析


菜单堆题,主函数显示不全,一开始我以为是花指令,后来看了别人的文章,知道这里是个跳表

跳表修复(Failed)

IDA EDIT里面有跳表创建功能

只不过我这里总是创建失败,现在还没搞明白什么原因,不过程序也不是很复杂,不耽误分析

程序功能

看菜单及逆向分析,程序一共有5各功能,增删改查及切换角色,这里一共有三个角色

切换角色这里需要输入密码,其算法是通过md5比较,跟ctf里面防ddos的工作量证明是一样的

我这里懒省事就直接抄了wp上的密码用了,反正主要也是调wp么

增的逻辑,三个角色Peppy Dad 和Mother都有自己的add函数,不过都差不多

比较关键的点是(以Peppy为例),只能申请20个chunk,同时分配的chunk只能越来越大

申请出来的chunk和size分开存放,并且三个角色分配出来的chunk指针虽然都是放到同一个chunkmap中,但是偏移不同
另外chunk的内容是按0x30为分割(三种角色都是这样),每次放16个字节的内容
三个角色分别是放前中后三个位置的16字节

查的逻辑

改的逻辑

这里也是按30个字节分割,三个角色分别只编辑前中后三个16字节

删的逻辑

删这里直接用的free,指针并没有清空,因此可能存在UAF

切换角色时,会调用切换chunkmap的函数



对比可以看到 这个函数就是在切换角色前保存当前的chunkmap
然后切换到对应角色的chunkmap

漏洞点

在chunkmap中有两个flag区域用来判断chunk是否已经delete了,一个是偏移0x120处,被edit使用,一个是偏移0x138处,而在每次切换role的时候,只保存了0x138,因此就可以通过切换role来触发UAF。

EXP分析

泄露地址


直接分配一个largebin chunk到largebin里面,然后free掉,通过fd和bk就能泄露libc,通过fd_nextsize和bk_nextsize可以泄露heapbase,

largebin attack


打完一次largebin attack,堆已经被破坏了,直接给fd和bk覆盖成指向bin头,这样能直接恢复到只有一个chunk的状态
第一次Largebin attack是在free_hook-0x8的地方写入了一个堆地址,假设已经在largebin中的chunk称为p1,新插入的chunk为p2,如果按原先largebin attack的分析
free_hook - 0x8的位置应该写入的是p2的地址,但是实际调试会发现此处写入的p1的地址

1
victim->bk_nextsize->fd_nextsize = victim,

victim显然是p2的地址,所以这里是错了吗,实际调试会发现,这里largebinattack走的根本不是上面走的流程
触发Largebin Attack用的是

因为bins里面已经没有0xa0大小的块了。所以要分配出来这个0xa0的chunk,就得切割后面刚插入的p2,代码如下

取出p2作为victim,然后调用unlink_chunk将其断链,在unlink_chunk中会走到这一段

p->bk_nextsize就是fakechunk,因此这里又将其fd_nextsize 赋值成了fd_nextsize也就是p1,这里就真相大白了,切割出来的chunk一个分配出去,一个会放到unsortedbin里面

第二次largebinattack是类似的,是向_IO_list_all处也写入了p1的值


这次attack的目的是把free_hook-0x20 这个fake_chunk打到tcache上,后面直接分配到

_IO_FILE_attack

在IDA里面,_IO_str_overflow这个函数在这里

查找对这个函数的引用

我们用的就是这个表_IO_str_jumps作伪造

进入这个函数仔细看下流程

malloc申请的大小是 2(fp->_IO_buf_end - fp->_IO_buf_base ) + 0x64
我们想分配到0xA0

这里(B8-A0)
2 + 0x64 = 0x94 ,是满足A0的,然后

memcpy是吧old_buf拷贝到刚才malloc的地方,malloc的就是free_hook-0x10(free_hook-0x20+0x10)
old_buf是_IO_buf_base
我们伪造的_IO_FILE

我们需要伪造另一个_IO_FILE,并将其chain指向上图我们伪造的正式的payload IO_FILE
为什么不直接用一个_IO_FILE去打,因为我们直接edit的chunk不能连续写,还记得前面三个角色每隔0x30字节写16字节吗,
上面填的0x148A0应该是我们触发gift后门,可以连续写入chunk的地址

触发流程

在 _IO_flush_all_lockp上下断

首先会到我们伪造的中转_IO_FILE
第一个fp->_IO_write_ptr > fp->_IO_write_base 失败
第二个fp->_mode > 0 失败,所以第一个FILE不会调用到_IO_OVERFLOW
到了正式的payload IO_FILE
因为我们设置了_IO_write_ptr是0xffffffff 其肯定大于_IO_write_base因此直接就满足了这里或的条件会调用到_IO_OVERFLOW,然后就是malloc到free_hook-0x10
memcpy把system写入到free_hook,调用free触发system来getshell

总结

现在的堆是真的卷

参考wp

https://bbs.pediy.com/thread-268245-1.htm#msg_header_h2_1
https://a1ex.online/2021/06/30/2021-XCTF-final%E9%A2%98%E8%A7%A3/
https://www.anquanke.com/post/id/242640