干过几年应急响应的朋友应该都有这种体会:Linux 服务器一旦被深层次植入,表面看到的“正常”往往是最不可信的。你敲ps看不到异常进程,netstat里连接干净得发白,ls列出的目录也没有多余文件,但业务就是在卡顿,出口流量就是在异常飙升。这种时候,真正在背后捣乱的,十有八九是 Rootkit——一类专门用来隐藏自身痕迹的恶意组件。这篇内容我会结合自己做过的应急排查经历,把 Linux 下常见的用户态、内核态隐藏手段拆开揉碎,完整走一遍从异常发现、取证排查到清理加固的实战流程,让隐藏的东西一件件现形。无论你是刚接触服务器运维的新手,还是已经在安全一线摸爬滚打的老手,这套思路都可以直接拿去用。
1. 先搞清楚对手:Rootkit 都藏在 Linux 的哪些隐蔽角落
1.1 用户态隐藏:动态库劫持与命令替换
用户态 Rootkit 的攻击思路很直接:既然管理员最终要靠命令来查看系统状态,那就直接让命令“说谎”。
最常见的手法是利用动态链接库的加载机制,通过LD_PRELOAD环境变量或者/etc/ld.so.preload文件,在程序启动时抢先加载一个恶意共享库。这个库可以劫持readdir、getdents、kill、connect这类系统调用,进程列表里看不到恶意进程,目录列表里看不到恶意文件,网络连接列表里看不到外连地址,原理就是“骗过”上层的用户态命令。
还有一种更粗暴的玩法,直接替换二进制的ps、netstat、ls、lsof等系统命令,或者替换它们依赖的共享库。管理员执行命令时,实际上跑的是恶意版本,输出内容当然是攻击者希望看到的“正常”结果。
识别这类问题的关键点在于:命令本身可能已经不可信了。所以我在排查时不会直接相信系统的ps和netstat,而是先去核对工具的可信性。
提示:如果你发现自己敲什么命令都“一切正常”,但同时 CPU 又莫名跑满、流量异常,第一反应不应该是“系统没事”,而应该是“我看到的可能都是假的”。
1.2 内核态隐藏:内核模块与系统调用挂钩
如果说用户态 Rootkit 是“换个皮骗人”,那内核态 Rootkit 就是直接“住进了系统最底层”。它以可加载内核模块(LKM)的形式运行在内核空间,可以修改系统调用表,让内核本身对某个 PID、某个文件、某个端口“选择性失明”。
这种隐藏有多彻底?对getdents系统调用挂钩后,不仅ls看不到恶意文件,连find、cat /proc/*也看不到;对tcp4_seq_show这类函数挂钩后,ss、netstat、甚至/proc/net/tcp都会漏掉对应的连接。管理员拿着标准工具翻来覆去找,结果就是在跟一个“瞎了的内核”对话。
内核态 Rootkit 的排查难度比用户态高很多,因为普通命令拿到的数据已经经过了内核层的“过滤”。这时候要么用静态编译的第三方工具绕开恶意模块的过滤,要么直接读取底层机制来对比,比如busybox里的命令往往不依赖系统的动态库,能在一定程度上绕开基于LD_PRELOAD的劫持。
1.3 网络层隐藏与持久化机制
隐藏进程和文件只是 Rootkit 的一半工作,另一半是“活下来”。攻击者通常会配置一整套持久化机制,确保机器重启后恶意模块还能重新加载:
- 写入
crontab,定时拉起后门进程; - 创建 systemd 服务,随开机自启;
- 修改
/etc/rc.local; - 在
/etc/profile、~/.bashrc中植入启动逻辑,用户一登录就被执行; - 写入 SSH 公钥到
/root/.ssh/authorized_keys,实现持久化远程登录; - 修改内核启动参数,提前加载恶意内核模块。
网络层面的隐蔽同样不可忽视。常见的有端口复用(复用 80/443 这类正常端口,很难被防火墙规则发现)、ICMP 隧道、DNS 隧道,以及基于合法协议封装的外联通道。这些手段不需要单独监听端口,所以常规的端口扫描和ss -lntp检查常常一无所获。
所以,排查 Rootkit 不能只盯着“进程”和“文件”,要把账号、计划任务、服务、内核模块、网络外联全部串起来看。
2. 应急响应的三板斧:先看现象,再取证据,最后处理
2.1 第一板斧:异常画像,快速定位“不对劲”的地方
接到告警之后,我的习惯是先花几分钟在脑子里画一张“异常画像”,不要急着敲各种命令。重点看四个方面:
- CPU 和内存:使用率异常高,但
top里找不到对应进程。这不是系统“自己好了”,而是进程被隐藏了。 - 网络连接:有持续不断的主动外连,或者存在大量 TIME_WAIT 连接到非业务地址;监听端口出现不在预期列表里的新端口。
- 账号与登录:
/etc/passwd中出现新 UID 为 0 的账号,/var/log/secure或auth.log中出现大量异常登录记录,特别是来自陌生地址的成功登录。 - 文件与时间戳:系统二进制文件的修改时间异常,
/tmp、/var/tmp、/dev/shm中多出可疑文件,/etc下的配置文件被改动过。
这一步的核心目的不是确认“被入侵了没有”,而是快速圈定“大概在哪个层面出了问题”,决定后续取证的方向。
2.2 第二板斧:证据保全,别让现场被自己破坏
很多新人在拿到服务器权限后第一件事就是rm -rf恶意文件,这个操作是大忌。恶意进程可能还活着,文件可能还依赖运行中的进程,日志和内存信息一旦被清掉,后续溯源就失去了最重要的一手材料。
我建议的顺序是:
- 先拍照或录屏记录当前屏幕状态、命令输出;
- 保存进程列表、网络连接、登录日志到外部存储;
- 有条件的话做内存转储(
/proc/kcore、/dev/mem或者利用gcore)和磁盘镜像; - 记录关键文件的时间戳、哈希值和权限;
- 一切都保存好之后,再进入分析阶段。
说得直白一点,应急和破案一样,第一要务是保护现场。进程杀掉、文件删掉、日志清掉,后面想还原攻击链路就很困难了。
2.3 第三板斧:建立可信基线意识
这是我从几次实战中踩坑之后养成的习惯:永远不要完全信任当前系统里的命令。
被 Rootkit 入侵的机器上,ps、ls、netstat、ss、lsof这些命令都可能被替换过。你拿一个可能被篡改的工具去查另一个也被篡改的模块,结果就是在垃圾数据上做分析。
一个比较实用的办法是准备一套静态编译的排查工具集,比如用busybox静态版本,或者把lsof、ps、netstat编成静态二进制,放到一个只读挂载的 U 盘里。平时收好,应急的时候用这套工具去查,结果才有参考价值。退一步讲,至少也要通过stat查看命令文件的修改时间,用rpm -V(针对 RPM 系系统)校验二进制文件的完整性,判断工具是否被替换过。
3. 让隐藏手段现形:从用户态到内核态的排查实操
3.1 用 /proc 的原始数据绕开进程隐藏
无论 Rootkit 怎么隐藏,只要进程还在内核里活着,/proc目录下就必然存在对应的进程目录。ps输出可以被篡改,但/proc是内核向外暴露的直接视图,是排查进程隐藏的核心数据源。
一个非常实用的做法是:直接把/proc下所有数字目录和ps的输出对比。Shell 脚本思路大致是这样:
# 列出 /proc 下所有 PID for pid in $(ls -d /proc/[0-9]* | awk -F'/' '{print $3}'); do # 检查该 PID 是否在 ps 输出中出现 if ! ps -p $pid > /dev/null 2>&1; then echo "可疑 PID(ps 中不可见): $pid" echo "进程名: $(cat /proc/$pid/comm 2>/dev/null)" echo "命令行: $(cat /proc/$pid/cmdline 2>/dev/null | tr '\0' ' ')" echo "启动时间: $(cat /proc/$pid/stat 2>/dev/null | awk '{print $22}')" fi done正常情况下,/proc下数字命名的目录都会对应ps里的进程。如果发现某个 PID 在/proc里存在但ps -p查不到,那基本可以确定是内核态隐藏手段在作怪。
网络层面的排查同理。很多管理员的习惯是netstat -anpt,但内核态 Rootkit 可以把这个命令输出里的连接抹掉。这时候可以去/proc/net/tcp直接看原始数据:
# 查看 TCP 连接原始表 cat /proc/net/tcp cat /proc/net/tcp6输出里的每一行对应一条连接,字段依次是本地地址、远端地址、连接状态等。和ss -anpt的输出做交叉对比,如果/proc/net/tcp里有连接但ss里看不到,或者连接信息对不上,就要高度警惕了。
# 一个简单的小脚本,打印 /proc/net/tcp 中 Established 状态的连接 awk 'NR>1 {split($2,local,":"); split($3,remote,":"); if ($4=="01") print "本地: " strtonum("0x"local[2]) ":" strtonum("0x"local[1]) " -> 远端: " strtonum("0x"remote[2]) ":" strtonum("0x"remote[1])}' /proc/net/tcp3.2 揭开 LD_PRELOAD 劫持的面纱
在排查用户态劫持时,我会按优先级做以下几件事。
第一,直接查看LD_PRELOAD环境变量和/etc/ld.so.preload文件:
echo $LD_PRELOAD cat /etc/ld.so.preload正常系统里,这两个位置通常是空的。只要里面有内容,尤其是路径比较深、文件名看着不像系统库的条目,就要马上追查。
第二,用ldd查看命令实际加载的动态库:
ldd /bin/ps ldd /usr/sbin/ss ldd /usr/bin/ls看输出里有没有指向/tmp、/var/tmp、/dev/shm、/usr/lib下可疑名称的共享库。动态库被劫持后,命令加载的库列表往往会多出若干“陌生面孔”。
第三,用strace跟踪命令执行过程中的系统调用,观察有没有读取可疑路径:
strace -f -e trace=openat,open,execve -o /tmp/ps.log /bin/ps然后在日志里搜索/tmp/、.so、preload相关的记录。这个操作虽然会慢一点,但能很清晰地看到命令启动时到底加载了什么。
我曾经遇到过一个情况:ldd /bin/ps输出完全正常,但strace里显示进程在启动早期就打开了一个位于隐藏目录下的.so文件。原因是攻击者修改了动态链接器的配置文件,让链接器在解析标准库之前先去加载恶意库,表面功夫做得很足。所以不能只看ldd的静态解析结果,动态跟踪才更可靠。
3.3 内核模块排查:从 /sys/module 到系统调用表
排查内核态 Rootkit 比用户态要费力气一些,但也不是无路可走。
先看当前已加载的模块列表:
lsmod cat /proc/modules ls -la /sys/module/正常情况下,lsmod读的本身就是/proc/modules的内容。如果 Rootkit 隐藏了模块,它会挂钩readdir之类的调用,让这三条命令的输出看起来一致,但真实的模块列表应该多出一些内容。
更有价值的做法是把/sys/module下的目录和/lib/modules/$(uname -r)/kernel/下的已知模块做对比。已知模块都是内核编译或安装驱动时放进去的,如果/sys/module里出现了一个在/lib/modules里找不到对应文件的模块名,那基本可以判断有问题:
# 列出当前已加载模块 for mod in $(ls /sys/module/); do # 在内核模块目录中查找对应文件 if ! find /lib/modules/$(uname -r) -name "${mod}.ko*" 2>/dev/null | grep -q .; then echo "可疑内核模块: $mod" fi done内核态检测的进阶手段是查看系统调用表是否被篡改。在较新的内核上,可以通过 kernel 提供的接口读取符号地址:
cat /proc/kallsyms | grep sys_call_table拿到sys_call_table地址后,用gdb或者systemtap去读取系统调用表中的函数指针,和/proc/kallsyms里导出的函数地址对比。如果某个系统调用的指针指向的是模块内存空间而不是内核自身的函数地址,那这个系统调用就被挂钩了。这个操作技术门槛较高,而且部分内核对/proc/kallsyms的读取做了权限限制,实际执行时要注意。
3.4 自动化工具:用扫描器做快速筛查
纯粹靠手工排查,效率不高,而且容易漏掉一些细节。我一般会用自动化工具先做一轮快速扫描,再针对可疑点做定向分析。
比较常用的有:
rkhunter:检测 Rootkit、恶意二进制和后门文件,自带已知 Rootkit 特征库;chkrootkit:同样用于检测常见 Rootkit 特征,脚本风格,运行简单;unhide:对比/proc、ps、sysctl等系统接口,找出被隐藏的进程和端口;busybox:静态编译的命令集合,用于获取相对可信的系统状态。
我通常跑的命令组合大概是这样:
# 扫描已知 Rootkit 特征 rkhunter --check --sk # 检测隐藏进程和端口 unhide proc unhide sys # 用 busybox 提供的静态 ps 和系统自带 ps 对比 busybox ps aux这里要特别提醒:自动扫描工具给出的结果只能作为线索,不能当作最终结论。rkhunter经常会把正常文件误报为可疑文件,尤其是服务器上装了自己编译的软件或者改了默认路径之后,误报率会明显上升。需要结合文件哈希、时间戳、网络行为和业务实际情况综合判断。
4. 一次典型排查实录:从异常外连到内核模块落网
4.1 现象与初步判断
有一次,某内部系统的服务器出现异常:业务方反馈系统响应变慢,并且安全设备告警提示该服务器持续向某个陌生地址发送数据,端口是 443,流量不小。
我登到机器上先看了一眼:
top -c -b -n 1输出显示 CPU 使用率总体偏高,但排在前面的是几个普通业务进程,并没有明显异常。然后我看了ss -anpt,发现确实有一条到陌生地址的 443 连接,但对应进程的 PID 显示为 “-”,也就是内核没有把这个连接的归属进程暴露出来。
这个细节很关键。常规情况下ss是可以正常显示 PID 和进程名的,如果 PID 变成-,要么是连接已经不存在于进程表中,要么就是进程信息被隐藏了。再配合 CPU 异常,我初步判断这不是简单的挖矿木马,而是有内核态隐藏能力的 Rootkit 在作怪。
4.2 一步步追踪,绕开骗人的表象
我立刻换了思路,不再依赖系统的ss和ps,而是直接用/proc原始数据交叉验证。
先看/proc/net/tcp:
cat /proc/net/tcp | awk 'NR>1 {print $2, $3, $4}'输出里确实有那条到陌生地址的连接,而且状态是01(ESTABLISHED)。但用ss -anpt和netstat -anpt查看时,这条连接的信息要么不完整,要么直接缺失。这就坐实了连接层数据被过滤。
接下来排查隐藏进程:
for pid in $(ls -d /proc/[0-9]* | awk -F'/' '{print $3}'); do if ! ps -p $pid > /dev/null 2>&1; then echo "not in ps: $pid" cat /proc/$pid/comm fi done很快就发现了一个不存在的进程项,PID 对应一个名字看起来很正常的随机字符串,/proc/$pid/exe指向一个位于/usr/lib/下的文件,但用ls /usr/lib检查时目录里根本没有这个文件。也就是说,文件层面也被隐藏了。
接着查用户态劫持:
cat /etc/ld.so.preload ldd /bin/ps strace -f -e trace=openat /bin/ps 2>&1 | grep /usr/lib/etc/ld.so.preload是空的,ldd也没有异常加载项,strace里没有可疑的.so读取记录。用户态这条线暂时排除,重心转向内核模块。
我检查了lsmod和/sys/module:
lsmod ls /sys/module/两者输出基本一致,表面看也没有可疑模块。但当我用前面的脚本把/sys/module与/lib/modules/$(uname -r)/kernel/做对比时,发现了一个模块名在内核模块目录里完全找不到对应.ko文件的异常项。
到这里基本可以确定,这个 Rootkit 以 LKM 形式加载,并且在内核层面对进程、文件、网络连接做了统一隐藏。我接着用find / -name查不到任何恶意文件实体,因为getdents被过滤了。但/sys/module下的模块目录是内核自身维护的,恶意模块没能在这一层隐藏得那么干净,这也是这次排查的重要突破口。
4.3 结论与证据链整理
为了确认模块的加载来源,我翻了/etc/modules、/etc/modules-load.d/和 systemd 的模块加载配置:
cat /etc/modules ls -la /etc/modules-load.d/结果在/etc/modules-load.d/下发现一个新建的.conf文件,里面只有一行模块名,和/sys/module里那个可疑模块对上了。时间戳显示它是在告警发生前一天被创建的,这正好和攻击时间线吻合。
我保存了以下证据:
/proc/net/tcp与ss输出的对比记录;- 隐藏进程的
/proc目录信息与comm内容; /sys/module的可疑模块名与.conf文件内容;- 相关文件的
stat时间戳和哈希值。
整个过程没有在获取完整证据前删除任何文件,也没有重启服务器,保证了内存和内核状态没有丢失。
5. 常见问题与排查技巧速查表
日常应急中我总结了一个速查表,遇到对应症状可以直接按表索骥:
| 异常症状 | 可能原因 | 快速排查方法 |
|---|---|---|
| CPU 跑满但 top 找不到进程 | 内核态隐藏 / 进程被劫持 | 遍历/proc与ps对比,用unhide proc |
ss显示连接但 PID 为 “-” | 进程归属信息被过滤 | 查看/proc/net/tcp原始数据 |
ps、netstat输出一切正常,但行为异常 | 命令被替换 /LD_PRELOAD劫持 | 用stat看命令时间戳,用busybox ps对比 |
| 登录日志出现大量陌生地址成功认证 | 账号后门 / SSH 密钥投毒 | 检查/root/.ssh/authorized_keys,检查/etc/passwd |
| 重启后恶意进程重新出现 | 持久化机制未清理干净 | 检查 crontab、systemd 服务、rc.local 等 |
lsmod无异常但模块行为可疑 | 模块信息被内核层隐藏 | 对比/sys/module与/lib/modules/模块目录 |
几条实操心得多说一句:
- 检查 crontab 的时候,不要只看当前用户的
crontab -l,还要看/etc/crontab、/etc/cron.d/、/var/spool/cron/,攻击者经常会部署在系统级计划任务里。 - 检查 SSH 后门时,
/root/.ssh/authorized_keys只是第一层,还要看/etc/ssh/sshd_config里是否被加了新的AuthorizedKeysFile路径,或者是否出现了新的用户目录。 - 清理 Rootkit 之前,我一般会先把服务器从业务流量中摘出去,切断对外服务,但不要立刻断网。保留现场外联通道有时候能让攻击者继续活动,从而观察更多行为,为溯源提供线索。
6. 排查之后的加固与长期建设
把 Rootkit 清掉只算完成了一半。如果不做系统性加固,同样的漏洞很可能在几周后被再次利用,甚至清理过程中漏掉的某个后门会让攻击者直接卷土重来。
6.1 内核与启动层面的防护
优先考虑启用 Secure Boot 和内核模块签名验证。很多主流 Linux 发行版已经支持签名内核模块,强制验证之后,未签名的恶意模块在加载阶段就会被拒绝,这是对付 LKM Rootkit 最有效的一层拦截。
同时可以限制module相关的访问控制,配置/etc/modprobe.d/里的黑名单或者使用内核参数,限制普通用户加载内核模块的能力。生产环境尽量做到非 root 用户没有insmod、modprobe的执行权限。
6.2 文件完整性与配置基线
部署文件完整性监控工具,比如AIDE或Tripwire,对系统关键二进制、配置文件、启动项做周期性校验。重点监控对象包括:
/bin、/sbin、/usr/bin、/usr/sbin下的所有二进制文件;/etc/ld.so.preload、/etc/ld.so.conf;/etc/crontab、/etc/cron.d/;- systemd 服务的
/etc/systemd/system/和/usr/lib/systemd/system/; - SSH 配置文件和
authorized_keys。
RPM 系的系统还可以用rpm -V做包内文件校验,能在较大程度上发现二进制文件被替换的问题。
6.3 日志外送与集中审计
日志只存在本机是很危险的,因为 Rootkit 完全有能力篡改或清理本机日志。把系统认证日志、sudo 日志、内核日志通过rsyslog或syslog-ng实时外送到集中日志平台,能保证攻击者即使上了机器,也无法通过删除本地日志来掩盖操作轨迹。
另外建议开启auditd,对关键路径的写入、关键命令的执行、账号变更等行为做审计记录:
auditctl -w /etc/passwd -p wa -k account-change auditctl -w /etc/ld.so.preload -p wa -k preload-change auditctl -w /bin/ps -p wa -k binary-change审计日志要设置独立的存储位置和访问权限,避免和系统日志放在一起被一起清理掉。
6.4 定期演练与基线快照
我个人的体会是:应急响应能力不是设备堆出来的,而是靠演练练出来的。平时就要做好系统基线快照,包括启动项列表、已安装内核模块列表、关键二进制哈希、开放端口列表。有了这些基线,再遇到异常时对比一下,排查效率能提升好几倍。
有条件的话,每半年做一次模拟应急演练,把核心服务器当靶机,故意放几个隐藏样本进去,让运维和安全团队实际跑一遍检测和清理流程。演练中暴露出来的“工具集缺失”、“权限不够”、“日志取不到”等问题,往往是真实事件中会要命的点。
写在最后
说实话,Rootkit 的排查不像安装软件那样有标准答案,更多时候是跟攻击者比拼“谁更了解系统底层”。我踩过最大的坑,就是一开始太相信系统命令的输出,在/proc数据面前绕了一大圈才绕回来。后来每次处理类似的应急事件,我都会先问自己一句:我现在看到的这些数据,真的来自内核吗?
另一个体会就是,千万不要等出了问题才想到准备工具。静态版busybox、校验过的排查脚本、系统基线快照,这些都要在平时准备好。真正到了应急现场,你能依靠的往往不是网络上的教程,而是手边早就备好的那套东西。