第二十二课:恶意代码分析实战(三)——Rootkit样本分析
好,咱们今天聊点硬核的。Rootkit,这玩意儿在安全圈里,属于那种「听着就头疼」的东西。它不像普通木马那样,藏个文件、改个注册表就完事了。Rootkit 的目标是——让你根本看不到它。
我早年做应急响应时,遇到过一台服务器,CPU 跑满,网络连接异常,但用 Process Explorer 看,干干净净。后来用 WinDbg 一挂,才发现内核层被人动了手脚。嗯,那就是个典型的 Rootkit。
Rootkit 的核心能力
说白了,Rootkit 就是一套「内核级隐身术」。它通过加载驱动进入内核态,然后对系统进行各种 Hook,让你用常规手段根本查不到它。
常见的功能包括:
- 驱动加载:通过服务管理器或手动映射,把恶意驱动加载到内核
- 隐藏进程:从 EPROCESS 链表中摘除自己的进程节点
- 隐藏文件:Hook 文件系统相关 API,过滤目录查询结果
- 内核 Hook:修改 SSDT、IDT 或直接 Patch 内核函数
关键认知:Rootkit 的可怕之处不在于它做了什么,而在于你「看不到它做了什么」。分析 Rootkit,本质上是在跟一个「会躲猫猫的对手」博弈。
驱动加载:Rootkit 的入口
Rootkit 要进内核,最正统的方式是加载一个驱动程序。Windows 下驱动加载的路径主要有两条:
- 服务管理器(SCM):通过 CreateService + StartService,这是常规驱动加载方式,容易被监控
- 手动映射(Manual Map):自己解析 PE 文件,分配内核内存,修复导入表,然后调用 DriverEntry。这种方式更隐蔽,但实现复杂
我见过一个样本,它用的是第二种方式。驱动文件本身是加密的,运行时才解密,然后通过一个合法的签名驱动做跳板,把恶意驱动映射进内核。嗯,这种手法在 APT 攻击中很常见。
隐藏进程:从内核链表中「消失」
Windows 内核维护着一个双向链表,把所有进程的 EPROCESS 结构串起来。任务管理器、Process Explorer 这些工具,都是遍历这个链表来获取进程列表的。
Rootkit 的隐藏手法很简单——把自己的 EPROCESS 节点从链表中摘除。代码大概长这样:
// 伪代码:从进程链表中摘除自己
VOID HideProcess(PEPROCESS EProcess) {
PLIST_ENTRY ListEntry = &EProcess->ActiveProcessLinks;
// 摘除节点
ListEntry->Flink->Blink = ListEntry->Blink;
ListEntry->Blink->Flink = ListEntry->Flink;
// 把自己指回自己,防止遍历时崩溃
ListEntry->Flink = ListEntry;
ListEntry->Blink = ListEntry;
}
避坑指南:我曾经调试一个样本时,发现进程虽然隐藏了,但句柄表里还有残留。后来才意识到,摘除链表只是第一步,还得清理句柄表中的引用。否则,某些内核组件依然能通过句柄找到它。
隐藏文件:Hook 文件系统
文件隐藏比进程隐藏更复杂。因为文件系统涉及多层:应用层 API(NtQueryDirectoryFile)、内核层 IRP 分发、以及底层文件系统驱动。
常见的做法是 Hook NtQueryDirectoryFile 这个系统调用。每次查询目录时,Rootkit 会检查返回的文件名,如果匹配自己的隐藏列表,就从结果中移除。
我见过一个比较狡猾的实现——它不直接修改返回数据,而是修改 IRP 的完成例程。这样,在数据返回给调用者之前,它有机会「过滤」一遍。嗯,这种手法更难检测,因为 Hook 点更深。
内核 Hook:SSDT 与 Inline Hook
内核 Hook 是 Rootkit 的「核心技能」。主要有两种方式:
| Hook 类型 | 原理 | 检测难度 |
|---|---|---|
| SSDT Hook | 修改系统服务描述表中的函数指针 | 中等 |
| Inline Hook | 直接修改函数开头的字节码,跳转到恶意代码 | 较高 |
SSDT Hook 比较「文明」,只是改个指针。但 Windows 有 PatchGuard(内核保护),会定期检查 SSDT 是否被篡改。所以现代 Rootkit 更倾向于 Inline Hook——直接修改函数的前几个字节,比如:
// Inline Hook 示例:修改 NtCreateFile 开头
// 原始字节:4C 8B D1 48 8B 49 F8 (mov r10, rcx; mov rcx, [rcx-8])
// 修改后:E9 xx xx xx xx (jmp MyHookFunction)
BYTE JmpCode[] = {
0xE9, // jmp 指令
0x00, 0x00, 0x00, 0x00 // 相对偏移,运行时计算
};
// 计算跳转偏移
ULONG_PTR TargetAddr = (ULONG_PTR)MyHookFunction;
ULONG_PTR SourceAddr = (ULONG_PTR)NtCreateFile;
*(ULONG*)(JmpCode + 1) = (ULONG)(TargetAddr - SourceAddr - 5);
// 写入 Hook
RtlCopyMemory(NtCreateFile, JmpCode, 5);
注意:Inline Hook 有个坑——如果你只修改了前 5 个字节,但原始指令长度超过 5 字节,那被覆盖的指令就不完整了。我见过一个样本因此导致系统蓝屏,因为它没处理好指令对齐。嗯,写 Rootkit 也是个技术活,写不好就自爆了。
使用 WinDbg 进行内核调试
分析 Rootkit,WinDbg 是必备工具。我个人习惯用双机调试:一台被调试机(跑样本),一台调试机(跑 WinDbg)。通过串口或网络连接。
常用命令:
!process 0 0:列出所有进程,看有没有隐藏的!drvobj:查看驱动对象,检查异常驱动!ssdt:查看 SSDT 表,检查函数指针是否被篡改lm:列出已加载模块,看有没有可疑驱动!chkimg:检查内核模块是否被 Patch
我记得有一次,我用 !process 0 0 发现进程数比任务管理器里多了两个。嗯,那就是被隐藏的进程。然后我用 !thread 查看它们的线程栈,顺藤摸瓜找到了恶意驱动。
知识体系结构图
下面这张图,帮你理清 Rootkit 分析的核心脉络:
实战分析流程
拿到一个 Rootkit 样本,我一般按这个步骤来:
- 静态分析驱动文件:看导入表、字符串、是否有数字签名
- 加载到虚拟机:用 WinDbg 双机调试,设置内核断点
- 监控驱动加载:用
!drvobj和lm检查新加载的驱动 - 检查 SSDT 和内核模块:用
!ssdt和!chkimg看有没有被 Hook - 对比进程列表:用
!process 0 0和任务管理器对比,找隐藏进程 - 跟踪文件操作:在文件系统相关函数上下断点,看有没有过滤行为
个人经验:我建议你在分析前,先拍一张「快照」——记录下干净的 SSDT 表、进程列表、驱动列表。这样样本加载后,对比起来一目了然。我曾经因为没拍快照,浪费了半天时间在找「到底哪个函数被 Hook 了」。
Rootkit 分析,说白了就是一场「猫鼠游戏」。你越了解内核的工作机制,就越能发现那些「不自然」的地方。嗯,这也是逆向工程的魅力所在——你永远不知道下一个样本会用什么新花样。
公众号:蓝海资料掘金营,微信 deep3321