Kernel pwn CTF 入門 – 1
01簡介
內核 CTF 入門,主要參考 CTF-Wiki。
02環境配置
調試內核需要一個優秀的 gdb 插件,這里選用 gef。
根據其他師傅描述,peda 和 pwndbg 在調試內核時會有很多玄學問題。
pip3 install capstone unicorn keystone-engineroppergit clone https://github.com/hugsy/gef.gitechosource `pwd`/gef/gef.py >> ~/.gdbinit
去清華源下載 Linux kernel 壓縮包并解壓:
curl -O -Lhttps://mirrors.tuna.tsinghua.edu.cn/kernel/v5.x/linux-5.9.8.tar.xzunxz linux-5.9.8.tar.xztar -xf linux-5.9.8.tar
進入項目文件夾,進行 makefile 配置
cd linux-5.9.8make menuconfig
在其中勾選
Kernel hacking -> Compile-time checks and compiler options ->Compile the kernel with debug infoKernel hacking -> Generic Kernel Debugging Instruments -> KGDB:kernel debugger
之后保存配置并退出
開始編譯內核(默認 32 位)
make -j 8 bzImage
不推薦直接 make -j 8,因為它會編譯很多很多大概率用不上的東西。
這里有些小坑:
缺失依賴項。解決方法:根據 make 的報錯信息來安裝依賴項。
sudo apt-get install libelf-dev
make[1]: *** No rule to make target 'debian/certs/debian-uefi-certs.pem',needed by 'certs/x509_certificate_list'. Stop.解決方法:將 .config 中的 CONFIG_SYSTEM_TRUSTED_KEYS 內容置空,然后重新 make。
## Certificates for signature checking#CONFIG_SYSTEM_TRUSTED_KEYS=""#置空,不要刪除當前條目
等出現了以下信息后則編譯完成:
Setup is 15420 bytes (padded to 15872 bytes).System is 5520 kBCRC 70701790Kernel: arch/x86/boot/bzImage is ready (#2)
最后在啟動內核前,先構建一個文件系統,否則內核會因為沒有文件系統而報錯:
Kernel panic - not syncing: VFS: Unable to mount root fs onunknown-block(0,0)
首先下載一下 busybox 源代碼:
wgethttps://busybox.net/downloads/busybox-1.34.1.tar.bz2tar -jxf busybox-1.34.1.tar.bz2
之后配置 makefile:
cd busybox-1.34.1make menuconfigmake -j 8
在 menuconfig 頁面中,
Setttings 選中 Build static binary (no shared libs), 使其編譯成靜態鏈接的文件(因為 kernel 不提供 libc)需要注意的是,靜態編譯與鏈接需要額外安裝一個依賴項 glibc-static。使用以下命令安裝:
# redhat/centos系列安裝:sudo yum install glibc-static# debian/ubuntu系列安裝sudo apt-get install libc6-dev
在 Linux System Utilities 中取消選中 Supportmounting NFS file systems on Linux < 2.6.23 (NEW)
當前版本默認沒有選中該項,因此可以跳過。
編譯完成后,使用 make install命令,將生成文件夾_install,該目錄將成為我們的 rootfs。
接下來在 _install 文件夾下執行以創建一系列文件:
mkdir -p procsys dev etc/init.d
之后,在 rootfs 下(即 _install 文件夾下)編寫以下 init 掛載腳本:
#!/bin/shecho"INIT SCRIPT"mkdir /tmpmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs none /devmount -t debugfs none /sys/kernel/debugmount -t tmpfs none /tmpecho -e "Boot took $(cut -d' ' -f1/proc/uptime) seconds"setsid /bin/cttyhack setuidgid 1000 /bin/sh
最后設置 init 腳本的權限,并將 rootfs 打包:
chmod +x ./init# 打包命令find . | cpio -o --format=newc >../../rootfs.img# 解包命令# cpio -idmv < rootfs.img
busybox的編譯與安裝在構建 rootfs 中不是必須的,但還是強烈建議構建 busybox,因為它提供了非常多的有用工具來輔助使用 kernel。
使用 qemu 啟動內核。以下是 CTF wiki 推薦的啟動參數:
#!/bin/shqemu-system-x86_64 \ -m 64M\ -nographic \ -kernel./arch/x86/boot/bzImage \ -initrd ./rootfs.img \ -append"root=/dev/ram rw console=ttyS0 oops=panicpanic=1 nokaslr" \ -smpcores=2,threads=1 \ -cpukvm64
本著減少參數設置的目的,這是筆者的啟動參數:
qemu-system-x86_64 \-kernel ./arch/x86/boot/bzImage \-initrd ./rootfs.img \-append "nokaslr"
減少啟動的參數個數,可以讓我們在入門時,暫時屏蔽掉一些不必要的細節。
這里只設置了三個參數,其中:
-kernel 指定內核鏡像文件 bzImage 路徑
-initrd 設置內核啟動的內存文件系統
-append "nokaslr" 關閉 Kernel ALSR 以便于調試內核注意:nokaslr 可 千萬千萬千萬別打成 nokalsr 了。就因為這個我調試了一個下午的 kernel……
是的 CTF Wiki 上的 nokaslr 也是錯的,它打成了 nokalsr (xs)
啟動好后就可以使用內置的 shell 了。
03
內核驅動的編寫與調試
1.構建過程
這里我們在 linux kernel 項目包下新建了一個文件夾:
linux-5.9.8 $ mkdir mydrivers
之后在該文件夾下放入一個驅動代碼ko_test.c,代碼照搬的 CTF-wiki:
#include <linux/init.h>#include <linux/module.h>#include <linux/kernel.h>MODULE_LICENSE("DualBSD/GPL");staticint ko_test_init(void){printk("This is a testko!\n");return0;}staticvoid ko_test_exit(void){printk("ByeBye~\n");}module_init(ko_test_init);module_exit(ko_test_exit);
代碼編寫完成后,放入一個 Makefile文件:
# 指定聲稱哪些內核模塊obj-m += ko_test.o # 指定內核項目路徑KDIR =/usr/class/kernel_pwn/linux-5.9.8 all:# -C 參數指定進入內核項目路徑# -M 指定驅動源碼的環境,使 Makefile 在構建模塊之前返回到驅動源碼目錄,并在該目錄中生成驅動模塊$(MAKE) -C $(KDIR) M=$(PWD) modules clean:rm -rf *.o *.ko *.mod.* *.symvers *.order
注意點:
1.Makefile 文件名中的首字母 M 一定是大寫,否則會報以下錯誤:
scripts/Makefile.build:44:/usr/class/kernel_pwn/linux-5.9.8/mydrivers/Makefile: No such file or directorymake[2]: *** No rule to make target '/usr/class/kernel_pwn/linux-5.9.8/mydrivers/Makefile'. Stop.
2.Makefile 中 obj-m 要與剛剛的驅動代碼文件名所對應,否則會報以下錯誤:
make[2]: *** No rule to make target '/usr/class/kernel_pwn/linux-5.9.8/mydrivers/ko_test.o', needed by '/usr/class/kernel_pwn/linux-5.9.8/mydrivers/ko_test.mod'. Stop.
3.如果make時遇到以下錯誤:
makefile:6: *** missing separator. Stop.
則使用 vim 打開Makefile,鍵入 i 以進入輸入模式,然后替換掉 make 命令前的前導空格為 tab,最后鍵入 :wq 保存修改。
最后使用 make 即可編譯驅動。完成后的目錄內容如下所示:
這里我們只關注 ko_test.ko。
$ tree .├── ko_test.c├── ko_test.ko├── ko_test.mod├── ko_test.mod.c├── ko_test.mod.o├── ko_test.o├── Makefile├── modules.order└── Module.symvers 0 directories, 9 files
2.運行過程
將新編譯出來的 *.ko 文件復制進 rootfs 文件夾(busybox-1.34.1/_install)下,
之后修改 busybox-1.34.1/_install/init 腳本中的內容:
這里需要提權 /bin/sh,目的是為了使用 root 權限啟動 /bin/sh,使得擁有執行 dmesg 命令的權限。
#!/bin/shecho "INIT SCRIPT"mkdir /tmpmount -t proc none /procmount -t sysfs none /sysmount -t devtmpfs none /devmount -t debugfs none /sys/kernel/debugmount -t tmpfs none /tmp+ insmod /ko_test.ko # 掛載內核模塊echo -e "Boot took $(cut -d' ' -f1/proc/uptime) seconds"setsid /bin/cttyhack setuidgid 1000 /bin/sh+ setsid /bin/cttyhack setuidgid 0 /bin/sh # 修改uid gid 為0 以提權/bin/sh 至root。+ poweroff -f # 設置 shell 退出后則關閉機器
重新打包 rootfs 并運行 qemu,之后鍵入 dmesg 命令即可看到 ko_test 模塊已被成功加載:

正常情況下,執行 qemu 會彈出一個小框 GUI。若想像上圖一樣將啟動的界面變成當前終端,則需在 qemu 啟動時額外指定參數:
-nographic-append"console=ttyS0"
3.調試過程
a.attach qemu
調試時最好使用 root 權限執行 /bin/sh,相關修改方法已經在上面說明,此處暫且不表。
在啟動 qemu 時,額外指定參數 -gdb tcp::1234 (或者等價的-s),之后 qemu 將做好 gdb attach 的準備。如果希望 qemu 啟動后立即掛起,則必須附帶 -S 參數。
同時,調試內核時,為了加載 vmlinux 符號表,必須額外指定 -append"nokaslr"以關閉 kernel ASLR。這樣符號表才能正確的對應至內存中的指定位置,否則將無法給目標函數下斷點。
qemu啟動后,必須另起一個終端,鍵入 gdb -q -ex"target remote localhost:1234",即可 attach 至 qemu上。
gdb attach 上 qemu 后,可以加載 vmlinux 符號表、給特定函數下斷點,并輸入 continue 以執行至目標函數處。
# qemu 指定 -S 參數后掛起,此時在gdb鍵入以下命令gef> add-symbol-file vmlinuxgef> b start_kernelgef> continue [Breakpoint 1, start_kernel () atinit/main.c:837]......
對于內核中的各個符號來說,我們也可以通過以下命令來查看一些符號在內存中的加載地址:
# grep <symbol_name> /proc/kalsymsgrep prepare_kernel_cred /proc/kallsymsgrep commit_creds /proc/kallsymsgrep ko_test_init /proc/kallsyms
坑點1:之前筆者編寫了以下 shell 腳本:
# 其他設置[...]# **后臺**啟動qemuqemu-system-x86_64 [other args] &# 直接在當前終端打開 GDBgdb -q -ex "targetremote localhost:1234"
但在執行腳本時,當筆者在 GDB 中鍵入 Ctrl+C 時, SIGINT 信號將直接終止 qemu 而不是掛起內部的 kernel。因此,gdb必須在另一個終端啟動才可以正常處理 Ctrl+C。
正確的腳本如下:
# 其他設置[...]# **后臺**啟動qemuqemu-system-x86_64 [other args] &# 開啟新終端,在新終端中打開 GDBgnome-terminal -e 'gdb-q -ex "target remote localhost:1234"'
坑點2:對于 gdb gef插件來說,最好不要使用常規的target remote localhost:1234語句(無需root權限)來連接遠程,否則會報以下錯誤:
gef? target remote localhost:1234Remote debugging using localhost:1234warning: No executable has been specified andtarget does not supportdetermining executable automatically. Try using the "file"command.0x000000000000fff0 in ?? ()[ Legend: Modified register | Code | Heap | Stack| String ]──────────────────────────────────── registers────────────────────────────────────[!] Command 'context' failed to execute properly, reason: 'NoneType' object has no attribute 'all_registers'
與之相對的,使用效果更好的 gef-remote 命令(需要root權限)連接 qemu:
# 一定要提前指定架構set architecturei386:x86-64gef-remote --qemu-mode localhost:1234
坑點3:如果 qemu 斷在 start_kernel時 gef 報錯:
[!] Command 'context' failed to execute properly, reason: max() arg is an empty sequence
直接單步 ni 一下即可。
b.attach drivers
1)常規步驟
首先,將目標驅動加載進內核中:
insmod <driver_module_name>
之后,通過以下命令查看 qemu 中內核驅動的 text 段的裝載基地址:
# 查看裝載驅動lsmod# 獲取驅動加載的基地址grep <target_module_name> /proc/modules
在 gdb 窗口中,鍵入以下命令以加載調試符號:
add-symbol-file mydrivers/ko_test.ko<ko_test_base_addr> [-s <section1_name> <section1_addr>] ...
注,與 vmlinux 不同,使用add-symbol-file 加載內核模塊符號時,必須指定內核模塊的 text 段基地址。
因為內核位于眾所周知的虛擬地址(該地址與 vmlinux elf 文件的加載地址相同),但內核模塊只是一個存檔,不存在有效加載地址,只能等到內核加載器分配內存并決定在哪里加載此模塊的每個可加載部分。因此在加載內核模塊前,我們無法得知內核模塊將會加載到哪塊內存上。故將符號文件加載進 gdb 時,我們必須盡可能顯式指定每個 section 的地址。
需要注意的是,加載符號文件時,越多指定每個 section 的地址越好。否則如果只單獨指定了 .text 段的基地址,則有可能在給函數下斷點時斷不下來,非常影響調試。
如何查看目標內核模塊的各個 section 加載首地址呢?請執行以下命令:
grep "0x" /sys/module/ko_test/sections/.*
2)例子
一個小小例子:調試 ko_test.ko 的步驟如下:
首先在 qemu 中的 kernel shell 執行以下命令
# 首先裝載 ko_test 進內核中insmod /ko_test.ko# 查看當前 ko_test 裝載的地址grep ko_test /proc/modulesgrep "0x" /sys/module/ko_test/sections/.*
輸出如下:

記錄下這些地址,之后進入 gdb 中,先按下 Ctrl+C 斷下 kernel,然后鍵入以下命令:
# 將對應符號加載至該地址處add-symbol-file mydrivers/ko_test.ko 0xffffffffc0002000 \ -s .rodata.str1.10xffffffffc000304c \ -s .symtab 0xffffffffc0007000 \ -s .text.unlikely0xffffffffc0002000# 下斷點b ko_test_initb ko_test_exit# 使其繼續執行continue

最后回到 qemu 中,在 kernel shell 中執行以下命令:
# 卸載 ko_testrmmod ko_tes
此時 gdb 會斷到 ko_test_exit 中:

如果在卸載了ko_test后,又重新加載 ko_test,
insmod ko_test
則 gdb 會立即斷到 ko_test_init 中:

這可能是因為指定了 nokaslr,使得相同驅動多次加載的基地址是一致的。
上面調試 kernel module 的 init 函數方法算是一個小 trick,它利用了 noaslr 環境下相同驅動重新加載的基地址一致 的原理來下斷。但最為正確的調試 init 函數的方式,還是得跟蹤 do_init_module 函數的控制流來獲取基地址。以下是一系列相關操作步驟:
跟蹤 do_init_module 函數是因為它在 load_module 函數中被調用。load_module函數將在完成大量的內存加載工作后,最后進入do_init_module 函數中執行內核模塊的 init 函數,并在其中進行善后工作。
load_module函數將被作為 SYSCALL函數的 init_module調用。
首先讓 kernel 跑飛,等到 kernel 加載完成,shell 界面顯示后,gdb 按下 ctrl + C 斷下,給 do_init_module函數下斷。該函數的前半部分將會執行內核模塊的 init 函數:
/* * This iswhere the real work happens. * * Keep ituninlined to provide a reliable breakpoint target, e.g. for the gdb * helpercommand 'lx-symbols'. */staticnoinline int do_init_module(struct module *mod){ [...] /* Start the module */ if(mod->init != NULL) ret =do_one_initcall(mod->init); // <- 此處執行 ko_test_init 函數 if(ret < 0) { gotofail_free_freeinit; } [...]}
gdb 鍵入 continue 再讓 kernel 跑飛。之后kernel shell 中輸入 insmod/ko_test.ko裝載內核模塊,此時gdb會斷下。在 gdb 中查看 mod->init 成員即可查看到 kernel moduleinit 函數的首地址。

要想看到當前 kernel module 的全部 section 地址,可以在 gdb 中鍵入以下命令
# 查看當前 module 的 sections 個數p mod->sect_attrs->nsections# 查看第 3 個section 信息p mod->sect_attrs->attrs[2]

有了當前內核模塊的全部 section 名稱與基地址后,就可以按照之前的方法來加載符號文件了。
c.啟動腳本
配環境真是一件麻煩到極點的事情,不過目前就到此為止了?
筆者將一系列啟動命令整合成了一個 shell 腳本,方便一鍵運行:
#! /bin/bash # 判斷當前權限是否為 root,需要高權限以執行 gef-remote --qemu-modeuser=$(env | grep "^USER" | cut -d "="-f 2)if [ "$user" != "root" ]thenecho"請使用root 權限執行"exitfi # 復制驅動至 rootfscp ./mydrivers/*.ko busybox-1.34.1/_install # 構建 rootfspushdbusybox-1.34.1/_installfind . | cpio -o --format=newc >../../rootfs.imgpopd # 啟動 qemuqemu-system-x86_64 \-kernel ./arch/x86/boot/bzImage \-initrd ./rootfs.img \-append "nokaslr" \-s \-S& # -s :等價于-gdb tcp::1234,指定qemu 的調試鏈接# -S :指定 qemu 啟動后立即掛起 # -nographic # 關閉QEMU 圖形界面# -append "console=ttyS0" # 和 -nographic 一起使用,啟動的界面就變成了當前終端 gnome-terminal -e 'gdb-x mygdbinit'
gdbinit 內容如下:
set architecturei386:x86-64add-symbol-file vmlinuxgef-remote --qemu-mode localhost:1234 b start_kernelc