一台 Linux 服务器 CPU 突然跑满。

你登录上去,发现一个陌生进程占用了接近全部计算资源。于是执行 kill -9,CPU 很快恢复正常。

事情结束了吗?

不一定。

几分钟后,那个进程可能重新出现;即使没有重新出现,攻击者也可能已经留下 SSH 公钥、计划任务、新账号、systemd 服务或者另一个没有被发现的入口。

真正的应急响应,不是回答“这个进程是不是病毒”,而是回答一个更难的问题:

这台主机现在还能不能被信任?

要回答这个问题,不能只盯着某一个文件、某一个 IP 或某一条告警,而需要重新建立一条完整的证据链:

  • 异常是从什么时候开始的?
  • 哪个进程在执行?
  • 谁启动了它?
  • 它与谁通信?
  • 它如何确保自己重启后还能回来?
  • 攻击者通过什么入口进入?
  • 是否使用这台主机继续访问了其他资产?
  • 即使删除恶意程序,系统本身是否仍然可信?

下面是一套我更推荐的 Linux 主机失陷调查思路。

它不是一张必须机械执行的命令清单,而是六组必须回答的问题。


一、第一件事不是执行命令,而是判断当前有多紧急

面对 Linux 主机异常,可以先把现场归为三种状态。

第一种:异常已经停止

例如:

  • 某个可疑进程已经退出;
  • 当前没有异常外联;
  • 没有陌生在线会话;
  • 业务仍然正常运行。

这种情况下,可以优先保全现场,然后逐步调查。

第二种:攻击可能仍在继续

例如:

1
2
3
who
w
ss -tnp state established

发现陌生 SSH 会话仍然在线,或者某个异常 shell 还保持着公网连接。

此时最危险的错误是立刻开始随意删除文件。因为攻击者可能仍然看到你的操作,并改变攻击路径、清理日志或者转移到其他主机。

第三种:风险正在快速扩大

例如:

  • 文件正在被持续加密;
  • 主机正在大量向外传输数据;
  • 发现连续 SSH 到其他内网机器;
  • 核心身份、构建、密钥、备份或管理系统受到影响;
  • 多台服务器同时出现相似现象。

这种情况下,优先级已经不是“把这一台服务器查清楚”,而是阻止影响范围继续扩大

因此,我通常先问三个问题:

攻击者还在线吗?

异常还在扩散吗?

这台机器是不是其他系统的信任源?

这三个问题决定接下来应该观察、隔离,还是立即升级。


二、先做一份“现场快照”,避免越查越没有证据

应急响应有一个很常见的问题:

第一次登录主机时,现场其实是最完整的。

但随着排查人员不断执行命令、修改文件、停止服务、删除进程,原始状态会越来越少。

所以在开始深度调查前,可以先收集一份基础快照。

下面这个脚本只使用常见系统命令,目的是保存:

  • 进程状态;
  • 网络连接;
  • 当前登录用户;
  • 最近登录历史;
  • 特权账号;
  • 最近变化文件;
  • 计划任务;
  • 临时目录内容。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#!/usr/bin/env bash

STAMP=$(date +%Y%m%d-%H%M%S)
CASE_DIR="/tmp/linux_check_$(hostname)_${STAMP}"

mkdir -p "$CASE_DIR"

echo "[+] collecting process information"
ps -eo pid,ppid,user,lstart,%cpu,%mem,args --sort=-%cpu \
> "$CASE_DIR/processes.txt"

echo "[+] collecting network information"
ss -anptue 2>/dev/null \
> "$CASE_DIR/network_all.txt"

ss -tnp state established 2>/dev/null \
> "$CASE_DIR/network_established.txt"

echo "[+] collecting login information"
who > "$CASE_DIR/who.txt"
w > "$CASE_DIR/w.txt"
last -Fa 2>/dev/null | head -100 \
> "$CASE_DIR/login_history.txt"

echo "[+] collecting privileged accounts"
awk -F: '$3==0 {print}' /etc/passwd \
> "$CASE_DIR/uid_zero_accounts.txt"

echo "[+] collecting scheduled tasks"
for account in $(cut -d: -f1 /etc/passwd); do
crontab -u "$account" -l 2>/dev/null \
> "$CASE_DIR/cron_${account}.txt"
done

ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily \
/etc/cron.weekly /etc/cron.monthly 2>/dev/null \
> "$CASE_DIR/system_cron_files.txt"

echo "[+] collecting recently changed files"
find / -xdev -mmin -1440 -type f 2>/dev/null \
> "$CASE_DIR/changed_last_24h.txt"

echo "[+] collecting temporary directory listing"
find /tmp /var/tmp /dev/shm -maxdepth 3 \
-printf '%TY-%Tm-%Td %TH:%TM:%TS %m %u %g %s %p\n' 2>/dev/null \
> "$CASE_DIR/temp_files.txt"

tar -czf "${CASE_DIR}.tar.gz" "$CASE_DIR" 2>/dev/null
sha256sum "${CASE_DIR}.tar.gz"

echo "[+] saved to ${CASE_DIR}.tar.gz"

这份脚本本身不会告诉你“有没有被入侵”。

它只是尽可能冻结一个时间点。

后面的核心工作,是从这些数据中寻找相互关联的异常。


三、不要先问“是不是木马”,先问“这个进程为什么在这里”

假设你通过 top 或监控平台发现某个进程长期占用大量 CPU。

先不要杀。

第一步可以查看它的基本身份:

1
2
3
4
5
6
7
8
PID=24680

ps -o pid,ppid,user,lstart,%cpu,%mem,args -p "$PID"

ls -la "/proc/$PID/exe"

cat "/proc/$PID/cmdline" | tr '\0' ' '
echo

例如:

1
2
PID    PPID USER    %CPU COMMAND
24680 9123 daemon 97.8 ./system-worker

真实执行路径:

1
/proc/24680/exe -> /dev/shm/.cache/system-worker

单独看到高 CPU,并不能说明这是恶意程序。

真正值得关注的是多个信号同时出现:

  • 程序运行在 /tmp/var/tmp/dev/shm
  • 文件位于隐藏目录;
  • 名称模仿系统组件;
  • 使用与业务无关的低权限账号运行;
  • CPU 长时间维持高位;
  • 同时连接陌生公网地址。

有时还能看到:

1
/proc/24680/exe -> /tmp/.worker (deleted)

这里的 (deleted) 很重要。

它代表磁盘中的原文件已经删除,但进程仍然存活,因此代码依然存在于进程映像中。

可以先保存样本:

1
2
3
4
5
6
7
mkdir -p /tmp/ir_samples

cp "/proc/$PID/exe" \
"/tmp/ir_samples/process_${PID}.bin"

sha256sum \
"/tmp/ir_samples/process_${PID}.bin"

这也是为什么“不应该看到可疑进程就马上杀掉”。

进程一旦退出,部分证据可能随之消失。


四、真正有价值的不是进程名,而是谁启动了它

攻击者完全可以把恶意程序命名成:

1
2
3
4
5
systemd
sshd
kworker
dbus
syslogd

因此,单靠名称几乎没有意义。

更有效的方法是向上追父进程。

1
2
3
4
5
ps -o pid,ppid,user,args -p "$PID"

PARENT=$(ps -o ppid= -p "$PID" | tr -d ' ')

ps -o pid,ppid,user,args -p "$PARENT"

继续向上查,直到找到真正来源。

例如你可能得到:

1
2
3
4
5
system-worker

/bin/sh /tmp/.health.sh

CROND

这时问题已经发生变化。

原来以为是“发现一个恶意进程”,现在实际发现的是:

有一个计划任务定期执行脚本,而脚本负责重新启动恶意程序。

如果只是:

1
kill -9 24680

一分钟以后,它可能又回来。

另一个典型链路是:

1
2
3
4
5
bash

php-fpm

systemd

Web 服务正常情况下很少需要直接创建交互式 shell。

因此:

1
2
3
4
php-fpm → bash
nginx → sh
apache → bash
tomcat → shell

通常值得重点调查 Web 漏洞利用、Webshell 或远程命令执行。

可以使用:

1
ps -eo pid,ppid,user,args --forest

或者围绕特定 PID 逐级检查。

调查入侵时,我更关注:

这个进程从哪里来?

而不是:

这个进程叫什么名字?


五、网络连接告诉你:这台主机现在正在和谁建立关系

主机失陷调查中,网络信息非常关键。

先看现有连接:

1
ss -anptue

只看已建立 TCP 会话:

1
ss -tnp state established

例如:

1
2
ESTAB 0 0 10.20.4.17:51132 203.0.113.77:443 \
users:(("system-worker",pid=24680,fd=7))

判断一条连接是否异常,不能只看端口。

443 不代表安全。

真正应该联合判断的是:

  • 连接由哪个进程发起;
  • 进程真实路径在哪里;
  • 远端是不是已知业务依赖;
  • 连接是否符合该程序的正常职责;
  • 是否持续、周期性重连;
  • DNS、SNI 或其他流量信息是否存在异常。

例如:

1
mysqld → 陌生公网地址:443

通常比:

1
nginx → 内网后端服务:443

更值得进一步调查。

可以再使用:

1
lsof -nP -i

交叉确认。

如果怀疑系统命令已经不可信,还可以读取:

1
2
cat /proc/net/tcp
cat /proc/net/tcp6

如果出现这种情况:

  • ss 看不到某条连接;
  • lsof 能看到;
  • /proc/net/tcp 也能看到。

那就应该开始怀疑:

系统工具是否已经被替换或被 Rootkit 干预?


六、账号调查的重点,不是“有没有陌生用户名”

攻击者不一定创建一个叫 hacker 的账号。

他可能选择:

  • 一个看起来像系统账号的名称;
  • 一个已有业务账号;
  • 一个新的 UID 0 账号;
  • SSH 公钥;
  • sudoers 权限;
  • 已窃取的管理员凭据。

检查额外 UID 0 账号

1
awk -F: '$3==0 && $1!="root" {print}' /etc/passwd

理想情况下没有输出。

如果出现:

1
svc-admin:x:0:0::/var/lib/svc:/bin/bash

就意味着这个账号拥有和 root 相同的用户 ID。

不要第一时间删除。

先保存:

1
2
3
4
5
6
cp -p /etc/passwd /tmp/passwd.snapshot
cp -p /etc/shadow /tmp/shadow.snapshot

sha256sum \
/tmp/passwd.snapshot \
/tmp/shadow.snapshot

然后调查:

  • 账号是什么时候出现的;
  • 谁创建了它;
  • 是否登录过;
  • 是否存在 SSH 公钥;
  • 是否执行过 sudo;
  • 是否被用于访问其他主机。

七、SSH 后门通常比恶意进程更值得担心

恶意进程被删除后,攻击者可能无法继续运行原程序。

但只要 SSH 公钥仍然存在,他随时可以重新回来。

因此需要检查:

1
find / -name authorized_keys -type f 2>/dev/null

然后逐个查看:

1
2
3
4
5
6
for keyfile in $(find / -name authorized_keys -type f 2>/dev/null); do
echo "========== $keyfile =========="
ls -la "$keyfile"
lsattr "$keyfile" 2>/dev/null
cat "$keyfile"
done

一个没有注释的 SSH key 并不自动等于恶意。

一个带有:

1
root@kali

注释的 key,也不应该仅凭字符串直接定罪。

真正可靠的判断方式是:

  • 是否存在资产登记;
  • 是否由已知管理员持有;
  • 添加时间是否与事件时间线重合;
  • 是否存在对应登录记录;
  • 是否被设置不可修改属性;
  • 是否出现在多个不相关服务器。

检查不可修改属性:

1
lsattr /root/.ssh/authorized_keys

例如:

1
----i-------------- /root/.ssh/authorized_keys

其中 i 表示 immutable。

攻击者有时会通过:

1
chattr +i

防止公钥文件被正常删除。


八、持久化不是一张“十大检查表”,而是在回答一个问题

很多排查文章会列出十几种 Linux 持久化方式。

但本质上,它们都在回答:

如果当前恶意进程消失,是什么机制能让它重新回来?

通常可以从四个方向寻找。

方向一:定时触发

例如 cron:

1
2
3
4
for user in $(cut -d: -f1 /etc/passwd); do
echo "===== $user ====="
crontab -u "$user" -l 2>/dev/null
done

以及:

1
2
3
4
5
6
7
grep -R . \
/etc/crontab \
/etc/cron.d \
/etc/cron.hourly \
/etc/cron.daily \
/etc/cron.weekly \
/etc/cron.monthly 2>/dev/null

值得注意的不是“每分钟执行一定恶意”,而是:

1
* * * * * /tmp/.sync.sh

这种任务是否具有合理业务解释。

尤其需要关注:

1
2
3
4
5
curl ... | sh
wget ... | bash
base64 -d | sh
/tmp/...
/dev/shm/...

方向二:系统启动时自动执行

查看 systemd:

1
2
3
systemctl list-unit-files --type=service --state=enabled

systemctl list-units --type=service --state=running

检查陌生服务的真实内容:

1
systemctl cat suspicious.service

或者:

1
cat /etc/systemd/system/suspicious.service

例如:

1
2
3
4
[Service]
ExecStart=/dev/shm/.agent
Restart=always
RestartSec=5

这里真正有意义的不是服务名,而是:

  • ExecStart 指向哪里;
  • 文件何时创建;
  • 谁创建;
  • 服务何时启用;
  • 二进制是否属于合法软件;
  • 它建立了哪些网络连接。

方向三:用户登录时自动执行

检查:

1
2
3
4
5
6
7
8
grep -RniE \
'curl|wget|base64|nohup|LD_PRELOAD|/tmp/|/dev/shm/' \
/etc/profile \
/etc/profile.d \
/root/.bashrc \
/root/.profile \
/home/*/.bashrc \
/home/*/.profile 2>/dev/null

例如:

1
nohup /tmp/.sync >/dev/null 2>&1 &

意味着每次用户进入 shell,都可能再次拉起程序。

也需要留意这种情况:

1
2
alias ps='/tmp/.fake_ps'
alias ls='/tmp/.fake_ls'

这不是传统意义上的自启动,但会干扰排查人员看到真实系统状态。


方向四:系统底层劫持

查看:

1
cat /etc/ld.so.preload 2>/dev/null

如果出现陌生动态库:

1
/usr/local/lib/.hidden.so

需要高度重视。

进一步检查:

1
2
3
4
grep -Rni "LD_PRELOAD" \
/etc \
/root \
/home 2>/dev/null

同时对比系统软件包完整性。

RHEL、CentOS 等 RPM 系发行版:

1
rpm -Va

Debian、Ubuntu 系:

1
debsums -c 2>/dev/null

特别关注这些程序是否异常:

1
2
3
4
5
6
7
ps
top
ss
netstat
ls
find
lsof

如果基础排查工具自身都被替换,那么这台主机返回的很多结果已经不再可信。


九、什么时候应该停止“清理”,直接考虑重建

这是 Linux 应急响应里最容易被忽略的问题。

很多团队的目标是:

找到恶意文件,把它删掉。

但安全团队真正需要判断的是:

删除之后,我还有什么依据证明这台系统是干净的?

例如出现以下情况:

  • /etc/ld.so.preload 存在恶意动态库;
  • 内核模块异常;
  • psssls 等系统程序被替换;
  • PAM 认证模块被修改;
  • Rootkit 已确认;
  • 内核层行为无法可信验证;
  • 攻击者获得 root 后长期驻留;
  • 无法确认所有持久化点;
  • 关键基础设施主机被完全控制。

此时,“手工删除恶意文件然后继续使用”通常不是一个可靠结论。

更合理的恢复路径是:

  1. 保留内存、磁盘和日志证据;
  2. 确认失陷时间窗口;
  3. 调查凭据泄露与横向移动;
  4. 从可信镜像重新构建系统;
  5. 轮换相关密码、SSH key、Token 和密钥;
  6. 修复最初入口;
  7. 对同类资产做横向排查。

重装只解决主机当前状态,不会自动解决攻击入口和凭据失陷。

这一点非常重要。


十、一个典型案例:为什么“杀掉挖矿进程”通常不够

假设监控发现 CPU 接近 100%。

执行:

1
ps -eo pid,ppid,user,%cpu,args --sort=-%cpu | head

看到:

1
24680  9123  nobody  98.6  ./system-worker

查看真实路径:

1
ls -la /proc/24680/exe

得到:

1
/proc/24680/exe -> /dev/shm/.cache/system-worker (deleted)

再看网络:

1
ss -tnp | grep 24680

得到:

1
2
ESTAB 0 0 10.20.4.17:51291 203.0.113.77:4444 \
users:(("system-worker",pid=24680,fd=5))

继续查父进程:

1
ps -o pid,ppid,user,args -p 24680

发现父 PID 为 9123

1
ps -o pid,ppid,user,args -p 9123

输出:

1
9123  731  root  /bin/sh /tmp/.health-check.sh

继续查:

1
ps -o pid,ppid,user,args -p 731

得到:

1
731  1  root  /usr/sbin/cron -f

现在攻击链已经很清楚:

1
2
3
4
5
6
7
cron

/tmp/.health-check.sh

/dev/shm/.cache/system-worker

公网服务器

因此正确处理重点并不是:

1
kill -9 24680

而是首先保留:

1
2
3
4
5
6
7
8
9
10
11
12
13
mkdir -p /tmp/incident_case

cp /proc/24680/exe \
/tmp/incident_case/process_24680.bin

cat /proc/24680/cmdline | tr '\0' ' ' \
> /tmp/incident_case/process_24680_cmdline.txt

cp -p /tmp/.health-check.sh \
/tmp/incident_case/

sha256sum /tmp/incident_case/* \
> /tmp/incident_case/SHA256SUMS

随后找到真正负责自动拉起进程的 cron 项。

例如:

1
2
grep -R "/tmp/.health-check.sh" \
/etc/cron* 2>/dev/null

或者:

1
2
3
for user in $(cut -d: -f1 /etc/passwd); do
crontab -u "$user" -l 2>/dev/null
done

如果只杀进程,没有移除启动机制,恶意程序很可能再次出现。

但即使完成这些操作,也还没有结束。

还必须继续回答:

  • 这个脚本是谁写进去的?
  • 攻击者通过 SSH 进入,还是利用了 Web 漏洞?
  • root 权限是什么时候获得的?
  • 有没有植入 SSH key?
  • 有没有新增 systemd 服务?
  • 有没有下载其他程序?
  • 有没有 SSH 到其他服务器?
  • 有没有窃取凭据?

这才是真正的失陷调查。


十一、通过时间线寻找真正的入口

攻击入口调查最好不要从“猜漏洞”开始。

更可靠的方法是先找到最早异常时间点

例如:

1
stat /tmp/.health-check.sh

或者:

1
stat /dev/shm/.cache/system-worker

假设最早可疑文件创建于:

1
2024-06-18 02:41:33

那么可以围绕这个时间点向前查。

SSH 日志:

1
2
3
grep "Jun 18 02:" \
/var/log/secure \
/var/log/auth.log 2>/dev/null

查看成功登录:

1
2
3
grep "Accepted" \
/var/log/secure \
/var/log/auth.log 2>/dev/null

查看失败登录:

1
2
3
grep "Failed password" \
/var/log/secure \
/var/log/auth.log 2>/dev/null

如果主机运行 Web 服务,再检查对应时间窗口内:

  • POST 请求;
  • 文件上传;
  • 5xx 异常;
  • 可疑 URI;
  • Web 服务进程产生 shell 的时间。

同时检查:

1
2
3
4
find /tmp /var/tmp /dev/shm \
-type f -newermt "2024-06-18 02:30:00" \
! -newermt "2024-06-18 03:00:00" \
-ls 2>/dev/null

把登录、文件、进程、网络和应用日志放到一条时间线上,通常比单独检查某一个 IOC 更容易接近真实入口。


十二、几种很容易误判的现象

kworker CPU 很高,不一定是挖矿

真实 Linux 内核线程通常显示为:

1
[kworker/0:1]

带方括号。

而攻击者可能使用:

1
2
3
kworkerd
kworker_d
kworker.0

进行伪装。

但名称只是一个信号,仍然应该继续检查:

1
ls -la /proc/<pid>/exe

crontab -l 没输出,不代表没有 cron 持久化

还可能存在于:

1
2
3
4
/etc/crontab
/etc/cron.d/
/etc/cron.hourly/
/etc/cron.daily/

或者其他用户自己的 crontab。


last 没有记录,不代表没人登录

日志可能:

  • 被清空;
  • 被轮转;
  • 被关闭;
  • 被攻击者修改。

应该继续检查:

1
2
stat /var/log/wtmp
ls -lh /var/log/wtmp*

以及 SSH 自身日志。


杀掉进程后马上复活,不一定是“木马杀不死”

先查:

1
ps -o pid,ppid,user,args -p <pid>

大多数情况下,真正的问题是:

  • cron;
  • systemd;
  • watchdog;
  • 父进程;
  • 容器编排;
  • 其他守护机制。

ss 没看到异常连接,不代表一定没有连接

可以交叉检查:

1
2
3
lsof -nP -i
cat /proc/net/tcp
cat /proc/net/tcp6

当多个数据源明显矛盾时,要开始怀疑系统本身的可信性。


十三、排查结束时,真正应该得到什么结论

一次 Linux 入侵调查,不应该只得到:

已删除恶意程序。

更完整的结论应该至少回答:

1. 是否确认失陷

例如:

  • 已确认;
  • 高度疑似;
  • 暂无足够证据;
  • 已排除。

2. 最早已知异常时间

明确:

1
T0 = 最早可验证的异常活动时间

3. 初始入口

例如:

  • SSH 凭据泄露;
  • 弱口令;
  • Web 应用漏洞;
  • 中间件漏洞;
  • 未授权服务;
  • 内部横向移动;
  • 暂时未知。

4. 攻击者获得了什么权限

例如:

  • 普通用户;
  • 应用账号;
  • root;
  • 容器权限;
  • 云凭据。

5. 使用了哪些驻留方式

例如:

  • cron;
  • systemd;
  • SSH 公钥;
  • sudoers;
  • 动态库劫持;
  • 新增账号。

6. 是否存在横向扩散

检查:

  • SSH;
  • SCP;
  • 内网扫描;
  • 凭据使用;
  • known_hosts;
  • 出口日志;
  • 其他主机相同 IOC。

7. 当前主机是否还能继续信任

这可能是整个调查最重要的结论。

可以粗略分成:

状态 处理思路
单一恶意进程,无提权、无持久化 清除、修复入口、强化监控
已获得权限并建立持久化 深度排查后谨慎恢复
root 长期失陷 优先考虑重建
系统命令、PAM、动态库或内核被修改 不应继续信任原系统
Rootkit 或内核态失陷 保留证据后从可信源重建

结语

Linux 入侵排查最容易犯的错误,是把“找到恶意文件”当成终点。

实际上,一个陌生进程只是入口。

真正需要建立的是一条完整逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
异常从何时开始

什么进程执行

谁启动这个进程

如何与外部通信

如何维持长期驻留

攻击者如何进入

是否获得更高权限

是否访问其他资产

当前系统是否仍然可信

因此,一次好的主机应急响应,不是“查杀木马”,而是重新回答:

发生了什么、影响了什么、攻击者还拥有什么,以及这台机器还能不能被继续信任。


© Rabbit 使用 Stellar 创建

✨ 营业:

共发表 56 篇Blog 🔸 总计 123.6k