2025 TRX CTF - /dev/mem
Author : 堇姬 Naup
前置
1 | mkdir -p sysfile |
把 cpio 解壓後,先把 pack.sh 寫好
之後來修 run.sh
1 | naup@naup-virtual-machine:~/Desktop/pwn/devmem$ cat run.sh |
他根據是否有提供 exploit 來決定要不要掛進去一個 disk
為了本地 debug 方便重寫一個 start.sh
1 |
|
有開 KPTI、SMAP、SMEP
analyze code
底下給了這些檔案
1 | naup@naup-virtual-machine:~/Desktop/pwn/devmem$ ls |
先來分析 source code
1 |
|
首先要先了解 /dev/mem
是啥
https://blog.csdn.net/skyflying2012/article/details/47611399
https://blog.csdn.net/linjiasen/article/details/103408631
我們可以通過 /dev/mem 在 userspace 來對 physical address 來做讀寫
與他互動的方式是 /usr/sbin/chall address value
他會將傳入的 address 跟 value 做 strtoul
將字符串 str 轉換為無符號長整型(unsigned long)數值,並將其存儲到 value 指向的變量中。這裡使用了基數 16,表示將字符串 str 按十六進制進行解析
lseek(dev, address, SEEK_SET):將文件指標(file pointer)移動到指定的偏移量位置。具體來說,它會將文件指標移動到 address 位置,並且這個偏移是相對於文件的開頭(由 SEEK_SET 指定)
並對 address 寫入 value
也就是有個 physical address 任意寫
第一個直覺的想法就是寫 modprobe_path
但 …
modprobe not be use
https://naupjjin.github.io/2025/04/04/Linux-Kernel-modprobe-EoP-and-2021-3kctf-echo-nerf/
1 | naup@naup-virtual-machine:~/Desktop/pwn/devmem$ cat kernel.config | grep USERMODEHELPER |
檢查了一下他開啟的 config 選項
當開啟 CONFIG_STATIC_USERMODEHELPER 時
modprobe 不會根據 kernel 傳入的 modprobe path 動態調整
這邊可以看到針對這項設定的 code
https://elixir.bootlin.com/linux/v6.13.7/source/security/Kconfig#L205
1 | config STATIC_USERMODEHELPER |
來追一下 code
最後他會呼叫 call_modprobe
原本他會傳入 modprobe_path 來去執行他
但更深入點去看 call_usermodehelper_setup
1 | argv[0] = modprobe_path; |
如果 CONFIG_STATIC_USERMODEHELPER 是開啟的,就會將 path 都設為 CONFIG_STATIC_USERMODEHELPER_PATH
沒有才會是傳入的 path
https://elixir.bootlin.com/linux/v6.13.7/source/kernel/umh.c#L355
1 |
|
broken KASLR
首先因為有開啟 KASLR 的關係
所以想 leak 兩個東西
Kernel physical base,也就是 kernel image 被載入的 physical address 實際 base address
及 Virtual base,也就是被映射的 base address,這樣子找 gadget 很方便
physical base address
首先就算開啟了 CONFIG_STATIC_USERMODEHELPER 我們一樣可以寫掉 modprobe_path,也就是說通過嘗試寫掉 modprobe_path
之後去檢查 /proc/sys/kernel/modprobe
值是否維持 /usr/sbin/modprobe
如果被寫掉就代表我們知道了 modprobe_path physicall address
之後去扣 offset 就可以得到 physical base address 了
不過到這裡為止,對我來說 physical address 是個黑箱
這邊展開了解
首先可以先看 config
1 | naup@naup-virtual-machine:~/Desktop/pwn/devmem$ cat kernel.config | grep CONFIG_PHYSICAL |
config 中指定了 kernel physical start 位置在 0x100 0000
並且他會對齊 0x20 0000
先放一下我在 virtual memory manage 文章的圖
這兩個是甚麼意思可以來追 source code
https://elixir.bootlin.com/linux/v6.14-rc4/source/arch/x86/include/asm/page_types.h
1 |
Kernel physical and Virtual base relation
kernel code 段開始的位置在 0xffffffff80000000
kernel start 經過我們下方寫的腳本可以計算出實際落在 0xffffffff81000000,原因是因為要加上 physical address start
1 |
|
這個位置 0xffffffff81000000 是 kernel virtual memory base (nokaslr)
0x1000000 是 kernel physical memory base
我們現在已經揭開了黑箱的 address 映射關係了
接下來來嘗試爆破 physical address base
但 … 當我 exploit 到這裡時,踩了一個坑
我在自己 VM 上測試時怎麼樣都測不出來 KASLR 對於 physical address base 的影響
我在開 KASLR 時直接對 0x2dc6c20
寫入時
發現有改寫到 modprobe_path
也就代表實際上 KASLR 對於 physical address 沒有任何影響
但實則不然
原因是我上述的啟動腳本指定的 memory 空間太小了,導致 KASLR 沒有隨機化成功 physical address
應該改成
1 |
|
KASLR physical address
接下來想透過觀察得方式來觀察 physical address base 變化
第一次測試
1 | 00100000-3ffdffff : System RAM |
第二次測試
1 | 00100000-3ffdffff : System RAM |
第三次測試
1 | 00100000-3ffdffff : System RAM |
我們可以觀察到一件是
KASLR 去隨機化 physical address 時 xxx00000 (x 的部分是會被隨機化的)
0x2dc6c20 的 0xc6c20 是不會被影響的,需要去爆破上半部分 xxx 的部分
開寫腳本,這邊有個小細節,爆破應該從高位開始爆破,因為低位已經存在許多 kernel code 或是已經被使用的部分,去複寫那些地方都會導致 kernel panic
1 |
|
到目前為止已經成功 leak kernel physical base 了
virtual address base
接下來要嘗試 leak virtual address base
先來了解甚麼是 kptr_restrict
可以去嘗試印出 /proc/sys/kernel/kptr_restrict
上的值,通常都是一個數字
https://blog.csdn.net/gatieme/article/details/78311841
- 2: 內核將符號 (%Kp) 地址打印為全0,root 和普通用戶都沒有權限讀取。
- 1: root 用戶有權限讀取,普通用戶無權限讀取。
- 0: root 和普通用戶都可以讀取。
我們在一般使用者時,沒有權力去讀取 /proc/kallsyms
但我們現在有 kernel physical address 任意寫
直接去把 kptr_restrict
寫掉就可以了
抓 offset 可以這樣做cat /proc/kallsyms | grep kptr_restrict
這樣就可以把 kptr_restrict 寫掉了
1 | u_int64_t kptr_restrict_offset = 0x1ff49e0; |
到目前為止 leak 已經結束,接下來要想提權方法
1 |
|
PS: 如果想 check 你 leak 的 text virtual base address 對不對可以 attach 上去確認
這個有可執行權限,大概率是 text 段的 base
所以成功
Control RIP
為何要 control rip 先埋個梗
不能用 modprobe 提權那就想到,可以去 overwrite 自己這支 process 的 cred 來提權
不過在講提權之前想先來看 tty_struct
、/dev/ptmx
What is TTY device
TTY(全名:Teletypewriter)原本是指早期的打字機式終端,但在現代 Linux/UNIX 系統中,TTY 已泛指各種終端設備(Terminal Devices)
pty (虛擬終端)
遠程 telnet 到主機或使用 ssh 時也需要一個終端交互,這就是虛擬終端pty(pseudo-tty)
/dev/ptmx
/dev/ptmx 是一個 特殊的字元裝置檔案,它是 Linux 系統中與 pseudo-terminal(偽終端,PTY) 有關的核心元件之一
簡單來說,pty的實現方法
source code about ptmx
整個使用大致會長這樣
當我們 ssh 上去機器
- 去 open
/dev/ptmx
分配 master/slave 偽終端 - /dev/ptmx (master) and /dev/pts/N (slave) 相互通訊
- getty / login / shell
https://elixir.bootlin.com/linux/v6.14-rc4/source/drivers/tty/pty.c#L873
1 | // /drivers/tty/pty.c#L873 |
/dev/ptmx 初始化,並註冊於這段
https://elixir.bootlin.com/linux/v6.14-rc4/source/drivers/tty/pty.c#L790
1 | static int ptmx_open(struct inode *inode, struct file *filp) |
這邊會去 allocate 一塊 tty
https://elixir.bootlin.com/linux/v6.14-rc4/source/drivers/tty/tty_io.c#L182
1 | int tty_alloc_file(struct file *file) |
展開來他會去 allocate 一塊 tty_file_private
https://elixir.bootlin.com/linux/v6.14-rc4/source/include/linux/tty.h#L247
1 | /* Each of a tty's open files has private_data pointing to tty_file_private */ |
回到 ptmx_open()
1 | tty = tty_init_dev(ptm_driver, index); |
之後就是去 init tty deivce 跟 add file
這邊直接上調用鏈
1 | tty_open(struct inode *inode, struct file *filp) |
n_tty_ops
來看看 /dev/ptmx 去調用 vtable 時,是如何呼叫這張 vtable 上的 function pointer
https://elixir.bootlin.com/linux/v6.14-rc4/source/drivers/tty/tty_io.c#L2678
1 | struct tty_ldisc *ld; |
這位置是 call ioctl 會進入的地方
通過 vmlinux-to-elf 提取出 vmlinux (這種方法成功保留了 debug symbol)
gdb vmlinux 後去 break 在 tty_ioctl 後
進去看到他會從 n_tty_ops + 0x38 這裡拿出 n_tty_ioctl
就是這個,所以我們找出了他調用 function pointer 的地方
https://elixir.bootlin.com/linux/v6.14-rc4/source/drivers/tty/n_tty.c#L2525
1 | static struct tty_ldisc_ops n_tty_ops = { |
這部分是 n_tty_ioctl 被 call 的地方
這個 function __x86_indirect_thunk_rcx
會跳到 rcx 上
rcx 根據上面就是存 n_tty_ioctl address
這樣就跳上去了
接下來來 exploit
我們去 overwrite n_tty_ops 的 ioctl
分別位在
n_tty_ops = kernel physical base + 0x1eff4e0
ioctl = n_tty_ops + 0x38
我們寫上去 0xaabbccddeeff 時,kernel panic 的 error message 死在 0xaabbccddeef5
有我們寫上去的痕跡,表示我們已經成功 control rip 了
1 |
|
之前打過的題目也是通過 control ops vtable 上的 function pointer 來打 ret2usr 執行 commits_cred
不過看了一下題目 bzimage 的版本是 v6.14-rc4
這種攻擊方式已經失效了
我們需要尋找別種方式來 EoP
任意讀
到這裡其實就會有個想法
我們知道每個 process (task_struct),並且都有 struct cred 用來管理自己的權限
如果我們能找到自己這支 process 的 cred struct address
並寫掉 UID、GID
就可以成功提權
但我們要找到自己 process 的 task_struct 並不容易
想到的方法是通過 task_struct PID 識別
然後去 traverse 整條 task_struct
那我們就得先做出 kernel memory 任意讀
kernel memory 任意讀
到這裡來揭開 control rip 的用處,通過執行我們構造的 gadget
來做出 kernel memory 任意讀
其實這步我也想了非常久
想法是,我們去 overwrite n_tty_ioctl pointer 成 gadgetmov rax, [rdx]
1 | long tty_ioctl(struct file *file, unsigned int cmd, unsigned long arg) |
tty_ioctl 會傳入
- rdi : fd
- rsi : cmd
- rdx : arg
1 | ld->ops->ioctl(tty, cmd, arg); |
之後去 call ops.ioctl
他的 rdx arg 會被傳入
使用這條 gadget
我們把 args 改成我們想要讀的 address
他會把上面的值給 rax
https://courses.cs.washington.edu/courses/cse378/10au/sections/Section1_recap.pdf
根據 calling convention 可以知道 rax 還有做為存 return value 的功能
思考一下,我們 call ioctl 後在 kernel space 實現完功能後,會將 retval 回傳回 userspace 吧
那 rax 值就不應該被清空
所以 rax 就會被順著帶回 userspace
也就是執行完 ioctl 直接撈 rax 就好了
要找到這條 gadget 我是用 kropr
https://github.com/zolutal/kropr
PS: 這邊就順便構造 virtual memory 的任意寫,畢竟之後用 virtual memory 比較方便,用 mov [rdx], esi
1 | void aaw_virtual_memory_4(int fd, int cmd, u_int64_t w_w_address){ |
小坑點的 kernel panic
當我們寫掉 ioctl 成我們自己的 gadget 時,當 exploit 跑完時,會 kernel panic
原因是結束 exploit 時會去 call ioctl 之類的,而原來的 gadget 所用的 mov rax, [rdx]
會引用到 userspace 的 address,而觸發保護
這份 panic 可以看到 rcx 存的是 gadget address
rdx userspace address
可以印證是 crash 在哪
因此 exploit 結束後記得寫回來
最後將 pointer 寫回來
1 | u_int64_t mov_rdx_to_rax = mov_rdx_to_rax_offset + kernel_virtual_base; |
exploit 任意讀
https://elixir.bootlin.com/linux/v6.14-rc4/source/drivers/tty/tty_io.c#L2683
1 | int retval; |
ioctl return value 是 int,所以一次只能撈 4 byte
所以通過 overwrite n_tty_ioctl 成我們的 gadget,讓 rdx address 上的 value 給 rax,通過 calling convention,rax 是 return value 不會被清掉,就可以撈出 kernel memory 上的 value 了
1 |
|
EoP
我們知道每個 process (task_struct),並且都有 struct cred 用來管理自己的權限
如果我們能找到自己這支 process 的 cred struct address
並寫掉 UID、GID
就可以成功提權
但我們要找到自己 process 的 task_struct 並不容易
想到的方法是通過 task_struct PID 識別
然後去 traverse 整條 task_struct
先了解 task 這件事
init process
init process 是 linux 啟動的第一個 process (又稱 idle process)
在 start_kernel 時被創建的
start kernel 會去 init 很多不同的東西
主要的 OS 資料結構,基礎設施及子系統都由這邊進行 init
首先會先自己設定本身的 task_struct (init_task)
https://elixir.bootlin.com/linux/v6.14-rc4/source/init/main.c#L901
1 | set_task_stack_end_magic(&init_task); |
最後會呼叫 rest_init()
,到此已經完成了 OS 最核心部份的初始化,基本上 OS 已經算可以動了
https://elixir.bootlin.com/linux/v6.14-rc4/source/init/main.c#L1098
1 | /* Do the rest non-__init'ed, we're now alive */ |
rest_init()
會 create 兩個 process,kernel_init 及 kthreadd
https://elixir.bootlin.com/linux/v6.14-rc4/source/init/main.c#L743
1 | /* |
最後自己會 call 進去這裡,成為 pid = 0 的 idle process
https://elixir.bootlin.com/linux/v6.14-rc4/source/kernel/sched/idle.c#L417
1 | void cpu_startup_entry(enum cpuhp_state state) |
https://danielmaker.github.io/blog/linux/start_kernel.html
task_struct
留個坑之後補
總之 tasks 會以 double linklist 方式串起來
PID0 <-> PID1 <-> PID …
https://elixir.bootlin.com/linux/v6.14-rc4/source/include/linux/sched.h#L791
找 offset 問題
嘗試了很多方法都沒辦法找到 PID、cred、task linklist offset
所以覺得直接逆 vmlinux 來找 offset
PID 找這裡
https://elixir.bootlin.com/linux/v6.14-rc4/source/kernel/exit.c#L992
1 | if (unlikely(!tsk->pid)) |
從這個往上追判斷式
PID offset : 0x9d0
cred 找這裡
https://elixir.bootlin.com/linux/v6.14-rc4/source/kernel/cred.c#L117
1 | real_cred = (struct cred *) tsk->real_cred; |
0x17c * sizeof(QWORD) = 0xbe0
tasks linklist 找這裡
https://elixir.bootlin.com/linux/v6.14-rc4/source/kernel/sched/rt.c#L386
先找他下面一點的 pushable_tasks
1 | struct list_head tasks; |
這行上面有
1 | static void enqueue_pushable_task(struct rq *rq, struct task_struct *p) |
offset 是 0x910 - struct head size(他是兩個 8 byte pointer,所以是 0x910 - 0x10 = 0x900)
EoP success
總之最後就寫個 traverse double linklist 的 code 找到自己 process 的 cred,在通過 aar 讀出 cred address,之後 aaw 都寫成 0,提權到 root
之後開個 shell
之前看 exploit
PS: 有個奇怪的問題我沒解決,就是原本會 crash,但是我把全域變數搬進一部分成區域變數就可以了
反正執行完腳本就提權了
demo
exploit
這份 exploit 不是百分百成功,因為有可能出現剛好不在我們爆破的 physical address base 範圍
1 |
|
after all
學到非常多,拿到了 physical address 任意寫不一定很好 exploit
其中還需要 leak 很多東西,另外在 modprobe 這條鏈不能用的情況下,可以去 traverse task 來 overwrite 對應的 cred