一台 Linux 服务器 CPU 突然跑满。
你登录上去,发现一个陌生进程占用了接近全部计算资源。于是执行 kill -9,CPU 很快恢复正常。
事情结束了吗?
不一定。
几分钟后,那个进程可能重新出现;即使没有重新出现,攻击者也可能已经留下 SSH 公钥、计划任务、新账号、systemd 服务或者另一个没有被发现的入口。
真正的应急响应,不是回答“这个进程是不是病毒”,而是回答一个更难的问题:
这台主机现在还能不能被信任?
要回答这个问题,不能只盯着某一个文件、某一个 IP 或某一条告警,而需要重新建立一条完整的证据链:
- 异常是从什么时候开始的?
- 哪个进程在执行?
- 谁启动了它?
- 它与谁通信?
- 它如何确保自己重启后还能回来?
- 攻击者通过什么入口进入?
- 是否使用这台主机继续访问了其他资产?
- 即使删除恶意程序,系统本身是否仍然可信?
下面是一套我更推荐的 Linux 主机失陷调查思路。
它不是一张必须机械执行的命令清单,而是六组必须回答的问题。
一、第一件事不是执行命令,而是判断当前有多紧急
面对 Linux 主机异常,可以先把现场归为三种状态。
第一种:异常已经停止
例如:
- 某个可疑进程已经退出;
- 当前没有异常外联;
- 没有陌生在线会话;
- 业务仍然正常运行。
这种情况下,可以优先保全现场,然后逐步调查。
第二种:攻击可能仍在继续
例如:
1 | who |
发现陌生 SSH 会话仍然在线,或者某个异常 shell 还保持着公网连接。
此时最危险的错误是立刻开始随意删除文件。因为攻击者可能仍然看到你的操作,并改变攻击路径、清理日志或者转移到其他主机。
第三种:风险正在快速扩大
例如:
- 文件正在被持续加密;
- 主机正在大量向外传输数据;
- 发现连续 SSH 到其他内网机器;
- 核心身份、构建、密钥、备份或管理系统受到影响;
- 多台服务器同时出现相似现象。
这种情况下,优先级已经不是“把这一台服务器查清楚”,而是阻止影响范围继续扩大。
因此,我通常先问三个问题:
攻击者还在线吗?
异常还在扩散吗?
这台机器是不是其他系统的信任源?
这三个问题决定接下来应该观察、隔离,还是立即升级。
二、先做一份“现场快照”,避免越查越没有证据
应急响应有一个很常见的问题:
第一次登录主机时,现场其实是最完整的。
但随着排查人员不断执行命令、修改文件、停止服务、删除进程,原始状态会越来越少。
所以在开始深度调查前,可以先收集一份基础快照。
下面这个脚本只使用常见系统命令,目的是保存:
- 进程状态;
- 网络连接;
- 当前登录用户;
- 最近登录历史;
- 特权账号;
- 最近变化文件;
- 计划任务;
- 临时目录内容。
1 |
|
这份脚本本身不会告诉你“有没有被入侵”。
它只是尽可能冻结一个时间点。
后面的核心工作,是从这些数据中寻找相互关联的异常。
三、不要先问“是不是木马”,先问“这个进程为什么在这里”
假设你通过 top 或监控平台发现某个进程长期占用大量 CPU。
先不要杀。
第一步可以查看它的基本身份:
1 | PID=24680 |
例如:
1 | PID PPID USER %CPU COMMAND |
真实执行路径:
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 | mkdir -p /tmp/ir_samples |
这也是为什么“不应该看到可疑进程就马上杀掉”。
进程一旦退出,部分证据可能随之消失。
四、真正有价值的不是进程名,而是谁启动了它
攻击者完全可以把恶意程序命名成:
1 | systemd |
因此,单靠名称几乎没有意义。
更有效的方法是向上追父进程。
1 | ps -o pid,ppid,user,args -p "$PID" |
继续向上查,直到找到真正来源。
例如你可能得到:
1 | system-worker |
这时问题已经发生变化。
原来以为是“发现一个恶意进程”,现在实际发现的是:
有一个计划任务定期执行脚本,而脚本负责重新启动恶意程序。
如果只是:
1 | kill -9 24680 |
一分钟以后,它可能又回来。
另一个典型链路是:
1 | bash |
Web 服务正常情况下很少需要直接创建交互式 shell。
因此:
1 | php-fpm → bash |
通常值得重点调查 Web 漏洞利用、Webshell 或远程命令执行。
可以使用:
1 | ps -eo pid,ppid,user,args --forest |
或者围绕特定 PID 逐级检查。
调查入侵时,我更关注:
这个进程从哪里来?
而不是:
这个进程叫什么名字?
五、网络连接告诉你:这台主机现在正在和谁建立关系
主机失陷调查中,网络信息非常关键。
先看现有连接:
1 | ss -anptue |
只看已建立 TCP 会话:
1 | ss -tnp state established |
例如:
1 | ESTAB 0 0 10.20.4.17:51132 203.0.113.77:443 \ |
判断一条连接是否异常,不能只看端口。
443 不代表安全。
真正应该联合判断的是:
- 连接由哪个进程发起;
- 进程真实路径在哪里;
- 远端是不是已知业务依赖;
- 连接是否符合该程序的正常职责;
- 是否持续、周期性重连;
- DNS、SNI 或其他流量信息是否存在异常。
例如:
1 | mysqld → 陌生公网地址:443 |
通常比:
1 | nginx → 内网后端服务:443 |
更值得进一步调查。
可以再使用:
1 | lsof -nP -i |
交叉确认。
如果怀疑系统命令已经不可信,还可以读取:
1 | cat /proc/net/tcp |
如果出现这种情况:
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 | cp -p /etc/passwd /tmp/passwd.snapshot |
然后调查:
- 账号是什么时候出现的;
- 谁创建了它;
- 是否登录过;
- 是否存在 SSH 公钥;
- 是否执行过 sudo;
- 是否被用于访问其他主机。
七、SSH 后门通常比恶意进程更值得担心
恶意进程被删除后,攻击者可能无法继续运行原程序。
但只要 SSH 公钥仍然存在,他随时可以重新回来。
因此需要检查:
1 | find / -name authorized_keys -type f 2>/dev/null |
然后逐个查看:
1 | for keyfile in $(find / -name authorized_keys -type f 2>/dev/null); do |
一个没有注释的 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 | for user in $(cut -d: -f1 /etc/passwd); do |
以及:
1 | grep -R . \ |
值得注意的不是“每分钟执行一定恶意”,而是:
1 | * * * * * /tmp/.sync.sh |
这种任务是否具有合理业务解释。
尤其需要关注:
1 | curl ... | sh |
方向二:系统启动时自动执行
查看 systemd:
1 | systemctl list-unit-files --type=service --state=enabled |
检查陌生服务的真实内容:
1 | systemctl cat suspicious.service |
或者:
1 | cat /etc/systemd/system/suspicious.service |
例如:
1 | [Service] |
这里真正有意义的不是服务名,而是:
ExecStart指向哪里;- 文件何时创建;
- 谁创建;
- 服务何时启用;
- 二进制是否属于合法软件;
- 它建立了哪些网络连接。
方向三:用户登录时自动执行
检查:
1 | grep -RniE \ |
例如:
1 | nohup /tmp/.sync >/dev/null 2>&1 & |
意味着每次用户进入 shell,都可能再次拉起程序。
也需要留意这种情况:
1 | alias ps='/tmp/.fake_ps' |
这不是传统意义上的自启动,但会干扰排查人员看到真实系统状态。
方向四:系统底层劫持
查看:
1 | cat /etc/ld.so.preload 2>/dev/null |
如果出现陌生动态库:
1 | /usr/local/lib/.hidden.so |
需要高度重视。
进一步检查:
1 | grep -Rni "LD_PRELOAD" \ |
同时对比系统软件包完整性。
RHEL、CentOS 等 RPM 系发行版:
1 | rpm -Va |
Debian、Ubuntu 系:
1 | debsums -c 2>/dev/null |
特别关注这些程序是否异常:
1 | ps |
如果基础排查工具自身都被替换,那么这台主机返回的很多结果已经不再可信。
九、什么时候应该停止“清理”,直接考虑重建
这是 Linux 应急响应里最容易被忽略的问题。
很多团队的目标是:
找到恶意文件,把它删掉。
但安全团队真正需要判断的是:
删除之后,我还有什么依据证明这台系统是干净的?
例如出现以下情况:
/etc/ld.so.preload存在恶意动态库;- 内核模块异常;
ps、ss、ls等系统程序被替换;- PAM 认证模块被修改;
- Rootkit 已确认;
- 内核层行为无法可信验证;
- 攻击者获得 root 后长期驻留;
- 无法确认所有持久化点;
- 关键基础设施主机被完全控制。
此时,“手工删除恶意文件然后继续使用”通常不是一个可靠结论。
更合理的恢复路径是:
- 保留内存、磁盘和日志证据;
- 确认失陷时间窗口;
- 调查凭据泄露与横向移动;
- 从可信镜像重新构建系统;
- 轮换相关密码、SSH key、Token 和密钥;
- 修复最初入口;
- 对同类资产做横向排查。
重装只解决主机当前状态,不会自动解决攻击入口和凭据失陷。
这一点非常重要。
十、一个典型案例:为什么“杀掉挖矿进程”通常不够
假设监控发现 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 | ESTAB 0 0 10.20.4.17:51291 203.0.113.77:4444 \ |
继续查父进程:
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 | cron |
因此正确处理重点并不是:
1 | kill -9 24680 |
而是首先保留:
1 | mkdir -p /tmp/incident_case |
随后找到真正负责自动拉起进程的 cron 项。
例如:
1 | grep -R "/tmp/.health-check.sh" \ |
或者:
1 | for user in $(cut -d: -f1 /etc/passwd); do |
如果只杀进程,没有移除启动机制,恶意程序很可能再次出现。
但即使完成这些操作,也还没有结束。
还必须继续回答:
- 这个脚本是谁写进去的?
- 攻击者通过 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 | grep "Jun 18 02:" \ |
查看成功登录:
1 | grep "Accepted" \ |
查看失败登录:
1 | grep "Failed password" \ |
如果主机运行 Web 服务,再检查对应时间窗口内:
- POST 请求;
- 文件上传;
- 5xx 异常;
- 可疑 URI;
- Web 服务进程产生 shell 的时间。
同时检查:
1 | find /tmp /var/tmp /dev/shm \ |
把登录、文件、进程、网络和应用日志放到一条时间线上,通常比单独检查某一个 IOC 更容易接近真实入口。
十二、几种很容易误判的现象
kworker CPU 很高,不一定是挖矿
真实 Linux 内核线程通常显示为:
1 | [kworker/0:1] |
带方括号。
而攻击者可能使用:
1 | kworkerd |
进行伪装。
但名称只是一个信号,仍然应该继续检查:
1 | ls -la /proc/<pid>/exe |
crontab -l 没输出,不代表没有 cron 持久化
还可能存在于:
1 | /etc/crontab |
或者其他用户自己的 crontab。
last 没有记录,不代表没人登录
日志可能:
- 被清空;
- 被轮转;
- 被关闭;
- 被攻击者修改。
应该继续检查:
1 | stat /var/log/wtmp |
以及 SSH 自身日志。
杀掉进程后马上复活,不一定是“木马杀不死”
先查:
1 | ps -o pid,ppid,user,args -p <pid> |
大多数情况下,真正的问题是:
- cron;
- systemd;
- watchdog;
- 父进程;
- 容器编排;
- 其他守护机制。
ss 没看到异常连接,不代表一定没有连接
可以交叉检查:
1 | lsof -nP -i |
当多个数据源明显矛盾时,要开始怀疑系统本身的可信性。
十三、排查结束时,真正应该得到什么结论
一次 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 | 异常从何时开始 |
因此,一次好的主机应急响应,不是“查杀木马”,而是重新回答:
发生了什么、影响了什么、攻击者还拥有什么,以及这台机器还能不能被继续信任。