凌晨两点半,手机警报把我从床上拽了起来。登录服务器一看,nginx已经退出,curl本机返回502;top里CPU飙到200%,free显示内存几乎见底。一开始我也以为是白天改的nginx location规则出了问题,想着回滚配置、重启nginx就完事。但翻了半天配置,读了几遍错误日志,什么异常都没有。直到我把top切到进程树视图,才在列表里看到两个陌生名字:kauditd0和kswapd0,各自占了接近100%的CPU。这两个名字看起来很像Linux内核线程,但真正的内核线程名字是带方括号的,而且不会以普通进程的形式出现在用户态进程列表里。那一刻我基本确定:不是nginx的锅,是服务器被植入了挖矿病毒。这篇文章就完整复盘这次从nginx异常关闭入手,到定位并清除挖矿病毒的整个处置过程,把每一步的判断依据和操作命令都摊开讲清楚,给同样遇到"服务器无故CPU飙高、nginx莫名退出"问题的运维同学一份可参考的处置思路。
1. nginx为何成了第一受害者:由CPU与内存异常牵引出的排查路径
1.1 先压住改配置的手:问题不一定在nginx本身
"服务器无故nginx异常关闭"这类问题,十次有八九次不是nginx自身代码或配置的bug,而是系统层面的资源危机波及了nginx。nginx退出的原因常见有两种:一是被内核OOM Killer杀掉,二是被入侵者手动kill掉。区别这两者的最快方式,是登录系统后直接看内核日志:
dmesg | grep -i "out of memory" | tail -20 journalctl -k --since "1 hour ago" | grep -i "oom"如果看到类似Out of memory: Kill process 12345 (nginx) score 850 or sacrifice child的记录,说明nginx是因为内存耗尽被内核主动杀掉的。这等于直接告诉你:nginx只是个受害者,真正的问题是内存被某些进程榨干了。
这时候再看一眼free -h的available列,如果已经接近0,那么即便你把nginx重启起来,也很可能因为内存不足再次被杀,甚至根本起不来。所以遇到nginx异常关闭,我建议先按"系统资源危机"来排查,而不是先翻nginx配置。很多人在这里容易走入误区——反复改worker_processes、调keepalive参数,纯属浪费时间。
1.2 top第一屏:CPU 200%背后的数量级判断
top里的%CPU有一个容易忽略的语义:单进程显示100%代表占满一个逻辑核心,200%就是占满了两个逻辑核。如果一台原本CPU负载在个位数的服务器,突然出现一个进程稳定占用200%且长时间不回落,基本可以认为有连续计算任务在跑,而挖矿就是最典型的连续计算任务。
实操时建议这样操作:
top # 进入后按 P 按CPU排序,再按 M 按内存排序 top 按数字1 # 查看每个逻辑核的使用情况我的观察顺序是:
- 名字:是否包含
kswapd、kauditd、kthreadd这类内核线程常用名。 - 数量:是否同时存在多个相同或相似进程名。
- 状态:是否长期处于R(运行)状态,且反复杀不掉。
- 父进程PPID:正常内核线程的PPID是2,用户态进程的PPID通常是1或具体服务PID。
在内存几乎耗尽的机器上,如果看到CPU被陌生进程占满、内存被莫名其妙吃光,这时候已经不是"优化配置"能解决的问题,而是"主机安全事件"处理流程的起点。
1.3 三个信号叠加,基本可以定性为入侵
我自己处理过几起类似事件后总结出一个判断标准,三个信号同时出现基本就可以停止"查nginx故障"这个方向:
- nginx在无配置变更、无流量突增的情况下退出,且内核OOM记录里有nginx被杀日志。
- 存在伪装成内核进程的可疑进程持续吃CPU。
- 内存available以肉眼可见的速度下降,且无法通过清理cache回收。
到这里,任务已经从"服务不可用排查"切换为"安全应急响应"。接下来最忌讳的动作是直接重启nginx,因为在入侵未清除前,重启只会让业务窗口反复抖动,同时打草惊蛇。正确做法是先把现场信息完整记录下来,再动手处置。
2. kswapd0与kauditd0的真面目:内核进程的伪装术与识别要点
2.1 正常内核线程长什么样:方括号与PPID=2
Linux进程分成用户态进程和内核线程两大类。内核线程由kthreadd创建,所以它们的父进程PPID基本都是2,并且进程名通常会带一对方括号。比如正常的交换线程在ps -ef里显示为:
root 102 2 0 00:00:00 [kswapd0] root 103 2 0 00:00:00 [kauditd]注意方括号[]的存在,以及PPID等于2,这两点是内核线程的标志性特征。而kswapd0这个交换线程本身只有在内存压力大时才干活,CPU占用平时很低;kauditd是内核审计相关线程,同样几乎不占CPU。换句话说,正常系统里不存在一个叫kswapd0或kauditd0的用户态进程,更不会常年稳定占用200%的CPU。
2.2 病毒为什么偏爱这些名字
挖矿病毒普遍选择伪装成内核线程名,原因很朴素:普通运维对内核线程往往不熟悉,看到kswapd0、kauditd0会先入为主以为是系统自带进程,不敢kill、不敢乱动。名字末尾再加个0,看起来像系统编号,更容易糊弄过去。这个套路其实很陈旧,但确实成功骗过了很多初级运维。
我甚至见过把进程名伪装成kworkerds、kthreads、sysupdate、networkservice的变种。所以与其靠死记几个名字,不如掌握一套通用的识别方法。判断依据永远是:进程的真实身份,而不是显示名。
2.3 用/proc文件系统撕掉伪装
要验证一个可疑进程到底是内核线程还是用户态木马,最直接的办法是查看/proc/PID/目录。内核线程没有用户态可执行文件,而挖矿进程一定有一个真实的落地文件。
# 查看可执行文件真实路径 ls -l /proc/<PID>/exe # 查看启动命令 cat /proc/<PID>/cmdline | tr '\0' ' ' # 查看进程身份与状态 cat /proc/<PID>/status | grep -E 'Name|PPid|State|VmRSS'正常内核线程的exe通常显示为方括号或"unknown",指向的路径不存在。而伪装进程的exe会指向真实文件,常见落地点包括/tmp/.x/、/var/tmp/、/dev/shm/、/usr/lib/...这类不太起眼的目录。文件修改时间往往是最近几天甚至当天。
2.4 判断"是不是病毒"的三条黄金法则
我在实际排查中总结出三条判断规则,命中两条以上基本就可以按病毒处理:
- 一个进程声称自己是内核线程,却在
/proc/PID/exe里有可执行文件路径,这是特征。 - 一个进程声称自己是内核线程,却在持续高频外联网络端口,这是特征。
- 一个进程声称自己是内核线程,但它关联的父进程链指向了crontab、systemd、sshd等用户态入口,这是特征。
这三条都不依赖病毒特征库,单纯从系统行为逻辑就能判断。在紧急处理时,这种"行为识别法"比依赖杀毒软件扫描更快速可靠。
3. 从200%CPU到完整证据链:逐步还原挖矿病毒的排查链路
3.1 第一步:用多种方式确认进程全貌,别盲信单一命令
top只能看到活跃进程,我习惯再用ps把完整快照打出来:
ps -eo pid,ppid,comm,args --sort=-pcpu | head -30 ps auxf | head -80如果怀疑系统命令被rootkit替换了,可以用busybox这种完全静态编译的工具交叉验证,或者直接从/proc文件系统手工遍历进程:
for pid in $(ls /proc | grep -E '^[0-9]+$'); do echo -n "$pid " cat /proc/$pid/comm 2>/dev/null done这份原始进程列表是之后所有处置操作的证据基础,建议先保存到本地,不要只截图。
3.2 第二步:沿着父子关系梳理整个守护链路
拿到可疑进程PID后,马上查它的父进程链。常见的挖矿持久化链路有三种,我发现后基本能快速确定清理范围:
crond -> 恶意脚本(sh) -> 挖矿进程:说明攻击者写入了计划任务,定时拉起。systemd -> 恶意service -> 挖矿进程:说明注册了systemd自启动服务。sshd -> 挖矿进程:说明攻击者利用某个漏洞拿到shell后直接运行的,可能没有做持久化。
这种父子关系恰好解释了"为什么杀完进程过几分钟又出现"。因为单纯kill -9主进程没有任何意义,父进程(cron或systemd)会按预定间隔重新把它拉起来。所以后面处置时,清理顺序非常重要。
3.3 第三步:根据exe路径定位落地文件与启动脚本
顺着/proc/PID/exe找到的路径,通常是二进制文件本体,但旁边往往还藏着下载器脚本、守护脚本、配置文件。建议做一次完整的时间线梳理:
ls -l /proc/<PID>/exe stat /tmp/.x/xxx lsattr /tmp/.x/xxx lsof -p <PID>stat的修改时间、lsattr的特殊属性,都能帮你判断哪些文件是同一批落地的。如果文件带i属性(immutable),删除前需要先chattr -i去除。很多应急新手直接rm发现删不掉,就是因为没检查隐藏属性。
3.4 第四步:计划任务、systemd服务与SSH后门全面检查
这一步是找到"守护机制"的关键,也是很多清理不彻底的人最容易漏的地方。我有一套固定检查清单:
# 计划任务 crontab -l cat /etc/crontab ls -la /etc/cron.d/ /etc/cron.hourly/ /etc/cron.daily/ cat /var/spool/cron/root # systemd 自启动服务 systemctl list-unit-files --state=enabled systemctl status 恶意服务名 # 开机启动 cat /etc/rc.local # SSH 后门 cat /root/.ssh/authorized_keys cat /etc/ld.so.preload tail -50 ~/.bashrc /etc/profile特别说明一下/etc/ld.so.preload,很多高级变种会在这里塞一个恶意so文件,把所有系统命令的进程信息都"过滤"掉,让ps、top、netstat看不到挖矿进程,整台机器看起来干干净净,但CPU就是100%。这是rootkit惯用手法,清理时只需要清空这个文件里的内容并删除恶意so。
3.5 第五步:网络连接与威胁情报确认
挖矿程序一定会连接矿池或钱包地址,抓一次网络连接快照能拿到重要的溯源线索:
ss -anpt | head -50把可疑进程的外联IP记录下来,去微步在线、奇安信等威胁情报平台查一下。如果确认是矿池IP或恶意IP,证据链就闭环了。处置前建议直接用防火墙或云安全组把出口流量限到白名单,防止挖矿程序继续外联下载新的载荷,也避免它把服务器当跳板。
4. 清理、修复与确认:处置挖矿感染的正确操作顺序
4.1 先留证据再动手:处置前的三条准备
很多人遇到这种情况的第一个冲动就是kill -9,但我不建议这么做。清理前至少完成三件事:
- 把进程快照、文件哈希、网络连接信息保存到本地,方便后续分析和溯源。
- 准备好静态编译的工具(如busybox),防止系统命令被替换后手里没有"干净"的排查工具。
- 对关键业务数据做一次快速备份,尤其是数据库,防止清理过程中出现误伤。
准备做完后,如果条件允许,先把服务器的外网出口在安全组层面收紧,只放开管理端口和你需要的业务端口,把挖矿进程的网络通道先断掉。
4.2 正确的清理顺序:先断持久化,再杀进程
我踩过一次坑之后总结出的顺序是:先删除或禁用所有持久化配置,最后杀主进程。
如果先杀主进程,守护进程会在几十秒内重新拉起挖矿程序,并且重新写回文件;如果先删文件再杀进程,进程在内存里还活着,照样挖矿。所以正确顺序是:
# 1. 清空/注释计划任务 crontab -r rm -f /etc/cron.d/xxx /var/spool/cron/root # 2. 禁用恶意 systemd 服务 systemctl disable <恶意服务名> # 3. 清空 ld.so.preload echo -n > /etc/ld.so.preload # 4. 移除落地文件(先 chattr -i 再去掉不可变属性) chattr -i /tmp/.x/xxx rm -rf /tmp/.x /var/tmp/xxx /dev/shm/xxx # 5. 最后杀主进程 kill -9 <PID>这个执行序列看起来简单,但每一步都有明确的"为什么":杀掉主进程之前,系统里已经没有能重新拉起它的定时任务和服务,也没有能重新写出文件的脚本,最后一步才能做到真正"断根"。
4.3 修复被篡改的系统组件:校验、重装与内核检查
清理完病毒文件之后,还需要确认系统自身是否被篡改。重点检查三块:
# 1. 命令与系统包完整性 rpm -Va | head -50 # CentOS/RHEL dpkg --verify | head -50 # Debian/Ubuntu # 2. LD_PRELOAD 污染 cat /etc/ld.so.preload # 确认已清空或删除 # 3. 异常内核模块 lsmod | grep -iE 'malware|unknown|可疑关键字'如果校验结果里ps、top、lsof、netstat、ss等命令被标记异常,说明系统命令包确实被替换过,需要用包管理器重装这些软件包,例如yum reinstall procps-ng net-tools或apt install --reinstall procps net-tools。另外,无论有没有发现后门,所有账号密码都建议立刻更换,authorized_keys里的陌生公钥逐条检查,不认识的直接删。
4.4 CPU回落、内存恢复与nginx业务复通的验证标准
清理完并不等于结束,必须用数字来验证。我的复查标准很简单:
top里%Cpu(s)回落到个位数,找不到刚才的可疑进程名。free -h的available数值明显回升到正常水位。- 用
ss -anpt复查网络连接,矿池IP不再出现。
此时再去启动nginx才是有意义的:
nginx -t systemctl start nginx curl -I http://127.0.0.1/这么做的逻辑是先把系统资源危机彻底解决,再恢复业务服务,否则nginx很可能再次成为OOM的牺牲品。启动后建议观察30分钟,确认nginx进程稳定、CPU没有再次爬升。
4.5 什么时候建议直接重装系统
这里说点实在话。如果排查中发现以下任何一条,我建议直接备份数据后重装系统,而不是继续手工清理:
- 加载了可疑内核模块,因为你无法确认内核层面被改了什么。
ps、top、lsof等系统命令被替换,且rpm -Va校验大量异常。/etc/ld.so.preload被污染过,因为攻击者有可能已经通过rootkit隐藏过更深的操作。- 无法完全确定攻击者在服务器上停留了多久、访问过哪些数据。
手工清理rootkit的时间成本通常比重装高得多,而且心理上总是没底。对于被深度入侵的主机,重装系统并重新部署业务,反而是一种更经济的止损方式。
5. 亡羊补牢:从这次入侵反推出的漏洞入口与基线加固方案
5.1 攻击入口哪里来:不要指望日志里有完美答案
处理完这起事件后,我尝试溯源过攻击入口,但这类入侵往往不是针对某个人的定向攻击,而是全网自动扫描。攻击者一般通过SSH弱口令、部署的中间件未授权访问、Web应用漏洞、系统漏洞未打补丁等入口进来,然后脚本化地植入挖矿程序。
我可以查/var/log/secure里的登录记录、history历史命令、可疑进程启动时间、文件时间戳等,但这些日志攻击者本身也可能会清除或篡改。所以我的态度是:与其花费大量精力做不一定有结果的溯源,不如直接把所有常见入口全部加固一遍,让服务器成为"难啃的骨头"。
5.2 可以直接抄的服务器加固清单
这里给你一份我实践过、可以直接照做的清单:
| 类别 | 措施 | 说明 |
|---|---|---|
| SSH | 禁止root远程登录 | 修改/etc/ssh/sshd_config中PermitRootLogin no |
| SSH | 使用密钥认证并禁用密码登录 | 在确认密钥可用后再关闭密码登录 |
| SSH | 限制登录来源IP | 防火墙或安全组只放行办公网段 |
| 中间件 | 修改默认端口 | Redis、MongoDB、MySQL等不要用默认端口裸奔 |
| 中间件 | 开启密码认证/禁用危险命令 | Redis的rename-command、MySQL强密码策略 |
| 系统 | 及时更新内核与软件包 | 关注漏洞公告,每月至少一次yum update/apt upgrade |
| 权限 | 关键文件加chattr保护 | 比如对/etc/crontab加上+i属性防止被写 |
| 监控 | 部署进程级告警 | 见下面的脚本示例 |
5.3 用最低成本搭一套异常进程告警
不是所有团队都有完善的监控系统,我提供一个极简方案:写一个shell定时任务,每分钟检查一次是否有CPU占用过高的可疑进程,发现就推送告警到企业微信或钉钉的机器人webhook。
#!/bin/bash # /usr/local/bin/check_cpu_proc.sh threshold=80 alert=0 ps -eo pid,comm,pcpu --sort=-pcpu --no-headers | awk -v t="$threshold" '$3 > t { printf "%s %s %s\n", $1, $2, $3 }' | while read pid name cpu; do # 排除系统常见进程 case "$name" in [kswapd0]|[kauditd]|kworker*|systemd|sshd|nginx|mysqld) continue;; esac echo "可疑进程: PID=$pid NAME=$name CPU=$cpu%" alert=1 done # 如果存在可疑进程,调用webhook发送告警 # curl -s -X POST -H 'Content-Type: application/json' \ # -d '{"msgtype":"text","text":{"content":"服务器异常进程告警"}}' \ # "https://your-webhook-url"这段脚本的好处是不依赖第三方agent,只要有cron就能跑。当然,真正的企业级监控还是建议接入node_exporter加Prometheus那套体系,但应急阶段这种轻量脚本已经足够帮你及时发现"CPU突然飙高"的异常了。
5.4 处理完后的第1、3、7天复查重点
最后再说说后续观察期。我在处理完这类事件后,会在第1天、第3天、第7天做三次复查,重点看四样东西:
- 进程列表里有没有再出现伪装的内核线程名。
crontab -l和/etc/cron.d/里有没有新增计划任务。ss -anpt里有没有新的可疑外联IP。- Web目录里有没有最近被修改的脚本文件,比如
find /www/ -type f -mtime -7 -name "*.php"。
如果这四样在七天里都干干净净,才敢把心放回肚子里。那次事件给我的直接改变是:现在遇到nginx异常退出,我第一反应是先查系统资源、查陌生进程,而不是急着改配置。服务器上的业务越多,越要保持这份警觉,一份清晰的排查checklist,比什么都管用。