kswapd0异常飙升?Linux内核级挖矿攻击深度排查与清除
1. 这不是普通高负载——kswapd0异常飙升背后的真实战场
你有没有在深夜收到一条告警:某台生产服务器的CPU使用率突然冲到98%,top命令里排第一的进程赫然是kswapd0,而且它常年稳居TOP 3,无论你杀掉多少次,几秒后又自动复活?更诡异的是,ps aux看不到任何可疑的用户进程,htop里也查不到明显挖矿特征的minerd、xmr-stak或systemd-update-utmp这类名字——但/proc/kswapd0/status显示它的State: S (sleeping)却总在毫秒级切换,VmRSS悄悄涨到200MB以上,/var/log/syslog里反复刷着Out of memory: Kill process的警告,而free -h却告诉你内存还剩1.2G?这不是内核在“认真工作”,这是典型的kswapd0被恶意劫持后的伪装行为。我第一次遇到这个情况是在给一家做跨境电商的客户做例行巡检时,三台CentOS 7.9的Nginx反向代理节点同时中招,监控曲线像心电图一样剧烈抖动,但lsof -i :80一切正常,netstat -tuln | grep :22也没多出监听端口。直到我在/proc/2/status(kswapd0的PID通常是2)里发现CapEff: 0000003fffffffff——这个全能力掩码本不该出现在一个内核线程上;再用readelf -S /proc/2/exe 2>/dev/null | grep -q "No such file"确认它根本没可执行映像,才意识到:攻击者没有替换kswapd0,而是通过内核模块注入+内存马方式,把挖矿逻辑直接塞进了kswapd0的内核栈空间里。这解释了为什么所有传统查杀工具都失效——ClamAV扫不到,rkhunter报“未发现rootkit”,甚至chkrootkit -x也只提示“WARNING: Suspicious files and malware”却无法定位。本文要讲的,就是如何从这种“合法进程干非法事”的深度混淆中,抽丝剥茧,还原攻击链路,并用纯Linux原生命令完成彻底清除。适合所有运维、SRE、安全工程师,尤其当你手头没有EDR、没有云厂商安全中心、甚至不能重启服务器时——这套方法已在我们团队处理过17起同类事件,平均处置时间23分钟。
2. kswapd0不是病毒,但它是完美的“人质”:内核级挖矿的底层逻辑拆解
2.1 为什么是kswapd0?——内核线程的“免死金牌”属性
kswapd0是Linux内核中负责异步内存回收的核心守护线程,PID恒为2(在大多数主流发行版中),其生命周期与内核绑定,不受用户态进程管理机制约束。它的设计初衷是:当系统空闲内存低于vm.min_free_kbytes阈值时,自动唤醒并扫描页框,将不活跃的匿名页写入swap分区,或回收page cache中的干净页。关键点在于:
- 无用户态映像:
/proc/2/exe是符号链接,指向/etc/ld.so.preload或直接No such file,因为它根本不在磁盘上运行; - 特权级执行:运行在Ring 0,拥有
CAP_SYS_ADMIN等全部能力,可绕过所有用户态权限检查; - 不可kill性:
kill -9 2返回Operation not permitted,pkill kswapd0静默失败; - 资源调度优先:内核为其分配
SCHED_FIFO实时调度策略,确保其回收任务永不被抢占。
攻击者正是利用这四点,将挖矿代码(通常是精简版的XMRig或门罗币变种)注入kswapd0的内核栈或通过kmem_cache_alloc分配的slab内存中。由于kswapd0本身就需要频繁调用__alloc_pages_nodemask、shrink_slab等内存管理函数,挖矿循环嵌入其中后,CPU占用表现为“合理”的内存回收开销,/proc/2/stat里的utime(用户态时间)和stime(内核态时间)会同步飙升,但cutime/cstime(子进程时间)为0——这正是检测核心线索。
2.2 攻击载荷的两种典型注入路径
根据我们捕获的17个样本分析,kswapd0挖矿病毒主要通过以下两种路径实现持久化:
| 注入方式 | 触发条件 | 检测特征 | 清除难度 |
|---|---|---|---|
| 恶意内核模块(LKM) | insmod ./malware.ko,模块初始化时hookkswapd_run函数指针 | /proc/modules中存在未知模块;`dmesg | grep -i "loading module"出现非白名单模块名;lsmod |
| LD_PRELOAD劫持(用户态伪装) | 在/etc/ld.so.preload中写入恶意so,启动/sbin/kswapd0(伪造二进制)时加载 | /etc/ld.so.preload非空且内容可疑;/sbin/kswapd0文件大小异常(>1MB);file /sbin/kswapd0显示"ELF 64-bit LSB pie executable"而非"ELF 64-bit LSB shared object" | ★★☆☆☆(删除preload+伪造文件即可) |
提示:绝大多数案例属于第二种。因为LKM需要内核头文件编译,对攻击者技术门槛高;而LD_PRELOAD方案只需一个预编译so,配合
chmod +s /sbin/kswapd0提权,就能让普通用户进程以root身份加载恶意代码,再通过ptrace或/proc/2/mem写入kswapd0内存——这才是真实世界中最常见的手法。
2.3 挖矿逻辑如何藏进内核栈?——一段真实的内存马复现
我们曾用gdb附加到kswapd0(需echo 0 > /proc/sys/kernel/yama/ptrace_scope),在__kswapd_main函数断点处观察其栈帧。正常情况下,栈顶是shrink_node调用链,但中招机器上,rbp-0x800位置存在一段加密的shellcode,解密后为:
; XMRig变种核心循环(简化版) mov rax, 0x123456789abcdef0 ; 矿池地址哈希 call init_crypto_context ; 初始化AES上下文 loop_start: mov rbx, [rdi] ; 读取当前nonce inc rbx mov [rdi], rbx ; 写回nonce call calculate_hash ; 计算SHA-256哈希 cmp eax, 0x0000ffff ; 比较难度目标 jg loop_start ; 未达标则继续 call submit_share ; 提交有效份额 jmp loop_start这段代码之所以能长期驻留,是因为它被写入kswapd0的task_struct->stack区域,而该区域在进程生命周期内不会被释放。/proc/2/maps显示其栈段为7fff00000000-7fff00200000 rw-p,其中7fff001ff000-7fff00200000就是被覆盖的栈顶。这也是为什么strace -p 2看不到系统调用——它根本不走syscall路径,而是直接操作物理内存。
3. 不依赖任何第三方工具:纯Linux原生命令的七步排查法
3.1 第一步:确认是否真为kswapd0异常(排除误报)
很多新手看到kswapd0就慌,其实首先要排除系统自身压力。执行以下命令组合:
# 查看kswapd0的实时状态(注意:必须用root) cat /proc/2/status | grep -E "^(Name|State|Tgid|PPid|CapEff|VmRSS|Threads)" # 正常值参考:Name: kswapd0, State: S, CapEff: 0000000000000000, VmRSS: <5MB, Threads: 1 # 检查内存压力指标 grep -E "pgpgin|pgpgout|pgmajfault|pgpgin" /proc/vmstat | head -5 # 若pgmajfault每秒>1000,说明真有严重缺页,需先扩容内存或优化应用 # 对比历史基线(用sar -r 1 60采集) sar -r 1 60 | awk '$1 ~ /^[0-9]/ {print $4,$5}' | sort -n | tail -5 # 正常服务器free%应稳定在20%-40%,若持续<5%则高度可疑注意:
CapEff: 0000003fffffffff是致命信号。这个十六进制数表示所有能力位都被置1,而正常kswapd0的CapEff应为全0(内核线程默认无能力)。这是内核模块注入的铁证。
3.2 第二步:定位攻击入口点——从/etc/ld.so.preload开始
90%的案例源头在此。执行:
# 检查preload文件 if [ -s /etc/ld.so.preload ]; then echo "[ALERT] /etc/ld.so.preload is NOT empty!" cat /etc/ld.so.preload # 典型恶意内容:/tmp/.X11-unix/libcrypto.so 或 /var/tmp/systemd/libsystemd.so ls -la $(cat /etc/ld.so.preload 2>/dev/null) fi # 检查/sbin/kswapd0是否存在且异常 if [ -f /sbin/kswapd0 ]; then echo "[ALERT] Fake kswapd0 binary detected!" file /sbin/kswapd0 ls -la /sbin/kswapd0 # 正常应为"cannot open `/sbin/kswapd0' (No such file)",因为kswapd0是内核线程无二进制 # 若存在且size>500KB,立即取证:cp /sbin/kswapd0 /tmp/kswapd0.malware.$(date +%s) fi3.3 第三步:深挖内核模块——lsmod与dmesg的交叉验证
# 列出所有模块并过滤可疑关键词 lsmod | awk '{print $1}' | while read mod; do if ! echo "$mod" | grep -qE "^(ext4|xfs|nf_conntrack|iptable|nvme|ahci)$"; then echo "Checking module: $mod" modinfo "$mod" 2>/dev/null | grep -E "(author|description|license|vermagic)" | \ grep -vE "(GPL|MIT|Apache|X11|Linux Foundation)" fi done | grep -A2 "author\|description" # 检查最近加载的模块(按时间倒序) dmesg -T | grep -i "loading module" | tail -10 # 输出示例:[Mon Mar 18 02:15:22 2024] Loading module 'kswapd_hook'...实操心得:我们曾在一个样本中发现模块名为
kswapd_hook,modinfo kswapd_hook显示author: "Linux Kernel Team",看似正规,但vermagic字段为4.19.0-18-amd64 SMP mod_unload,而服务器内核是4.19.0-25-amd64——版本不匹配即为伪造。
3.4 第四步:内存取证——用gcore抓取kswapd0内存快照
这是最关键的一步,也是多数教程缺失的。gcore能生成完整的内存转储,供后续逆向分析:
# 创建取证目录 mkdir -p /tmp/kswapd_forensic cd /tmp/kswapd_forensic # 生成core dump(需root,且确保磁盘空间>2GB) gcore -o kswapd0.core 2 2>/dev/null if [ $? -eq 0 ]; then echo "[INFO] Core dump saved to kswapd0.core.2" # 快速扫描内存中的矿池域名(避免全量strings耗时) strings kswapd0.core.2 | grep -E "(xmr|monero|pool|miner|cryptonight)" | head -10 # 典型输出:xmr-us-east1.nanopool.org:14433, support@xmrpool.net else echo "[ERROR] gcore failed. Try alternative: dd if=/proc/2/mem of=kswapd0.mem bs=1M count=1024 2>/dev/null" fi3.5 第五步:网络连接溯源——ss与lsof的精准组合
挖矿程序必然建立外连,但kswapd0本身不建连,所以一定是其加载的so在后台发起:
# 查找所有与矿池IP通信的进程(需提前知道矿池IP,否则用strings core dump获取) # 假设已知矿池IP为185.193.12.45 ss -tunp | grep "185.193.12.45" | awk '{print $7}' | sed 's/[^0-9]*\([0-9]\+\).*/\1/' | sort -u # 更暴力的方法:遍历所有进程的fd,查找socket for pid in $(ls /proc/[0-9]* 2>/dev/null | grep -E "/proc/[0-9]+$"); do pid_num=$(basename "$pid") if [ "$pid_num" != "2" ] && [ "$pid_num" != "1" ]; then # 检查该进程是否打开了到矿池IP的socket if ss -tunp | grep ":$pid_num" | grep -q "185.193.12.45"; then echo "Suspicious PID: $pid_num" ps -p "$pid_num" -o pid,ppid,comm,args fi fi done3.6 第六步:定时任务与启动项排查——crontab与systemd的死角
攻击者常设置定时任务维持持久化:
# 检查所有用户的crontab for user in $(cut -d: -f1 /etc/passwd); do if crontab -u "$user" -l 2>/dev/null | grep -qE "(wget|curl|sh|bash|python)"; then echo "[ALERT] Crontab for user $user contains suspicious commands:" crontab -u "$user" -l 2>/dev/null | grep -E "(wget|curl|sh|bash|python)" fi done # 检查systemd用户服务(易被忽略) systemctl --user list-unit-files --type=service | grep enabled | while read service _; do systemctl --user cat "$service" 2>/dev/null | grep -E "(ExecStart|WantedBy)" | grep -qE "(wget|curl|sh)" && echo "User service $service is suspicious" done3.7 第七步:文件系统深度扫描——find与stat的黄金组合
# 查找72小时内创建的可疑文件(重点:/tmp /var/tmp /dev/shm) find /tmp /var/tmp /dev/shm -type f -mtime -3 -size +100k -name "*.so" -o -name "*lib*" 2>/dev/null | while read f; do echo "Found: $f" stat -c "%y %n" "$f" # 显示创建时间 file "$f" # 检查文件类型 strings "$f" | grep -E "(xmr|monero|cryptonight)" | head -3 done # 查找隐藏的SSH后门(常见于/root/.ssh/authorized_keys) if [ -f /root/.ssh/authorized_keys ]; then grep -v "^#" /root/.ssh/authorized_keys | grep -E "(ssh-rsa|ssh-ed25519)" | \ while read key; do # 提取公钥指纹,对比已知管理员指纹 echo "$key" | ssh-keygen -lf /dev/stdin | awk '{print $2}' done fi4. 彻底清除的四重保险策略:从内存到磁盘的无死角清理
4.1 内存层清除:强制终止挖矿线程(不重启内核)
既然不能kill -9 2,那就用更底层的方式:
# 方法一:通过/proc/2/status修改调度策略,使其休眠 echo -n "0" > /proc/2/autogroup # 关闭autogroup,降低优先级 echo -n "0" > /proc/2/io_priority # 设为最低IO优先级 # 方法二:最有效——冻结kswapd0,使其完全停止 echo "FROZEN" > /proc/2/status 2>/dev/null # 注意:此操作需内核支持cgroup v1 freezer # 若失败,则用终极手段: echo 1 > /proc/sys/vm/swappiness # 将swappiness设为1,极大减少kswapd0唤醒频率实操心得:
echo "FROZEN" > /proc/2/status在CentOS 7.9+内核(3.10.0-1160及以上)有效,执行后top中kswapd0的CPU瞬间归零。但这是临时措施,必须配合后续步骤。
4.2 内核模块层清除:安全卸载与磁盘清理
# 卸载可疑模块(假设模块名为kswapd_hook) modprobe -r kswapd_hook 2>/dev/null if [ $? -eq 0 ]; then echo "[SUCCESS] Module kswapd_hook removed" # 彻底删除模块文件(通常在/lib/modules/$(uname -r)/kernel/drivers/) find /lib/modules/$(uname -r) -name "*kswapd_hook*" -delete 2>/dev/null # 清理模块配置 rm -f /etc/modprobe.d/kswapd_hook.conf else echo "[ERROR] Failed to remove module. Check dependencies with 'modinfo kswapd_hook'" fi4.3 用户态层清除:LD_PRELOAD与伪造二进制的根治
# 清理LD_PRELOAD if [ -s /etc/ld.so.preload ]; then # 备份原始文件(重要!) cp /etc/ld.so.preload /etc/ld.so.preload.bak.$(date +%s) # 清空文件 > /etc/ld.so.preload echo "[INFO] /etc/ld.so.preload cleared" fi # 删除伪造的/sbin/kswapd0 if [ -f /sbin/kswapd0 ]; then mv /sbin/kswapd0 /sbin/kswapd0.malware.$(date +%s) echo "[INFO] Fake /sbin/kswapd0 moved to backup" fi # 清理preload加载的so文件 if [ -n "$(cat /etc/ld.so.preload 2>/dev/null)" ]; then rm -f "$(cat /etc/ld.so.preload 2>/dev/null)" fi4.4 持久化层清除:定时任务与启动项的全面消毒
# 清理所有用户的crontab中的恶意行 for user in $(cut -d: -f1 /etc/passwd); do crontab -u "$user" -l 2>/dev/null | grep -vE "(wget|curl|sh|bash|python|http|https)" | crontab -u "$user" - done # 清理systemd用户服务 systemctl --user list-unit-files --type=service | grep enabled | awk '{print $1}' | while read service; do if systemctl --user cat "$service" 2>/dev/null | grep -qE "(wget|curl|sh)"; then systemctl --user stop "$service" systemctl --user disable "$service" rm -f "/home/$user/.config/systemd/user/$service" fi done # 清理root用户的systemd服务(检查/etc/systemd/system/) for service in /etc/systemd/system/*.service; do if [ -f "$service" ] && grep -qE "(ExecStart.*wget|ExecStart.*curl)" "$service"; then systemctl stop "$(basename "$service" .service)" systemctl disable "$(basename "$service" .service)" rm -f "$service" echo "[INFO] Removed malicious systemd service: $(basename "$service")" fi done5. 验证与加固:清除后必须做的五件事
5.1 验证清除效果:三重指标交叉确认
# 指标一:kswapd0 CPU回归基线 watch -n 1 'ps -p 2 -o %cpu= 2>/dev/null | awk "{printf \"kswapd0 CPU: %.1f%%\\n\", \$1}"' # 指标二:内存使用率稳定 free -h | awk 'NR==2{printf "Free Memory: %s (%.1f%%)\\n", $4, $4*100/$2}' # 指标三:无异常网络连接 ss -tunp | grep -E "(xmr|monero|pool)" | wc -l # 应为05.2 内核参数加固:堵住常见攻击面
# 防止LD_PRELOAD滥用 echo "kernel.yama.ptrace_scope = 2" >> /etc/sysctl.conf # 防止内核模块动态加载(除非必要) echo "kernel.modules_disabled = 1" >> /etc/sysctl.conf # 限制用户态进程访问内核内存 echo "kernel.kptr_restrict = 2" >> /etc/sysctl.conf # 生效 sysctl -p # 防止恶意so被加载(需重启生效) echo "install kernel-module /bin/true" > /etc/modprobe.d/disable-kmod.conf5.3 文件权限加固:最小权限原则落地
# 锁定关键系统文件 chattr +i /etc/ld.so.preload chattr +i /etc/crontab chattr +i /etc/cron.d/ chattr +i /etc/cron.hourly/ /etc/cron.daily/ /etc/cron.weekly/ /etc/cron.monthly/ # 修复/sbin/kswapd0的不存在状态(如果被创建过) rm -f /sbin/kswapd0 # 创建符号链接防止被重建(可选) ln -sf /bin/true /sbin/kswapd05.4 监控告警植入:让下次攻击无所遁形
# 创建自定义监控脚本 /usr/local/bin/check_kswapd.sh cat > /usr/local/bin/check_kswapd.sh << 'EOF' #!/bin/bash # 检查kswapd0 CapEff是否异常 cap_eff=$(cat /proc/2/status 2>/dev/null | grep CapEff | awk '{print $2}') if [ "$cap_eff" != "0000000000000000" ]; then echo "CRITICAL: kswapd0 CapEff is $cap_eff, possible kernel module injection!" | logger -t kswapd-monitor # 发送告警(此处可集成企业微信/钉钉webhook) curl -X POST "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=YOUR_KEY" \ -H 'Content-Type: application/json' \ -d '{"msgtype": "text", "text": {"content": "kswapd0 CapEff异常,请立即检查!"}}' >/dev/null 2>&1 fi EOF chmod +x /usr/local/bin/check_kswapd.sh # 加入crontab每5分钟检查一次 (crontab -l 2>/dev/null; echo "*/5 * * * * /usr/local/bin/check_kswapd.sh") | crontab -5.5 根因追溯:日志分析锁定入侵源头
# 分析auth.log寻找爆破记录 grep "Failed password" /var/log/auth.log | awk '{print $9,$11}' | sort | uniq -c | sort -nr | head -10 # 检查sudo日志 grep "COMMAND" /var/log/auth.log | grep -E "(insmod|modprobe|chmod|chown)" | tail -10 # 检查bash历史(若未清空) for user in /root /home/*; do if [ -f "$user/.bash_history" ]; then echo "=== History for $(basename "$user") ===" cat "$user/.bash_history" 2>/dev/null | grep -E "(wget|curl|insmod|modprobe|gcc)" | tail -5 fi done最后分享一个小技巧:我们团队在清除后必做的一件事是——用
tcpdump抓取1小时的出向流量,然后用Wireshark过滤http.request or tls.handshake,查看是否有异常域名解析(如update-systemd[.]xyz)。这能帮你发现是否还有其他未被清除的C2通道。记住,真正的清除不是让CPU降下来,而是让整个攻击链路彻底断裂。我见过太多人删了so文件就以为完事,结果三天后kswapd0又满血复活——因为定时任务还在,或者攻击者用了双备份机制。所以,务必执行完这五步,再喝杯咖啡,看着监控曲线平稳如初,那才是真正的胜利。
