- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
导读
在 Linux Kernel Pwn 的题目中,漏洞往往隐藏在内核模块(Loadable Kernel Module,LKM)中,而调试与利用的第一步,就是能够亲手把一个内核驱动编译为.ko文件并成功装载进目标内核。本文基于 ctf-wiki 仓库中「编译内核驱动」一章(build-kernel-module.md)展开,完整讲解驱动源码、Makefile 的编写方法,以及obj-m、KDIR、-C、M等关键参数的含义,并结合仓库中 QEMU 模拟环境、内核下载与编译 与 Kbuild 构建系统 等章节,带你走通「编写驱动 → 编译成.ko→ 装入 rootfs → 在 QEMU 中 insmod 加载并验证」的完整链路。读完本文,你将掌握内核模块的标准编译姿势,并能将其复用到任意 CTF 内核题目的环境搭建中。
为什么内核 pwn 要先学会编译驱动模块
在 基础知识点 一节中已经说明:Linux 内核采用宏内核(monolithic kernel)架构,为弥补可扩展性与可维护性不足,引入了**可装载内核模块(LKM)**机制。LKM 可以像积木一样被随时装载进内核、从内核中卸载,常见的 LKM 包括设备驱动、文件系统驱动和各类内核扩展模块。大多数 CTF 中的 kernel 漏洞就出现在 LKM 中——出题人会给你一个存在漏洞的驱动,再配上一个包含bzImage、rootfs.cpio、boot.sh的启动环境。
因此,编译并加载一个「测试驱动」是搭建内核调试环境的必经步骤:它既能验证你的内核源码与编译工具链是否可用,也能验证 rootfs 与 QEMU 启动脚本是否正常,为后续分析真实漏洞驱动打下基础。ctf-wiki 的 environment 目录 正是围绕「搭建内核调试分析环境」这一目标组织的,本文是其中的核心一环。
编写一个最小的内核驱动模块
要编译一个驱动,首先需要一份驱动源码。仓库给出的示例驱动ko_test代码结构非常精简,只包含模块的初始化函数与退出函数:
#include <linux/init.h> #include <linux/module.h> #include <linux/kernel.h> MODULE_LICENSE("Dual BSD/GPL"); static int ko_test_init(void) { printk("This is a test ko!\n"); return 0; } static void ko_test_exit(void) { printk("Bye Bye~\n"); } module_init(ko_test_init); module_exit(ko_test_exit);逐个解读这份源码中的要素:
#include <linux/init.h>与#include <linux/module.h>:前者提供module_init()/module_exit()宏与__init/__exit等标记,后者提供内核模块框架所需的基本声明;#include <linux/kernel.h>则提供printk等内核态函数原型。MODULE_LICENSE("Dual BSD/GPL"):声明模块许可证。不声明或声明了非 GPL 兼容许可证的模块,在加载时内核会将其标记为「污染内核」(taints kernel),并限制部分仅对 GPL 模块开放的符号(EXPORT_SYMBOL_GPL)的使用。ko_test_init():模块被装载时自动执行的初始化函数。它调用printk输出一条日志并返回 0,表示初始化成功。ko_test_exit():模块被卸载时自动执行的退出函数。module_init(ko_test_init);与module_exit(ko_test_exit);:这两个宏把上面的两个函数注册为内核模块的入口与出口。
与用户态printf不同,printk输出的内容不一定直接显示到终端,但一定会写入内核环形缓冲区,可通过dmesg查看——这一点在后续验证模块加载时会用到。仓库中更完整的版本(简体中文版 LKM 开发文档)还展示了带__init/__exit标记、MODULE_AUTHOR声明以及 Kbuild 组织方式的进阶写法,可作为扩展阅读。
编写驱动 Makefile
驱动源码写好后,还需要一个 Makefile 来驱动内核的模块构建系统。仓库给出的 Makefile 如下:
obj-m += ko_test.o KDIR =/home/iromise/dev/kernel/linux-5.4.98/ all: $(MAKE) -C $(KDIR) M=$(PWD) modules clean: rm -rf *.o *.ko *.mod.* *.symvers *.order仓库原文对其中三个核心要素做了说明,这里结合内核构建系统的行为进一步展开:
obj-m += ko_test.o:obj-m用来指定要编译成模块的目标列表。+=表示向该列表追加项,ko_test.o是源码ko_test.c对应的中间目标文件,内核的构建系统会最终把它链接成ko_test.ko。与之相对的是obj-y,它表示把目标**编译进内核镜像(vmlinux)**而不是独立模块。如果你有多个源文件(比如a.c、b.c共同构成模块ko_test),可以写成ko_test-objs := a.o b.o的形式,再由obj-m += ko_test.o引用。KDIR:标识内核源码目录,为驱动编译提供内核头文件、模块构建脚本与规则。仓库示例硬编码为/home/iromise/dev/kernel/linux-5.4.98/,对应前文内核下载与编译章节中下载的 5.4.98 内核源码。在通用 Linux 发行版上,更常见的做法是利用内核头文件包软链接:KDIR = /lib/modules/$(shell uname -r)/build,这样编译出的模块与当前运行内核版本严格匹配。需要注意的是,编译内核模块要求内核源码目录已经配置过(生成了 .config)并至少执行过一次模块编译准备,否则会缺少Module.symvers、include/generated/等构建产物;若使用自编译内核,编译前需先在内核源码目录执行make modules预处理。$(MAKE) -C $(KDIR) M=$(PWD) modules:这是整条命令的核心,三个要素缺一不可:-C $(KDIR):让 make 先进入内核源码目录,以该目录下的顶层 Makefile 作为构建入口。M=$(PWD):注意M并不是 Makefile 的常规选项,而是内核根目录下 Makefile 中使用的变量。它的作用是让内核构建系统在构造模块之前,返回到M所指向的目录(即驱动源码所在目录),并在该目录中生成驱动模块。这解释了为什么在驱动目录里敲一条make,最终.ko文件却产生在驱动源码目录下。modules:指定执行内核顶层 Makefile 中的modules目标,也就是执行内核模块的编译行为。
clean目标用于清理编译产物(*.o、*.ko、*.mod.*、*.symvers、*.order),在重新编译前执行可以避免旧产物干扰。仓库对应的简体中文版文档中给出了等价但更通用化的写法,并额外声明了.PHONY: clean伪目标,值得对照参考(见 Kbuild 构建系统)。
执行编译:从 .c 到 .ko
写好源码与 Makefile 后,在驱动源码目录下直接运行:
make由于all是 Makefile 中的第一个目标,直接执行make默认就会运行它。整个编译流程实际会经历:进入KDIR指定的内核目录 → 读取其顶层 Makefile → 根据M=$(PWD)回到驱动目录 → 由obj-m += ko_test.o驱动编译ko_test.c→ 链接生成ko_test.ko。
编译完成后,目录中会出现以下关键产物:
ko_test.ko:最终的内核模块文件。LKMs 与用户态可执行程序同样采用 ELF 格式,因此可以直接用 IDA、Ghidra 等逆向工具打开分析(参考 基础知识点 中的 LKM 章节)。ko_test.o:单个目标文件。ko_test.mod.o、ko_test.mod.c:模块元数据相关文件,记录了模块依赖、许可证、入口点等信息。Module.symvers、modules.order:内核构建系统生成的符号版本与构建顺序信息。
如果编译报错,优先检查三处:KDIR路径是否指向真实存在且已配置的内核源码;obj-m中的目标名是否与源文件名一致(ko_test.o对应ko_test.c);内核源码目录是否已经过make modules_prepare或完整编译。
装载、验证与卸载内核模块
模块编译成功后,可以在目标环境中用insmod直接装载:
insmod /ko_test.ko装载后,模块的初始化函数ko_test_init()会被调用,其printk输出进入内核缓冲区,可用dmesg查看。仓库在 QEMU 模拟环境 的「加载驱动」小节中给出了实际运行效果:
[ 2.019440] ko_test: loading out-of-tree module taints kernel. [ 2.020847] ko_test: module verification failed: signature and/or required key missing - tainting kernel [ 2.025423] This is a test ko!两行taints kernel警告是正常的:前者表示这是树外(out-of-tree)模块,后者表示模块缺少内核签名验证所需的密钥。第三行This is a test ko!正是ko_test_init()中printk的输出,说明模块已成功加载并执行了初始化代码。
与之配套的常用操作命令还包括(见 基础知识点):
rmmod ko_test:从内核卸载模块,触发ko_test_exit()(本示例会输出Bye Bye~)。lsmod:列出当前已加载的模块,确认驱动是否装载成功。modprobe:自动处理模块依赖关系的装载/卸载工具。
在 QEMU 内核调试环境中加载驱动
在真实 CTF 场景中,驱动通常是随内核一起在 QEMU 中启动的。完整的实践流程如下(详细步骤见 QEMU 模拟环境):
- 编译内核与构建 rootfs:按 内核下载与编译 编译出
bzImage(位于内核源码arch/x86/boot/bzImage)与vmlinux,并用 busybox 制作rootfs.img。 - 把驱动放进文件系统:将编译好的
ko_test.ko拷贝到 busybox 的_install目录下,再重新用find . | cpio -o --format=newc > ../rootfs.img打包。 - 修改 init 启动脚本:在 init 脚本中追加
insmod /ko_test.ko,使内核启动后自动装载驱动:
#!/bin/sh echo "INIT SCRIPT" mkdir /tmp mount -t proc none /proc mount -t sysfs none /sys mount -t devtmpfs none /dev mount -t debugfs none /sys/kernel/debug mount -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 poweroff -f- 启动并验证:用
qemu-system-x86_64加上-kernel ./bzImage -initrd ./rootfs.img -append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 kaslr" -smp cores=2,threads=1 -cpu kvm64启动后,进入 shell 执行dmesg即可看到上节所述的加载日志。
此外,System.map 使用指南 还补充了内核调试中的符号处理技巧:当vmlinux被 stripped 时,可用编译生成的System.map(每行记录「符号地址 / 符号类型 / 符号名」)通过脚本批量导入 IDA,恢复符号信息后再结合add-symbol-file ./your_module.ko addr_of_ko加载驱动模块的符号,展开后续的漏洞分析。这与你编译出的.ko文件直接相关——.ko的符号表正是分析驱动内部逻辑的第一手资料。
进阶:使用 Kbuild 组织模块源码
仓库的简体中文版 LKM 开发文档 展示了更工程化的组织方式:将源码放在src/子目录并编写src/Kbuild文件,与驱动根目录的通用 Makefile 分离:
# src/Kbuild MODULE_NAME ?= a3kmod obj-m += $(MODULE_NAME).o ccflags-y += -I$(src)/include $(MODULE_NAME)-y += main.oobj-m:指定要编译的模块列表,含义与本文第三部分一致。ccflags-y:编译选项,这里用-I$(src)/include引入自定义头文件目录。$(MODULE_NAME)-y:指明$(MODULE_NAME).o依赖的源文件(main.o对应main.c),多个源文件时逐行列出。
根目录 Makefile 则只负责转发构建请求,并优先使用发行版内核头文件软链接作为KDIR:
A3KMOD_ROOT_DIR=$(shell pwd) A3KMOD_SRC_DIR=$(A3KMOD_ROOT_DIR)/src LINUX_KERNEL_SRC=/lib/modules/$(shell uname -r)/build all: @$(MAKE) -C $(LINUX_KERNEL_SRC) M=$(A3KMOD_SRC_DIR) modules clean: @$(MAKE) -C $(LINUX_KERNEL_SRC) M=$(A3KMOD_SRC_DIR) clean .PHONY: clean对比两种组织方式可以看出:obj-m、-C、M=这几个要素是内核模块构建的通用骨架,KDIR 的指定则可根据环境灵活选择「自编译内核源码路径」或「/lib/modules/$(uname -r)/build」。理解了这一层,你在面对任何 CTF 内核题目时,都能快速为题目提供的驱动源码搭起编译环境,产出可供调试的.ko文件。
总结
从一份十几行的ko_test.c和一个四行核心的 Makefile 出发,本文完整走通了 Linux 内核驱动的编译全流程:obj-m声明模块目标、KDIR指定内核源码、-C与M=驱动内核构建系统在驱动目录产出.ko;随后通过insmod/dmesg验证加载、rmmod验证卸载,并进一步把驱动打包进 busybox rootfs、在 QEMU 启动脚本中自动加载,最终衔接System.map与符号导入完成可调试的内核环境闭环。这套流程是 ctf-wiki 内核调试环境搭建系列(environment 目录)的关键一步,也是后续阅读内核堆利用、ROP、提权等高级章节前必须掌握的技能。
- 文档
- 网络安全
- 教程
【免费下载链接】ctf-wiki
Come and join us, we need you!
相关推荐
CTF-Wiki 内核 Pwn 环境搭建(一):Linux 内核源码下载、签名校验与编译
CTF Wiki 内核 Pwn 环境搭建(一):Linux 内核源码下载、签名校验与编译 内核漏洞利用与调试(Kernel Pwn)的第一步,是获得一份「可调试
文档网络安全教程终极Linux内核模块编程指南:从零构建完整设备驱动项目
终极Linux内核模块编程指南:从零构建完整设备驱动项目 Linux内核模块编程是深入理解操作系统底层工作原理的关键途径。本指南基于最新5.x和6.x内核版本,
文档教程操作系统驱动开发KernelSU 内核构建指南:从 GKI 内核源码同步到集成编译的完整流程
KernelSU 内核构建指南:从 GKI 内核源码同步到集成编译的完整流程 导读 本文基于 KernelSU 官方文档 how to build.md htt
操作系统驱动开发
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考