有学到更多的会慢慢更新的

介绍:

(自2.29版本新增加了对 top chunk size 的合理性的检查,就失效了)
House of Force 是一种经典的堆利用技术,核心思想是通过溢出修改堆的 top chunk 元数据(尤其是大小字段),从而控制后续 malloc() 的分配位置,实现任意地址写。
 常与堆溢出(如 getsstrcpy 导致的溢出)结合,溢出需精准覆盖 Top Chunk 的 size 字段(通常需先分配多个块,使 Top Chunk 紧邻溢出块)

原理:

在于 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,并获取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; // 更新top指针指向剩余块
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 的漏洞。
(unsigned long) (size) >= (unsigned long) (nb + MINSIZE)
一般的做法是把 top chunk 的 size 改为无符号数最大值(如 64 位的 0xFFFFFFFFFFFFFFFF),因为在进行比较时会把 size 转换成无符号数,所以无论如何都可以通过验证。

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 指针更新,接下来的堆块就会分配到这个位置,用户只要控制了这个指针就相当于实现任意地址写任意值。
topchunk 的 size 也会更新,其更新的方法如下
victim = av->top; size = chunksize(victim); remainder_size = size - nb; set_head(remainder, remainder_size | PREV_INUSE);
所以,如果我们想要下次在指定位置分配大小为 x 的 chunk,我们需要确保 remainder_size 不小于 x+ MINSIZE。

本质:绕过内存边界检查

正常情况下,堆管理器会限制堆的扩展范围(通过 brk 或 mmap 系统调用),但篡改 size 后,堆管理器被欺骗,认为 Top Chunk 足够大,从而允许分配地址 “跳” 到任意位置,忽略了实际的内存边界。这就是经典的 House of Force 漏洞利用的核心原理。

利用条件:

  • glibc 2.29版本前
  • 能够以溢出等方式控制到 top chunksize
  • 能够自由地控制堆分配尺寸的大小
  • 分配的次数不能受限制

知识点:

Top Chunk 的正常分配逻辑

当 malloc 无法从 fastbin、tcache 等空闲链表分配内存时,会尝试从 Top Chunk 切割:

  • 堆管理器会检查Top Chunk 的大小是否 ≥ 请求大小 + MINSIZE(确保切割后剩余部分仍能作为有效堆块)。
  • 若满足,就从 Top Chunk 起始地址切割出 请求大小 的块,剩余部分成为新的 Top Chunk。
  • 公式:新块地址 = Top Chunk 当前地址新 Top Chunk 地址 = 原 Top Chunk 地址 + 请求大小

篡改 size 为极大值的关键作用

(溢出需覆盖到 top chunk 的 size 字段时篡改后的 size 需满足 (size & 1) == 1(即 PREV_INUSE 位为 1),否则 glibc 会认为前一个块空闲,触发合并逻辑,导致利用失败。
强制设置 PREV_INUSE 位:p64(0xFFFFFFFFFFFFFFFF | 0x1))
当 Top Chunk 的 size 被改为极大值(如 -1(0xFFFFFFFFFFFFFFFF)),堆管理器会误认为 Top Chunk 有 “无限大” 的空间,此时:

  • 无论 malloc 请求多大的内存(即使远超过实际堆大小),堆管理器都会认为 Top Chunk 能满足需求。
  • 分配逻辑会简化为:新块地址 = 原 Top Chunk 地址新 Top Chunk 地址 = 原 Top Chunk 地址 + 请求大小

实现 “任意地址分配”

假设原 Top Chunk 的地址为 addr_top,我们希望分配到目标地址 target(如 __malloc_hook),只需控制 malloc 的请求大小:

  • 计算 请求大小 = target - addr_top。
  • 当 malloc(请求大小) 被调用时,堆管理器会直接从 addr_top 切割出 请求大小 的块,新块的地址就是 addr_top + 0 = addr_top,而新 Top Chunk 的地址会变成 addr_top + 请求大小 = target
  • 此时,若再次 malloc 任意大小,分配的地址就是 target(新 Top Chunk 的起始地址),从而实现向 target 写入数据(即 “任意地址写”)。

举例:覆盖 malloc_hook

  1. 篡改 Top Chunk 的 size:通过堆溢出等方式,将 Top Chunk 的 size 改为 0xFFFFFFFFFFFFFFFF
  2. 计算请求大小:假设 addr_top = 0x555555759000__malloc_hook = 0x7ffff7dd1b10,则请求大小为 0x7ffff7dd1b10 - 0x555555759000(一个很大的数值)。
  3. 第一次 malloc:调用 malloc(请求大小),堆管理器会将新 Top Chunk 的地址设为 0x7ffff7dd1b10(即 __malloc_hook 的地址)。
  4. 第二次 malloc:调用 malloc(8),此时分配的地址就是 0x7ffff7dd1b10,写入的数据会直接覆盖 __malloc_hook

小坑:

(来自大佬的提醒)
实例代码:

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
#include<stdlib.h>
#include <stdio.h>
#include <unistd.h>

char *chunk_list[0x100];

void menu() {
puts("1. add chunk");
puts("2. delete chunk");
puts("3. edit chunk");
puts("4. show chunk");
puts("5. exit");
puts("choice:");
}

size_t get_num() {
size_t num;
scanf("%llu", &num);
return num;
}

void add_chunk() {
puts("index:");
int index = get_num();
puts("size:");
size_t size = get_num();
chunk_list[index] = malloc(size);
}

void delete_chunk() {
puts("index:");
int index = get_num();
free(chunk_list[index]);
}

void edit_chunk() {
puts("index:");
int index = get_num();
puts("length:");
int length = get_num();
puts("content:");
read(0, chunk_list[index], length);
}

void show_chunk() {
puts("index:");
int index = get_num();
puts(chunk_list[index]);
}

int main() {
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);

while (1) {
menu();
switch (get_num()) {
case 1:
add_chunk();
break;
case 2:
delete_chunk();
break;
case 3:
edit_chunk();
break;
case 4:
show_chunk();
break;
case 5:
exit(0);
default:
puts("invalid choice.");
}
}
}

patch.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

VERSION=2.27
LIBC_NAME=libc.so.6
LD_NAME=ld-linux-x86-64.so.2

gcc pwn.c -o pwn -g
cp /glibc/${VERSION}/amd64/lib/${LIBC_NAME} .
cp /glibc/${VERSION}/amd64/lib/${LD_NAME} .

chmod 777 ${LIBC_NAME}
chmod 777 ${LD_NAME}

patchelf --replace-needed libc.so.6 ./${LIBC_NAME} ./pwn
patchelf --set-interpreter ./${LD_NAME} ./pwn

他对我们输入数据的类型有着一定的限制也就是他在get-num的时候的获取的类型需要为一个机器字长的数据如果他构造的是一个int也是不行的尤其是size位

思路:

(自己整理的一点手法思路)

修改 malloc_hook 为 system,在 top chunk 前写入 ‘/bin/sh’

  1. 先通过一些方法或漏洞进行libc基址泄露
  2. 然后通过溢出等方式修改 top chunk 的 size 大小为 0xFFFFFFFFFFFFFFFF | 0x1,同时得到 top chunk 地址
  3. 根据 malloc_hook 地址和 top chunk 地址计算偏移量
    (要考虑创建堆块时堆块头大小减去一定的大小(大部分情况最好的是0x20))
  4. 创建偏移大小的堆块使得 top chunk 靠近 malloc_hook
  5. 创建一个堆块,内容是system地址
    (0x20的话,创建的堆块内容起始输入地点就是 malloc_hook)
  6. 创建堆块,size 值给 top chunk 地址,触发 system(‘/bin/sh’)
    (size 值原本我们给的是数值,会把这个数值给rdi,现在给的是 top chunk 地址,给rdi的值就是 top chunk 的内容)

例题:

BUUCTF-gyctf_2020_force
[羊城杯 2023 决赛]easy_force