house of pig这种利用方式来源于XCTF 2021 final中的同名题,其原题使用的是libc 2.31版本,本文就根据这道题学习一下这种漏洞利用方式。
这是一道C++ pwn,但漏洞本身与C++不同于C的特性关系不大。
一共提供了5个选项:
增,删,改,看,修改用户。我们一个一个来看。
A. 逆向分析
A.1 add message
除了修改用户之外,其他4个选项的具体操作因用户而异,一共有3个用户,peppa、mummy和daddy,对于add message操作而言,3个用户的操作基本相同,只有几个地方有差别。
上图是peppa的add message函数,peppa可以遍历0~19的索引,并添加一个大小在0x90~0x430的chunk,在mummy的add函数中只能遍历0~\9的索引,并添加大小在0x90~0x450的chunk。对于peppa和mummy,其分配的chunk大小只能一次比一次大或者本次与上一次相等,daddy则没有这个限制。对于daddy,则是0~4的索引和0x90~0x440的chunk。另外,在daddy函数中如果添加chunk的索引为4,则还可以再分配一个大小为0xE8的chunk并写入最大长度为0xE8的内容。 3个用户在add之后可以立即向新分配的chunk中写入内容,但不是chunk中任何位置都能写。对于peppa,将chunk空间以48字节为大小分组,每一组48字节空间只能写前面16字节,对于mummy则是只能写中间16字节,对于daddy只能写后面16字节。在写入后,会设置两个标志位为0。
A.2 view message
查看,没有什么好说的,3个用户可以查看的索引范围和可以add的索引范围相同。而且查看时需要有一个标志位为0。这个标志位是add中设置的两个标志位中的第一个。本题限制view的次数最多为2次。
A.3 edit message
编辑,3个用户可以查看的索引范围和可以add的索引范围相同。而且查看时需要有一个标志位为0。这个标志位和view message的标志位相同。本题限制edit的次数最多为8次。
A.4 delete message
删除,3个用户可以查看的索引范围和可以add的索引范围相同。删除后会将两个标志位置为1。
A.5 change role
修改用户。这个部分的主体部分在main函数中实现。分析一下检查函数:
该函数使用了MD5计算摘要值,如何判断?查看MD5_initialize函数可以发现,这里定义了4个MD5计算的关键魔数,因此不难判断。
在检查函数中,会对输入值与MD5摘要进行判断,判断条件:MD5摘要需要等于某个值,输入值的第一位应该是A或B或C。首字母不同,能够转换到的用户也不同。我们当然不能追求计算出来3个值使得其MD5摘要值相等,但该MD5的比较有bug:
上图就是保存的MD5值,注意到第三个字节为’\0’,但原程序中比较MD5值使用的是strcmp
函数,因此最多只会比较前面3个字节的值。计算出三个摘要的前3字节等于固定值的字符串还是可以实现的,下面是计算的代码,计算结果已经附在后面:
1 | from hashlib import md5 |
根据程序输出,我们获得了三个字符串,在转换用户时只需要输入这3个字符串就能够进行任意的用户转换操作。其中A表示peppa,B表示mummy,C表示daddy。
在检查函数通过之后,如果我们会更换用户,则会将原来用户分配的chunk复制到一个程序预先分配号的一块空间,然后将新用户的chunk以及标志位等从那一块空间中复制出来。
但是这里需要注意从mmap出来空间中复制过来的只有第二个标志位,第一个标志位并没有被复制过来。
B. 漏洞分析
本题的漏洞就在于用户的分配上。由于新用户只是复制了第二个标志位,对于某个chunk的索引而言,如果原用户的两个对应标志位均为0,而新用户的两个标志位为1,则用户转换后,两个标志位分别为0和1。注意view message和edit message检查的都是第1个标志位是否为0,对于新用户而言,这个索引原本的chunk是已经被释放的,但这样一来我们就可以再一次访问这个chunk,这就产生了UAF。
但只有一个UAF,应该如何利用本题的漏洞呢?这就需要介绍一下house of pig这种利用方式的思路了。
该攻击方式适用于 libc 2.31及以后的新版本 libc,本质上是通过 libc2.31 下的 largebin attack以及 FILE 结构利用,来配合 libc2.31 下的 tcache stashing unlink attack 进行组合利用的方法。主要适用于程序中仅有 calloc 函数来申请 chunk,而没有调用 malloc 函数的情况。
本题中,我们可以申请到在tcache保存大小范围的chunk,也可以申请到大于tcache大小的chunk,因此就完美符合这个条件。
house of pig在本题(libc 2.31版本)利用方式的本质是:想办法将__free_hook
保存到tcache中,然后使用一个伪造的_IO_FILE
结构体,并想办法将该假结构体链到_IO_list_all
(最简单的方法是直接修改_IO_list_all
的值到这里),并在glibc检测到内存分配出错时能够转到该_IO_FILE
结构体执行_IO_str_overflow
,在_IO_str_overflow
中连续进行malloc
、memcpy
、free
三个操作,通过memcpy
将system
函数地址写到__free_hook
,通过后面紧跟着的free
来getshell。
下面,我们就开始进行本题的漏洞利用。
C. 漏洞利用
首先写一下交互函数:
1 | from pwn import * |
C.1 准备tcache stashing unlink attack的堆环境
注意tcache stashing unlink attack需要有两个chunk进入small bins,如果首先进行large bin attack,将会产生一些large bin chunk和unsorted bin chunk,此时如果分配较小的chunk,这两个bins中的chunk都有可能会进行拆分,进而扰乱small bins结构,因此最好能够在一切开始之前首先准备好small bins环境,毕竟small bins中的chunk相对而言是比较稳定的,不会被拆分,只有需要分配对应大小的chunk时才可能发生变化。这种对于不同操作顺序的考虑应该是在进行多次尝试后才能最终确定的,考虑到本题中严格的限制条件,选手很有可能会因为没有使用正确的操作顺序而迟迟无法获得推进。这也提醒我们在遇到困难时可以尝试修改相对独立的操作之间的顺序,以寻找突破口。
tcache stashing unlink的堆环境要求有5个chunk位于同一个tcache bins中,同时有2个相同大小的chunk位于small bins,之后通过修改small bins中链首chunk的bk指针可以将任意地址链入到tcache。
- step 1: 使用mummy分配5个chunk并释放进入tcache。本操作使用了5个mummy的chunk,mummy剩余5个chunk可以使用。
- step 2: 使用peppa用户分配较大的chunk并释放占满tcache。
- step 3: 使用peppa用户分配相同大小的1个chunk并释放进入unsorted bin
- step 4: 使用mummy用户分配较小chunk使peppa用户的chunk被拆分,计算大小使得拆分后的free chunk大小等于tcache中chunk的大小,此时free chunk在拆分后将会进入small bins。
- step 5: 重复步骤2~4,但需要占满另外一个tcache,不能只通过占满一个tcache使两个chunk进入small bins,因此第二次执行步骤2应该填满一个存更大chunk的tcache,然后mummy对应分配的chunk也增加一些。
在上述步骤完成后,堆中应该有1个chunk进入small bins,1个chunk进入unsorted bin,5个位于tcache,这7个chunk的大小相同,进入small bins的chunk是在第二次进行步骤2时需要分配比其大的chunk时将其从unsorted bin转入small bins中的。上述步骤完成后,peppa将会使用16个chunk,mummy将会使用7个chunk。下图即为该步骤完成后的堆环境,需要进行攻击的是大小为0xA0的chunk。
C.2 获取libc地址和堆地址
既然我们需要__free_hook
的地址,就应该获得libc的基地址。这个基地址很好获得,只需要分配一个tcache装不下的chunk然后释放掉,通过UAF读取前面的16字节即可。此时这里应该保存的是unsorted bin的地址。
在步骤A执行时,可以顺便获取到libc的基地址,只需要在unsorted bin中存在chunk时通过UAF进行view操作即可。
同理,我们也可以通过UAF读取到tcache中chunk保存的堆地址。
这一步不需要另外分配其他的chunk,为下面的步骤节约出了chunk。两次view的机会也全部用完,后面将不能使用view查看,不过我们已经获得了足够的信息。
在此之后我们就要正式进行house of pig的利用。
C.3 第一次large bin attack
首先我们需要将_free_hook
周围的空间变成一个假chunk,这可以通过large bin attack轻松实现。
高版本libc的large bin attack攻击方式如上图所示(摘自本人以前的博文),我们按照这种方式进行一次攻击。考虑到large bin中的bk_nextsize
的偏移为0x20,因此需要使用mummy用户的chunk作为large bin chunk,这样可以修改到bk_nextsize
。操作思路如下:
- step 1: 在mummy用户下分配0x450大小(带头部,下同)的chunk
- step 2: 在peppa用户下分配0x440大小的chunk
- step 3: 释放mummy用户的0x450大小的chunk
- step 4: 在mummy用户下分配0x460大小的chunk,这一步可以让mummy的0x450 chunk进入large bin
- step 5: 在peppa用户下释放0x440大小的chunk,这一步就构造好了large bin attack的堆环境
- step 6: 通过UAF修改mummy用户0x450 free chunk中的
bk_nextsize
为__free_hook
附近的地址
注意这里对分配大小的控制,large bins的前面几个bins是以0x40为大小进行划分,如果分配chunk的大小就为0x450和0x440,这两个chunk可以链入到一个bin中,这是实现large bin attack的前提,如果不能链入同一个bin,就无法对bk_nextsize
进行操作。然后考虑到需要让较大chunk进入large bins,必须要能够分配一个更大的chunk,这里mummy分配0x460的chunk就可以将0x450的chunk链入到large bins。下图是第一次large bin attack之后的bins情况。
C.4 第二次large bin attack
第二次large bin attack,我们的目标是将未来的假_IO_FILE
地址写到_IO_list_all
中。上一次large bin attack中使用的large bin是可以重用的,我们将bk_nextsize
指针改到其他位置还能够再一次进行攻击。第二次large bin attack应该写的具体的堆地址应该根据堆环境进行确定,选择的偏移至关重要。为了方便起见,我们的伪造_IO_FILE
结构体应该在daddy分配索引为4的chunk时附加送给我们的一个chunk中进行构造。向_IO_list_all
中写入的是large bin chunk的地址,如果想要这里同时也指向假_IO_FILE
指针,就需要计算好chunk的分配数量,在calloc(0xE8)
时能够正好让这个chunk被拆分,这样就实现了此处可写。
- step 1: 在上一步预先多分配0x440的chunk,在这里释放
- step 2: 修改
bk_nextsize
的值为_IO_list_all-0x20
- step 3: 触发large bin attack
C.5 tcache stashing unlink attack以及构造_IO_FILE
在第一次large bin attack之后,我们将一个堆地址写到了__free_hook-10
的位置,接下来就需要通过tcache stashing unlink attack将这个地址用_IO_str_overflow
函数中的malloc
函数分配出来。
在第一步我们已经在tcache和small bins中构造好了攻击环境,下面只需要分配即可。注意这里tcache stashing unlink attack将__free_hook
所在地址转移到tcache一定是在daddy分配到最后一个chunk时才能触发,这样可以接上后续写一个0xE8的chunk伪造_IO_FILE
结构体。
这里我们看一下2.31版本libc中_IO_str_overflow
的源码:
1 | int |
重点注意从21行到36行的内容,首先是计算了_IO_FILE
结构体中缓冲区的长度len,计算方式是_IO_buf_end - _IO_buf_base
,参见下面的_IO_FILE
结构体定义:(注:伪造的实际上是_IO_FILE_complete_plus
结构体,其等于_IO_FILE
+_IO_FILE_complete
附加内容+vtable
)
1 | struct _IO_FILE |
然后使用malloc
函数申请一块空间,这块空间的大小等于len * 2 + 100
,因此我们需要计算好len
的值好让malloc
能够分配到tcache中__free_hook
附近空间。然后调用了memcpy
函数将old_buf
中的内容复制到new_buf
中,在前面所有工作都完成的情况下,这里的new_buf
就应该是__free_hook
附近的地址,而old_buf
这个地址是我们可以控制的,是我们写在假_IO_FILE
结构体中的,因此我们可以让_old_buf
指向一个写有system
函数地址的空间,然后通过memcpy
函数将其复制到__free_hook
中。
之后,调用free
函数,其参数是old_buf
这个地址,我们只需要让这个地址开头写有/bin/sh
即可执行system("/bin/sh")
。这里的内容需要进行精心设计,要控制好偏移的值,同时还需要保证写入到_IO_list_all
的堆地址就是假FILE
结构体的地址。注意到_IO_list_all
的堆地址实际上是large bin中的chunk地址,因此可以通过在最后calloc
时切割这个large bin chunk实现对该地址及后面大块空间的完全控制。
至于我们如何让程序执行_IO_str_overflow
这个函数,很简单。这个函数的地址是保存在_IO_str_jumps
这个结构体中的,在一般程序正常运行的情况下,_IO_list_all
保存有指向标准输入输出的FILE
结构体,其中的vtable
指向的应该是_IO_file_jumps
,而_IO_file_jumps
与_IO_str_jumps
是一个结构体类型的实例,二者的不同之处是,_IO_file_jumps
用于一个FILE
结构体在出现异常时调用的函数列表,我们在假FILE
结构体中将vtable
写成_IO_str_jumps
,实际上就是将程序的执行流从_IO_file_overflow
改成_IO_str_overflow
。这也是house of pig利用的思想精髓所在。
exp: (其中假FILE
结构体中某些字段的设置也有讲究,这个会在下面的演示程序中说明)
1 | import time |
由此,我们就完全解决了house of pig这个问题,但不难发现,这种利用方式需要使用__free_hook
,而这个钩子在更高版本的libc中是不存在的。那么在2.35这样的libc中,我们又应该如何进行利用呢?
其实可以发现,house of pig调用了_IO_str_overflow
这个函数,如果我们将vtable
也进行伪造,就相当于可以执行任意函数。这个函数的第一个参数就是伪造的FILE
结构体自身,如果在结构体开头写入字符串/bin/sh
,然后通过伪造的vtable
调用system
函数,也能够实现同样的功能,再不济要是用了沙箱,也还可以用传统手法——setcontext
绕一遍,不过那样的话,题目的流程就太长了。
D. glibc 2.31版本house of pig演示程序
下面是笔者写的演示程序,如有错误请联系笔者指正。
1 |
|