0x00 前言
- 本文是对
CTF-WIKI
中House Of Spirit
的一道例题的解析,这道题费了不小功夫,也算是弄明白了。
- 参考链接:
- 题目下载地址:下载地址
0x01 温习
什么是House of Spirit
House of Spirit 是 the Malloc Maleficarum
中的一种技术。
该技术的核心在于在目标位置处伪造 fastbin chunk,并将其释放,从而达到分配指定地址的 chunk 的目的。
要想构造 fastbin fake chunk,并且将其释放时,可以将其放入到对应的 fastbin 链表中,需要绕过一些必要的检测,即
- fake chunk 的 ISMMAP 位不能为 1,因为 free 时,如果是 mmap 的 chunk,会单独处理。
- fake chunk 地址需要对齐, MALLOC_ALIGN_MASK
- fake chunk 的 size 大小需要满足对应的 fastbin 的需求,同时也得对齐。
- fake chunk 的 next chunk 的大小不能小于
2 * SIZE_SZ
,同时也不能大于av->system_mem
。 - fake chunk 对应的 fastbin 链表头部不能是该 fake chunk,即不能构成 double free 的情况。
利用场景
(1)想要控制的目标区域的前段空间与后段空间都是可控的内存区域
一般来说想要控制的目标区域多为返回地址或是一个函数指针,正常情况下,该内存区域我们输入的数据是无法控制的,想要利用hos攻击技术来改写该区域,首先需要我们可以控制那片目标区域的前面空间和后面空间,示意图如下。
(2)存在可将堆变量指针覆盖指向为可控区域,即上一步中的区域
利用思路
(1)伪造堆块
hos的主要意图就是在可控1及可控2构造好数据,将它伪造成一个fastbin。
(2)覆盖堆指针指向上一步伪造的堆块。
(3)释放堆块,将伪造的堆块释放入fastbin的单链表里面。
(4)申请堆块,将刚刚释放的堆块申请出来,最终使得可以往目标区域中写入数据,实现目的。
0x02 分析
1 | Arch: amd64-64-little |
主要保护都关了,可以执行shellcode。
漏洞点1
sub_400A8E函数
中
1 | int sub_400A8E() |
存在off-by-one
漏洞。当输入48个字符的时候,会连带着将RBP里的值打印出来。这是因为read函数读取字符串的时候不会在末尾加上\x00
漏洞点2
sub_400A29函数
中
1 | int sub_400A29() |
由于buf的大小只有 0x40-0x8 = 0x38,但是却读入了0x40字节,会覆盖掉dest的指针,而dest是一个堆指针,这样就满足了HOS利用条件之一存在可将堆变量指针覆盖指向为可控区域
小细节
漏洞2满足了HOS利用条件之一,实际上另一个利用条件在sub_400A8E函数
输入ID的地方也得到了满足,只是由于IDA反编译的一些问题,没有显示出来,但是从汇编界面看的话
能够看到程序将rax(里面存放的Myread函数的返回值,也就是咱们输入的ID)里的值存放到了rbp-0x38
的位置。
- 每个栈帧的大小可以通过IDA查看,例如下图就是sub_400A8E函数的栈帧,也能看出来它栈帧大小是0x50
从整个程序的栈结构来看:
1 | +------------+----- - |
也就是形成了这种格局。
其他功能
1 | int sub_4009C4() |
1 | int checkin() |
1 | void checkout() |
- 程序依然提供checkin、checkout、exit函数。
思路
将shellcode.ljust(48,’a’)输入到name中,通过
off-by-one
漏洞打印出来main函数栈底,通过上面结构图能够算出shellcode的地址,选取一个处在money中的位置作为fake_chunk在money中伪造堆块size,在id里面输入的是下一个堆块的size(大小不能小于
2 * SIZE_SZ
,同时也不能大于av->system_mem
),同时通过堆溢出漏洞覆盖掉destfree掉刚才伪造的堆块,使其进入fastbin
申请堆块,申请出来以后还是在老位置。
输入数据到刚申请的堆块中,覆盖掉rip,让其指向shellcode,完成劫持,执行shellcode。
0x03 Let’s do it!
shellcode可以选用现成的,也可以通过pwntools的shellcraft生成。
1 | shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05" |
泄露rbp 并选择合适的fakechunk地址
1 | payload = shellcode.ljust(46, 'a') |
- 注意这里shellcode的地址,可以根据前面的栈结构图来求得,
0x50 = 0x30(name) + 0x8(rbp) + 0x8(rip) + 0x10(main栈帧)
伪造下一个堆块的size
1 | c.recvuntil('id ~~?') |
这个size没有太严格要求,只需要大小不能小于 2 * SIZE_SZ
,同时也不能大于av->system_mem
在money中伪造堆块并覆盖dest
1 | c.recvuntil('money~') |
- 这里前面用p64(0)有好处,就是能够使
sub_400A29
中的strcpy(dest, &buf);
不起作用。 - 这么构造也能和前面我们所选取的fakechunk的地址fake_addr相呼应。具体可以gdb调试一下就能看出来了。
- 这里选取p64(0x41)是因为这样能够使ID正好相当于我们伪造chunk的nextchunk的size,具体可以画画图或者调试一下就可以了。
free然后重新申请,并劫持程序
1 | #free |
- 需要注意的几个点都加了注释了。
由于我们的栈结构在这几步时大概是这样的:
1 | +------------+----- 低地址 |
当我们执行exit以后,栈帧还原,按照地址从低往高的顺序执行rip,当执行到被我们劫持,也就是被我们改成shellcode地址的rip的时候,就能执行shellcode从而getshell了~
0x04 完整exp
1 | from pwn import * |