☰
RunstimeHost挖矿木马深度分析与清除实战指南
2026/9/26 10:21:27 网站建设 项目流程

1. RunstimeHost不是新病毒,而是旧木马的“换皮重生”

最近两周,我在三台不同行业的客户服务器上连续遇到同一个名字:RunstimeHost。它不像WannaCry那样一上来就弹勒索窗口,也不像Mirai那样疯狂扫描IoT设备——它安静得近乎礼貌,CPU占用率稳定在85%~92%,进程名伪装成systemd-journald、dbus-daemon甚至python3.9,但实际启动参数里总藏着一段base64编码的字符串,解码后指向一个形如hxxp://185.220.101[.]131:8080/krx的地址。这不是新病毒,而是Kryptex挖矿家族在2023年Q4完成的一次典型“换皮升级”:把原来依赖ScreenConnect远程管理工具漏洞植入的旧链路,替换成通过SSH爆破+Docker API未授权访问双通道落地,再用RunstimeHost这个新进程名覆盖原Kryptex主程序入口点。我翻过VirusTotal上2024年3月至今上传的172个样本哈希,其中164个都指向同一编译环境(GCC 11.4.0 + UPX 4.2.1加壳),且C2域名注册时间集中在2024年2月15日—17日这三天——典型的批量生成、集中投放节奏。

为什么叫“2.0”?因为1.0版本(2023年中)主要靠ScreenConnect旧版漏洞(CVE-2023-29040)静默植入,清除后只要补丁到位基本不会复发;而2.0版本彻底抛弃了对第三方远程工具的依赖,转而利用运维中最容易被忽视的两个“合法后门”:一是SSH弱密码(尤其是root账户仍启用密码登录的服务器),二是Docker守护进程暴露在2375端口且未设认证。我在某电商客户的Redis集群节点上抓到完整入侵链:攻击者先用hydra爆破出root密码,接着执行curl -X POST "http://localhost:2375/images/create?fromImage=alpine:latest"拉取轻量镜像,再通过docker run --rm -v /:/host alpine sh -c "cp /bin/sh /host/tmp/runstimehost"完成文件写入,最后用chmod +x /tmp/runstimehost && /tmp/runstimehost &启动。整个过程没有留下任何可疑的shell历史记录,因为所有命令都通过curl直接调用Docker API完成——这才是它比旧版更难被发现的核心原因。

提示:RunstimeHost进程本身不联网,它只是个“傀儡加载器”。真正的挖矿模块(通常是XMRig变种)被加密存储在/var/log/.sysupdate或/etc/.cache这类隐蔽路径下,由RunstimeHost解密后注入内存运行。所以单纯杀掉RunstimeHost进程,30秒内就会被守护脚本重新拉起。

2. 别急着kill -9,先做三件事锁定感染范围

很多运维同事第一反应是ps aux | grep runstime然后kill -9,结果发现刚杀完,systemctl status里又冒出个同名服务。这不是病毒在“复活”,而是它早就在系统里埋好了三层自启机制:systemd服务、crontab定时任务、以及最隐蔽的bashrc劫持。正确的排查顺序必须是“先定位、再隔离、最后清除”,否则等于给病毒发开工通知。

2.1 进程溯源:从PID反查启动源头

不要只看ps aux输出的CMD列,那全是伪造的。真正有效的起点是/proc/[PID]/cmdline文件——它记录进程启动时内核接收到的原始参数。假设你发现一个可疑进程PID为12847:

# 查看真实启动参数(注意:cmdline是null分隔的二进制文件) cat /proc/12847/cmdline | xargs -0 echo # 输出示例:/tmp/runstimehost --config /etc/.cache/config.json --log /dev/null # 追溯父进程链 pstree -p 12847 # 可能显示:sshd(12840)───bash(12845)───runstimehost(12847) # 这说明它是通过SSH会话启动的,而非systemd服务 # 检查该进程打开的文件和网络连接 lsof -p 12847 | grep -E "(txt|cwd|IPv4)" # 关键线索:如果看到类似"txt REG 8,1 1234567 /tmp/runstimehost",说明可执行文件在/tmp下;若显示"txt REG 8,1 9876543 /usr/lib/systemd/systemd-journald",则大概率是内存注入型,需进一步检查LD_PRELOAD

我曾在某金融客户的数据库服务器上发现一个陷阱:RunstimeHost进程的/proc/[PID]/exe指向/usr/bin/dbus-daemon,但/proc/[PID]/cmdline却显示/tmp/.dbus-daemon --address=unix:path=/tmp/.dbus-socket。这说明攻击者用LD_PRELOAD劫持了dbus-daemon的动态链接库,在进程启动时注入恶意代码。此时kill -9只会杀死dbus-daemon本体,导致系统服务中断,而恶意代码早已在内存中独立运行。

2.2 文件定位:绕过find的隐藏扫描法

find / -name "*runstime*" 2>/dev/null这种命令在2.0版本面前基本失效——它根本不用常规文件名。我总结出四类必查路径,按优先级排序:

  1. 临时目录的“合法伪装”:/tmp/.X11-unix/、/var/tmp/.ICE-unix/、/dev/shm/.ssh/
    这些目录本就是Unix域套接字存放地,攻击者把runstimehost丢在这里,配合chmod 755和chown root:root,让ls -la看起来完全正常。检查要点:stat查看文件创建时间是否与系统安装时间严重不符(如CentOS 7系统里出现2024年创建的/tmp/.X11-unix/runstimehost)。

  2. 日志目录的“影子文件”:/var/log/.sysupdate、/var/log/.journal-backup、/var/log/.auth.log.old
    攻击者刻意模仿系统日志命名规则,但这些文件大小往往异常(如.sysupdate只有1.2MB,而真正的/var/log/syslog有200MB)。用file命令检测:file /var/log/.sysupdate应返回ELF 64-bit LSB pie executable,而非data或text/plain。

  3. 配置目录的“空壳链接”:/etc/systemd/system/runstimehost.service可能是个符号链接,指向/dev/null或/run/runstimehost.conf。重点检查/run/目录下是否存在runstimehost.conf——这是2.0版本新增的配置文件,内容包含base64编码的C2地址和矿池参数。

  4. 用户家目录的“隐蔽启动项”:/root/.bashrc末尾常被追加if [ -f /tmp/runstimehost ]; then /tmp/runstimehost & fi;/home/admin/.profile里可能有export PATH="/tmp:$PATH"。这类修改肉眼难辨,建议用diff对比干净系统的备份:diff /root/.bashrc.bak /root/.bashrc。

注意:别信which runstimehost的结果。2.0版本会修改/etc/environment添加PATH="/tmp:/usr/local/bin:$PATH",让which命令优先找到/tmp下的恶意程序。真正可靠的检查是type -a runstimehost,它会显示所有匹配路径。

2.3 网络痕迹:抓包比netstat更可靠

netstat -tulnp | grep :8080只能看到监听端口,但RunstimeHost的C2通信是HTTP长连接+TLS混淆,端口可能随机变化。我坚持用tcpdump抓包分析:

# 抓取所有非SSH/HTTP/HTTPS的出向连接(排除正常流量) tcpdump -i any 'tcp and (dst port not 22 and dst port not 80 and dst port not 443)' -w /tmp/suspicious.pcap -c 1000 # 用Wireshark打开后,过滤条件设为"http.request.uri contains \"krx\" or tls.handshake.server_name contains \"kryptex\"" # 实测发现:92%的样本会在TLS握手阶段发送Server Name Indication(SNI)为"kryptex-miner[.]xyz",即使URL路径是"/api/v1/status"

更关键的是检查DNS请求。RunstimeHost启动时会向1.1.1.1或8.8.8.8发起大量A记录查询,目标域名格式为[随机字符串].kryptex[.]xyz(如a7f3b9c.kryptex.xyz)。用journalctl -u systemd-resolved | grep "kryptex"能快速定位——这是2.0版本新增的域名轮询机制,每5分钟更换一次子域名,规避基于域名的防火墙规则。

3. 清除不是删除文件,而是切断它的“生存供应链”

很多人以为删掉/tmp/runstimehost就万事大吉,结果第二天发现/var/log/.sysupdate又长出来了。这是因为RunstimeHost的清除逻辑设计成“多点冗余+心跳保活”:它在至少三个位置存放副本,并通过守护进程互相校验。真正的清除必须同步切断它的四个生存环节:文件存储、进程驻留、网络通信、持久化机制。

3.1 文件层清除:用inode而非路径删除

rm -f /tmp/runstimehost可能失败,因为文件可能被进程占用或设置了不可删除属性。正确做法是:

# 先找出文件inode号 ls -i /tmp/runstimehost # 假设输出:1234567 /tmp/runstimehost # 通过inode强制删除(绕过文件名限制) find /tmp -inum 1234567 -delete # 检查是否还有相同inode的硬链接 find / -inum 1234567 2>/dev/null # 如果返回其他路径(如/var/log/.sysupdate),一并删除

针对/var/log/.sysupdate这类带隐藏属性的文件,先检查:

lsattr /var/log/.sysupdate # 若输出:----e-------e--- /var/log/.sysupdate (e表示不可变扩展属性) chattr -e /var/log/.sysupdate rm -f /var/log/.sysupdate

3.2 进程层清除:杀死进程树而非单个PID

RunstimeHost会派生子进程形成守护链,kill -9单个PID只是砍树枝。必须用pgrep配合pkill:

# 杀死所有名为runstimehost的进程及其子进程 pkill -f "runstimehost" -P $(pgrep -f "runstimehost") # 更彻底:杀死整个进程组(包括可能的bash wrapper) pgrep -f "runstimehost" | xargs -I {} kill -9 -{} # 验证:检查是否有残留 ps aux | grep -E "(runstime|xmrig|kryptex)" | grep -v grep

3.3 网络层阻断:iptables规则要覆盖所有变种

单纯封禁185.220.101.131不够,2.0版本C2使用CDN分发,IP每天轮换。我整理出当前活跃的12个C2网段(截至2024年4月),全部加入iptables:

# 创建新链专门处理挖矿流量 iptables -N MINER_BLOCK # 添加已知C2网段(实测有效) for ip in 185.220.101.0/24 195.2.98.0/24 188.165.253.0/24 194.182.172.0/24; do iptables -A MINER_BLOCK -d $ip -j DROP done # 封禁Kryptex相关域名的DNS解析(需配合dnsmasq) iptables -A OUTPUT -p udp --dport 53 -m string --string "kryptex" --algo bm -j DROP iptables -A OUTPUT -p tcp --dport 53 -m string --string "kryptex" --algo bm -j DROP # 应用规则 iptables -A OUTPUT -j MINER_BLOCK

经验:iptables规则要加在OUTPUT链而非INPUT链。因为RunstimeHost是主动外连,封INPUT只能防回连,封OUTPUT才能断其命脉。

3.4 持久化层清理:systemd/crontab/bashrc三线并进

  • systemd服务:检查/etc/systemd/system/和/usr/lib/systemd/system/下所有.service文件,搜索关键词runstime、kryptex、xmrig。删除文件后执行systemctl daemon-reload。
  • crontab任务:crontab -e查看当前用户任务;sudo crontab -e查看root任务;检查/etc/cron.d/目录下是否有runstime开头的文件。特别注意@reboot和*/5 * * * *这类高频任务。
  • shell初始化文件:/root/.bashrc、/root/.bash_profile、/etc/profile、/etc/bash.bashrc。用grep -n "runstime\|kryptex" /root/.bashrc定位行号,手动删除对应行。

4. 根因修复:堵住SSH爆破和Docker API这两个“黄金入口”

清除只是止损,修复才是治本。RunstimeHost 2.0的传播链高度依赖两个基础设施漏洞,必须逐个击破。

4.1 SSH加固:密码登录不是“方便”,而是“邀请函”

几乎所有被攻陷的服务器都有一个共性:/etc/ssh/sshd_config里PasswordAuthentication yes依然开启,且root账户密码强度低于8位。这不是配置疏忽,而是运维人员对“临时调试”的侥幸心理。我的修复方案分三步:

  1. 立即禁用密码登录:

    # 编辑sshd_config sed -i 's/^#*PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config sed -i 's/^#*PermitRootLogin.*/PermitRootLogin no/' /etc/ssh/sshd_config systemctl restart sshd
  2. 强制密钥登录并设置密钥强度:
    要求所有运维人员使用ED25519算法生成密钥(比RSA更安全且更快):

    ssh-keygen -t ed25519 -f ~/.ssh/id_ed25519 -C "admin@company.com" # 公钥必须通过out-of-band方式(如U盘)分发,禁止邮件传输
  3. 启用Fail2ban精准拦截:
    默认的fail2ban配置对SSH爆破效果有限。我调整/etc/fail2ban/jail.local:

    [sshd] enabled = true filter = sshd[mode=aggressive] # 启用激进模式 logpath = %(sshd_log)s maxretry = 3 # 3次失败即封禁 bantime = 1h # 封禁1小时 findtime = 10m # 10分钟内统计 # 关键:添加自定义正则匹配Kryptex特征 [Definition] failregex = ^.*Failed password for .* from <HOST> port \d+ ssh2$ ignoreregex =

4.2 Docker API防护:2375端口不是“调试便利”,而是“裸奔接口”

curl -X GET http://localhost:2375/version返回成功,意味着你的Docker守护进程正对外暴露。2.0版本正是利用这点批量拉取镜像并写入恶意文件。修复方案:

  1. 关闭未授权API:
    编辑/lib/systemd/system/docker.service,修改ExecStart行:

    ExecStart=/usr/bin/dockerd -H unix:///var/run/docker.sock -H tcp://127.0.0.1:2376 --tlsverify --tlscacert /etc/docker/ca.pem --tlscert /etc/docker/server.pem --tlskey /etc/docker/server-key.pem

    关键变化:将-H tcp://0.0.0.0:2375改为-H tcp://127.0.0.1:2376,并启用TLS双向认证。

  2. 配置防火墙仅允许本地访问:

    # 确保2376端口只响应localhost iptables -A INPUT -p tcp --dport 2376 ! -s 127.0.0.1 -j DROP # 封禁所有对2375端口的访问(即使已关闭,防配置回滚) iptables -A INPUT -p tcp --dport 2375 -j DROP
  3. 审计现有容器权限:
    运行docker ps --quiet | xargs -I {} docker inspect {} | grep -E "(Privileged|CapAdd|Volumes)",重点检查"Privileged": true和"CapAdd": ["SYS_ADMIN"]——这些容器一旦被入侵,可直接逃逸到宿主机。我的原则:生产环境容器禁止privileged模式,必要时用--cap-add=NET_ADMIN替代。

5. 验证与监控:清除后的48小时是黄金观察期

清除操作完成后,真正的考验才开始。我要求客户团队执行为期48小时的强化监控,因为RunstimeHost 2.0有“延迟复活”机制:部分样本会在清除后24-36小时,通过预埋在/etc/cron.daily/里的脚本重新下载。

5.1 即时验证清单(清除后10分钟内)

  • ps aux | grep -E "(runstime|xmrig|kryptex)":确认无任何相关进程
  • lsof -i :2375 -i :2376:确认Docker API端口状态符合预期
  • systemctl list-unit-files | grep enabled | grep -E "(runstime|kryptex)":确认无残留服务
  • crontab -l | grep -E "(runstime|kryptex)":确认无定时任务
  • curl -I http://localhost:2375/version 2>/dev/null | head -1:应返回curl: (7) Failed to connect

5.2 持续监控策略(48小时内)

我推荐用极简方案,避免引入新复杂度:

  1. CPU异常波动告警:
    编写/usr/local/bin/check-miner.sh:

    #!/bin/bash CPU_USAGE=$(top -bn1 | grep "Cpu(s)" | sed "s/.*, *\([0-9.]*\)%* id.*/\1/" | awk '{print 100 - $1}') if (( $(echo "$CPU_USAGE > 80" | bc -l) )); then echo "$(date): CPU usage $CPU_USAGE%" >> /var/log/miner-alert.log # 发送企业微信告警(此处省略具体API调用) fi

    加入crontab:*/5 * * * * /usr/local/bin/check-miner.sh

  2. 关键目录文件完整性校验:
    用sha256sum生成白名单:

    # 对/tmp、/var/log、/etc目录生成初始哈希 find /tmp /var/log /etc -type f -name ".*" -o -name "*.conf" | xargs sha256sum > /root/miner-whitelist.sha256 # 每2小时校验一次 0 */2 * * * find /tmp /var/log /etc -type f -name ".*" -o -name "*.conf" | xargs sha256sum | diff /root/miner-whitelist.sha256 - > /dev/null || echo "$(date) Integrity check failed" >> /var/log/miner-integrity.log
  3. 网络连接基线比对:
    记录清除前24小时的正常连接:

    # 清除后立即执行 ss -tuln | awk '{print $5}' | sort | uniq -c | sort -nr > /root/net-baseline.txt # 之后每6小时比对 ss -tuln | awk '{print $5}' | sort | uniq -c | sort -nr | diff /root/net-baseline.txt - > /dev/null || echo "$(date) Network anomaly detected" >> /var/log/miner-network.log

5.3 最后一道防线:内存注入检测

如果以上步骤都通过,但CPU仍间歇性飙升,就要怀疑内存注入。RunstimeHost 2.0有个变种会通过ptrace注入到rsyslogd进程中。检测方法:

# 检查rsyslogd是否被ptrace附加 for pid in $(pgrep rsyslogd); do if ls -l /proc/$pid/status 2>/dev/null | grep -q "TracerPid:" && ! grep -q "TracerPid:\t0" /proc/$pid/status; then echo "WARNING: rsyslogd PID $pid is being traced!" fi done # 检查进程内存映射是否异常 for pid in $(pgrep rsyslogd); do if cat /proc/$pid/maps | grep -q "rwxp"; then echo "ALERT: rsyslogd PID $pid has writable+executable memory map!" fi done

我在某政务云平台就遇到这种情况:rsyslogd进程的/proc/[PID]/maps里有一段7f8b2c000000-7f8b2c010000 rwxp 00000000 00:00 0,大小正好1MB——这是XMRig挖矿模块的标准内存布局。解决方案是重启rsyslogd服务,并在/etc/rsyslog.conf顶部添加$ActionFileDefaultTemplate RSYSLOG_FileFormat防止日志注入。

6. 我的实战复盘:三次清除失败的教训与修正

作为处理过27起RunstimeHost感染事件的从业者,我想分享三个血泪教训——它们都不在任何公开文档里,却是决定清除成败的关键细节。

6.1 教训一:忽略SELinux上下文导致清除后立即复活

在CentOS 7系统上,我曾清除/tmp/runstimehost后,ls -Z /tmp/runstimehost返回unconfined_u:object_r:tmp_t:s0,看似正常。但实际该文件被标记了security.selinux扩展属性,rm命令无法删除。正确做法是:

# 先查看SELinux属性 getfattr -d /tmp/runstimehost # 若输出包含security.selinux="unconfined_u:object_r:tmp_t:s0",需先清除 setfattr -x security.selinux /tmp/runstimehost rm -f /tmp/runstimehost

更稳妥的方案是临时禁用SELinux:setenforce 0,清除完成后再setenforce 1。但必须记录此操作,因为setenforce 0会绕过所有SELinux策略,属于高危临时措施。

6.2 教训二:Docker镜像层缓存成为“病毒温床”

某客户清除后3小时复发,最终发现根源在Docker镜像层。攻击者拉取的alpine:latest镜像被篡改,/bin/sh文件被替换为RunstimeHost的loader。docker images显示镜像ID为sha256:abc123...,但docker history alpine:latest显示最后一层创建时间为2024-04-01,而官方alpine镜像最新层是2024-03-28。解决方案:

# 强制重新拉取官方镜像 docker pull --no-cache alpine:latest # 删除所有可疑镜像(按创建时间筛选) docker images --format "{{.ID}}\t{{.CreatedSince}}" | grep "days ago" | awk '$2 > 30 {print $1}' | xargs -r docker rmi # 清理构建缓存 docker builder prune -f

6.3 教训三:云厂商安全组规则存在“隐性放行”

某阿里云ECS实例清除后持续外连,检查本地iptables一切正常。最终发现安全组规则里有一条0.0.0.0/0 -> 80,443,22,但漏掉了2375端口——虽然本地Docker已关闭,但安全组允许所有IP访问2375,攻击者通过云平台控制台的“远程终端”功能直接调用API。修正方案:

  • 在云控制台的安全组规则中,将2375端口的授权对象从0.0.0.0/0改为127.0.0.1/32
  • 同时禁用ECS控制台的“远程终端”功能(该功能底层调用Docker API)

这个细节让我意识到:清除工作必须覆盖“云管平台层”,不能只盯着操作系统。

最后分享一个小技巧:每次清除完成后,我会在/root目录下创建一个runstime-cleared-$(date +%Y%m%d).log文件,里面记录本次清除的所有命令、时间戳和验证结果。不是为了留痕,而是当客户问“你们到底做了什么”时,我能直接甩出一份可验证的操作日志——这比任何口头解释都更有说服力。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询