nginx异常关闭背后:服务器挖矿病毒排查与清除实战
发布时间:2026/10/10 12:56:19 锦皓数字建站

凌晨两点半手机警报把我从床上拽了起来。登录服务器一看nginx已经退出curl本机返回502top里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 正常内核线程长什么样方括号与PPID2Linux进程分成用户态进程和内核线程两大类。内核线程由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 PIDstat的修改时间、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 --stateenabled 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 noSSH使用密钥认证并禁用密码登录在确认密钥可用后再关闭密码登录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 threshold80 alert0 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% alert1 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比什么都管用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。