XHLJ 2021-easykernel - writeup
Author : 堇姬 Naup
analyze
一樣的題目給了三個文件
1 | naup96321@naup96321-virtual-machine:~/Desktop/ekernel$ tree |
先解包 file system
1 | mkdir -p sysfile |
順便寫 pack.sh 打包
1 | find . | cpio -o --format=newc > ../rootfs.img.gz |
有開 KPTI、KASLR、SMEP
1 | / # dmesg | grep -i "Page Table" |
1 |
|
根據 init 可以知道,他會 load test.ko 這個 kernel driver
目標一樣是提權到 root
1 |
|
syscall and pt_regs
syscall 的本質是一種從 user space 切換到 kernel space 的「受控」方法,它允許使用者程式請求作業系統執行特權操作,例如開檔案、讀寫記憶體、建立程序、分配資源等
通過 gate,進到 kernel space 他會去調用 entry_SYSCALL_64
把 register 的值一個一個壓到 kernel stack 上
https://elixir.bootlin.com/linux/v5.8/source/arch/x86/entry/entry_64.S#L94
這東西就會組成一個結構,叫做 pt_regs 實質上位於 kernel stack 最底部
順帶一提,pt_regs 是在高位記憶體的,因為 stack 由高往低長
https://elixir.bootlin.com/linux/v5.8/source/arch/x86/include/uapi/asm/ptrace.h#L44
1 | struct pt_regs { |
總之這是一個連續 8 byte 的記憶體區段
我們在進入 syscall 前實質上是可以控制這段的
只要控制 register 值後 syscall,就可以預先壓入想要寫的值
通過這個結構體 ROP 核心思想就是預先在 userspace 壓上想跑的 ROP
之後控制 RIP 後,將 RIP 跳到 add rsp, val
來搬移 kernel stack 到這塊結構上
cool commit protect pt_regs
reverse test.ko
根據不同的 ioctl 分成四個功能
這邊看組語比較清楚
show (ioctlcode == 0x40)
ioctl 前三個參數是 fd (rdi), ioctlcode (rsi), args(rdx)
他會比較 ioctlcode 是否等於 0x40
1 | push rbp |
如果是就跳至這裡,r8 是 rdx (args), edx 設為 0x18 (從 userspace copy 的 size),rdi 是 kernel stack 上的值
1 | loc_2C9: |
如果複製成功就跳至這裡 call show
並傳入 rbp - 0x40
1 | lea rdi, [rbp-40h] |
這裡的 rbp - 0x40 是 index
1 | __int64 __fastcall show(_QWORD *a1) |
args 根據傳入長度可以先推得,剩下兩個沒使用到
大概率是 size 跟 *buf
1 | struct args{ |
edit (ioctlcode == 0x50)
若大於 0x40 跳至左邊
1 | ja short loc_1B8 |
如果等於 0x50 會進到這個分支,如果不是直接 return
這個分支 edx 是 0x18 (args 長度)
rsi 是 args
rdi 存從 userspace copy 的 args
1 | mov edx, 18h |
成功進到這條
他會檢查傳入的第一個是不是小於 0x20
並且第一個傳入會是 rax
1 | mov rax, [rbp-40h] |
之後會進到這裡,他會去 addrList 搜 pointer
可以推得 rbp - 0x40 上的是 index
r12 存了分配 chunk address
1 | mov eax, eax |
之後做
把 args 第二個項目給 r13
第三個項目是 r14
1 | mov r13, [rbp-38h] |
如果成功進到這條
傳入到 void __check_object_size(const void *ptr, unsigned long n, bool to_user);
這裡 rdi 是 r12 是 chunk pointer
rsi 是 r13 是要檢查的 object 大小,所以可以知道第二個是 size
1 | xor edx, edx |
之後去 copy userspace 的東西到 kernel (r12 這裡是 chunk pointer)
r14 是第三個項目,這裡是 userspace buffer pointer
這裡可以看出,他是 edit chunk
傳入的 args 結構是 (順便說第一個 show 結構跟這個一樣)
1 | struct args{ |
alloc (ioctlcode == 0x20)
esi 是 ioctlcode,他會比較是不是 0x20
1 | ja short loc_1B8 |
rdx 是 0x10
r8 是 args
rdi 是 kernel space stack,把 args copy 進來
注意一下,這裡只 copy 了 0x10,與先前的不一樣
1 | loc_226: |
rbp - 0x40 是第一個參數
並比較是不是小於 0x20 (size 不可以大於 0x20)
是就繼續往下走
這裡的 rdi 也會放在 kmalloc size 位,esi 是第二個參數,是 kmalloc 分配的 flags,rax 是 kmalloc 回傳值,會給 rbx
所以這裡第一個參數是 size
1 | mov rdi, [rbp-40h] |
r12 是第一個參數
r13 是第二個參數
1 | mov r12, [rbp-40h] |
r12 是 size 已經確定了
rax 是 kmalloc chunk pointer
r13 是從 userspace copy 的 pointer
並把他 copy 到 chunk pointer
1 | xor edx, edx |
省略一下,這裡的 index 是從 0 開始往上加的
最後他會把 pointer 丟進 addrList
1 | mov ds:addrList[rdx*8], rbx |
綜上就可以看出這是一個 malloc,並且第一次順便賦值給 chunk pointer
1 | struct args{ |
free (ioctlcode == 0x30)
這部分不用看組語,直接看 IDA 反編譯結果
1 | if ( !copy_from_user(&v12, v2, 8LL) ) |
他會 copy 8 bytes
這 8 bytes 是 index
最後把對應的 chunk pointer free 掉
這裡傳入的結構就是
1 | struct args{ |
這裡有個問題,就是沒有清空原本存在 addrList 的 pointer
導致有 UAF 產生
腳本架構
先把互動腳本寫完
1 |
|
利用
先 leak kernel base
先 kmalloc 一塊 0x20 大小的 chunk,他會從 memory pool kmalloc-32 中拿出一塊 object 來用
之後把他 free 掉
這樣就會有一個 addrList 中有指向已經被 free 掉的 pointer 了
接下來去 open /proc/self/stat
他會從 kmalloc-32 拿出一塊分配給 seq_operation
1 | struct seq_operations { |
他是一個 vtable,上面有許多 kernel function pointer
通過他可以用來 leak kernel base
1 |
|
接下來來找一些 gadget 來用
我們打算通過 commits_cred(init_cred()); 提權
這邊構造
1 | pop_rdi |
之後去算 offset 看 rsp 要搬多少
https://elixir.bootlin.com/linux/v5.19/source/fs/seq_file.c#L171
當我們去修改 seq_operations 的 start
他會去 call rax 時,上面原本的 pointer 被你改成了 0xaabbccdd
所以會 crash,代表我們控制到 RIP

另外可以看到 rsp+0x198 上長這樣
代表上面是我們寫上去的 gadget
只要把 rsp 搬到這就可以執行 ROP

如果用這條 gadget add rsp, 0x190; pop rbx; pop r12; pop rbp; ret;
他會剛好落在 r14 跑 ROP
所以從 r14 開始寫 ROP gadget
另外我們要跳上去 swapgs_restore_regs_and_return_to_usermode
需要 stack 有
1 | dummy |
剛好 kernel stack 這塊緊接著之前堆 ROP 的位置
另外這個 function 前面剛好有一堆 pop
所以要剛好 pop 到第一個 dummy 前,計算一下要多 pop 9 次
1 | pop r15 ; Alternative name is '__irqentry_text_end' |
詳細 stack 狀況可以看這個

總之跳上去後返回 usermode 就可以 get root shell 了
exploit
1 |
|