Linux rootkit源码解析:内核攻防与系统调用劫持实战
发布时间:2026/10/29 1:22:50 锦皓数字建站

简介这是一份面向Linux内核安全初学者与系统编程爱好者的rootkit源码学习资料聚焦内核模块开发与系统底层机制探究。资源以C语言源码为核心配合Makefile构建脚本、Python辅助工具及说明文档完整呈现了一个小型rootkit从编译到加载的基本流程可用于理解模块隐藏、进程隐藏等典型功能的实现思路。压缩包共46个文件整体约36KB包含C源码、头文件、Makefile、Python脚本、Markdown说明及少量LaTeX论文素材结构紧凑适合逐文件研读。目前已有369人学习下载说明其在入门实践群体中具有一定参考价值。读者可借此熟悉内核模块的编译加载方式、系统调用挂钩的基本手法以及隐藏技术背后的核心逻辑为后续深入学习内核安全、反rootkit检测或系统加固打下基础也可作为课程实验与安全研究的对照样本。1. 一份 Linux rootkit 源码能让你看清内核攻防的底牌很多人第一次听说 Linux rootkit 源码脑子里蹦出来的是“黑客工具”“木马”“不敢碰”。但如果你在做内核开发、安全研究或者系统运维这份源码其实是一份极好的内核机制教材。它把 Linux 内核里那些平时藏在文档背后的东西——系统调用表、VFS 层、内核模块加载、/proc 文件系统——全部拉到台面上让你看到它们是怎么被“劫持”和“伪装”的。这份资源适合三类人想深入理解 Linux 内核行为的安全研究者、需要做主机入侵检测的运维工程师、以及正在学习内核模块编程的开发者。它不是一个能直接跑起来搞破坏的成品而是一套需要你读懂、编译、调试、观察的代码集合。你拿到的是一份“解剖图”不是一把“万能钥匙”。2. 先搞懂 rootkit 在内核里动了哪些手脚2.1 用户态与内核态的分界线Linux 把内存分成用户空间和内核空间用户程序跑在低权限的 Ring 3内核代码跑在高权限的 Ring 0。rootkit 源码的核心价值就是展示一个攻击者拿到 root 之后如何从用户态“钻进”内核态并在里面长期潜伏。常见做法是编译一个内核模块.ko 文件用insmod加载进去。模块一旦加载就和内核其他代码平起平坐能读写内核数据结构、注册钩子函数、甚至修改系统调用表。你读这份源码时第一遍不要纠结每一行汇编先找到模块入口函数module_init和出口函数module_exit看它在初始化时干了什么、退出时有没有清理干净。这是判断一份 rootkit 源码质量的基本功。2.2 系统调用表劫持的原理系统调用是用户程序进入内核的唯一正规通道。Linux 内核里有一张表sys_call_table里面按编号存放了所有系统调用处理函数的地址。rootkit 最经典的招式就是找到这张表把某个表项替换成自己的函数地址。比如把sys_read换成hacked_read用户执行cat /etc/passwd时实际调用的是 rootkit 的函数它可以在返回数据前把特定行过滤掉。源码里通常会看到kallsyms_lookup_name这个函数用来动态获取sys_call_table的地址因为从内核 2.6 之后这张表不再导出给模块直接使用。你需要在源码里找到这个查找逻辑理解它为什么依赖kallsyms以及不同内核版本下这个接口的变化。2.3 文件隐藏与进程隐藏的实现路径隐藏是 rootkit 的看家本领。文件隐藏一般通过劫持sys_getdents或sys_getdents64实现因为ls、find这些命令最终都靠这个系统调用来读取目录项。源码里会定义一个过滤规则比如文件名包含某个前缀就跳过不返回。进程隐藏则更复杂一些有的通过劫持sys_kill让kill -0失效有的直接修改task_struct链表把进程从/proc里摘出去。你在读这部分时重点看它怎么判断“要隐藏的目标”是硬编码文件名、PID还是通过某种通信机制动态下发。这决定了这份源码是教学演示还是具备实战能力。2.4 内核模块编译环境搭建要编译这份源码你需要一台 Linux 机器内核头文件必须和当前运行内核版本完全一致。用uname -r查看版本然后安装对应的linux-headers-$(uname -r)包。如果是 Ubuntu/Debian 系命令是sudo apt install linux-headers-$(uname -r)CentOS/RHEL 系则是sudo yum install kernel-devel-$(uname -r)。装完之后进入源码目录通常会有一个 Makefile里面用obj-m指定要编译的模块名。直接执行make如果一切顺利会生成.ko文件。这里有个血泪经验内核版本不匹配是编译失败的头号原因报错信息里出现invalid module format或者找不到Module.symvers基本都是头文件版本对不上。# 查看当前内核版本 uname -r # 安装对应版本的内核头文件Ubuntu/Debian sudo apt update sudo apt install linux-headers-$(uname -r) # 进入源码目录并编译 cd linux-rootkit-src make # 加载模块需要 root 权限 sudo insmod rootkit.ko # 查看模块是否加载成功 lsmod | grep rootkit # 卸载模块 sudo rmmod rootkit上面这段命令是编译和加载的最小闭环。make依赖源码目录里的 Makefile它通常长这样obj-m rootkit.o然后通过make -C /lib/modules/$(shell uname -r)/build M$(PWD) modules调用内核构建系统。insmod把模块插入内核lsmod确认它出现在列表里。rmmod负责卸载但很多 rootkit 源码在卸载时会故意不清理干净或者直接让模块无法卸载这是你需要警惕的地方。测试时建议在虚拟机里做快照因为一旦模块出问题轻则系统卡死重则直接 panic。3. 从源码到可运行模块编译、加载与行为观察3.1 源码目录结构与关键文件拿到一份 rootkit 源码先别急着make。用tree或者ls -R看一眼目录结构。典型的布局是一个主.c文件比如rootkit.c一个Makefile可能还有README和几个辅助头文件。主文件里会包含linux/module.h、linux/kernel.h、linux/syscalls.h这些内核头。你要找的第一个关键点是MODULE_LICENSE(GPL)没有这行内核会拒绝加载或者加载后报警告。第二个关键点是module_init和module_exit指定的函数名顺着这两个函数往下读就能理清整个模块的生命周期。有些源码会把功能拆成多个文件比如hide_file.c、hide_proc.c那就按功能模块逐个击破。3.2 编译参数与 Makefile 解读Makefile 是编译的指挥棒。除了obj-m你还会看到KERNELDIR变量指向/lib/modules/$(shell uname -r)/build这是内核源码树的路径。PWD是当前目录。make -C $(KERNELDIR) M$(PWD) modules这行命令的意思是切换到内核源码树目录利用那里的构建系统但只编译当前目录下的模块。如果你要交叉编译比如给 ARM 开发板编译需要指定ARCH和CROSS_COMPILE变量。编译过程中如果报error: implicit declaration of function ‘kallsyms_lookup_name’说明你的内核版本已经不再导出这个符号需要换用kprobes或者其他方式获取地址。这是新版内核上跑老 rootkit 源码最常见的翻车点。// 典型的模块初始化函数片段 static int __init rootkit_init(void) { // 获取 sys_call_table 地址 sys_call_table (unsigned long *)kallsyms_lookup_name(sys_call_table); if (!sys_call_table) { printk(KERN_ERR Failed to find sys_call_table\n); return -1; } // 保存原始系统调用 original_read (void *)sys_call_table[__NR_read]; // 关闭写保护替换系统调用 write_cr0(read_cr0() ~0x10000); sys_call_table[__NR_read] (unsigned long)hacked_read; write_cr0(read_cr0() | 0x10000); printk(KERN_INFO Rootkit loaded\n); return 0; }这段代码展示了系统调用劫持的核心步骤。kallsyms_lookup_name用来找到sys_call_table的内存地址。write_cr0操作的是控制寄存器 CR0 的写保护位第 16 位因为现代内核默认开启写保护不关掉它直接改系统调用表会触发 fault。original_read保存原始函数指针方便后续恢复。hacked_read是自定义的替换函数。注意printk的日志级别KERN_INFO会输出到内核日志用dmesg可以看到。这段代码在不同内核版本上可能需要调整比如write_cr0在新内核里可能被native_write_cr0替代或者干脆用set_memory_rw来修改页面属性。3.3 加载后的行为验证模块加载成功后你需要验证它到底起没起作用。如果源码实现了文件隐藏先创建一个测试文件比如touch /tmp/hidden_test然后ls /tmp看它还在不在。如果进程隐藏生效了ps aux里应该看不到某个特定进程。但要注意很多 rootkit 只隐藏特定前缀的文件你得先读源码里的过滤规则。验证时用dmesg -w实时看内核日志模块里的printk会告诉你它执行到了哪一步。如果加载后系统直接卡死大概率是系统调用表地址找错了或者替换函数里出现了空指针解引用。这时候只能重启所以再次强调虚拟机快照是你的后悔药。3.4 卸载与残留检查rmmod rootkit之后别以为就干净了。检查dmesg有没有报错lsmod确认模块已消失。然后手动验证被劫持的系统调用是否恢复如果之前隐藏了文件现在应该能看到了。有些 rootkit 会在卸载时故意不恢复系统调用表导致内核状态错乱。你可以在源码的module_exit函数里看它有没有把sys_call_table[__NR_read]重新指向original_read。如果没有那这份源码就是“一次性”的加载后只能重启。另外检查/proc/kallsyms里有没有残留的符号虽然模块卸载后符号通常会消失但有些 rootkit 会注册 kprobe 或者 tracepoint这些需要单独清理。4. 避坑与排查那些让你系统崩溃的瞬间4.1 编译报错 “invalid module format”现象insmod时提示invalid module formatdmesg里显示disagrees about version of symbol module_layout。原因编译模块用的内核头文件版本和当前运行内核版本不一致。解决用uname -r确认运行版本然后安装完全对应的linux-headers包。如果是在容器里编译容器内的内核版本可能和宿主机不同必须在宿主机上编译或者挂载宿主机的/lib/modules和/usr/src。4.2 加载后系统无响应或直接 panic现象insmod命令敲下去终端卡死SSH 断连虚拟机黑屏。原因系统调用表地址找错替换函数里访问了非法内存或者关闭写保护后没有及时恢复导致其他 CPU 核心崩溃。解决在虚拟机里操作提前拍快照。检查kallsyms_lookup_name的返回值是否为空在替换系统调用前加printk确认地址。如果是多核 CPU修改 CR0 的操作需要用stop_machine或者确保在单核模式下进行否则其他核心可能在你修改期间访问到半更新的表项。4.3 文件隐藏失效ls仍然能看到现象模块加载成功dmesg显示正常但目标文件依然出现在ls输出里。原因劫持的系统调用不对。现代 Linux 的ls和find用的是getdents64而你只劫持了getdents。或者过滤逻辑写错了比如比较文件名时用了strcmp但目标文件名带了路径前缀。解决用strace ls /tmp看实际调用的系统调用号确认是getdents64还是getdents。然后在源码里找到对应的__NR_getdents64宏替换正确的表项。过滤逻辑里打印出每次比较的文件名用dmesg观察。4.4 模块无法卸载 “Device or resource busy”现象rmmod报错Module rootkit is in use或者Device or resource busy。原因模块的引用计数没有归零通常是因为在初始化时注册了 kprobe、创建了 proc 文件或者占用了其他内核资源但没有在退出函数里释放。解决检查module_exit函数确保每一个register_都有对应的unregister_每一个create_都有对应的remove_。如果源码本身就没写清理逻辑那这个模块就是设计成不可卸载的只能重启。4.5 内核日志被刷屏或丢失现象dmesg里看不到自己加的调试信息或者被其他内核消息淹没。原因printk的日志级别太低或者 ring buffer 太小被冲掉。解决用printk(KERN_ERR ...)提高级别或者直接printk(KERN_EMERG ...)。也可以调整 ring buffer 大小echo 1048576 /sys/kernel/debug/tracing/buffer_size_kb如果 tracing 可用。更稳妥的办法是用trace_printk配合 ftrace但需要额外配置。5. 进阶用 ftrace 和 kprobes 动态观察 rootkit 行为读源码是一回事看它实际运行时内核里发生了什么是另一回事。ftrace 和 kprobes 是内核自带的动态追踪工具不需要你重新编译内核也不需要加载额外模块。你可以用它们来验证 rootkit 到底有没有成功劫持系统调用以及劫持后内核的执行路径变成了什么样。先看 ftrace 的基本用法。挂载 tracefsmount -t tracefs nodev /sys/kernel/tracing。然后设置要追踪的函数echo sys_read /sys/kernel/tracing/set_ftrace_filter开启追踪echo function /sys/kernel/tracing/current_tracer最后echo 1 /sys/kernel/tracing/tracing_on。这时候你执行cat /etc/passwdftrace 会记录下sys_read的调用。但如果 rootkit 已经把sys_read替换成了hacked_read你会发现 ftrace 里显示的函数名变成了hacked_read因为 ftrace 追踪的是实际被调用的函数地址。这是一个非常直接的验证手段。kprobes 更灵活可以在任意内核指令地址上插入探测点。比如你想知道sys_call_table在内存里的具体地址可以写一个简单的 kprobe 模块在kallsyms_lookup_name返回时打印结果。或者用perf probe命令行工具perf probe --add kallsyms_lookup_name然后perf record -e probe:kallsyms_lookup_name -a sleep 5最后perf script看输出。这样你不需要改一行 rootkit 源码就能观察到它初始化时调用了哪些内核函数。还有一个技巧用crash工具分析内核转储。如果你在虚拟机里让 rootkit 加载后触发一次 crash比如echo c /proc/sysrq-trigger会生成一个vmcore文件。用crash vmlinux vmcore打开然后sys_call_table命令可以直接打印系统调用表的当前内容对比原始内核的符号表就能精确看到哪些表项被替换了。这个方法适合做深度分析但需要你提前准备好vmlinux文件带调试符号的内核镜像。我自己的习惯是每拿到一份 rootkit 源码先在虚拟机里拍快照编译加载用 ftrace 确认劫持生效再用dmesg看模块日志最后rmmod并检查系统调用是否恢复。如果恢复失败直接回滚快照。这套流程走下来基本能摸清这份源码的底细。从那以后我每次分析内核模块都强制走一遍 ftrace 验证因为源码里写的和实际跑的有时候真的是两码事。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。