PWN堆house of spirit-1
- 参考博客:好好说话之Fastbin Attack(2):House Of Spirit_fastbin attack house of spirit-CSDN博客
- 参考书籍:杨超的《CTF竞赛权威指南——pwn篇》
- 用到的仓库:github的how2heap仓库
前言
-
学习了
UAF、double_free、unlink、off-by-one,之后基本上对堆稍微有点了解。 -
个人感觉,堆漏洞的成因就是
UAF和堆溢出,而栈的漏洞的成因基本上就是栈溢出,其他的就是格式化字符串漏洞,漏洞的成因感觉就是这几种。但是利用方式很多,所以学来学去都是在学被人挖掘出来的利用方法。在学这些利用方法的时候会不由感慨,那些开创漏洞利用的人脑洞真大。所以怎么感觉pwn到最后变misc那些脑洞题了… -
初步了解了一些漏洞成因和漏洞利用后,就可以开始学习
house of系列的利用方法了。目前打算主线直接学习house of系列,按照Glibc堆利用之house of系列总结 - roderick - record and learn!这篇文章的顺序学习,学到哪些利用,之前没涉及到的再分出支线去学习。 -
堆的漏洞利用可以进行以下三种分类:
glibc版本对应的漏洞利用,具体可以看how2heap这个github仓库shellphish/how2heap: A repository for learning various heap exploitation techniques.house of分类的漏洞利用(house-of系列算是堆利用的进阶方式,建议等学完bins_attack之后再学习house of系列)bins_attack以及其他的attack分类利用
house-of-spirit介绍
house-of-spirit是堆利用house-of系列的一种方法。这种方法目前只了解到是针对fast_bin的attack和tcache_bin的攻击,所以这个利用方法将被分成两个部分,第一个部分是介绍glibc2.26之前的还没有引入tcache_bin的攻击。第二部分是介绍glibc2.26之后引入了tcache_bin后针对tcache_bin的攻击- 在学习
house-of-spirit之前首先要对Linux下的堆管理器即ptmalloc2有一定的了解,还要知道UAF漏洞、double_free漏洞以及off-by的两种漏洞,能简单利用UAF和double、这个两个漏洞对fast_bin进行攻击和利用。这样就可以进一步学习house-of-spirit。 - 对于
house-of-spirit这个漏洞,与对堆管理器中unlink的攻击关系其实不大。可以根据自己的节奏去选择学习顺序。 - 本次介绍的是对
glibc2.26之前的版本还没引入tcache_bin,针对的是fast_bin的攻击。
利用方式
- 先来比较直观和简单地介绍一下
house-of-spirit的利用方式。这个利用方式是double_free和UAF漏洞的进一步利用。 - 首先
house-of-spirit的堆利用方式,是一种用于获得某块内存区域控制权的技术(即你可以实现对该块内存任意写入数据)。 house-of-spirit是通过伪造一个堆块fake_chunk,并且有一个指针ptr指向该fake_chunk,加上我们可以使用free()函数,将指向这个fake_chunk的ptr指针给free掉,这时该fake_chunk的就会被放入fast_bin对应大小堆块的链表头部。当我们再次使用malloc函数使用一个堆块,这时fake_chunk就会被申请出来,被用户使用,这样我们就可以对该fake_chunk进行写入或者修改数据。- 接下来通过图片来直观感受一下
house-of-spirit的这个利用方式。 - 如图
fast_bin链表这边有两个空闲的真chunk,并且都放在了fast_bin中。

- 这时我们在某个内存空间中(可能是
.bss段、栈上或者其他内存空间)伪造了一个chunk,称为fake_chunk,并且有一个指针ptr指向该fake_chunk

- 恰好这个程序中的
free函数能释放ptr这个指针。所以当我们释放ptr这个指针后,就会将这个fake_chunk放入fast_bin这个链表中(该链表采用头插法)

- 当我们再次使用
malooc函数申请适当大小的内存,就可以将这个fake_chunk申请给用户写入数据。

- 以上就是
house_of_spirit的利用过程,现在来总结一下house_of_spirit的利用方式,以及house_of_spirit与fast bin double free的区别houser_of_spirit是通过伪造一个堆块fake_chunk(该堆块并不是通过malloc申请回来的),利用free的机制将该fake_chunk放入fast_bin链表之中,再次通过malloc将该fake_chunk申请,从而实现任意内存读写- 而
double freee是将本来通过malloc申请的堆块chunk1(该堆块是合法的),通过两次释放chunk1,将同一个堆块即chunk1放入两次到fast_bin中,这样我们第一次申请chunk1,就可以修改chunk1中的fd指针,使其指向任意内存地址,当我们再次申请chunk1时,这个任意内存就会被放入我们的fast_bin链表之中,再次使用malloc申请,我们就可以申请到任意内存并对该内存进行读写。 - 区别:
house_of_spirit是构造fake_chunk想办法放入bin中链表,通过再次申请达到任意地址读写。而double是通过合法chunk的两次释放和两次申请,从而在第三次申请chunk时能申请到任意地址,对任意地址读写
伪造条件
-
由于
free的时候会对堆块进行检查,即验证堆块的合法性,所以我们在伪造堆块的时候就要满足一定的条件,这样才能绕过检查,继续进行house_of_spirit的利用。 -
接下来说明一下伪造需要满足的条件:
fake_chunk标志位、fake_chunk的内存地址对齐、fake_chunk的size大小、fake_chunk的next_chunk的大小、fake_chunk的连续释放(即double_free的错误) -
ptmalloc所申请的堆块,数据结构如下:

fake_chunk的标志位:
- 在伪造一个
fake_chunk的时候,由于堆块结构中的size部分的最低三位是标志位,所以这些标志位要满足一下条件:P标志位设置为1:该标志位表示的是该堆块物理地址上相邻的前一个堆块是否处于空闲状态,1表示处于使用状态。当标志位设置为0的时候就会在free的时候会触发unlink机制,导致合并一个不存在的地址空间,会引发程序的崩溃。有些题目好像不满足也可以,对P标志位要求没那么严格M标志位设置为0:M标志位表示的是IS_MAPPED,记录当前chunk是否由mmap分配。当这个位置的标记设置为1,就表示这个堆块内存时调用mmap分配的,在处理这个chunk的时候就会单独处理。N标志位设置为0:一般攻击的都是主线程下的堆(目前还没有打过多线程的堆题),所以该标志位应该被设置为0
fake_chunk的内存对齐:
- 堆内存在申请的时候都是8字节对齐或者16字节对齐的,也就是说
fake_chunk的prev_size的最低位地址、P标志位对应的地址应该为0xXXXXXXX0。也就是需要16字节对齐。(32位程序下是0xXXXXXXX8,也就是8字节对齐) - 在
free的时候会对内存是否对齐进行检查,如果检查到所free的堆块内存没有对齐就会出现报错,或者是程序无法运行下去。
fake_chunk的size大小:
fake_chunk是要放入fast_bin中的,所以我们的size大小就要满足挂入fast_bin链表的要求,即size的大小要小于0x80字节- 如果要放入
tcache_bin中就要满足放入tcache_bin中的size大小 - 还需要注意一点就是
size的值也必须是16字节对齐,即满足0x10的整数倍(32位程序要满足0x8字节对齐)
fake_chunk的next_chunk的大小:
- 所谓
fake_chunk的next_chunk指的就是与fake_chunk物理地址上相邻的且地址高于fake_chunk的一段内存空间(因为是伪造的堆块,所以该段内存空间其实不是堆块) - 当我们释放
fake_chunk的时候,会有一个检查机制,ptmalloc2会通过,如下计算确定next_chunk的地址,并对其size进行检查
1 | next_chunk_address = fake_chunk_address + fake_chunk->size |
- 所以
next_chunk的size大小应该为满足以下两个条件:size要满足0x10字节对齐,即必须为16字节的整数倍(32位系统则是0x8字节对齐)size还要满足小于128kb
- 这样设置的目的就是在
chunk连续释放的时候,能够保证伪造的fake_chunk在释放后能够挂在fast_bin中的main_arena的前面。这样我们再次使用malloc申请适当大小的堆块时,就会直接申请到fake_chunk
fake_chunk的连续释放:
- 在
double_free中如果直接连续free相同的堆块,程序就会因为检测到double_free而无法运行,所以我们在进行double_free利用的时候就释放一次目标堆块后,要释放另一个堆块,之后才能再次释放目标堆块。这就是需要我们绕过double_free的检查机制 - 所以
fake_chunk在释放完一次后,不能马上再进行释放,这样就会导致double_free检查不通过,导致程序无法运行。
总结:所以我们要构造的fake_chunk要满足如下条件


实验
- 本实验的代码时在
how2heap中glibc2.23版本的house-of-spirit,并通过阅读程序,模拟出攻击流程 - 我将其源码进行汉化,源码如下:
源码
1 |
|
1 |
|
- 该程序先调用了
malloc(1)开辟了一个堆块内存,初始化了一下堆块内存。如果没有malloc(1)在gdb动态调试的时候就会出现堆块没有初始化的提示。

- 使用
malloc后在栈上创建了一个多的数据,进行堆块的伪造。

- 然后根据输出提示,设置了伪造堆块的相关数据。

- 使用gdb进行动态调试后就会看到栈上的数据是这样分布的,并且现在
fast_bin是空的

- 之后我们通过
free(a)即free(&fake_chuns[1]),就把伪造的堆块放入到了fastbin链中。这样就完成了house of spirit的利用。

题目
house_of_spirit_level_1
- 题目来源:lctf2016_pwn200,在buuctf上有相应的环境,附件也可以从那边下载。这边我使用patch的方法,使得我在
wsl的环境下使用glibc2.23进行动态调试,这样就可以避免glibc版本过高的原因从而导致出现tcache_bin的问题
level_1分析1
- 现在先使用
IDA对该程序进行静态分析,在静态分析的时候运行程序,这样能更好的理清楚程序的运行逻辑。先check一下该程序的保护机制。发现保护机制开的很少。

- 然后使用IDA对该程序进行逆向分析。先来先查看
main函数,这里的main函数比较简单先是对输入输出进行初始化,然后再调用一个func函数。

- 之后查看
func函数,结合程序运行的结果看,这个函数会先输出提示who are u,然后逐个字节的读取用户的输入,知道遇到换行符。还会将用户输入打印出来,并且提示输入id,之后就是调用两个目前还不知道什么功能的函数。

- 之后将
sub_4007DF这个函数重新命名为func2,将sub_400A29这个函数重新命名为func3,再进入func2进行查看函数的具体操作。- 该函数先是逐个字节读取用户的输入,一共读取4个字节或者遇到换行符结束读取。
- 在读取的时候还会对读入
id进行检查,检查该id,如果输入的id不在十进制的数字里面,那么就不会执行接下去的代码。 - 之后还会使用
atoi函数,该函数将我们输入的id原本是字符的形式,将其转换为整型的形式,并返回给v2,还会将该值作为返回值返回。

- 之后再来查看
func3的函数,执行的具体功能。该函数实现以下功能:- 开辟一个
0x40大小的堆内存,将malloc返回的地址给dest变量 - 输入提示字符串
give me money~,提示我们输入0x40字节大小的数据,这里并不存在栈溢出 - 之后我们会把输入的数据原来存储在
buf中,现在复制到dest这边,即复制到malloc开辟的堆内存这边。 - 最后将
dest的值(即堆块的地址)赋值给ptr这个指针,注意ptr这个指针是存储在.bss段上的全局变量这个可能有用 - 之后会调用
sub_4009C4这个函数,将该函数命名为func4
- 开辟一个


- 接下来查看
func4函数里面的运行过程,发现该函数就是一个经典菜单的形式。- 先会调用
func2这个功能,实现用户对选项的选择。 - 输入
1的时候就调用check_in这个函数 - 输入
2的时候就调用check_out这个函数 - 输入
3就退出该程序
- 先会调用

- 接下来先查看
check_out这个函数,该函数实现的功能是释放掉全局变量指针ptr所指向的内存地址,并将该ptr指针置零,从而避免UAF的漏洞利用。

- 接下来查看
check_in函数- 该函数会先检查
ptr是已经指向一个指针,如果已经指向一个指针,那么程序就不会执行后续的语句 - 通过
func2读取数字,并判断读取的数字是否在0-128范围内 - 之后使用
malloc函数申请一个前面所读取数字大小的堆块。 - 最后可以向所申请的堆块写入数据。
- 该函数会先检查

level_1分析2
- 现在对该程序进行动态调试,查看该程序是否有溢出点或者其他的漏洞。
- 对于
func函数这边,存在一个off-by-one的利用,可以通过将v2这个数组填满48个字节,然后就可以避免换行符,这样这边就没有\x00对进行截断,这时printf就会将rbp所指向的栈地址的存储的数据泄露出来,从而知道了栈的地址。


- 接下来看
func3这个函数,这个函数在会使用read函数在栈上写入0x40个数据,但是buf只有56字节(即0x38)的栈上存储空间,这时最后的0x8字节就会把dest这个指针给覆盖掉。- 所以
func3存在一个溢出,可以修改dest的值,并且之后会把dest赋值给ptr这个全局变量的指针。 - 在分析1中可以知道
free()函数free的是ptr指针所指向的地址。
- 所以


level_1分析3
- 这个时候我们即泄露了栈地址,又能通过溢出修改
dest这个指针,从而间接修改ptr指针。现在思路就比较明显,既然可以泄露栈地址,那么就可以利用栈伪造一个fake_chunk。通过house-of-spirit这个对利用技术,将栈上的堆放入到fast_bin中,在申请回来这样就可以对比较大块的栈地址进行写入。一般就可以修改返回地址。接下来要查看一下如何伪造这个堆块。栈上有什么数据可以提供我们伪造堆块。 - 对于
func()函数这个输入点进行分析,发现并不能使用func()函数这个输入点进行堆块的伪造,这是因为在堆块的伪造过程中必然会出现\x00这个截断操作。这就导致我们在使用printf函数进行格式化输出的时候栈上的地址泄露不出来。

- 对于
func2可写入栈上的数据有限,也没办法完成对堆块的伪造,所以先来分析一下func3是否能在修改指针的时候对堆块进行伪造,发现这是可以行的,这时我们在栈上进行堆块的伪造,并不会影响我们ptr指针的修改


- 所以我们选择该此处对堆块的伪造,然后我们再来分析一下栈上高地址处是否可以有
0x10整数倍的,并且小于128kb大小的,还要满足prev_size的最低位为0xXXXX0这样我们就可以伪造出fake_chunk并且成功的把堆块给链到fastbin上。- 查看栈后发现这个地方明显是可以被控制的,而控制这个栈数据的也就只能是
func或者func2的执行过程。 - 我们发现这个数据是存储在
func函数输入的更低地址,所以应该是在func这个函数控制的。
- 查看栈后发现这个地方明显是可以被控制的,而控制这个栈数据的也就只能是

- 我们查看
func的变量时,发现逐字节输入,控制循环的变量i最终的结果为0x30(存储位置是位于更低地址的栈那边),但是这个0x30我们希望用来伪造size,可是这个栈地址是0xXXXXX0开始的,这并不能满足堆块内存对齐的问题

- 这时还有一个
0x30是怎么来的,对于反编译的代码是没有体现出来的。我们需要去看func的汇编代码,这时我们发现调用完func2后,存储返回值的寄存器rax会有一个操作,也就是将func2的返回值复制到栈上。 - 而
func2返回的也就是我们输入数字对应的整数。所以我们要伪造该堆块,在func调用func2时,就要像这边输入48即0x30(或者其他符合要求的数据。)


- 这样我们就满足了伪造堆块的条件,现在我们要通过栈地址计算我们所申请堆块的
size大小,和我们要伪造堆块的size

- 还有我们要修改的指针是指向
0x7ffcddf7d710,但是我们泄露的指针是泄露到更高地址。所以我们还要计算偏移,所以我们最后会将指针修改为leak_addr-0x110+0x60

- 通过伪造堆块我们就可以释放该堆块,将该堆块放到
fast_bin链上,这时编写部分exp进行动态调试查看fake_chunk是否被添加到fast_bin链表上
1 | from pwn import * |
- 这时我们就发现我们伪造的
fake_chunk被加到了fastbin上

- 这时我们再使用
malloc申请回来,我们就可以对该栈进行写,并且还可以修改返回地址。之前在查看保护机制时,有注意到栈是具有可执行权限的,所以我们就向我们申请的堆块写入shellcode,并修改返回地址到栈上我们写入shellcode的开头。这样就可以getshell了

level_1_exp
1 | from pwn import * |
house_of_spirit_level_2
- 题目来源:2014 hack.lu oreo,再次挑战该题,被这题薄纱了俩次QAQ
- 题目在该链接的github仓库有:https://github.com/ctf-wiki/ctf-challenges/tree/master/pwn/heap/fastbin-attack/2014_hack.lu_oreo
- 这题主要之前没打出来还是因为在Docker环境中这题不知道为什么在
exp中gdb.attach()动态调试不了,不能边打边动调,就发现不了原因。然后之前的house_of_spirit也没理解到位。今天就不用Docker环境,直接在ubuntu22.04做了一下,发现也可以打tcachebin对这题没啥影响,所以就使用ubuntu22.04的环境进行动态试和打了。 - 又打了一下发现还是不行,还是要去Docker环境打,下次如果遇到Docker环境不能动调的,要去试试一下
patchelf了
level_2分析1
- 先来查看一下保护机制,发现是一个
32位的程序,并且开启了canary保护,但是没有开启PIE和RELRO保护。所以可以修改got表

- 然后使用IDA对该二进制文件进行逆向分析。先来查看
main函数。main函数的逻辑如下:- 先初始化了三个全局变量,其中
dword_804A2A4和dword_804A2A0先初始化为0 dword_804A2A7指向的是unk_804A2C0的这个位置- 之后输出字符串,之后执行一个函数,为了方便代码审计,将该函数命名为
func() - 这边还要注意一个问题,这个程序并没有进行初始化输入输出,所以该程序并不是
0缓冲。程序一开始会申请一个比较大的堆块作为缓冲区
- 先初始化了三个全局变量,其中

- 现在来查看一下
func()这个函数中的内部。发现是一个堆菜单的和选项。switch()函数这边调用了一个函数,用于输入选项,这里将其重新命名为choose(),其作用就是让用户选择操作对应的选项。- 之后选项
1就是add操作,所以将选项1所执行的函数命名为add()。选项2是show操作,所以将选项2所执行的函数命名为show() - 选项
3对应的是order操作,将选项3执行的函数命名为order() - 选项
4对应的是编辑一个message,所以将选项4执行的函数命名为edit_message() - 选项
5对应的操作就是展示add和order的状态。所以将选项5执行的函数命名为stats()重命名的结果如下 choose()函数的功能在下图


- 之后来查看一下
add函数的具体功能- 首先有一个指针变量(全局变量存储在
.bss)段,将该变量重新命名为ptr。该ptr会先把之前的值存储在v1中,然后该ptr会重新指向一个新申请的堆块(该堆块申请大小为0x38,实际上该堆块的size为0x40)。 - 申请成功后会像该堆块的
0x3c-0x3f(算上chunk头)字节处存储存储着v1变量的值,即前一个申请堆块的地址。 - 之后会从该堆块的
0x21(算上chunk头)字节处写入数据,作为Rifle name:,这里会存在一个溢出。会进行一个操作,将换行符变成\x00这个字符(这个是通过sub_80485EC()进行的操作) - 之后会从该堆块的
0x8(算上chunk头)字节处写入数据,作为description:,这里貌似也存在溢出,也可以修改指向前一个堆块的指针(不过用前面的就足够了)。写入后也会将末尾的换行符替换成\x00。 - 最后有一个全局变量会自增,方便查看程序,就将该全局变量重新命名为
Rifle_count。
- 首先有一个指针变量(全局变量存储在

- 从
add()函数中就可以了解到我们所申请堆块的结构是向这样的。

- 接下来查看
show(),这个show()函数会按照新申请的堆块到旧申请的堆块这样的一个顺序,逐条打印出Rifle name和description

- 之后查看
order()函数,该函数的执行流程如下:- 会将
ptr指针的值赋值给v1,检查Rifle_count是否存在 - 当
Rifle_count有的时候就会按照从新申请的堆块到旧申请的堆块这个顺序,逐个释放指针 - 之后
order_count这个全局变量就会自增。
- 会将

- 查看一下
edit_message函数,这个函数就会向message_ptr指向的地址读入128字节,最后还会调用sub_80485EC()将末尾的换行符修改为\x00

- 之后查看一下
stats():- 这个函数就是输出
rifles的状态,如果有message_ptr,也会输出该message_ptr所指向的值。
- 这个函数就是输出

- 逆向分析一遍程序后,我们再来查看
main函数的4个全局变量,现在就比较清晰了

level_2分析2
- 静态分析结束后,我们来进行动态调试。既然是
house_of_spirit,所以我们一定是要通过溢出来修改我们的指针,使其指向我们伪造的堆块,这样就可以将伪造的堆块释放到这fastbin链表上面去。 - 由于这个函数没有后门函数,所以我们肯定是要去打
libc。所以我们要先泄露一下libc的地址。我们可以先申请一个堆块,通过溢出修改char_ptr的值,使其指向printf_got表,这样我们就可以泄露libc的地址。在我们打印第二个堆块的name和des的时候我们接收到的des开头的几个字节就是printf函数的地址


-
这样我们就把
libc的地址泄露出来了,接下来就需要伪造一个堆块,然后将该堆块释放,使其能加入fastbin链表,之后我们就可以申请该堆块,对该堆块的内容进行任意修改。 -
这题伪造堆块的地方有两处。这俩处都在
.bss段上,但是有一处伪造的堆块,我们之后申请该堆块是对我们后续没有用的。 -
这两处所在地方如下图所示。
- 这两处其中
第一处伪造比较麻烦,第二处刚好是我们message存储的地方,我们可以对该地方进行任意写的操作所以比较好伪造。 - 但是对后面的利用有用的其实要伪造
第一处,因为第一处我们伪造释放后,重新申请回来,我们可以修改message_ptr的值,使其指向got表的地址,然后我们可以劫持某个函数的got表,将其修改为system函数的地址。这样就可以执行system("/bin/sh")从而getshell。而第二处
- 这两处其中

- 所以我们就需要对
第一处进行堆块的伪造,这样之后才能利用我们所申请的堆块。由于堆块的内存对齐机制,我们要从0x804A2A0开始伪造prev_size字段,这样才能满足对齐的要求。而我们申请的堆块size位都是0x40大小,所以我们要让我们伪造的堆块size位为0x40(这里最好是0x40)如果是0x41的话之后我们再申请一个堆块size位可能会变成0x42可能会造成程序的一些问题。

- 这时我们在泄露
libc的地址后还要申请0x3F个堆块,我们在申请0x3F个堆块的时候就要通过溢出修改该char_ptr的值,使其指向我们伪造堆块的用户使用的地址,也就是0x804A2A8,我们要将我们伪造堆块中char_ptr的值要置为0。同时我们还要伪造与我们伪造堆块物理地址相邻且更高的堆块的size位。(这两个地方都比较好修改,因为都在message这个全局变量里面)


- 这样就将堆块伪造好了,这样我们释放的时候就可以绕过
free的检查机制,将我们所伪造的堆块放入fastbin链表中,接下来我们将堆块释放后看看。(由于环境原因之后的动态调试调试不了,没办法贴图片了。)
level_2分析3
- 接下来总结一下本题的利用思路:
- 先通过溢出,修改指针,将指针修改为
printf_got表的地址,然后通过show()将printf函数的地址给输出出来。从而达到泄露libc地址的作用 - 然后通过伪造堆块将
.bss段的一部分作为fake_chunk,链到fastbin链表上。 - 申请该
fake_chunk通过对申请到的fake_chunk写入数据,达到修改message_ptr的值。 - 最后通过
edit_message()劫持scanf_got表(即将该其got表的值修改为system的地址),之后我们再发送/bin/sh\x00从而达到system("/bin/sh")的目的。这样我们就getshell了
- 先通过溢出,修改指针,将指针修改为
level_2_exp
- exp如下:
1 | from pwn import * |
总结
- 在遇到
house_of_spirit的时候要注意- 是否能通过溢出控制要free的地址
- 注意题目中的计数器
- 如果有多个地方可以伪造,注意伪造到哪个地方对后续有用。
- 注意伪造堆块的
size位和next_size位。 - 还要注意程序逻辑,如果当程序释放完
fake_chunk后还要再继续释放,可能就会出现问题,这时就要在fake_chunk中写入适当的数据,绕过程序逻辑。

