LinkPro: eBPF rootkit 深度分析
在一次与AWS托管基础设施被入侵相关的数字调查中,发现了一个针对GNU/Linux系统的隐蔽后门。该后门的功能依赖于安装两个eBPF模块:一个用于隐藏自身,另一个用于在收到“魔包”时被远程激活。本文详细介绍了该rootkit的功能,并展示了在此案例中观察到的感染链,该感染链使其被安装在AWS EKS环境的多个节点上。
目录
- 引言
- 感染链
- LinkPro Rootkit
- 结论
- MITRE ATT&CK 映射 — LinkPro
- 入侵指标 (IOCs) 表 — LinkPro
- YARA 规则
引言
eBPF(扩展的伯克利包过滤器)是一项在Linux中被采用的技术,因其众多用例(可观测性、安全性、网络等)以及能够在内核上下文中运行同时从用户空间进行编排的能力而受到欢迎。威胁行为者越来越多地滥用它来创建复杂的后门并逃避传统的系统监控工具。
诸如BPFDoor、Symbiote和J-magic等恶意软件展示了eBPF在创建被动后门方面的有效性,这些后门能够监控网络流量并在收到特定的“魔包”时激活。此外,更复杂的开源工具如ebpfkit(一个概念验证)和eBPFeXPLOIT,以及用Golang开发的编排器,充当了rootkit的角色,其功能范围从建立秘密命令与控制(C2)通道到进程隐藏和容器逃逸技术。
在最近调查一个被入侵的AWS托管基础设施时,Synacktiv CSIRT确定了一个相对复杂的感染链,导致在GNU/Linux系统上安装了隐蔽的后门。该后门依赖于安装两个eBPF模块:一个用于隐藏自身,另一个用于在收到“魔包”时被远程激活。
感染链
取证分析确定了一个暴露在互联网上的存在漏洞的Jenkins服务器(CVE-2024-23897)为入侵源。该服务器作为威胁行为者的初始访问点,随后攻击者转移到托管在多个Amazon EKS(Elastic Kubernetes Service,标准模式)集群上的集成和部署流水线。
攻击者从Jenkins服务器开始,在多个Kubernetes集群上部署了一个名为kvlnt/vv的恶意Docker镜像(在我们通知之前,该镜像托管在hub.docker.com上,随后被支持团队删除)。该Docker镜像包含一个Kali Linux基础镜像和两个附加层。
这些层将/app文件夹添加为工作目录,然后向其中添加三个文件:
-
/app/start.sh:一个bash脚本,作为Docker镜像的入口点。其目的是启动ssh服务,执行/app/app后门程序和/app/link程序。1 2 3 4 5#!/bin/bash sed -i -e 's/#PermitRootLogin /PermitRootLogin yes\n#/g' /etc/ssh/sshd_config /etc/init.d/ssh start ./app & ./link -k ooonnn -w mmm000 -W -o 0.0.0.0/0 || tail -f /var/log/wtmp -
/app/link:一个名为vnt的开源程序,充当VPN服务器并提供代理功能。它连接到社区中继服务器vnt.wherewego.top:29872。这使得威胁行为者可以从任何IP地址连接到被入侵的服务器,并将其用作代理来访问基础设施上的其他服务器。/app/start.sh脚本中指定的命令行参数如下:-k ooonnn:标识中继服务器上虚拟VLAN的令牌-w mmm000:用于加密客户端之间通信的密码(AES128-GCM)-W:启用客户端和服务器之间的加密(RSA+AES256-GCM),以防止令牌泄漏和中间人攻击。-o 0.0.0.0/0:允许转发到所有网段。
-
/app/app:一个下载器恶意软件,用于从S3存储桶检索加密的恶意载荷。联系的URL是https[:]//fixupcount.s3.dualstack.ap-northeast-1.amazonaws[.]com/wehn/rich.png。在观察到的情况下,这是一个内存中的vShell 4.9.3载荷,它通过WebSocket与其命令控制服务器(56.155.98.37)通信。Synacktiv CSIRT将此下载器命名为vGet,因为在此案例中它直接与vShell相关联。
vShell是一个已有文档记录的后门,曾被UNC5174使用。其源代码在大约一年前已不再在GitHub上提供。然而,一个较新的版本4.9.3及其(破解的)许可证可供下载,使得各种行为者可以使用vShell。
然而,关于vGet没有开源出版物,它是用Rust开发的并且被剥离了符号。此恶意代码在执行开始时创建一个从/dev/null到/tmp/.del的符号链接,然后下载vShell载荷。vShell在执行过程中,在打开终端(应操作者要求)时初始化环境变量HISTFILE=/tmp/.del。目的是确保命令历史不会写入文件(例如.bash_history)。因此,这两个程序之间可能存在联系,并且vGet可能是专门为了直接在内存中执行vShell而开发的,不会在磁盘上留下痕迹。
恢复的vGet样本几乎没有符号,除了引用在Rust依赖项的绝对路径中定义的用户名cosmanking,例如:
/Users/cosmanking/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/ureq-2.12.1/src/request.rs。
对于Docker镜像,配置了以下挂载点:
- 挂载点:
/mnt - 源(主机):
/ - 目标(容器):
/mnt - 访问权限:读写
- 类型:绑定
此配置允许威胁行为者逃逸容器的上下文(正在运行的镜像),以root权限访问根分区的整个文件系统。
从kvlnt/vv pod的/app/app(vGet)进程,执行了一个cat命令,目的是检索主机上(特别是其他pod中)可用的凭据(身份验证令牌、API密钥、证书…)。以下是此命令的一个简短摘录:
|
|
在此Docker镜像部署几周后,在多个Kubernetes节点以及生产服务器上观察到了另外两个恶意代码的执行。后者尤其因为经济动机成为攻击小组的目标。
第一个恶意代码是一个投递器,嵌入了另一个在内存中执行的vShell后门(v4.9.3),这次通过DNS隧道进行通信。关于这个投递器,它不同于SNOWLIGHT(已在一些发布中观察到用于投递vShell),但目的相同。解密过程分两步进行。以下是Synacktiv CSIRT分析的样本摘录:
步骤1:解密第一个shellcode,直接执行 步骤2:shellcode解密并加载嵌入的ELF vShell后门到其内存中
最终的有效载荷是未被记录的,Synacktiv CSIRT将其命名为LinkPro,是一个利用eBPF技术的后门,由于其隐蔽性、持久性和内部网络横向移动能力,可被描述为rootkit。
LinkPro Rootkit
LinkPro针对GNU/Linux系统,用Golang开发。Synacktiv CSIRT之所以将其命名为LinkPro,是因为定义其主模块的符号是:github.com/link-pro/link-client。GitHub帐户link-pro没有公开的存储库或贡献。LinkPro使用eBPF技术,仅在收到“魔包”时激活,并在受感染的系统上隐藏自身。
LinkPro Rootkit 样本
| 属性 | 值 |
|---|---|
| SHA256 | d5b2202b7308b25bda8e106552dafb8b6e739ca62287ee33ec77abe4016e698b (被动后门)1368f3a8a8254feea14af7dc928af6847cab8fcceec4f21e0166843a75e81964 (主动后门) |
| 文件类型 | ELF 64-bit LSB executable, x86-64, executable/linux/elf64 |
| 文件大小 | 8710464 字节 |
| 威胁类型 | Linux Rootkit |
| 观察到的文件名 | .tmp~data.ok; .tmp~data.pro; .tmp~data.resolveld |
LinkPro嵌入了四个ELF模块:一个共享库、一个内核模块和两个eBPF模块:
LinkPro 嵌入的 ELF 二进制文件
| SHA256 | 类型 | 大小 |
|---|---|---|
b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7 |
共享对象 | 14.2 KiB |
9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075 |
内核对象 | 573.0 KiB |
364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3 |
BPF | 18.8 KiB |
b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164 |
BPF | 35.4 KiB |
下面详细介绍了各个ELF模块。然而,内核模块从未被LinkPro使用(没有实现加载它的函数)。
配置与通信
根据其定义的配置,LinkPro可以以两种方式运行:被动或主动。其配置通过两种不同的方式获取:
- 要么嵌入在二进制文件中,结构化为JSON,并以关键字
CFG0开头, - 要么其默认参数直接硬编码在main函数中。在两个样本中都观察到了这种方法。
最后,命令行参数也会在运行时被考虑以修改默认值:
|
|
在调查中观察到的-addsvc参数用于激活持久化机制。
以下是LinkPro实现的配置结构:
|
|
ConnectionMode有两种可能的值:reverse或forward。
- 反向连接模式对应于被动模式,后门监听来自C2的命令。在此模式下,会安装eXpress Data Path(XDP)和Traffic Control(TC)类型的两个eBPF程序,目的是仅在接收到特定TCP数据包时才激活C2通信通道。
- 前向连接模式对应于主动模式,后门主动与其C2服务器建立通信。在此模式下,不安装XDP/TC eBPF程序。
在受感染的信息系统上识别出的两个样本具有以下配置:
LinkPro TailConfig
| 字段 | d5b2202b (被动模式 HTTP) | 1368f3a8 (主动模式 HTTP) |
|---|---|---|
| ServerAddress | 1.1.1.1 (未使用) | 18.199.101.111 |
| ServerPort | 6666 | 2233 |
| SecretKey | 0 | 3344 |
| SleepTime | 10 | 10 |
| JitterTime | 2 | 2 |
| Protocol | http | http |
| DnsDomain | dns.example.com | dns.example.com |
| DNSMode | tunnel | tunnel |
| DnsServer | 0 | 0 |
| Debug | false | false |
| Version | 1.0.0 | 1.0.0 |
| ConnectionMode | reverse | forward |
| ReversePort | 2233 | 2233 |
DNS字段仅在通过DNS协议通信的情况下使用。
解析其配置后,LinkPro使用以下信息生成唯一的客户端ID:
SHA1sum(hex:"0123456789abcdeffedcba9876543210" | Hostname | Current user | Executable path | Machine ID | MAC Address | "nginx" )
Machine ID对应于/etc/machine-id或(如果不存在)/proc/sys/kernel/random/boot_id中的值。
对于前向(主动)模式,有五种可能的通信协议:
- HTTP
- WebSocket
- UDP(原始)
- TCP(原始)
- DNS(直接/隧道)
对于反向(被动)模式,仅使用HTTP协议。提供三个URL:
/reverse/handshake:识别操作员的ID(server_idHTTP请求参数)并返回状态success。/reverse/heartbeat:返回客户端信息(如果指定了request_client_info参数)并返回状态ok。/reverse/operation:执行操作员的命令。
交换内容以JSON格式结构化,并使用配置中指定的SecretKey XOR密钥加密。
然后,按以下顺序执行步骤:
- 安装“隐藏”eBPF模块
- 如果“隐藏”模块安装失败,或者已通过命令行参数
-ebpf 0禁用:在/etc/ld.so.preload中安装共享库 - 如果使用反向模式,安装“敲门”eBPF模块
- 安装持久化
- 执行C2命令
- 中断时,删除各个模块
被动样本d5b2202b用于说明以下描述。
LD PRELOAD 模块
LinkPro LD PRELOAD 模块样本
| 属性 | 值 |
|---|---|
| SHA256 | b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7 |
| 文件类型 | ELF 64-bit LSB shared object, x86-64, executable/linux/so64 |
| 文件大小 | 14552 字节 |
| 威胁类型 | Linux 动态链接器劫持 |
| 观察到的文件名 | libld.so |
LinkPro修改/etc/ld.so.preload配置文件以指定其嵌入的libld.so共享库的路径,目的是隐藏可能揭示后门存在的各种工件。libld.so的不同步骤如下:
- 在内存中保存
/etc/ld.so.preload的内容 - 将嵌入在LinkPro二进制文件中的
libld.so提取到/etc/libld.so- 如果需要,以读写权限挂载
/etc:mount -o remount,rw /etc
- 如果需要,以读写权限挂载
- 分配足够的权限,使
libld.so可以被所有用户加载和执行:chmod 0755 /etc/libld.so - 将
/etc/ld.so.preload文件的内容替换为/etc/libld.so
由于/etc/libld.so路径存在于/etc/ld.so.preload中,LinkPro安装的libld.so共享库会被所有需要/lib/ld-linux.so的程序加载。这包括所有使用共享库的程序,例如glibc。
一旦libld.so在程序执行时被加载(例如/usr/bin/ls),它会在glibc之前钩住几个libc函数,以修改可能揭示LinkPro存在的结果。以下是观察到的被钩住函数的行为:
fopen和fopen64:libld.so钩子检查进程是否尝试打开/proc/net/tcp、/proc/net/tcp6、/proc/net/udp或/proc/net/udp6。这些文件提供有关活动TCP/UDP连接的信息。如果是,则执行真实的fopen函数。然后,恶意库检索这些文件的内容并删除LinkPro的网络痕迹。具体来说,任何包含端口2233(LinkPro的监听端口)作为源或目的地的行都会被删除。最后,如果进程尝试打开名为ld.so.preload的文件,则返回“No Such File Or Directory”错误。
关于网络信息,netstat使用/proc/net文件来显示正在进行的连接。而ss则使用netlink协议来检索此信息。因此,可以使用ss命令检测到机器上正在监听端口2233,如下图所示(openat是fopen使用的系统调用):
此外,LinkPro进程名称不会显示在ss命令的输出中(如果添加-p标志),这要归功于下面解释的getdents钩子。
getdents和getdents64:getdents系统调用用于列出目录中的文件。在这种情况下,libld.so执行getdents,然后检查是否存在:- 包含关键字
.tmp~data(Linkpro后门)、libld.so、sshids和ld.so.preload的文件名。 - 命令行包含关键字
.tmp~data的进程目录(在/proc/下,即PID)。 如果找到,dirent条目将被下一个条目覆盖。
- 包含关键字
注意:符号toyincang和toyincanglib指定关键字.tmp~data和libld.so。“toyincang”在普通话拼音中可以解释为“to conceal”(隐藏)。
kill:kill系统调用允许向进程发送信号,并将目标的PID作为参数。libld.so实现的钩子检查目标进程的命令行(/proc/<pid>/cmdline)是否存在关键字.tmp~data(Linkpro后门)。如果找到,则不执行libc的kill函数。open和open64:如果进程尝试打开名为ld.so.preload的文件,则返回“No Such File Or Directory”错误。readdir和readdir64:libld.so钩子执行合法的readdir函数(列出目录中的文件),然后检查是否存在:- 包含关键字
.tmp~data、libld.so、sshids、ld.so.preload和.system(持久化机制激活后包含LinkPro的文件夹)的文件名。 - 命令行包含关键字
.tmp~data的进程目录(在/proc/下,即PID)。 如果找到,readdir钩子返回空结果。
- 包含关键字
下图展示了libld.so效果的演示。指定命令行参数-ebpf 0以明确禁用“隐藏”模块,从而激活LD Preload库。
总之,由LinkPro加载的libld.so试图从其他动态链接的程序中隐藏LinkPro后门和libld.so本身的网络痕迹(监听或目标端口)和文件系统痕迹。
“隐藏” eBPF 模块
LinkPro “隐藏” eBPF 模块样本
| 属性 | 值 |
|---|---|
| SHA256 | b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164 |
| 文件类型 | ELF 64-bit LSB relocatable, eBPF |
| 文件大小 | 36224 字节 |
| 威胁类型 | Linux eBPF Rootkit |
“隐藏”模块由多个Tracepoint和Kretprobe类型的eBPF程序组成。
Tracepoint类型的eBPF程序是附加到Linux内核定义的静态跟踪点的程序。它们被放置在内核代码中的特定位置,例如在系统调用、内存分配、任务调度等处。特别是,系统调用的跟踪点位于入口(tracepoint/syscalls/sys_enter_syscall)或出口(tracepoint/syscalls/sys_exit_syscall)。
Kprobes允许将eBPF程序附加到内核中的几乎任何函数(其入口点)。而Kretprobes则在函数返回时触发。这允许拦截和修改系统调用的结果。
LinkPro rootkit安装这些eBPF程序,并利用它们的功能来隐藏其进程和网络活动。
“隐藏”模块安装
首先,LinkPro使用ebpf-go模块将嵌入的“隐藏”ELF模块解析为特定对象(CollectionSpec)。可以在内存中找到已加载的隐藏模块的不同eBPF对象,即映射和程序。映射是可以在程序之间共享的数据结构。
LinkPro更新pids_to_hide_map映射,添加LinkPro的当前PID(进程ID)以及命令行(-pid参数)指定的PID列表。
LinkPro更新main_ebpf_progs映射,添加LinkPro加载的eBPF程序(“隐藏”和“敲门”模块)的文件描述符。
然后,LinkPro创建BPF链接,将eBPF程序附加到内核中的检测点。由LinkPro后门加载的“隐藏”eBPF模块向getdents和getdents64系统调用添加钩子,目标与libld.so共享库相同,即隐藏LinkPro特定的文件。此外,钩子被放置在sys_bpf系统调用的入口和出口,以隐藏其eBPF映射和程序。以下是实现的钩子:
tracepoint/syscalls/sys_enter_getdents->HandleGetdentsEntertracepoint/syscalls/sys_enter_getdents64->HandleGetdents64Entertracepoint/syscalls/sys_exit_getdents->HandleGetdentsExittracepoint/syscalls/sys_exit_getdents64->HandleGetdents64Exttracepoint/syscalls/sys_enter_bpf->HandleBpfEnterkretprobe/sys_bpf->HandleBpfExit
handle_getdents
附加到getdents和getdents64跟踪点的函数行为与libld.so类似。拦截getdents系统调用(sys_exit_getdents)的结果。eBPF程序设置过滤器以隐藏:
- 特定文件和目录:任何包含关键字
.tmp~data或.system的条目。 - 特定进程:进程目录(在
/proc/下),其标识符(PID)列在pids_to_hideeBPF映射中(最多可包含10个PID)也会被隐藏。
getdents系统调用返回一个dirent结构的链表,其中每个结构代表一个文件或目录,并包含其名称、inode和条目大小等信息,这允许计算下一个元素的位置。与libld.so在getdents上的钩子不同,dirent条目不会被下一条记录直接覆盖。
下图展示了如何修改dirent结构的链表以隐藏名为.tmp~data.resolveld的文件。
eBPF修改前
该列表是一个连续的记录序列。每个dirent通过其自身长度(d_reclen)隐式指向下一个。
| dirent (File A) | dirent (.tmp~data.resolveld) | dirent (File B) |
|---|---|---|
d_reclen=24 |
d_reclen=32 |
d_reclen=24 |
d_name="File A" |
d_name=".tmp~data.resolveld" |
d_name="File B" |
指向.tmp~data.resolveld的开头 |
指向File B的开头 |
指向末尾 |
d_reclen值是任意给出的示例。
eBPF修改后
eBPF程序检测到.tmp~data.resolveld。然后,它修改前一条记录(File A)的长度(d_reclen),为其添加.tmp~data.resolveld的长度。
| dirent (File A) | dirent (.tmp~data.resolveld) - 已跳过 | dirent (File B) |
|---|---|---|
d_reclen=24+32=56 |
d_reclen=32 |
d_reclen=24 |
d_name="File A" |
d_name=".tmp~data.resolveld" |
d_name="File B" |
现在指向File B的开头 |
指向末尾 |
相同的技术在eBPFeXPLOIT项目中实现,并添加了要隐藏的文件名和目录。
handle_ebpf
实现了两个函数:HandleBpfEnter,链接到syscall/sys_enter/bpf跟踪点;以及HandleBpfExit,链接到sys_bpf的Kretprobe。此处的目的是隐藏eBPF程序的存在,使其对bpftool等工具不可见。观察到的代码与eBPFeXPLOIT项目中实现的代码基本相同,除了添加了额外的检查和两个bpf_printk调用(可能用于调试)。
|
|
bpf_printk的输出记录在特殊文件/sys/kernel/debug/tracing/trace_pipe中。需要root权限才能读取其内容:
|
|
handlerBpfEnter跟踪点将类型为BPF_PROG_GET_NEXT_ID、BPF_MAP_GET_NEXT_ID和BPF_LINK_GET_NEXT_ID的BPF命令保存在映射中。sys_bpf系统调用执行后,触发handlerBpfExit钩子。
检索关联命令的结构,现在包含系统调用的结果,即下一个程序、映射或链接的ID。
如果cmd_info.attr_pr.next_id的值等于main_ebpfs_progs映射中存在的某个对象的ID,那么sys_bpf的返回值将被替换为-ENOENT(没有那个文件或目录)。
下图展示了“隐藏”模块加载的演示(-ebpf 1参数,默认值),将eBPF程序从bpftool实用程序中隐藏:
请注意,eBPF映射和链接的FD未包含在LinkPro的main_ebpf_progs映射中,这是一个错误:尽管程序被正确隐藏,但仍然可以列出它们的eBPF映射和链接,以及与LinkPro关联的PID。
另一个重要的点是,bpf_override_return辅助函数仅在Linux内核编译时启用了CONFIG_BPF_KPROBE_OVERRIDE配置选项时才可用。例如,对于相对较新的Amazon Linux或Ubuntu 24.04内核就是这种情况。如果所需的内核选项未启用,BPF验证器将拒绝该程序并阻止“隐藏”模块加载。如果失败,则使用替代方法加载libld.so库,作为回退解决方案来隐藏部分LinkPro的工件。
“敲门” eBPF 模块
LinkPro “敲门” eBPF 模块样本
| 属性 | 值 |
|---|---|
| SHA256 | 364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3 |
| 文件类型 | ELF 64-bit LSB relocatable, eBPF |
| 文件大小 | 19249 字节 |
| 威胁类型 | Linux eBPF Rootkit |
“敲门”模块包含两个由LinkPro加载的eBPF程序。
第一个名为xdp_ingress,是XDP(eXpress Data Path)类型。
XDP提供了一种通过eBPF程序处理网络数据包的机制。它位于处理链的非常早期,在驱动程序级别,并且在经典的Linux网络堆栈的上游。XDP eBPF程序使用返回码(例如XDP_PASS、XDP_DROP、XDP_REDIRECT)来确定Linux内核应对网络数据包采取的操作。
第二个名为tc_egress,是TC(Traffic Control)类型。
tc是由iproute2包引入的一个工具,允许控制接口上的入站(ingress)和出站(egress)网络流量。可以将BPF程序附加到不同的TC控制点,例如在发送之前过滤某些数据包。TC位于驱动程序和网络堆栈之间,即XDP的下游。XDP程序只能附加到入站流量,不能附加到出站流量,这在此上下文中证明了使用TC的合理性。
“敲门”模块安装
安装xdp_ingress和tc_egress程序需要几个步骤。
- 检测用于与互联网通信的网络接口(例如
eth0)。 - 在BPF FS中创建一个
fire目录。路径:/sys/fs/bpf/fire。BPF FS是一个伪虚拟文件系统(仅驻留在内存中),允许存储BPF程序和映射,以及固定对象(允许通过BPF FS中的伪文件保持对这些对象的引用,以确保其持久性)。 - 将“敲门”模块加载到内存中(
CollectionSpec对象)。 - 用LinkPro配置中
reverse_port属性的值更新conf_mapBPF映射:在此上下文中为端口2233。 - 安装
xdp_ingress程序:- 卸载已链接到网络接口的任何XDP程序:
ip link set dev eth0 xdp off - 通过创建BPF链接将
xdp_ingress程序附加到网络接口
- 卸载已链接到网络接口的任何XDP程序:
- 安装
tc_egress程序- 将
tc_egress程序固定到/sys/fs/bpf/fire/tc_egress。这意味着它已经被另一个进程(LinkPro)加载到内存中,并已固定在BPF虚拟文件系统中。 - 通过以下
tc命令将tc_egress程序附加到网络接口:- 准备接口:
tc qdisc replace dev eth0 clsact- 在
eth0接口上创建或替换排队规则(qdisc)为clsact(分类器操作),提供两个附加点,ingress(入站数据包)和egress(出站
- 在
- 准备接口:
- 将