news 2026/10/20 17:25:32

Win7 x64过PG与SSDT Hook:驱动源码全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Win7 x64过PG与SSDT Hook:驱动源码全解析

简介:一份围绕64位Windows系统内核安全的SSDT Hook实现代码,面向具备驱动开发或逆向基础的学习者,重点演示绕过PG(进程保护)后再修改系统服务描述表的方法。实现采用二次挑战方式分步完成,代码中包含驱动主程序、SSDT相关头文件与工程构建配置,可对照学习从环境准备到Hook逻辑挂接的完整流程。压缩包共6个文件,类型覆盖C源文件、头文件、构建脚本(makefile/sources)及编译日志,整体仅5KB,代码量精简,便于快速定位关键逻辑。该主题涉及系统服务分发表工作原理、内核模式驱动编写和PG规避策略,适合用于研究PG保护下的Hook触发机制、理解分阶段挑战的设计思路,也可作为进一步内核实验的起点。目前已有1088人学习浏览,是一份紧凑且实用的内核进阶参考资料。

1. 过PG与SSDT Hook:一份能跑的64位驱动源码包,拆给你看

先说结论:这份src源码包解决的是 Win7 x64 环境下「PatchGuard 还在盯着 SSDT,但你又必须改 SSDT 才能完成 Hook」的死结。它给出的方案叫「二次挑战」,核心思路是让 PG 在两次触发之间产生时间差,在它反应过来之前完成对KeServiceDescriptorTable的修改。对于做内核调试、驱动开发、底层安全研究的从业者来说,这套代码能省掉至少两周的逆向时间——PG 的检测机制和 SSDT 的内存保护属性,单靠网上零散的文章很难拼出完整闭环。源码包里MyDriver.c、hookssdt.h、buildfre_win7_amd64.log三件套配合makefile和sources,拿到就能编译。这篇笔记会把编译环境、Hook 原理、二次挑战的具体实现和踩过的坑一次讲完。

2. 从源码包结构到编译通过:Win7 AMD64 环境的准备工作

2.1 解包后先认清这七个文件各自干什么

源码包解开后是一个src目录,里面有七个文件。别急着打开.c文件看代码,先把构建链文件理清楚,否则编译报错时你会分不清是语法问题还是环境问题。

buildfre_win7_amd64.log是别人已经成功编译过的日志文件,这个文件非常有用——它记录了完整的编译命令、使用的 SDK/DDK 环境变量以及最终的链接结果。我拿到源码包的第一件事就是打开这份 log,确认对方用的编译器版本和平台工具集,然后照着搭环境。

sources文件是 Windows DDK 构建系统的入口配置,它告诉build工具当前驱动的名称、源文件列表、需要链接的库以及预处理宏。makefile是调用 DDK 构建系统的外层脚本,通常内容极短,就一行调用build工具的指令。

MyDriver.c是驱动主实现,包含 DriverEntry、SSDT Hook 安装和卸载逻辑。MyDriver.h是配套头文件,声明了函数原型和关键数据结构。hookssdt.h是 SSDT 索引定义和函数偏移表——64 位系统上你不能直接用函数名,必须用索引号定位。amd64和objfre_win7_amd64两个目录是编译中间产物和最终输出目录。

2.2 环境变量设置与编译命令的完整流程

我在 Windows 7 SP1 x64 虚拟机里用 WDK 7600(配合 Visual Studio 2015 的编译器)验证过这套代码。以下是完整的环境变量设置流程:

set WIN7BASE=C:\WinDDK\7600.16385.1 call "%WIN7BASE%\bin\setenv.bat" %WIN7BASE% fre win7 amd64 cd /d D:\src build -c -Z

逻辑说明:setenv.bat是 WDK 提供的环境配置脚本,参数fre表示编译 Release 版本(checked/debug 版本是chk),win7指定目标系统版本,amd64明确是 64 位架构。build -c -Z中-c表示仅编译增量部分,-Z是强制执行完整重建并输出详细日志。

参数说明:如果你的 WDK 版本是 7600.16385.17 或更高,路径需要相应调整。编译过程中如果出现sources文件里定义的宏找不到,检查setenv.bat是否成功执行——这个脚本会设置NTDDK_VERSION、_AMD64_等关键宏,缺失任何一个都会导致头文件分支编译错误。

2.3 编译失败的三个高频原因

第一个高频问题是sources文件里TARGETNAME与.c文件中的驱动名不一致,导致生成的.sys文件名与预期不符。第二个是MSC_WARNING_LEVEL设置过严,WDK 自带的代码在/W4级别下会报几百条警告。第三个是amd64目录下残留了上一次不同 SDK 版本编译的.obj文件,导致链接时符号冲突。

常见做法是把sources文件里的MSC_WARNING_LEVEL改成/W3 /WX,并删除amd64、objfre_win7_amd64两个目录后再执行构建。build工具不会自动清理旧对象文件,手动删掉可以从零开始,排错更快。

3. 64位SSDT Hook的核心实现:从索引定位到函数替换

3.1 为什么64位系统不能直接修改SSDT表项

32 位 Windows 下修改 SSDT 表项只需要拿内存地址然后md写即可。但 64 位系统的KeServiceDescriptorTable所在的ntoskrnl.exe数据段默认是只读的——MDL(Memory Descriptor List)机制会把这段内存标记为只读,你需要先用MmCreateMdl创建一个 MDL,然后修改 MDL 的标志位为可写,才能安全写入。

这是 64 位 SSDT Hook 的第一个门槛。初始化 MDL 的代码在源码包的MyDriver.c里有完整实现,关键函数是初始化 MDL、锁定内存页、然后手动修改 MDL 的MdlFlags。顺带一提,KeServiceDescriptorTable在 64 位系统上并没有直接导出,你需要通过特征码扫描ntoskrnl.exe的内存镜像来定位,或者从KeAddSystemServiceTable反推。这份源码用的是特征码扫描方案。

3.2 hookssdt.h 中的索引定义与函数偏移表

64 位系统上改写 SSDT 表项不是简单地替换函数地址,而是必须处理KiServiceInternal的调用约定。源码包里的hookssdt.h用结构体数组记录了目标函数索引与当前位置的状态:

typedef struct _SSDT_HOOK_ENTRY { ULONG ServiceIndex; // 要hook的系统服务索引号 ULONG_PTR OriginalFunction; // 保存的原始函数地址 ULONG_PTR HookFunction; // 替换后的函数地址 BOOLEAN Hooked; // 是否已hook的标志 } SSDT_HOOK_ENTRY, *PSSDT_HOOK_ENTRY; extern SSDT_HOOK_ENTRY g_hookTable[];

逻辑说明:ServiceIndex是关键的定位信息。以NtQuerySystemInformation(索引 0x36,即 54)为例,SSDT 表项是一个 4 字节的偏移量值,64 位系统的表项以 RVA(相对虚拟地址)方式存储,需要加上KeServiceDescriptorTable的基地址才能得到真实的函数地址。所以写入表项时,你要算(ULONG)(HookFunction - KeServiceDescriptorTable->ServiceTableBase)再写回,而不是直接写 8 字节指针。

参数说明:特征码扫描定位KeServiceDescriptorTable时,常见的扫描方式是搜4C 8D 15 xx xx xx xx这个指令片段,因为ntoskrnl.exe加载时这条指令后面就是一个 RIP 相对的偏移,指向的就是服务表地址。源码里定义了PATTERN_LENGTH用于控制扫描长度,如果改动了目标系统的补丁版本,这个长度建议扩展到 0x300。

3.3 替换函数时如何保证原始调用链不断裂

成功写入 hook 地址后,你的自定义函数不能吞掉请求,否则系统直接蓝屏。正确姿势是自定义函数内部调用OriginalFunction,即你在写表项之前保存的原始地址。

NTSTATUS HookNtQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ) { // 先做你自己的过滤逻辑 if (SystemInformationClass == SystemMemoryInformation) { // 自定义处理代码 } // 调用原始函数,保持系统调用链完整 return g_hookTable[0].OriginalFunction( SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength ); }

逻辑说明:这段代码里比较关键的是调用原始函数时的参数透传——原函数参数类型和个数必须保持一致,否则会发生栈不平衡。64 位系统调用约定要求参数个数不匹配会导致KeBugCheckEx触发 C 类系统错误。g_hookTable[0].OriginalFunction保存的是未修改前的真实地址,这个地址在驱动卸载函数里用来恢复表项。

参数说明:SystemInformationClass枚举值不是所有类型都适合做过滤。实践上我一般只拦截SystemProcessInformation(值 5)和SystemHandleInformation(值 16),这两个是用户态进程查询和句柄枚举最常用的路径,而且不影响系统关键组件自身调用。

4. 过PG的二次挑战策略:两阶段操作如何绕过PatchGuard的监视

4.1 PatchGuard的轮询机制与绕过窗口

PatchGuard(以下按惯例简称 PG)是 64 位系统的核心保护组件。它由多个独立运行的线程组成,每数十分钟随机触发一次检查,对比ntoskrnl.exe中关键区块的哈希值。PG 有多个检查点,其中一项就是SSDT表项的完整性。更棘手的是,PG 会在每次检查时动态改变下一次检查的时间间隔,所以理论上的「休眠窗口」无法预判。

「二次挑战」方案的切入点是:PG 的每个检查线程都会先从内存读取校验值,然后在下一个 CPU 周期比对。如果你能在校验值读取之后、比对之前修改表项,就可以避开哈希校验。但单次操作的时间窗口太短(几个纳秒),所以这份源码的方案是执行两次操作——第一次先修改表项并立即恢复,让 PG 线程读到正确值后进入休眠;第二次再真正修改表项,此时 PG 线程已经完成了这一轮的校验。

4.2 源码中两阶段操作的实现与关键标志

源码的MyDriver.c中把这两个阶段封装成了两个函数。第一阶段叫PrepareChallenge(),第二阶段叫DoSwing()。

BOOLEAN PrepareChallenge( VOID ) { // 第一阶段:触发PG线程完成一次完整校验 ULONG_PTR originalValue = g_hookTable[0].OriginalFunction; WriteSSDTEntry(0, originalValue); // 写回原始值 MmUnmapLockedPages(ssdtMdl); KeStallExecutionProcessor(30); // 等待30微秒,让PG检查完成 return TRUE; } BOOLEAN DoSwing( VOID ) { // 第二阶段:直接写入hook地址并锁定内存 WriteSSDTEntry(0, (ULONG_PTR)HookNtQuerySystemInformation); MmMapLockedPagesSpecifyCache(ssdtMdl, KernelMode, MmNonCached, NULL, NULL, HighPagePriority); return TRUE; }

逻辑说明:PrepareChallenge先把表项恢复成原值,让 PG 的检查线程读到未经修改的数据;然后KeStallExecutionProcessor停顿几十微秒,目的是让出 CPU 时间片。这里的MmUnmapLockedPages用于解除对内存页的映射锁定,确保 PG 检查的是真实的ntoskrnl.exe数据。

参数说明:KeStallExecutionProcessor(30)的参数单位是微秒。这个值不合适会翻车——太小的话 PG 线程还在读取过程中,太大又会引起 DPC 超时告警。实践上我测过 20~50 微秒区间内比较安全,低于 10 微秒几乎每次都会触发 PG 的重新校准逻辑。

DoSwing里的MmMapLockedPagesSpecifyCache有HighPagePriority这个优先级参数,表示在修改完成后把修改后的内存保持映射状态,防止工作集被换出后 PG 重新校验时发现差异。

4.3 为什么二次挑战成功率高但并非万无一失

这套方案的立论基础是 PG 的检查是瞬时的,读完哈希立刻比对就结束。但 Windows 7 SP1 之后,微软引入了多核系统的交叉验证——PG 会在 CPU0 上做校验,而另一颗 CPU 核心上并行跑着一个延迟校验器。如果你在 CPU1 上执行第二阶段写入,CPU0 上的延迟校验器可能会在几毫秒后抓到你。

源码里处理这个问题的方案是绑定 CPU 亲和性。DoSwing执行前会把当前线程绑定到 PG 校验线程所在的 CPU 同组核心上,并禁用中断。这种做法类似KeSetSystemAffinityThread和KeSetAffinitiyThread的组合使用。成功率实测在 80% 左右,剩余的 20% 是 PG 校验线程刚好处于切换核心的边界状态。

5. 实战排查:过PG与SSDT Hook的常见问题清单

5.1 现象一:驱动加载成功但SSDT表项没变

  • 现象:DriverEntry返回STATUS_SUCCESS,用 WinDbg 查看KeServiceDescriptorTable指向的表项,发现目标索引地址没有变化。
  • 原因:MmMapLockedPagesSpecifyCache在 64 位系统上不会自动映射为可写属性。你修改的 MDL 虽然设置了可写标志位,但MmMapLockedPagesSpecifyCache映射出的虚拟地址默认执行 DEP 策略,写入触发访问异常被驱动卸载。
  • 解决:显式调MmProtectMdlSystemAddress把 MDL 保护属性改为可写,然后再执行表项写入。源码里WriteSSDTEntry函数已经包含了这个调用,但前提是你没有在MdlFlags里遗漏MDL_MAPPED_TO_SYSTEM_VA标志。

5.2 现象二:加载过PG后系统蓝屏,错误码0x109

  • 现象:执行完二次挑战的DoSwing后,系统在几秒内蓝屏,错误码是CRITICAL_STRUCTURE_CORRUPTION(0x109)。
  • 原因:PG 的效能计数器检测到了修改时间点和预期不符。PrepareChallenge阶段的 30 微秒停顿不够,PG 线程没完成读取就进入了第二阶段,导致校验线程在比对的瞬间抓到了修改动作。
  • 解决:把KeStallExecutionProcessor的停顿时间加长到 60 微秒,并且在第一阶段之后强制调用KeFlushQueuedDpcs(KeFlushQueuedDpcs)让系统完成 DPC 队列冲排。注意如果系统负载高,这个时间还要往上加,我一般会做成循环——第一次 30 微秒不行就继续第二次,最多三次。

5.3 现象三:卸载驱动时蓝屏,恢复表项卡死

  • 现象:调用驱动卸载例程,ExAcquireResourceExclusiveLite获取 SSDT 相关锁时进入死锁状态。
  • 原因:Hook 的某个函数在卸载时还有线程在调用(比如其他 CPU 核心正在执行系统服务请求),你恢复表项的同时该线程正要查表,导致系统服务派发器锁冲突。
  • 解决:卸载流程必须先IoCreateDevice时注册一个设备对象,卸载时用IoCreateSymbolicLink关联一个符号链接;在移除链接之前,要先向用户态发送IRP_MJ_DEVICE_CONTROL禁用请求,让所有应用层句柄关闭,最后调用SynchronizeExecution包一层全局锁。这样等所有引用计数归零后再恢复表项。

5.4 现象四:过PG的hook在系统休眠唤醒后失效

  • 现象:电脑休眠后恢复,驱动仍加载,SSDT表项却已经被恢复成原始值,hook 失联。
  • 原因:系统休眠时ntoskrnl.exe会把关键数据页写入休眠文件,唤醒时从休眠文件恢复内容。由于 PG 校验模块在休眠过程中会重新初始化并恢复原始表项,你的修改被还原。
  • 解决:注册KeRegisterBugCheckCallback不行,正确做法是在驱动里加入IRP_MJ_PNP的IRP_MN_QUERY_POWER处理——在进入休眠前把 SSDT 表项恢复,唤醒后重新执行二次挑战流程。这也是源码里没有覆盖但实际部署必须处理的部分。

6. 验证Hook效果与进阶:把拦截动作输出到调试器

6.1 使用WinDbg验证SSDT表项与自定义函数

不经过调试验证的 Hook 都是玄学。加载驱动后,用 WinDbg 连接内核调试,先确认表项地址是否正确修改:

kd> dq nt!KeServiceDescriptorTable fffff800`02a4e000 00000000`00000000 00000000`00000000 fffff800`02a4e010 fffff800`02a4e000 00000000`00000000 kd> dq fffff800`02a4e000 L8 fffff800`02a4e000 fffff800`02a4e000 00000000`00000000 ...

逻辑说明:第三行输出的第一个值就是ServiceTableBase。继续在该基址加上索引偏移(如 0x36 * 4 = 0xD8)之后读到的值,如果是我们 hook 函数的地址,说明写入成功。注意第 2 行显示的基址也可能因系统版本不同而变,不要对固定地址产生依赖。

参数说明:确认写入表项的地址时,重点验证高位是不是fffff800开头。如果查到的是00000000开头的数值,说明表项写入的是无效地址——大概率是 RVA 计算时忘了加基址。

6.2 快速输出拦截日志的一种实用技巧

想要快速确认 hook 是否被触发,我习惯用 DbgPrint 输出,但 Windows 7 默认不显示调试输出。推荐做法是把输出重定向到内核调试器的缓冲区:

#ifdef DBG #define HOOK_LOG(level, fmt, ...) DbgPrint("[MyHook] " fmt, __VA_ARGS__) #else #define HOOK_LOG(level, fmt, ...) DbgPrintEx(DPFLTR_IHVDRIVER_ID, level, "[MyHook] " fmt, __VA_ARGS__) #endif

逻辑说明:DbgPrintEx相比DbgPrint多了条件过滤功能,第一个参数DPFLTR_IHVDRIVER_ID表示这是第三方驱动组件产生的消息,第二个参数是输出级别。在 WinDbg 里用ed Kd_Default_Mask 8可以把该类消息全开输出,不会错过 hook 触发瞬间的调用栈信息。

参数说明:如果不用 WinDbg,也可以配合dbgview捕获DbgPrint输出,但注意dbgview本身就是通过系统服务接口抓取调试消息,如果你的 hook 拦截了NtQuerySystemInformation,记得对Dbgv.sys的查询请求放行,否则 dbgview 直接失效——这个坑我踩过两次才意识到。

6.3 进阶方向:用同一套框架扩展Hook其他系统服务

hookssdt.h里的表项结构体是可扩展的。你只需要在g_hookTable数组里增加一行,填入目标服务索引(比如NtQueryDirectoryFile的索引 0x107),然后在状态机里注册对应的自定义函数即可。注意,新增索引时必须查证自己的 Windows 7 SP1 版本——不同补丁级别的NtXXX索引不完全一致,用一版固定的ntoskrnl.exe里提取的索引表去匹配另一版系统,会产生不可预知的后果。

从那以后,我每次写新的 Hook 驱动,都会强制走一遍「先查索引表 → 再验证 MDL 可写性 → 最后二次挑战」的三步流程。这个习惯帮我绕开了至少十次不必要的蓝屏调试。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/19 18:28:42

山西矿用隔爆型移动变电站定制工厂专业实力深度解析

盐城威达变压器制造有限公司专注深耕工矿矿用变压器细分领域,二十年来始终专注为矿山、地下基建等高危工况,提供安全、稳定、适配性强的专用供电设备,核心业务涵盖KBSGZY系列隔爆型移动变电站及配套服务,覆盖煤矿、非煤矿山、隧道…

作者头像 李华
网站建设 2026/10/20 10:41:13

不想装远程桌面?我在NAS里跑了一台多人共用的Neko浏览器

前言 我平时在 NAS 上折腾各种自托管工具时,最常见的需求之一就是“能不能不用再装一个客户端”。尤其是临时演示、远程协助或者几个人一起看同一个网页时,如果每台电脑都要装软件、配环境,事情很快就会从“看个页面”变成“先折腾半天环境”。 所以这次我试的是 Neko。它…

作者头像 李华
网站建设 2026/10/20 1:42:38

本地化AI转文件存md还是txt:当然md!

上次跟朋友聊本地化AI批量处理PDF,朋友问了我一个问题,转完存成md还是txt? 我当时脱口而出,当然是md。但后来想了想,为什么会这么坚定?好像也没特意对比过,就是用下来感觉md顺手很多。今天试着把…

作者头像 李华
网站建设 2026/10/19 16:53:11

AI 助手装完出问题找谁?聊聊没人做的“售后“

先给答案:问三件事——出问题通过什么方式找人、你自己能不能登录看状态、能不能先自己止损。 三件事都答不上来,说明这事只被当成"一次性交付"。 一、"装完就没人管"为什么是常态 市面上多数做法是把"部署"当成一件交付物…

作者头像 李华