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/statuskswapd0的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 permittedpkill 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挖矿病毒主要通过以下两种路径实现持久化注入方式触发条件检测特征清除难度恶意内核模块LKMinsmod ./malware.ko模块初始化时hookkswapd_run函数指针/proc/modules中存在未知模块dmesggrep -i loading module出现非白名单模块名lsmodLD_PRELOAD劫持用户态伪装在/etc/ld.so.preload中写入恶意so启动/sbin/kswapd0伪造二进制时加载/etc/ld.so.preload非空且内容可疑/sbin/kswapd0文件大小异常1MBfile /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是内核线程无二进制 # 若存在且size500KB立即取证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_hookmodinfo 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, supportxmrpool.net else echo [ERROR] gcore failed. Try alternative: dd if/proc/2/mem ofkswapd0.mem bs1M count1024 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 --typeservice | 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 --typeservice | 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 NR2{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?keyYOUR_KEY \ -H Content-Type: application/json \ -d {msgtype: text, text: {content: kswapd0 CapEff异常请立即检查}} /dev/null 21 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又满血复活——因为定时任务还在或者攻击者用了双备份机制。所以务必执行完这五步再喝杯咖啡看着监控曲线平稳如初那才是真正的胜利。