Remco SprootenRuben Groenewoud

解除 PUMAKIT 的武装

PUMAKIT 是一款复杂的、可加载内核模块 (LKM) 的 rootkit,它采用先进的隐蔽机制来隐藏其存在并保持与命令和控制服务器的通信。

阅读时长 30 分钟恶意软件分析
Declawing PUMAKIT

PUMAKIT 概览

PUMAKIT 是一种复杂的恶意软件,最初是在 VirusTotal 上进行例行威胁搜寻时发现的,并以其二进制文件中发现的开发人员嵌入字符串命名。其多阶段架构包括一个投放器 (cron)、两个内存驻留可执行文件 (/memfd:tgt/memfd:wpn)、一个 LKM rootkit 模块和一个共享对象 (SO) 用户区 rootkit。

恶意软件作者称为 “PUMA” 的 rootkit 组件使用内部 Linux 函数跟踪器 (ftrace) 来挂钩 18 个不同的系统调用和几个内核函数,使其能够操纵核心系统行为。使用独特的方法与 PUMA 交互,包括使用 rmdir() 系统调用进行权限提升,以及使用专用命令来提取配置和运行时信息。通过其分阶段部署,LKM rootkit 确保它仅在满足特定条件(例如,安全启动检查或内核符号可用性)时激活。这些条件通过扫描 Linux 内核来验证,并且所有必要的文件都作为 ELF 二进制文件嵌入在投放器中。

内核模块的主要功能包括权限提升、隐藏文件和目录、隐藏自身(防止被系统工具检测到)、反调试措施,以及与命令和控制 (C2) 服务器建立通信。

主要结论

  • 多阶段架构:该恶意软件结合了一个投放器、两个内存驻留可执行文件、一个 LKM rootkit 和一个 SO 用户区 rootkit,仅在特定条件下激活。
  • 高级隐蔽机制:使用 ftrace() 挂钩 18 个系统调用和几个内核函数,以隐藏文件、目录和 rootkit 本身,同时躲避调试尝试。
  • 独特的权限提升:利用非常规的挂钩方法(如 rmdir() 系统调用)来提升权限并与 rootkit 交互。
  • 关键功能:包括权限提升、C2 通信、反调试和系统操纵,以保持持久性和控制。

PUMAKIT 的发现

在 VirusTotal 上进行例行威胁搜寻期间,我们发现了一个名为 cron 的有趣二进制文件。该二进制文件于 2024 年 9 月 4 日首次上传,检测结果为 0,这引起了人们对其潜在隐蔽性的怀疑。经过进一步检查,我们发现了另一个相关工件,/memfd:wpn(已删除)71cc6a6547b5afda1844792ace7d5437d7e8d6db1ba995e1b2fb760699693f24,它是在同一天上传的,检测结果也为 0。

引起我们注意的是这些二进制文件中嵌入的独特字符串,暗示可能操纵 /boot/ 中的 vmlinuz 内核包。这促使我们对这些样本进行了更深入的分析,从而获得了关于其行为和目的的有趣发现。

PUMAKIT 代码分析

PUMAKIT,以其嵌入的 LKM rootkit 模块(恶意软件作者命名为“PUMA”)和 SO 用户区 rootkit Kitsune 命名,它采用多阶段架构,从启动执行链的投放器开始。该过程从 cron 二进制文件开始,该文件创建两个内存驻留可执行文件:/memfd:tgt(已删除)/memfd:wpn(已删除)。虽然 /memfd:tgt 用作良性的 Cron 二进制文件,但 /memfd:wpn 用作 rootkit 加载器。加载器负责评估系统条件、执行临时脚本 (/tmp/script.sh) 并最终部署 LKM rootkit。LKM rootkit 包含一个嵌入的 SO 文件 - Kitsune - 以便从用户空间与 rootkit 交互。此执行链如下所示。

这种结构化设计使 PUMAKIT 仅在满足特定条件时才执行其有效负载,从而确保隐蔽性并降低检测的可能性。该过程的每个阶段都经过精心设计,以隐藏其存在,利用内存驻留文件并精确检查目标环境。

在本节中,我们将深入探讨不同阶段的代码分析,探索其组件及其在实现这种复杂的多阶段恶意软件中的作用。

阶段 1:Cron 概述

cron 二进制文件充当投放器。下面的函数充当 PUMAKIT 恶意软件样本中的主要逻辑处理程序。其主要目标是

  1. 检查命令行参数中是否包含特定关键字 ("Huinder")。
  2. 如果未找到,则完全从内存中嵌入并运行隐藏的有效负载,而无需将其放入文件系统。
  3. 如果找到,则处理特定的“提取”参数,将其嵌入的组件转储到磁盘,然后正常退出。

简而言之,恶意软件试图保持隐蔽。如果通常运行(没有特定参数),它将执行隐藏的 ELF 二进制文件,而不会在磁盘上留下任何痕迹,这可能会伪装成合法进程(例如 cron)。

如果参数中未找到字符串 Huinder,则执行 if (!argv_) 中的代码

writeToMemfd(...):这是无文件执行的一个标志。memfd_create允许二进制文件完全存在于内存中。恶意软件将其嵌入的有效载荷(tgtElfpwpnElfp)写入匿名文件描述符,而不是将其放到磁盘上。

fork()execveat():恶意软件fork出一个子进程和父进程。子进程将其标准输出和错误重定向到/dev/null,以避免留下日志,然后使用execveat()执行“武器”有效载荷(wpnElfp)。父进程等待子进程,然后执行“目标”有效载荷(tgtElfp)。这两个有效载荷都是从内存中执行的,而不是从磁盘上的文件中执行的,这使得检测和取证分析更加困难。

选择execveat()很有意思——这是一个较新的系统调用,允许执行由文件描述符引用的程序。这进一步支持了该恶意软件执行的无文件性质。

我们已经确定tgt文件是一个合法的cron二进制文件。它在rootkit加载器(wpn)执行后加载到内存并执行。

执行后,二进制文件在主机上保持活动状态。

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

以下是此进程的文件描述符列表。这些文件描述符显示了由dropper创建的驻留在内存中的文件。

root@debian11-rg:/tmp# ls -lah /proc/2138/fd
total 0
dr-x------ 2 root root  0 Dec  6 09:57 .
dr-xr-xr-x 9 root root  0 Dec  6 09:57 ..
lr-x------ 1 root root 64 Dec  6 09:57 0 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 1 -> /dev/null
l-wx------ 1 root root 64 Dec  6 09:57 2 -> /dev/null
lrwx------ 1 root root 64 Dec  6 09:57 3 -> '/memfd:tgt (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 4 -> '/memfd:wpn (deleted)'
lrwx------ 1 root root 64 Dec  6 09:57 5 -> /run/crond.pid
lrwx------ 1 root root 64 Dec  6 09:57 6 -> 'socket:[20433]'

按照引用,我们可以看到加载到示例中的二进制文件。我们可以简单地将字节复制到一个新文件中,以便使用偏移量和大小进行进一步分析。

提取后,我们发现以下两个新文件

  • Wpn: cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfe
  • Tgt: 934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136

我们现在有了这两个内存文件的转储。

第二阶段:内存驻留可执行文件概述

检查/memfd:tgt ELF文件,很明显这是一个默认的Ubuntu Linux Cron二进制文件。该二进制文件似乎没有经过任何修改。

/memfd:wpn文件更有趣,因为它是负责加载LKM rootkit的二进制文件。此rootkit加载器试图通过将其伪装成/usr/sbin/sshd可执行文件来隐藏自己。它检查特定的先决条件,例如是否启用了安全启动以及所需的符号是否可用,如果所有条件都满足,则加载内核模块rootkit。

查看Kibana中的执行情况,我们可以看到该程序通过查询dmesg来检查是否启用了安全启动。如果满足正确的条件,则会在/tmp目录中放置并执行一个名为script.sh的shell脚本。

此脚本包含基于其压缩格式检查和处理文件的逻辑。

这是它的作用

  • 函数c()使用file命令检查文件,以验证它们是否为ELF二进制文件。如果不是,则该函数返回错误。
  • 函数d()尝试使用各种实用程序(如gunzipunxzbunzip2等)基于支持的压缩格式的签名来解压缩给定的文件。它使用greptail来定位和提取特定的压缩段。
  • 该脚本尝试定位和处理文件($i)到/tmp/vmlinux

执行/tmp/script.sh后,文件/boot/vmlinuz-5.10.0-33-cloud-amd64用作输入。tr命令用于定位gzip的魔术数字(\037\213\010)。随后,使用tail提取从字节偏移量+10957311开始的文件部分,使用gunzip解压缩,并另存为/tmp/vmlinux。然后验证生成的文件以确定它是否是有效的ELF二进制文件。

重复此序列多次,直到脚本中的所有条目都已传递到函数d()

d '\037\213\010' xy gunzip
d '\3757zXZ\000' abcde unxz
d 'BZh' xy bunzip2
d '\135\0\0\0' xxx unlzma
d '\211\114\132' xy 'lzop -d'
d '\002!L\030' xxx 'lz4 -d'
d '(\265/\375' xxx unzstd

此过程如下所示。

在运行完脚本中的所有项目后,将删除/tmp/vmlinux/tmp/script.sh文件。

该脚本的主要目的是验证是否满足特定条件,如果满足,则使用内核对象文件设置用于部署rootkit的环境。

如上图所示,加载器在Linux内核文件中查找__ksymtab__kcrctab符号,并存储偏移量。

一些字符串显示,rootkit开发人员在其dropper中将其rootkit称为“PUMA”。根据条件,该程序会输出诸如以下消息:

PUMA %s
[+] PUMA is compatible
[+] PUMA already loaded

此外,内核对象文件包含一个名为.puma-config的部分,这加强了与rootkit的关联。

第三阶段:LKM rootkit概述

在本节中,我们将仔细研究内核模块,以了解其底层功能。具体来说,我们将检查其符号查找功能、钩子机制以及它修改以实现其目标的关键系统调用。

LKM rootkit概述:符号查找和钩子机制

LKM rootkit操纵系统行为的能力始于它对系统调用表的使用,以及它对kallsyms_lookup_name()进行符号解析的依赖。与针对5.7及以上内核版本的现代rootkit不同,该rootkit不使用kprobes,这表明它是为较旧的内核设计的。

这个选择很重要,因为在内核版本5.7之前,kallsyms_lookup_name()是导出的,并且可以被模块轻松利用,即使是没有适当许可的模块也是如此。

在2020年2月,内核开发人员讨论了取消导出kallsyms_lookup_name(),以防止未经授权或恶意模块滥用。一种常见的策略是添加虚假的MODULE_LICENSE("GPL")声明以规避许可检查,从而允许这些模块访问未导出的内核函数。LKM rootkit演示了这种行为,从其字符串中可以明显看出

name=audit
license=GPL

这种对GPL许可证的欺诈性使用确保rootkit可以调用kallsyms_lookup_name()来解析函数地址并操纵内核内部。

除了其符号解析策略外,内核模块还采用ftrace()钩子机制来建立其钩子。通过利用ftrace(),rootkit有效地拦截系统调用,并将其处理程序替换为自定义钩子。

例如,上面的代码片段中显示了使用unregister_ftrace_functionftrace_set_filter_ip的证据。

LKM rootkit概述:已挂钩的系统调用概述

我们分析了rootkit的系统调用钩子机制,以了解PUMA对系统功能的干扰范围。下表总结了rootkit挂钩的系统调用、相应的挂钩函数及其潜在用途。

通过查看 cleanup_module() 函数,我们可以看到 ftrace() 钩子机制是如何通过使用 unregister_ftrace_function() 函数来撤销的。这保证了回调不再被调用。之后,所有的系统调用都将返回到原始的系统调用,而不是被钩住的系统调用。这给了我们一个清晰的被钩住的所有系统调用的概览。

在接下来的章节中,我们将更仔细地研究一些被钩住的系统调用。

LKM rootkit 概述: rmdir_hook()

内核模块中的 rmdir_hook() 在 rootkit 的功能中起着至关重要的作用,它使其能够操纵目录删除操作以进行隐藏和控制。此钩子不仅限于拦截 rmdir() 系统调用,而且还将其功能扩展到强制特权提升和检索存储在特定目录中的配置详细信息。

此钩子有几个检查机制。该钩子期望 rmdir() 系统调用的前几个字符为 zarya。如果满足此条件,则被钩住的函数会检查第 6 个字符,该字符是要执行的命令。最后,检查第 8 个字符,该字符可以包含要执行的命令的进程参数。结构如下所示:zarya[字符][命令][字符][参数]。任何特殊字符(或没有)都可以放在 zarya 与命令和参数之间。

截至发布日期,我们已识别出以下命令

命令目的
zarya.c.0检索配置
zarya.t.0测试工作状态
zarya.k.<pid>隐藏 PID
zarya.v.0获取正在运行的版本

在 rootkit 初始化时,使用 rmdir() 系统调用钩子来检查 rootkit 是否已成功加载。它通过调用 t 命令来完成此操作。

ubuntu-rk:~$ rmdir test
rmdir: failed to remove 'test': No such file or directory
ubuntu-rk:~$ rmdir zarya.t
ubuntu-rk:~$

在不存在的目录上使用 rmdir 命令时,会返回错误消息“没有这样的文件或目录”。在 zarya.t 上使用 rmdir 时,不返回任何输出,这表明内核模块已成功加载。

第二个命令是 v,用于获取正在运行的 rootkit 的版本。

ubuntu-rk:~$ rmdir zarya.v
rmdir: failed to remove '240513': No such file or directory

不是将 zarya.v 添加到“删除'目录'失败”错误中,而是返回 rootkit 版本 240513

第三个命令是 c,用于打印 rootkit 的配置。

ubuntu-rk:~/testing$ ./dump_config "zarya.c"
rmdir: failed to remove '': No such file or directory
Buffer contents (hex dump):
7ffe9ae3a270  00 01 00 00 10 70 69 6e 67 5f 69 6e 74 65 72 76  .....ping_interv
7ffe9ae3a280  61 6c 5f 73 00 2c 01 00 00 10 73 65 73 73 69 6f  al_s.,....sessio
7ffe9ae3a290  6e 5f 74 69 6d 65 6f 75 74 5f 73 00 04 00 00 00  n_timeout_s.....
7ffe9ae3a2a0  10 63 32 5f 74 69 6d 65 6f 75 74 5f 73 00 c0 a8  .c2_timeout_s...
7ffe9ae3a2b0  00 00 02 74 61 67 00 08 00 00 00 67 65 6e 65 72  ...tag.....gener
7ffe9ae3a2c0  69 63 00 02 73 5f 61 30 00 15 00 00 00 72 68 65  ic..s_a0.....rhe
7ffe9ae3a2d0  6c 2e 6f 70 73 65 63 75 72 69 74 79 31 2e 61 72  l.opsecurity1.ar
7ffe9ae3a2e0  74 00 02 73 5f 70 30 00 05 00 00 00 38 34 34 33  t..s_p0.....8443
7ffe9ae3a2f0  00 02 73 5f 63 30 00 04 00 00 00 74 6c 73 00 02  ..s_c0.....tls..
7ffe9ae3a300  73 5f 61 31 00 14 00 00 00 73 65 63 2e 6f 70 73  s_a1.....sec.ops
7ffe9ae3a310  65 63 75 72 69 74 79 31 2e 61 72 74 00 02 73 5f  ecurity1.art..s_
7ffe9ae3a320  70 31 00 05 00 00 00 38 34 34 33 00 02 73 5f 63  p1.....8443..s_c
7ffe9ae3a330  31 00 04 00 00 00 74 6c 73 00 02 73 5f 61 32 00  1.....tls..s_a2.
7ffe9ae3a340  0e 00 00 00 38 39 2e 32 33 2e 31 31 33 2e 32 30  ....89.23.113.20
7ffe9ae3a350  34 00 02 73 5f 70 32 00 05 00 00 00 38 34 34 33  4..s_p2.....8443
7ffe9ae3a360  00 02 73 5f 63 32 00 04 00 00 00 74 6c 73 00 00  ..s_c2.....tls..

由于有效载荷以空字节开头,因此通过 rmdir shell 命令运行 zarya.c 时,不会返回任何输出。通过编写一个包装系统调用并打印十六进制/ASCII 表示的小型 C 程序,我们可以看到返回的 rootkit 配置。

rootkit 没有使用 kill() 系统调用来获取 root 权限(像大多数 rootkit 那样),而是利用 rmdir() 系统调用来实现此目的。rootkit 使用 prepare_creds 函数将与凭证相关的 ID 修改为 0 (root),并在此修改后的结构上调用 commit_creds,以在其当前进程中获取 root 权限。

要触发此函数,我们需要将第 6 个字符设置为 0。此钩子的一个警告是,它为调用者进程提供 root 权限,但不保留它们。当执行 zarya.0 时,什么也不会发生。但是,当使用 C 程序调用此钩子并打印当前进程的权限时,我们会得到结果。下面显示了使用的包装器代码的片段

[...]
// Print the current PID, SID, and GID
pid_t pid = getpid();
pid_t sid = getsid(0);  // Passing 0 gets the SID of the calling process
gid_t gid = getgid();

printf("Current PID: %d, SID: %d, GID: %d\n", pid, sid, gid);

// Print all credential-related IDs
uid_t ruid = getuid();    // Real user ID
uid_t euid = geteuid();   // Effective user ID
gid_t rgid = getgid();    // Real group ID
gid_t egid = getegid();   // Effective group ID
uid_t fsuid = setfsuid(-1);  // Filesystem user ID
gid_t fsgid = setfsgid(-1);  // Filesystem group ID

printf("Credentials: UID=%d, EUID=%d, GID=%d, EGID=%d, FSUID=%d, FSGID=%d\n",
    ruid, euid, rgid, egid, fsuid, fsgid);

[...]

执行该函数,我们可以得到以下输出

ubuntu-rk:~/testing$ whoami;id
ruben
uid=1000(ruben) gid=1000(ruben) groups=1000(ruben),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd)

ubuntu-rk:~/testing$ ./rmdir zarya.0
Received data:
zarya.0
Current PID: 41838, SID: 35117, GID: 0
Credentials: UID=0, EUID=0, GID=0, EGID=0, FSUID=0, FSGID=0

为了利用此钩子,我们编写了一个小的 C 包装器脚本,该脚本执行 rmdir zarya.0 命令,并检查它现在是否可以访问 /etc/shadow 文件。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <errno.h>

int main() {
    const char *directory = "zarya.0";

    // Attempt to remove the directory
    if (syscall(SYS_rmdir, directory) == -1) {
        fprintf(stderr, "rmdir: failed to remove '%s': %s\n", directory, strerror(errno));
    } else {
        printf("rmdir: successfully removed '%s'\n", directory);
    }

    // Execute the `id` command
    printf("\n--- Running 'id' command ---\n");
    if (system("id") == -1) {
        perror("Failed to execute 'id'");
        return 1;
    }

    // Display the contents of /etc/shadow
    printf("\n--- Displaying '/etc/shadow' ---\n");
    if (system("cat /etc/shadow") == -1) {
        perror("Failed to execute 'cat /etc/shadow'");
        return 1;
    }

    return 0;
}

成功。

ubuntu-rk:~/testing$ ./get_root
rmdir: successfully removed 'zarya.0'

--- Running 'id' command ---
uid=0(root) gid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),117(lxd),1000(ruben)

--- Displaying '/etc/shadow' ---
root:*:19430:0:99999:7:::
[...]

尽管 rmdir() 函数中还有更多可用的命令,但我们现在将继续介绍下一个,并可能将其添加到未来的出版物中。

LKM rootkit 概述: getdents() 和 getdents64() 钩子

rootkit 中的 getdents_hook()getdents64_hook() 负责操纵目录列表系统调用,以向用户隐藏文件和目录。

getdents() 和 getdents64() 系统调用用于读取目录条目。rootkit 钩住这些函数以过滤掉任何符合特定条件的条目。具体来说,任何试图列出目录内容的用户都会隐藏前缀为 zov_ 的文件和目录。

例如

ubuntu-rk:~/getdents_hook$ mkdir zov_hidden_dir

ubuntu-rk:~/getdents_hook$ ls -lah
total 8.0K
drwxrwxr-x  3 ruben ruben 4.0K Dec  9 11:11 .
drwxr-xr-x 11 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ echo "this file is now hidden" > zov_hidden_dir/zov_hidden_file

ubuntu-rk:~/getdents_hook$ ls -lah zov_hidden_dir/
total 8.0K
drwxrwxr-x 2 ruben ruben 4.0K Dec  9 11:11 .
drwxrwxr-x 3 ruben ruben 4.0K Dec  9 11:11 ..

ubuntu-rk:~/getdents_hook$ cat zov_hidden_dir/zov_hidden_file
this file is now hidden

在这里,可以使用其完整路径直接访问文件 zov_hidden。但是,当运行 ls 命令时,它不会出现在目录列表中。

阶段 4:Kitsune SO 概述

在深入研究 rootkit 时,在内核对象文件中发现了另一个 ELF 文件。提取此二进制文件后,我们发现这是 /lib64/libs.so 文件。经过检查,我们遇到了对诸如 Kitsune PID %ld 之类的字符串的多次引用。这表明开发人员将 SO 称为 Kitsune。Kitsune 可能负责 rootkit 中观察到的某些行为。这些引用与 rootkit 如何通过 LD_PRELOAD 操纵用户空间交互的更广泛的背景相一致。

此 SO 文件在实现此 rootkit 的持久性和隐蔽机制中起着作用,并且它在攻击链中的集成证明了其设计的复杂性。我们现在将展示如何检测和/或预防攻击链的每个部分。

PUMAKIT 执行链检测与预防

本节将显示不同的 EQL/KQL 规则和 YARA 签名,这些规则和签名可以预防和检测 PUMAKIT 执行链的不同部分。

阶段 1:Cron

在执行 dropper 时,一个不常见的事件会保存在 syslog 中。该事件指出,一个进程已使用可执行堆栈启动。这很不常见,值得关注

[  687.108154] process '/home/ruben_groenewoud/30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f' started with executable stack

我们可以通过以下查询来搜索它

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message: "started with executable stack"

此消息存储在 /var/log/messages/var/log/syslog 中。我们可以通过 Filebeat 或 Elastic 代理 系统集成 读取 syslog 来检测它。

阶段 2:内存驻留的可执行文件

我们可以立即看到一个不寻常的文件描述符执行。可以通过以下 EQL 查询来检测到它

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.executable like "/dev/fd/*" and not process.parent.command_line == "runc init"

此文件描述符将保留为 dropper 的父级,直到该进程结束,从而导致通过此父进程执行多个文件

file where host.os.type == "linux" and event.type == "creation" and process.executable like "/dev/fd/*" and file.path like (
  "/boot/*", "/dev/shm/*", "/etc/cron.*/*", "/etc/init.d/*", "/var/run/*"
  "/etc/update-motd.d/*", "/tmp/*", "/var/log/*", "/var/tmp/*"
)

在删除 /tmp/script.sh(通过上面的查询检测到)之后,我们可以通过查询文件属性发现和解压缩活动来检测其执行

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and 
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name in ("file", "unlzma", "gunzip", "unxz", "bunzip2", "unzstd", "unzip", "tar")) or
  (process.name == "grep" and process.args == "ELF") or
  (process.name in ("lzop", "lz4") and process.args in ("-d", "--decode"))
) and
not process.parent.name == "mkinitramfs"

该脚本继续通过 tail 命令来搜索 Linux 内核镜像的内存。可以通过以下查询来检测到它以及其他内存搜索工具。

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and
(process.parent.args like "/boot/*" or process.args like "/boot/*") and (
  (process.name == "tail" and (process.args like "-c*" or process.args == "--bytes")) or
  (process.name == "cmp" and process.args == "-i") or
  (process.name in ("hexdump", "xxd") and process.args == "-s") or
  (process.name == "dd" and process.args : ("skip*", "seek*"))
)

一旦 /tmp/script.sh 执行完毕,就会创建 /memfd:tgt (已删除)/memfd:wpn (已删除)tgt 可执行文件(即良性的 Cron 可执行文件)会创建一个 /run/crond.pid 文件。这并非恶意行为,而是一个可以通过简单查询检测到的工件。

file where host.os.type == "linux" and event.type == "creation" and file.extension in ("lock", "pid") and
file.path like ("/tmp/*", "/var/tmp/*", "/run/*", "/var/run/*", "/var/lock/*", "/dev/shm/*") and process.executable != null

如果满足所有条件,wpn 可执行文件将加载 LKMrootkit。

阶段 3:Rootkit 内核模块

可以通过应用以下配置,通过 Auditd Manager 检测到内核模块的加载。

-a always,exit -F arch=b64 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules
-a always,exit -F arch=b32 -S finit_module -S init_module -S delete_module -F auid!=-1 -k modules

并使用以下查询:

driver where host.os.type == "linux" and event.action == "loaded-kernel-module" and auditd.data.syscall in ("init_module", "finit_module")

有关利用 Auditd 与 Elastic Security 增强您的 Linux 检测工程经验的更多信息,请查看我们在 Elastic Security Labs 站点上发布的 Linux 检测工程与 Auditd 研究。

初始化时,LKM 会污染内核,因为它未签名。

audit: module verification failed: signature and/or required key missing - tainting kernel

我们可以通过以下 KQL 查询来检测此行为:

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:"module verification failed: signature and/or required key missing - tainting kernel"

此外,LKM 有错误的代码,导致它多次发生段错误。例如:

Dec  9 13:26:10 ubuntu-rk kernel: [14350.711419] cat[112653]: segfault at 8c ip 00007f70d596b63c sp 00007fff9be81360 error 4
Dec  9 13:26:10 ubuntu-rk kernel: [14350.711422] Code: 83 c4 20 48 89 d0 5b 5d 41 5c c3 48 8d 42 01 48 89 43 08 0f b6 02 41 88 44 2c ff eb c1 8b 7f 78 e9 25 5c 00 00 c3 41 54 55 53 <8b> 87 8c 00 00 00 48 89 fb 85 c0 79 1b e8 d7 00 00 00 48 89 df 89

这可以通过一个简单的 KQL 查询来检测,该查询在 kern.log 文件中查询段错误。

host.os.type:linux and event.dataset:"system.syslog" and process.name:kernel and message:segfault

加载内核模块后,我们可以通过 kthreadd 进程看到命令执行的痕迹。rootkit 会创建新的内核线程来执行特定命令。例如,rootkit 会以短间隔执行以下命令:

cat /dev/null
truncate -s 0 /usr/share/zov_f/zov_latest

我们可以通过如下查询来检测这些以及更多潜在的可疑命令:

process where host.os.type == "linux" and event.type == "start" and event.action == "exec" and process.parent.name == "kthreadd" and (
  process.executable like ("/tmp/*", "/var/tmp/*", "/dev/shm/*", "/var/www/*", "/bin/*", "/usr/bin/*", "/usr/local/bin/*") or
  process.name in ("bash", "dash", "sh", "tcsh", "csh", "zsh", "ksh", "fish", "whoami", "curl", "wget", "id", "nohup", "setsid") or
  process.command_line like (
    "*/etc/cron*", "*/etc/rc.local*", "*/dev/tcp/*", "*/etc/init.d*", "*/etc/update-motd.d*",
    "*/etc/ld.so*", "*/etc/sudoers*", "*base64 *", "*base32 *", "*base16 *", "*/etc/profile*",
    "*/dev/shm/*", "*/etc/ssh*", "*/home/*/.ssh/*", "*/root/.ssh*" , "*~/.ssh/*", "*autostart*",
    "*xxd *", "*/etc/shadow*"
  )
) and not process.name == "dpkg"

我们还可以通过分析 rmdir 命令中不寻常的 UID/GID 更改来检测 rootkit 的提权方法。

process where host.os.type == "linux" and event.type == "change" and event.action in ("uid_change", "guid_change") and process.name == "rmdir"

根据执行链,可能还会触发其他一些行为规则。

一个 YARA 签名来统治一切

Elastic Security 创建了一个 YARA 签名来识别 PUMAKIT(Dropper(cron)、rootkit 加载器(/memfd:wpn)、LKM rootkit 和 Kitsune 共享对象文件)。该签名如下所示:

rule Linux_Trojan_Pumakit {
    meta:
        author = "Elastic Security"
        creation_date = "2024-12-09"
        last_modified = "2024-12-09"
        os = "Linux"
        arch = "x86, arm64"
        threat_name = "Linux.Trojan.Pumakit"

    strings:
        $str1 = "PUMA %s"
        $str2 = "Kitsune PID %ld"
        $str3 = "/usr/share/zov_f"
        $str4 = "zarya"
        $str5 = ".puma-config"
        $str6 = "ping_interval_s"
        $str7 = "session_timeout_s"
        $str8 = "c2_timeout_s"
        $str9 = "LD_PRELOAD=/lib64/libs.so"
        $str10 = "kit_so_len"
        $str11 = "opsecurity1.art"
        $str12 = "89.23.113.204"
    
    condition:
        4 of them
}

观察结果

本研究讨论了以下可观察物。

可观察物类型名称参考
30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1fSHA256cronPUMAKIT Dropper
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (已删除)PUMAKIT 加载器
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (已删除)Cron 二进制文件
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soKitsune 共享对象引用
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfPUMAKIT 变体
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soKitsune 共享对象变体
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.soKitsune
sec.opsecurity1[.]art域名PUMAKIT C2 服务器
rhel.opsecurity1[.]art域名PUMAKIT C2 服务器
89.23.113[.]204ipv4-addrPUMAKIT C2 服务器

结论

PUMAKIT 是一种复杂且隐蔽的威胁,它使用高级技术,如系统调用挂钩、内存驻留执行和独特的特权提升方法。它的多架构设计突显了针对 Linux 系统的恶意软件日益增长的复杂性。

Elastic Security Labs 将继续分析 PUMAKIT,监控其行为,并跟踪任何更新或新变种。通过改进检测方法和分享可操作的见解,我们的目标是让防御者始终领先一步。