服务器 CPU 莫名其妙 800%:一次挖矿木马的完整排查记录
监控告警:CPU 使用率长期 100%,负载 12(4 核机器)。但 top 打开一看,排在最前面的进程名字很陌生,或者干脆看起来是正常的系统进程名,可 CPU 占用高得离谱。这是典型的被植入挖矿程序的信号。
挖矿木马跟"服务器被入侵"是两件事:入侵是入口(弱密码 SSH、应用漏洞、Redis 未授权、弱口令后台),挖矿是入侵者利用你 CPU 的方式。很多站长发现被入侵时,服务器已经在给别人挖了一两个月的矿了。
本文按发现 → 定位 → 清除 → 加固四步来讲,每一步都给出可直接执行的命令和判读依据。
一、发现:五个挖矿的典型信号
信号 1:CPU 长时间高占用,但没找到归属业务
top -c -o %CPU
# 按 CPU 排序,看 TOP 是什么进程
# 更干净的视角:不显示内核线程,看真正的用户态高占用
ps -eo pid,ppid,user,%cpu,%mem,etime,cmd --sort=-%cpu | head -20重点观察 etime(运行时长)。如果某个进程已经跑了 30 天不停、重启后又在,那就是常驻型挖矿。
信号 2:陌生进程名或伪装成系统进程
常见伪装手法:
- 名字叫
kworker、kthreadd、mysql、nginx、systemd-xxx,但看着就不对(比如名字后面带随机后缀) - 名字是一串随机字符,如
kjhawef、xnrig - 进程名里带
[kworker/0:1]这种方括号的其实是内核线程,正常;但如果用ps能看到完整路径那就是用户态假货
# 内核线程 vs 用户态进程:看 cmdline 是否可读
# 内核线程的 /proc/PID/cmdline 通常是空的
for p in $(pgrep kworker); do
echo "== $p =="
tr '\0' ' ' < /proc/$p/cmdline 2>/dev/null; echo
readlink -f /proc/$p/exe
done
# 如果 kworker 有 exe 路径,就是伪装进程,重点怀疑对象信号 3:可疑的外连连接
矿机必须连矿池,所以一定会有出站连接。查当前网络连接,看有没有连到陌生 IP 的 ESTABLISHED:
ss -tnp state established
# 关注 Foreign Address 是境外 IP、端口在 3333/4444/5555/7777/14444 等矿池常用端口
# 查某个进程的网络连接
ss -tnp | grep pid=$(pgrep 可疑进程名)
# 查可疑 IP 归属(用 whois 或在线查询)
whois 1.2.3.4 | grep -iE 'country|org-name'信号 4:异常的计划任务(持久化的关键)
木马要在重启后复活,一定会写 crontab 或 systemd 单元。这是最容易发现、也最容易被遗漏的地方,共五个位置:
# 1) root 与其他用户的 crontab
crontab -l
for u in $(awk -F: '$3>=1000 || $1=="root" {print $1}' /etc/passwd); do
echo "== $u =="; crontab -u "$u" -l 2>/dev/null
done
# 2) 系统级 cron 文件
ls -la /etc/cron.d/ /etc/cron.daily/ /etc/cron.hourly/ /etc/cron.minutely/
grep -rn "curl\|wget\|base64\|/tmp/\|/dev/shm" /etc/cron* 2>/dev/null
# 3) systemd 单元(自有 unit 与 override 都要看)
systemctl list-units --type=service --state=running | grep -viE 'systemd|ssh|cron|network'
ls -la /etc/systemd/system/ | grep -viE 'multi-user|getty|dbus'
systemctl list-timers --all
# 4) init.d 与 rc.local
ls -la /etc/init.d/ ; cat /etc/rc.local 2>/dev/null
# 5) 用户级 systemd(容易被忽略)
ls -la /root/.config/systemd/user/ ~/*/.config/systemd/user/ 2>/dev/null信号 5:异常的文件与隐藏目录
挖矿程序一般藏在 /tmp、/dev/shm、/var/tmp 这些"看起来是临时目录"的地方,或者伪装进 /usr/bin、/usr/local/bin。
# 找最近 30 天新增的可执行文件(排除正常包管理路径)
find /tmp /dev/shm /var/tmp /usr/local/bin /usr/bin -type f -executable -mtime -30 -ls 2>/dev/null
# 找被设置成不可变(chattr +i)的文件——木马常用这招阻止你删除
lsattr -R /usr/bin /usr/local/bin /tmp 2>/dev/null | grep -i ' i'
# 找隐藏目录里的大文件(矿机本体通常几 MB,还可能带看门狗)
find / -type d -name ".*" -maxdepth 4 2>/dev/null | head -30
du -sh /tmp/* /dev/shm/* 2>/dev/null | sort -rh | head -10/dev/shm/* 这行特别重要。/dev/shm 是 tmpfs,重启就清空,所以木马作者常把"下载器"放这儿,再用 cron 定时重新下载,实现"重启复活"。很多站长重启一次发现 CPU 降下来了,过两天又上去了,就是这个机制。
二、定位:从高占用进程顺藤摸瓜
步骤 1:锁定进程,读它的真实身份
PID=$(ps -eo pid,%cpu --sort=-%cpu --no-headers | head -1 | awk '{print $1}')
echo "PID=$PID"
# 真实可执行文件路径(软链接会展开)
readlink -f /proc/$PID/exe
# 命令行参数(能看到连的矿池地址)
tr '\0' ' ' < /proc/$PID/cmdline; echo
# 工作目录
readlink -f /proc/$PID/cwd
# 打开的文件(能看到矿池配置文件)
ls -la /proc/$PID/fd/ 2>/dev/null
# 环境变量(有时含矿池配置)
tr '\0' '\n' < /proc/$PID/environ | grep -iE 'pool|wallet|worker'
# 父进程链
ps -o pid,ppid,cmd -p $PID
while [ "$(ps -o ppid= -p $PID | tr -d ' ')" != "1" ]; do
PID=$(ps -o ppid= -p $PID | tr -d ' ')
ps -o pid,ppid,cmd -p $PID
done关键判读:readlink /proc/$PID/exe 如果输出 (deleted),说明可执行文件已被删除但进程还在跑——这是木马清理痕迹的常见手法,此时直接 kill 掉进程即可,磁盘上已经没有这个文件了(但**一定要先找到它是被谁拉起来的**,否则会复活)。
步骤 2:确认是不是矿机(而不是你的正常服务)
挖矿程序的 CPU 特征:单核或多核 100% 满跑、常驻、无 IO、无网络流量波动规律。用一个简单的观察法确认:
# 看该进程的上下文切换与 CPU 时间增长
pidstat -p $PID 1 5 # 需安装 sysstat
# 看它的线程数(矿机通常线程数 = CPU 核数,甚至起多个进程占满所有核)
ls /proc/$PID/task | wc -l
nproc如果线程数接近 nproc、每个线程都跑满、且没有对应的业务功能,基本可以确认。
还有一种变种是无文件挖矿:进程名伪装成 [kworker] 之类,可执行文件通过 memfd_create 在内存中创建,磁盘上找不到文件。判断方法:
# 找磁盘上不存在的进程可执行文件(memfd 的特征)
for p in /proc/[0-9]*; do
pid=${p#/proc/}
exe=$(readlink -f $p/exe 2>/dev/null)
case "$exe" in
*memfd*|*" (deleted)"|"") echo "SUSPECT $pid -> $exe" ;;
esac
done步骤 3:找出复活机制(这一步决定清理是否彻底)
直接 kill 进程只能管几分钟。必须找到:是谁在启动它。按优先级检查:
# 检查 cron 里有没有下载/执行脚本
grep -rn "curl\|wget\|chmod +x\|/dev/shm\|/tmp" /var/spool/cron/ /etc/cron.d/ /etc/crontab 2>/dev/null
# 检查 systemd 里有无可疑 unit
grep -rlniE "curl|wget|/tmp/|/dev/shm|base64" /etc/systemd/system/ /lib/systemd/system/ 2>/dev/null
# 检查有没有设置开机自启的 rc 脚本
grep -rn "shm\|/tmp" /etc/rc*.d/ /etc/init.d/ 2>/dev/null | head
# 检查 SSH 授权密钥(入侵者可能留了后门 key)
cat /root/.ssh/authorized_keys
find / -name authorized_keys -exec echo "== {} ==" \; -exec cat {} \; 2>/dev/null
# 检查有没有被加进 sudoers
grep -rn . /etc/sudoers /etc/sudoers.d/ 2>/dev/null | grep -v '^#' | grep -vE '^.*?(Defaults|%sudo|root)'三、清除:按正确顺序操作
前提:先断开网络,防止边清边被重新感染
# 保留 SSH 通道的前提下封掉其他出站(防止回连下载)
# 更安全的做法:在云控制台把安全组出站改为只允许必要端口
# 或者本地临时限制
iptables -I OUTPUT -p tcp --dport 3333 -j DROP
iptables -I OUTPUT -p tcp --dport 4444 -j DROP
iptables -I OUTPUT -p tcp --dport 5555 -j DROP
iptables -I OUTPUT -p tcp --dport 14444 -j DROP顺序:先断复活源 → 再杀进程 → 再删文件
顺序错了会导致木马立刻复活,白干。正确流程:
# 1) 先禁用/删除 cron 里的恶意条目
crontab -l > /root/cron_backup_$(date +%s).txt # 备份(取证用)
crontab -l | grep -vE "可疑关键词" | crontab - # 或直接 crontab -r 清空后重加正常条目
rm -f /etc/cron.d/可疑文件
rm -f /var/spool/cron/crontabs/用户名 # 用户级
# 2) 停掉可疑 systemd 服务并 mask(mask 防止被其他机制拉起)
systemctl stop 可疑服务名
systemctl disable 可疑服务名
systemctl mask 可疑服务名
rm -f /etc/systemd/system/可疑服务名.service
systemctl daemon-reload
# 3) 解除文件不可变属性(否则删不掉)
chattr -i /path/to/可疑文件
# 目录也要解
chattr -i -R /可疑目录
# 4) 杀进程(先 TERM 再 KILL)
kill -TERM $PID
sleep 3
kill -KILL $PID
# 5) 删除文件
rm -f /path/to/可疑文件
rm -rf /可疑目录
# 6) 确认清理干净
ps -eo pid,%cpu,cmd --sort=-%cpu | head
ls -la /tmp /dev/shm特别注意:不要只删不改
挖矿木马的入口还在的话,删完几天内必然再次被植入。清理的同时必须做入口修复:
# SSH:禁用密码登录,改用密钥
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl reload sshd
# 检查 Redis 是否开在公网且无密码(最常见的入口之一)
ss -tlnp | grep 6379
# 修复:bind 127.0.0.1 + requirepass + 禁用危险命令
# /etc/redis/redis.conf
# bind 127.0.0.1
# requirepass 强密码
# rename-command FLUSHALL ""
# rename-command CONFIG ""
# rename-command EVAL ""
# 检查有没有被添加的登录用户(UID 0 的隐藏账号)
awk -F: '$3==0 {print}' /etc/passwd
awk -F: '$2=="" {print "空密码用户: " $1}' /etc/shadow
# 检查 SSH 登录历史,找出入侵时间点
last -i | head -30
grep -iE "Accepted|Failed" /var/log/auth.log | tail -50四、加固:让下一次进不来
清理完成后的加固清单,按收益排序:
- SSH 全面收口:禁密码登录、禁 root 直登、改默认端口(降低噪音)、装 fail2ban 或 sshguard 自动封 IP。
- 所有对外服务做最小暴露:Redis / MySQL / Elasticsearch / Docker API 一律 bind 内网,用
ss -tlnp全量核对一次监听地址。 - Web 应用与面板及时更新:宝塔面板、WordPress 插件、未打补丁的 CMS 是重灾区。
- Webshell 排查:如果入口是 Web,一定要扫一遍网站目录里的可疑 PHP 文件(常见特征:
eval($_POST、base64_decode、超短文件名、修改时间异常)。
# WebShell 快速排查
find /var/www -type f -name "*.php" -mtime -60 -ls 2>/dev/null | head -40
grep -rlnE "eval\s*\(\s*\\\$_(POST|GET|REQUEST)|base64_decode\s*\(\s*\\\$_|assert\s*\(\s*\\\$_|preg_replace\s*\(.*/e" /var/www 2>/dev/null | head -30
# 按文件大小找异常:WebShell 通常只有几百字节
find /var/www -type f -name "*.php" -size -2k -mtime -90 2>/dev/null | head -30- 装个轻量 HIDS:
auditd监控关键目录写入、rkhunter/chkrootkit定期扫 rootkit、AIDE做文件完整性基线。
# AIDE 初始化基线(装完立即做一次,之后的变化才看得见)
apt install -y aide
aideinit
mv /var/lib/aide/aide.db.new /var/lib/aide/aide.db
# 之后每天跑一次比对
aide --check | grep -A5 "changed"- 加一条 CPU 异常告警,早发现早处理。挖矿木马最诚实的特征就是 CPU,这是最便宜的检测手段。
#!/bin/bash
# check_cpu_abuse.sh 挂 cron 每 5 分钟
LOAD=$(awk '{print $1}' /proc/loadavg)
CORES=$(nproc)
THRESHOLD=$(echo "$CORES * 1.5" | bc)
if [ "$(echo "$LOAD > $THRESHOLD" | bc)" -eq 1 ]; then
echo "HIGH LOAD: $LOAD cores=$CORES at $(date)" >> /var/log/cpu_abuse.log
ps -eo pid,user,%cpu,etime,cmd --sort=-%cpu | head -6 >> /var/log/cpu_abuse.log
fi五、如果机器已经不可信,别硬清
最后给一个诚实的建议:如果排查过程中发现了内核级 rootkit(AIDE 报出 /lib/modules 有异常改动、lsmod 有陌生模块、ps 输出与实际进程数对不上)、或者入侵者已经拿到了 root,最省事最安全的做法是重装系统。
在有备份的前提下,重装的成本通常是 1 小时;而在一台已被 root 的机器上"精心清理",你永远无法 100% 确认清干净了——木马作者只要留一个后门,之前所有清理工作都归零。判断标准很简单:
- 只用最基础手段(cron、进程、文件)就找到木马 → 可以清理
- 发现 rootkit 迹象、
ps/ls/netstat等基础工具本身被替换 → 重装
六、总结:一份可执行的排查顺序
把本文浓缩成你下次遇到时能直接照着做的顺序:
ps --sort=-%cpu找到高占用进程,readlink /proc/PID/exe读真实路径- 查五个持久化位置:用户 crontab、
/etc/cron*、systemd unit、init.d/rc.local、用户级 systemd - 查
/tmp、/dev/shm、/var/tmp里的近期可执行文件 - 查
/root/.ssh/authorized_keys和 UID 0 用户 - 先删 cron/systemd,再杀进程,再删文件(顺序不能反)
- 修复入口:SSH、Redis、应用补丁、WebShell
- 加固:fail2ban + auditd/AIDE + CPU 告警
- 发现 rootkit 迹象 → 直接重装,别恋战
挖矿木马本身不破坏数据,但它在的两周里你的服务器性能、SEO 排名(响应时间变慢直接影响)都在受损,而且入侵者既然能装矿机,就能顺手装别的。所以发现之后,按上面的顺序走一遍,把入口真正堵上,这事才算完。