无文件攻击检测防御实战:Linux 内存驻留排查

无文件攻击检测是 Linux 主机安全排查中优先级最高、也最容易被漏掉的一环。攻击者把载荷直接投放到内存中执行,磁盘上不留下任何可执行文件,基于文件哈希与文件完整性校验的防护手段会全部失效。本文给出可落地的无文件攻击检测与防御配置,覆盖 memfd 匿名内存文件执行、LD_PRELOAD 预加载注入、ptrace 远程进程注入三条主流路径,并提供审计规则、巡检脚本与实时捕获方案。

适用场景

  • 服务器已确认存在异常外联或被植入挖矿模块,但 /tmp、/dev/shm、网站目录中找不到可疑文件。
  • 使用 AIDE、Tripwire、rpm -Va 等文件完整性工具巡检结果全部正常,但主机仍持续向陌生 IP 发起连接。
  • 需要为一批生产服务器补齐内存执行层面的检测能力,并把告警接入现有日志平台。
  • 安全事件应急阶段,需要快速判断某个异常进程是否属于无文件内存马。

前置条件

  • 操作系统:CentOS/Rocky 8+、Ubuntu 20.04+ 或同代内核的发行版,内核版本 4.17 以上(memfd_create 自 3.17 引入,但配套的溯源能力在 4.17 后更完整)。
  • 权限:root。读取 /proc/<PID>/maps、加载 audit 规则均需要特权。
  • 组件:auditd(审计规则)、psmisc、procps-ng;可选 bpftrace(实时捕获)、yara(内存特征匹配)。
  • 空闲磁盘:至少 2 GB,用于内存镜像与取证留存。
  • 注意:同一台主机上同时叠加多条高频 audit 规则会带来可观的 CPU 开销,生产环境建议以 bpftrace 承担实时检测、audit 只保留关键系统调用。

原理说明

为什么磁盘扫描抓不到

常规入侵检测依赖“文件落地”这一前提:木马必须先写入磁盘,才能被病毒特征库或完整性校验发现。无文件攻击直接跳过这一环,把机器码写入进程虚拟内存并跳转执行,磁盘上只留下无害的父进程,因此文件层面的检测天然失明。

三条主流落地路径

  • memfd_create 匿名内存文件:进程调用 memfd_create() 在内核中创建一块匿名内存对象,拿到一个文件描述符。该对象在 /proc/<PID>/fd/ 中表现为符号链接 /memfd:name (deleted),随后通过 fexecve() 或 /proc/self/fd/<N> 直接执行。因为从未经过 open() 写盘,文件系统上不存在对应 inode。
  • LD_PRELOAD 预加载注入:通过修改 /etc/ld.so.preload 或进程环境变量 LD_PRELOAD,让动态链接器在加载正常库之前先加载攻击者的共享对象。该对象可以劫持 readdir、open 等函数,把自身从目录列表中隐藏,实现“看得见进程但看不见文件”。
  • ptrace 远程注入:使用 ptrace(PTRACE_ATTACH) 附加到一个已运行的正常进程,通过 PTRACE_POKETEXT 写入 shellcode,修改指令寄存器后放行。被注入进程的 /proc/<PID>/maps 中会出现匿名可执行段,但可执行文件路径仍指向原始合法程序。

统一的可观测特征

三条路径最终的痕迹都落在 /proc 与内核审计事件上,共性特征包括:可执行映射指向 (deleted) 文件;出现同时具备 rwx 权限的匿名内存段;短命进程反复 fork 并立即 exec;父进程为 Web 服务(如 nginx、tomcat)却派生出 shell;以及向陌生公网地址的 443/8080 外联。检测方案就是围绕这几类特征构建。

操作步骤

步骤一:建立可信基线

先记录当前正常状态,后续差异才有比较意义。基线内容以“可执行的匿名映射”与“监听中的 elf 进程”为主:

mkdir -p /var/lib/sec-baseline
# 记录所有进程的匿名可执行映射(rwx 或 x 且无对应文件)
for pid in /proc/[0-9]*; do
  awk -v p="$pid" '/r-xp|rwxp/ && /\/dev\/zero|\/\(deleted\)|^[0-9a-f]+-[0-9a-f]+ rwxp/ {print p, $0}' "$pid/maps" 2>/dev/null
done > /var/lib/sec-baseline/anon_exec.map
# 记录 elf 类型的网络监听进程
ss -lntup 2>/dev/null | grep -v 'users:' > /var/lib/sec-baseline/listen.txt
wc -l /var/lib/sec-baseline/anon_exec.map /var/lib/sec-baseline/listen.txt

基线文件需要只读保护,避免攻击者回写污染:chattr +i /var/lib/sec-baseline/anon_exec.map。

步骤二:用 auditd 捕获内存执行与注入

audit 规则只保留高价值事件,避免全量 execve 造成性能塌陷。memfd_create 与 ptrace 的 POKETEXT 是核心监控点:

cat > /etc/audit/rules.d/60-fileless.rules <<'EOF'
## 匿名内存文件创建
-a always,exit -F arch=b64 -S memfd_create -k memfd_create
-a always,exit -F arch=b32 -S memfd_create -k memfd_create
## 进程注入:PTRACE_POKETEXT(4) / PTRACE_POKEDATA(5) / PTRACE_SETREGS(13)
-a always,exit -F arch=b64 -S ptrace -F a0=4 -k ptrace_inject
-a always,exit -F arch=b64 -S ptrace -F a0=5 -k ptrace_inject
-a always,exit -F arch=b64 -S ptrace -F a0=13 -k ptrace_inject
## 动态链接器预加载文件被改写
-w /etc/ld.so.preload -p wa -k ld_preload
## 内存密钥与临时映射相关
-a always,exit -F arch=b64 -S init_module -S finit_module -k kernel_module
EOF
augenrules --load
auditctl -l | grep -E 'memfd|ptrace'

关键参数说明:-F a0=4 中的 a0 表示第一个系统调用参数,PTRACE_POKETEXT 的常量值为 4;-k 后是自定义标签,便于后续用 ausearch -k 精准检索。

步骤三:实时捕获取自 memfd 的执行

audit 只能看到 memfd 被创建,要确认它是否真的被执行,用 bpftrace 挂在内核进程执行点上一条规则即可,开销远低于审计全量 execve:

cat > /usr/local/bin/memfd-exec.bt <<'EOF'
#!/usr/bin/env bpftrace
tracepoint:sched:sched_process_exec
/strcontains(args->filename, "memfd:") || strcontains(args->filename, "/proc/self/fd/")/
{
  printf("[memfd-exec] comm=%s pid=%d ppid=%d file=%s\n",
         comm, pid, curtask->real_parent->pid, args->filename);
  print(str(args->filename));
}
EOF
bpftrace /usr/local/bin/memfd-exec.bt

若内核未启用 BTF(未开启 CONFIG_DEBUG_INFO_BTF),把 args->filename 换成 str(args->filename) 的兼容写法,或在 Ubuntu 上安装 linux-tools-$(uname -r) 后重试。

步骤四:巡检脚本排查注入痕迹

把下列脚本放入 crontab 每 5 分钟执行一次,输出即是一份可疑清单:

cat > /usr/local/bin/fileless-scan.sh <<'EOF'
#!/bin/bash
# 1) LD_PRELOAD 全局劫持
[ -s /etc/ld.so.preload ] && echo "[!] ld.so.preload: $(cat /etc/ld.so.preload)"
# 2) 进程环境中的 LD_PRELOAD
for e in /proc/[0-9]*/environ; do
  tr '\0' '\n' < "$e" 2>/dev/null | grep -q '^LD_PRELOAD=' \
    && echo "[!] LD_PRELOAD in ${e%/environ}"
done
# 3) 可执行但已删除的映射
for m in /proc/[0-9]*/maps; do
  awk -v p="${m%/maps}" '$0 ~ /(deleted)/ && $2 ~ /x/ {print "[!]", p, $0}' "$m" 2>/dev/null
done
# 4) 无文件支撑的 rwx 段
for m in /proc/[0-9]*/maps; do
  awk -v p="${m%/maps}" '$2 == "rwxp" && NF == 5 {print "[!] rwx-anon", p, $0}' "$m" 2>/dev/null
done
# 5) Web 服务派生 shell
ps -eo pid,ppid,comm,args --no-headers | awk '$3 ~ /^(sh|bash|dash)$/ {print "[?] shell:", $0}'
EOF
chmod +x /usr/local/bin/fileless-scan.sh
/usr/local/bin/fileless-scan.sh

步骤五:处置与取证

确认可疑进程后不要直接 kill,先把内存镜像与进程信息留存,再中断执行并封禁外联:

PID=12345
mkdir -p /root/ir/$PID
cp /proc/$PID/maps /proc/$PID/status /proc/$PID/cmdline /root/ir/$PID/ 2>/dev/null
cat /proc/$PID/environ | tr '\0' '\n' > /root/ir/$PID/environ.txt
# 导出进程内存(需 gdb 或 dd 配合 /proc/pid/mem)
gcore -o /root/ir/$PID/core $PID 2>/dev/null || \
  dd if=/proc/$PID/mem of=/root/ir/$PID/mem.bin bs=1M 2>/dev/null
# 提取外联目标后封禁
ss -tnp | grep "pid=$PID" | awk '{print $5}' | cut -d: -f1 | sort -u | tee /root/ir/$PID/peers.txt
while read ip; do nft add element inet filter blackhole "{ $ip }" 2>/dev/null; done < /root/ir/$PID/peers.txt
kill -STOP $PID

配置验证

检测能力必须验证,否则等于没有。用一个完全无害的 POC 触发三条路径的特征:

# 验证 memfd 检测:编译并运行一个内存执行的探针
cat > /tmp/poc_memfd.c <<'EOF'
#define _GNU_SOURCE
#include <sys/mman.h>
#include <unistd.h>
int main(void){ int fd = memfd_create("poc", 0); (void)fd; return 0; }
EOF
gcc -o /tmp/poc_memfd /tmp/poc_memfd.c && /tmp/poc_memfd
# 预期:bpftrace 或 ausearch 中出现 memfd_create 记录
ausearch -k memfd_create --start recent -i | tail -5
# 验证注入检测:对自身测试进程附加(不写内存,仅触发 attach)
sleep 300 & SP=$!
gdb -p $SP -batch -ex detach 2>/dev/null
ausearch -k ptrace_inject --start recent -i | tail -3
kill $SP
# 验证巡检脚本:制造一个已删除的可执行映射
python3 -c "
import ctypes, os, tempfile
" 2>/dev/null; echo "巡检输出行数: $(/usr/local/bin/fileless-scan.sh | wc -l)"

预期结果:ausearch 能检索到 memfd_create 与 ptrace_inject 记录,bpftrace 探针在内存执行探针程序时打印 [memfd-exec] 行,巡检脚本输出可读的可疑清单。三条路径全部命中即为验证通过。

常见问题

FAQ 1:巡检发现大量 (deleted) 映射,是否都是恶意?

不是。(deleted) 只表示文件在映射建立后被删除或替换,属于正常运行现象,典型来源有三类:一是软件包升级后进程仍在运行旧版本二进制(记录时显示 /usr/bin/nginx (deleted));二是字体缓存、glibc 本地化数据等被 mmap 后随即 unlink;三是容器运行时在 /dev/shm 中的临时映射。判断要点是结合执行权限与进程来源:只有“映射区块带 x 权限 + 路径为 /memfd: 或 /dev/shm + 父进程是 Web 服务”时,才需要升级为高危事件。建议在基线阶段把正常来源全部登记,巡检时做白名单过滤。

FAQ 2:auditd 规则叠加后 CPU 明显升高,如何取舍?

根因是规则粒度过粗。全文监控 execve 这类高频调用,在繁忙的 Web 服务器上每秒可产生上万条审计事件。三条优化路径:一是只监控本次列出的低频率系统调用(memfd_create、ptrace 特定参数、模块加载),不要监控全部 execve;二是调大内核缓冲 -b 16384 并设置 --backlog_wait_time 0,避免缓冲打满后丢事件;三是把实时检测交给 bpftrace,audit 只做留存与追溯。落地时可以先在灰度节点压测,观察 auditd 进程 CPU 占比与 aureport --summary 的事件量,再决定是否推广。

总结

无文件攻击检测的思路不是“找一个文件”,而是盯住“执行行为”这一层。本文给出的三层防线可以独立部署,也可以整体落地:auditd 规则负责留痕与追溯,覆盖 memfd_create、ptrace 注入与 ld.so.preload 篡改;bpftrace 探针负责实时告警,在执行发生的瞬间拿到进程与父进程信息;巡检脚本负责周期性核查,把已删除可执行映射、匿名 rwx 段与 Web 进程派生 shell 这类特征汇总成可读清单。三者配合后,磁盘无文件的攻击载荷同样会被记录并阻断。

最后提醒一点:无文件攻击通常只是攻击链的中间环节,它前面往往有一次成功的应用层漏洞利用(文件上传、反序列化、命令注入),后面往往跟着持久化与外联。检测能力上线后,应同步收紧应用层的文件上传与命令执行入口,才能把攻击面真正压下来。