Remco SprootenRuben Groenewoud

去爪 PUMAKIT

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

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

PUMAKIT 概览

PUMAKIT 是一种复杂的恶意软件,最初是在 VirusTotal 的常规威胁搜寻中发现的,其命名源于在其二进制文件中发现的开发人员嵌入的字符串。其多阶段架构包括一个植入程序( cron )、两个驻留在内存中的可执行文件( /memfd:tgt/memfd:wpn )、一个 LKM rootkit 模块和一个共享对象 (SO) 用户空间 rootkit 组成。

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

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

关键要点

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

PUMAKIT 发现

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

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

PUMAKIT代码分析

PUMAKIT 以其嵌入式 LKM rootkit 模块(恶意软件作者将其命名为“PUMA”)和 SO 用户空间 rootkit Kitsune 命名,采用多阶段架构,从启动执行链的 dropper 开始。该过程从cron二进制文件开始,它会创建两个驻留在内存中的可执行文件: /memfd:tgt (deleted)/memfd:wpn (deleted)/memfd:tgt是一个良性的 Cron 二进制文件,而/memfd:wpn则充当着 rootkit 加载程序。加载程序负责评估系统条件、执行临时脚本( /tmp/script.sh )并最终部署 LKM 根工具包。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() :恶意软件分叉为子进程和父进程。子进程将其标准输出和错误重定向到/dev/null以避免留下日志,然后使用execveat()执行“武器”有效载荷( wpnElfp )。父进程等待子进程,然后执行“目标”负载( tgtElfp )。两种有效载荷都是从内存执行的,而不是从磁盘上的文件执行的,这使得检测和取证分析更加困难。

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

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

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

> ps aux
root 2138 ./30b26707d5fb407ef39ebee37ded7edeea2890fb5ec1ebfa09a3b3edfc80db1f

以下是此进程的文件描述符的列表。这些文件描述符显示了植入器创建的内存驻留文件。

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

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

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

检查/memfd:tgt ELF 文件,很明显这是默认的 Ubuntu Linux Cron 二进制文件。二进制文件似乎没有被修改。

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

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

该脚本包含根据文件的压缩格式检查和处理文件的逻辑。

它的作用如下:

  • 函数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 开发人员在投放器中将其 rootkit 称为“PUMA”。根据条件,程序将输出如下消息:

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

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

第 3 阶段:LKM rootkit 概述

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

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

LKM 根工具包操作系统行为的能力始于其对系统调用表的使用以及对 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()函数,我们可以看到使用unregister_ftrace_function()函数可以还原ftrace()挂钩机制。这保证了回调不再被调用。随后,所有系统调用都将返回指向原始系统调用而不是挂钩的系统调用。这为我们提供了所有被挂钩的系统调用的清晰概览。

在下面的部分中,我们将仔细研究一些挂钩的系统调用。

LKM rootkit 概述:rmdir_hook()

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

这个钩子已经进行了几次检查。该钩子期望rmdir()系统调用的第一个字符是zarya 。如果满足此条件,挂钩函数将检查第 6 个字符,即要执行的命令。最后,检查第 8 个字符,它可以包含正在执行的命令的进程参数。结构如下: zarya[char][command][char][argument]zarya和命令和参数之间可以放置任何特殊字符(或无)。

截至发布日期,我们已确定以下命令:

命令用途
zarya.c.0Retrieve the config
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添加到“无法删除‘ directory ’”错误中,而是返回 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时不会返回任何输出。通过编写一个小型 C 程序来包装系统调用并打印十六进制/ASCII 表示形式,我们可以看到返回的 rootkit 的配置。

该 rootkit 不会像大多数 rootkit 那样使用kill()系统调用来获取 root 权限,而是会利用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 执行链的不同部分。

第一阶段:Cron

一旦执行投放器,系统日志中就会保存一个不常见的事件。该事件表明一个进程已经通过可执行堆栈启动。这是不常见的,但看起来很有趣:

[  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 代理系统集成读取系统日志来检测到这一点。

第 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"

此文件描述符将一直是投放器的父进程,直到进程结束,从而导致通过此父进程执行多个文件:

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 (deleted)/memfd:wpn (deleted)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。

第三阶段: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 网站上发布的使用 Auditd 的 Linux 检测工程研究。

初始化时,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 滴管
cb070cc9223445113c3217f05ef85a930f626d3feaaea54d8585aaed3c2b3cfeSHA256/memfd:wpn (deleted)PUMAKIT 装载机
934955f0411538eebb24694982f546907f3c6df8534d6019b7ff165c4d104136SHA256/memfd:tgt (deleted)Cron 二进制文件
8ef63f9333104ab293eef5f34701669322f1c07c0e44973d688be39c94986e27SHA256libs.soKitsune 共享对象参考
8ad422f5f3d0409747ab1ac6a0919b1fa8d83c3da43564a685ae4044d0a0ea03SHA256some2.elfPUMAKIT 变体
bbf0fd636195d51fb5f21596d406b92f9e3d05cd85f7cd663221d7d3da8af804SHA256some1.soKitsune 共享对象变体
bc9193c2a8ee47801f5f44beae51ab37a652fda02cd32d01f8e88bb793172491SHA256puma.koLKM rootkit
1aab475fb8ad4a7f94a7aa2b17c769d6ae04b977d984c4e842a61fb12ea99f58SHA256kitsune.so狐狸
sec.opsecurity1[.]art域名PUMAKIT C2 服务器
rhel.opsecurity1[.]art域名PUMAKIT C2 服务器
89.23.113[.]204IPv4 地址PUMAKIT C2 服务器

结束语

PUMAKIT 是一种复杂而隐秘的威胁,它使用系统调用挂钩、内存驻留执行和独特的权限提升方法等先进技术。其多架构设计凸显了针对 Linux 系统的恶意软件日益复杂化。

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