15、进程保护与反调试:进程隐藏、进程防杀、调试寄存器保护、反调试检测、VT技术基础
大家好,欢迎来到第十五章。说实话,这一章的内容,是我个人觉得整个课程里最“刺激”的部分。为什么?因为我们要聊的是攻防对抗——你的驱动怎么保护自己,怎么不被别人调试,怎么在系统里“隐身”。
我在做安全产品的时候,遇到过不少次驱动被干掉的情况。有一次,我们的监控驱动刚加载上去,不到三分钟就被一个恶意软件给卸载了。嗯,从那以后,我就开始深入研究进程保护和反调试。说白了,这就是一场猫鼠游戏,但我们要做那只更聪明的猫。
15.1 进程隐藏:让对手找不到你
进程隐藏,听起来很玄乎,其实核心思路就一个:让系统里的枚举工具看不到你的进程。常见的枚举方式有两种:用户态的 CreateToolhelp32Snapshot 和内核态的 ZwQuerySystemInformation。
我个人习惯在内核层动手脚。因为用户态 hook 太容易被绕过了,你 hook 了 ntdll,人家直接 syscall 就跳过去了。
15.1.1 DKOM 方式隐藏进程
DKOM(Direct Kernel Object Manipulation)是经典手法。Windows 内核维护了一个双向链表来管理所有进程的 EPROCESS 结构。你只要把这个节点从链表里摘掉,任务管理器就看不到了。
代码大概长这样:
// 摘除进程节点
VOID HideProcess(PEPROCESS TargetEProcess) {
PLIST_ENTRY ListEntry = &TargetEProcess->ActiveProcessLinks;
// 摘除前要关中断,防止并发访问
KIRQL OldIrql = KeRaiseIrqlToDpcLevel();
// 从双向链表中摘除
ListEntry->Blink->Flink = ListEntry->Flink;
ListEntry->Flink->Blink = ListEntry->Blink;
// 自己指向自己,防止被遍历时崩溃
ListEntry->Flink = ListEntry;
ListEntry->Blink = ListEntry;
KeLowerIrql(OldIrql);
}
15.1.2 回调方式隐藏
除了 DKOM,还有一种更“文明”的方式:利用进程创建回调。你可以注册一个 PsSetCreateProcessNotifyRoutine,当目标进程创建时,在回调里做手脚——比如修改它的进程名,或者直接阻止它被枚举。
不过这种方式有个缺点:它只能隐藏新创建的进程,对已经存在的进程无效。
15.2 进程防杀:让对手杀不掉你
进程防杀,说白了就是让 TerminateProcess 或者 ZwTerminateProcess 失效。我见过最粗暴的做法是 hook NtTerminateProcess,但说实话,这太容易被 PatchGuard 检测到了。
我个人推荐的做法是:保护进程的句柄权限。
15.2.1 修改进程句柄的访问掩码
当其他进程试图打开你的进程时,你可以拦截 ObOpenObjectByPointer 或者 NtOpenProcess,把访问掩码里的 PROCESS_TERMINATE 位给清掉。
// 伪代码:拦截 NtOpenProcess
NTSTATUS MyNtOpenProcess(PHANDLE ProcessHandle, ACCESS_MASK DesiredAccess, ...) {
// 检查目标进程是否是我们保护的进程
if (TargetProcess == ProtectedProcess) {
// 去掉终止权限
DesiredAccess &= ~PROCESS_TERMINATE;
DesiredAccess &= ~PROCESS_VM_WRITE;
DesiredAccess &= ~PROCESS_SUSPEND_RESUME;
}
// 调用原始函数
return OriginalNtOpenProcess(ProcessHandle, DesiredAccess, ...);
}
PROCESS_TERMINATE,还要去掉 PROCESS_SUSPEND_RESUME 和 PROCESS_VM_WRITE。为什么?因为有些恶意软件会先挂起你的进程,然后再注入代码。我遇到过这种情况,当时差点翻车。
15.3 调试寄存器保护:防止被下硬件断点
调试寄存器(Dr0~Dr7)是硬件断点的核心。攻击者可以通过设置 Dr0~Dr3 来在特定地址触发断点。作为驱动开发者,我们要保护自己的关键代码不被下硬件断点。
保护思路其实很简单:定期检查并清空调试寄存器。
// 清空当前线程的调试寄存器
VOID ClearDebugRegisters() {
__writefsdword(0x10, 0); // Dr0
__writefsdword(0x14, 0); // Dr1
__writefsdword(0x18, 0); // Dr2
__writefsdword(0x1C, 0); // Dr3
__writefsdword(0x20, 0); // Dr6
__writefsdword(0x24, 0); // Dr7
}
你可能会问:“我清掉了,别人又设上怎么办?”嗯,这就是为什么我们要用定时器或者线程循环来反复检查。我个人习惯在关键函数入口处加一个检查点,比如在 Dispatch 函数里。
15.4 反调试检测:识别谁在调试你
反调试检测,说白了就是“看看周围有没有人盯着我”。常见的检测手段包括:
- 检测 PEB 的 BeingDebugged 标志:这是最基础的,但容易被绕过。
- 检测 NtGlobalFlag:如果进程被调试,这个标志会被设置。
- 检测断点指令:扫描代码段,看看有没有
0xCC(int 3)指令。 - 检测时间差:执行一段代码,看看耗时是否异常——因为单步调试会拖慢速度。
我比较推荐的是检测断点指令。因为其他方式都可以被用户态 hook 绕过,但代码段里的 0xCC 是实实在在的。
// 检测代码段是否有断点
BOOLEAN CheckForBreakpoints(PVOID Address, ULONG Size) {
PUCHAR Code = (PUCHAR)Address;
for (ULONG i = 0; i < Size; i++) {
if (Code[i] == 0xCC) {
return TRUE; // 发现断点
}
}
return FALSE;
}
15.5 VT技术基础:硬件虚拟化的降维打击
VT(Virtualization Technology)是 Intel 的硬件虚拟化技术。在驱动保护领域,VT 可以说是“降维打击”——因为它运行在比操作系统更低的层级(Ring -1)。
说白了,VT 技术可以让你监控和控制整个操作系统。你可以拦截所有的中断、异常、指令执行。反调试工具在 VT 面前,基本就是透明的。
15.5.1 VT 的核心概念
| 概念 | 说明 |
|---|---|
| VMX Root Mode | VMM(虚拟机监视器)运行的模式,拥有最高权限 |
| VMX Non-Root Mode | 客户机(包括操作系统)运行的模式 |
| VM Entry / VM Exit | 在 Root 和 Non-Root 模式之间切换 |
| VMCS | 虚拟机控制结构,保存了虚拟机的状态和控制信息 |
15.5.2 VT 在反调试中的应用
利用 VT,你可以做到:
- 拦截调试寄存器访问:当攻击者尝试设置 Dr0~Dr7 时,触发 VM Exit,然后你可以在 VMM 里拒绝这个操作。
- 拦截断点异常:当
int 3触发时,VMM 可以决定是否让客户机处理。 - 隐藏内存:你可以把驱动的代码页映射到不同的物理地址,让调试器找不到真正的代码。
下面是一个简单的 VT 拦截流程示意图:
你看,通过 VT,你可以在 VMM 层完全控制客户机对调试寄存器的访问。攻击者即使在内核态,也无法绕过——因为他根本不知道 VMM 的存在。
15.6 本章小结
这一章我们聊了进程保护与反调试的五个方面:
- 进程隐藏:DKOM 和回调方式,让对手找不到你。
- 进程防杀:修改访问掩码,让对手杀不掉你。
- 调试寄存器保护:清空 Dr0~Dr7,防止硬件断点。
- 反调试检测:扫描代码段,识别谁在调试你。
- VT 技术基础:硬件虚拟化的降维打击。
说实话,这些技术在实际项目中要组合使用。比如,你可以用 VT 来保护你的反调试代码,再用反调试代码来保护你的进程隐藏逻辑。层层嵌套,才能让对手无从下手。
好了,这一章就到这里。记住,保护自己之前,先要理解对手。多想想“如果我是攻击者,我会怎么干”,这样才能写出更坚固的驱动。