
简介压缩包内是一份 Windows 驱动隐藏与断链隐藏的完整源码工程适合驱动开发、系统安全与逆向对抗方向的工程师尤其适合已有内核基础、希望研究反调试和隐蔽加载策略的读者。资源共有 43 个文件RAR 压缩包约 5.88MB包含解决方案与项目工程、驱动主程序源码、关键头文件、INF 驱动安装定义以及编译生成的 PDB 符号、OBJ 目标文件、日志和构建状态记录既可逐行阅读驱动隐藏实现也可借助符号与日志复现编译、定位构建问题。工程头文件涉及进程加载器获取、系统模块结构定义等关键数据结构主源码覆盖驱动断链隐藏、回调隐藏与针对 TP 调试工具的规避思路。已有 1540 人学习下载作为内核驱动隐藏与系统安全方向的代码样例具有较高参考价值。1. HideDriver 到底在藏什么驱动可见性的三条暴露路径与 TP 的交叉验证如果你写内核驱动大概率经历过这种事驱动一加载\Driver\目录里就多了一个名字用户态工具随时能枚举出来想做一个产品级的自保护又不想一上来就碰进程保护这种重模块。HideDriver 这类驱动隐藏技术解决的问题就是把驱动对象、设备对象和内核模块链表上的痕迹从常规枚举中暂时摘掉。它不改变驱动功能只改变系统看到它的方式。这篇文章不站在恶意对抗的角度讲而是站在内核开发与安全评估视角把驱动断链隐藏的原理、最小复现路径、验证方法和踩坑记录讲清楚。适合内核驱动开发者、反外挂对抗研究者和安全产品的测试工程师——你不需要把系统搞挂也能把整个链路摸明白。2. 驱动暴露自己的三条路径\Driver\目录、设备对象与模块链表的枚举逻辑任何一个内核驱动加载后至少有三个地方会留下痕迹。只藏其中一个没有意义因为凡是做驱动枚举的组件几乎都是三条路一起查。先把这三条路看清后面断链隐藏才谈得上“藏得住”。2.1 用 ZwOpenDirectoryObject 枚举\Driver\一条路径看清驱动名对象管理器有自己的命名空间\Driver\是一个目录对象里面放的是驱动对象。普通驱动加载后系统会把这个驱动对象插到\Driver\下面名字和驱动服务名一致。用户态工具查驱动最直接的办法就是打开这个目录逐个读目录项。内核里做这件事的标准调用是ZwOpenDirectoryObject加ZwQueryDirectoryObject。NTSTATUS EnumDriverDirectory() { NTSTATUS status; HANDLE hDir NULL; UNICODE_STRING dirName RTL_CONSTANT_STRING(L\\Driver); OBJECT_ATTRIBUTES oa; InitializeObjectAttributes(oa, dirName, OBJ_KERNEL_HANDLE, NULL, NULL); // 打开对象目录需要 DIRECTORY_QUERY 权限 status ZwOpenDirectoryObject(hDir, DIRECTORY_QUERY, oa); if (!NT_SUCCESS(status)) { return status; } // 第一次调用返回 STATUS_BUFFER_TOO_SMALL并给出所需大小 PVOID buf NULL; ULONG bufLen 0; ULONG context 0; status ZwQueryDirectoryObject(hDir, buf, bufLen, FALSE, TRUE, context, bufLen); if (status STATUS_BUFFER_TOO_SMALL) { buf ExAllocatePool2(POOL_FLAG_NON_PAGED, bufLen, DrvE); } // 正式枚举每次返回若干个目录项 status ZwQueryDirectoryObject(hDir, buf, bufLen, FALSE, TRUE, context, bufLen); // 遍历缓冲里的 POBJECT_DIRECTORY_INFORMATION 数组打印 Name / TypeName ExFreePoolWithTag(buf, DrvE); ZwClose(hDir); return status; }这段逻辑里需要重点理解两个参数。第一个是DIRECTORY_QUERY打开目录对象的最低权限缺了它枚举接口会直接返回STATUS_ACCESS_DENIED。第二个是ZwQueryDirectoryObject的ReturnSingleEntry参数这里传FALSE表示一次返回多个目录项效率更高但缓冲大小必须够所以要先调一次拿大小。实际项目里很少自己写这段因为 WinDbg 的!object \Driver就能看全但安全产品做内核枚举时用的就是这套调用理解它才能知道断链应该断在哪里。2.2 IoCreateDevice 与符号链接第二个藏不掉的暴露面驱动对象只是入口真正干活的是设备对象。IoCreateDevice创建设备对象时如果传了设备名设备会被放进\Device\目录如果再调IoCreateSymbolicLink建一个\DosDevices\下的符号链接用户态就能直接用这个链接打开设备。对隐藏需求来说设备对象比驱动对象更难藏因为设备对象必须存在而且设备树里有一个层级关系很多枚举逻辑不查\Driver\直接遍历设备树。NTSTATUS CreateDeviceWithLink(PDRIVER_OBJECT DriverObject) { PDEVICE_OBJECT devObj NULL; UNICODE_STRING devName RTL_CONSTANT_STRING(L\\Device\\TpHideDemo); UNICODE_STRING linkName RTL_CONSTANT_STRING(L\\DosDevices\\TpHideDemo); // 创建设备对象设备类型与特征决定暴露方式 NTSTATUS status IoCreateDevice( DriverObject, // 驱动对象 sizeof(MY_DEVICE_EXT), // 设备扩展大小 devName, // 设备名NULL 表示匿名设备 FILE_DEVICE_UNKNOWN, // 设备类型 0, // 设备特征 FALSE, // Exclusive是否独占 devObj); if (NT_SUCCESS(status)) { // 建立用户态可见符号链接 status IoCreateSymbolicLink(linkName, devName); } return status; }这里有三个参数容易踩坑。DeviceName传 NULL 时设备对象不进入\Device\目录但后面没法建符号链接因为符号链接的目标必须是有名字的设备。DeviceType用FILE_DEVICE_UNKNOWN最省事但反作弊内核组件对已知设备类型会做白名单对比未知类型反而更显眼。Exclusive设TRUE表示设备只允许一个句柄打开调试时会遇到「明明没人在用打开就是失败」的情况实验里一般设FALSE。2.3 TP 这类组件怎么扫三层交叉枚举与特征匹配如果只藏驱动对象设备对象和模块链表仍然会暴露。这里说得直白一点TP 这类反作弊组件加载后的第一件事不是找你某个驱动名而是同时枚举\Driver\目录、遍历PsLoadedModuleList内核模块链表、扫描设备对象树。三份结果摆在一起做交叉比对凡是「在模块链里但不在\Driver\目录里」的驱动就会被标成异常项。这也是为什么驱动断链隐藏只做一个动作根本不够必须同时处理多个暴露点。3. 驱动断链隐藏的核心做法断哪条链、怎么断、断完有哪些边界断链隐藏的本质是「摘链表节点」。Windows 内核里驱动相关的链主要有两条一条是对象管理器里\Driver\目录的哈希桶另一条是PsLoadedModuleList双向链表。断链就是先把前驱和后继的指针接好再把自己从链中间抽出去。3.1 断链的两个位置\Driver\目录项与 PsLoadedModuleListPsLoadedModuleList是内核模块的完整链表每个驱动加载后都有一份LDR_DATA_TABLE_ENTRY挂在里面。WinDbg 的lm命令、内核调试器的模块枚举、各种内核工具都是遍历这条链拿到驱动基址和名字。断链第一层就是把自己的LDR_DATA_TABLE_ENTRY从这条双向链表里摘掉。// 示意代码从 PsLoadedModuleList 摘除当前模块 // 注意直接操作链表必须关中断、提升 IRQL并配合模块链自旋锁 PLDR_DATA_TABLE_ENTRY entry (PLDR_DATA_TABLE_ENTRY)DriverObject-DriverSection; PLIST_ENTRY prev entry-InLoadOrderLinks.Blink; PLIST_ENTRY next entry-InLoadOrderLinks.Flink; // 让前后节点互相指向绕过当前节点 prev-Flink next; next-Blink prev; // 把当前节点的指针指向自己避免悬空访问 entry-InLoadOrderLinks.Flink entry-InLoadOrderLinks; entry-InLoadOrderLinks.Blink entry-InLoadOrderLinks;这段代码只是示意看完应该立刻意识到三个问题。第一摘链动作发生在DISPATCH_LEVEL还得配合PsLoadedModuleList对应的锁否则并发枚举会直接访问到半个链表状态。第二摘完链之后 DriverSection 依然指向LDR_DATA_TABLE_ENTRY这个结构本身在卸载时会被释放如果卸载路径没处理干净就会访问已释放内存。第三也是最容易被忽略的摘链只影响模块枚举不影响设备对象和\Driver\目录里的名字所以它从来不是完整隐藏只是把第一层暴露源摘掉。3.2 用 IoCreateDriver 创建匿名驱动对象最小复现路径如果只是想观察「驱动对象不出现在\Driver\目录」的效果有一个比摘链温和得多的做法加载时用IoCreateDriver创建驱动对象并且第一个参数传 NULL。这种情况下对象管理器不会给驱动对象分配目录项\Driver\里查不到它但驱动照样可以创建设备、挂分发例程。NTSTATUS DriverEntry(PDRIVER_OBJECT SystemDriverObject, PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(SystemDriverObject); UNREFERENCED_PARAMETER(RegistryPath); // 创建“无名”驱动对象参数为 NULL 时不进入对象目录 NTSTATUS status IoCreateDriver(NULL, AnonymousDriverInit); return status; }这个方案的代价是系统传入的SystemDriverObject仍然在\Driver\里你只是额外造了一个不在目录里的驱动对象。真正常见的隐藏型驱动不会走服务加载而是用别的加载方式直接执行驱动代码绕开系统标准的驱动注册过程。对实验来说IoCreateDriver最适合用来理解对象管理器的工作方式而不是作为最终方案。3.3 断链带来的三个直接后果卸载蓝屏、句柄泄漏、行为指纹暴露断链最直接的后果是卸载时蓝屏。模块链被摘掉后IoDeleteDriver会尝试通过 DriverSection 还原模块信息链都断了还原过程访问到的是断裂指针一次0xC0000005就出来了。第二个后果是句柄泄漏\Driver\目录项被摘除后还有别的句柄引用这个驱动对象引用计数不为零IoDeleteDriver也不愿意释放。第三个后果是行为指纹暴露断链本身是一个状态异常TP 这类组件会记录「模块链里缺了谁、\Driver\里多出来的又是谁」断链逃得过名称枚举逃不过行为检测。所以做断链之前先想清楚你面对的是哪种枚举。4. 动手复现一个最小可见性控制驱动IoCreateDriver、匿名设备与动态开关前面把原理讲透了现在给一个能在测试机里跑通的最小实验。目标不是做一个完整的隐藏驱动而是验证同一个驱动代码改变创建驱动对象的方式后\Driver\目录里的可见性发生了什么变化。整个实验在受控虚拟机里完成驱动不含任何破坏逻辑。4.1 设计取舍匿名驱动对象比 DKOM 摘链更适合拿来复现对比几类方案选型逻辑很清楚。DKOM 摘链效果最彻底但需要直接操作内核链表蓝屏概率高调一次要重启一次测试机驱动对象不命名方案只改一个 API 参数风险低能直观看到\Driver\目录变化设备匿名方案能让设备对象不进入\Device\目录但后续没法与用户态通信。实验选第二类方案配合一个注册表开关来控制走哪条路径既有复现价值又不会把自己陷入无限蓝屏的坑里。方案实现复杂度可见性效果卸载风险适用场景DKOM 摘链高模块链、目录项都查不到极高对抗型产品验证IoCreateDriver 匿名低新驱动对象不出现在目录低可见性控制实验设备匿名创建低设备对象不进目录中只做内核功能、不需要用户态通信表格里的结论要结合自己的场景看。如果你做的是安全产品需要驱动常驻且允许用户态枚举那就不该做隐藏而该做签名和可信度建设。如果你只是想知道「对象管理器里为什么看不见某个驱动」IoCreateDriver匿名方案是最短路径。4.2 DriverEntry 双路径根据隐藏开关选择驱动对象创建方式驱动的 DriverEntry 里做一个分支。读注册表参数HideDriver如果为 1 就调用IoCreateDriver(NULL, ...)走匿名路径否则走系统传入的标准驱动对象路径。这个开关设计让你能在一台机器上对比两种加载方式的差异。NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { NTSTATUS status; BOOLEAN hide ReadHideFlag(RegistryPath); // 从服务参数键读 DWORD HideDriver if (hide) { // 匿名路径创建不进入 \Driver\ 目录的驱动对象 status IoCreateDriver(NULL, AnonymousDriverInit); } else { // 标准路径直接使用系统传入的 DriverObject status CreateDevice(DriverObject); } return status; }ReadHideFlag的常见实现是ZwQueryValueKey读HKLM\SYSTEM\CurrentControlSet\Services\驱动名\Parameters\HideDriver。注意这里读的是驱动自己的服务注册表项而不是全局配置避免影响系统里其他驱动的行为。IoCreateDriver的第一个参数传 NULL 是关键它让新驱动对象没有名字进不了\Driver\目录但匿名驱动对象的DriverSection仍然指向一个有效模块结构只是不在模块链表里注册。4.3 设备对象与符号链接隐藏驱动也要给自己留一个 IOCTL 通道驱动对象藏了设备不能藏否则内核功能无法被触发。设计上让匿名驱动初始化例程创建设备设备名仍然叫\Device\TpHideDemo这样设备对象会在\Device\目录下暴露但是不创建符号链接。用户态在默认情况下打开不了这个设备必须通过驱动内部逻辑动态建立符号链接后才能访问。NTSTATUS AnonymousDriverInit(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { PDEVICE_OBJECT devObj NULL; UNICODE_STRING devName RTL_CONSTANT_STRING(L\\Device\\TpHideDemo); NTSTATUS status IoCreateDevice( DriverObject, sizeof(MY_DEVICE_EXT), devName, FILE_DEVICE_UNKNOWN, 0, FALSE, devObj); if (!NT_SUCCESS(status)) { return status; } DriverObject-MajorFunction[IRP_MJ_DEVICE_CONTROL] DispatchIoctl; DriverObject-DriverUnload AnonDriverUnload; g_AnonDriverObject DriverObject; // 保存供卸载时使用 return STATUS_SUCCESS; }设备名\Device\TpHideDemo会在\Device\目录里出现这是有意保留的因为后面 IOCTL 逻辑要能用IoCreateSymbolicLink指向它。FILE_DEVICE_UNKNOWN是最通用的设备类型不会激活特定类驱动的过滤关系。sizeof(MY_DEVICE_EXT)给设备扩展结构预留空间用来保存设备自旋锁、状态标志这类每设备数据比用全局变量干净。4.4 动态现身用 IRP_MJ_DEVICE_CONTROL 控制符号链接的建立与删除符号链接是用户态看到驱动设备的唯一合法入口。常态下不建符号链接设备对象虽然存在但普通程序不知道用什么路径打开它。IOCTL 分发例程里做两个分支收到IOCTL_TpHideShow就创建符号链接收到IOCTL_TpHideHide就删除符号链接。这样驱动既能被自己的管理程序找到又能随时切换可见状态。NTSTATUS DispatchIoctl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(Irp); ULONG code irpSp-Parameters.DeviceIoControl.IoControlCode; NTSTATUS status STATUS_SUCCESS; switch (code) { case IOCTL_TpHideShow: { UNICODE_STRING linkName RTL_CONSTANT_STRING(L\\DosDevices\\TpHideDemo); UNICODE_STRING devName RTL_CONSTANT_STRING(L\\Device\\TpHideDemo); status IoCreateSymbolicLink(linkName, devName); // 建立可见入口 break; } case IOCTL_TpHideHide: { UNICODE_STRING linkName RTL_CONSTANT_STRING(L\\DosDevices\\TpHideDemo); status IoDeleteSymbolicLink(linkName); // 删除可见入口 break; } default: status STATUS_INVALID_DEVICE_REQUEST; break; } Irp-IoStatus.Status status; Irp-IoStatus.Information 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return status; }这个开关设计对应的真实场景是驱动平时不暴露用户态接口管理程序需要通信时临时建立符号链接通信结束后立刻删除。整个过程对用户态枚举来说驱动设备是「闪现」的。但它不是严格意义的隐藏因为\Device\TpHideDemo这个名字在\Device\目录里一直存在。到这里实验已经能让你看清驱动对象、设备对象、符号链接三者的可见性边界分别在哪里。5. 排查与避坑驱动隐藏最容易翻车的 5 个场景这部分全是我自己在受控环境下测试时踩过的坑。每一条都是「现象→原因→解决」的结构照着排查能省好几个晚上。5.1 现象断链后驱动卸载直接蓝屏第一次做模块链摘除实验卸载驱动时蓝屏错误码集中在0xC0000005和0x50。原因很直接模块链表被摘除后IoDeleteDriver需要 DriverSection 里的信息来清理驱动延伸对象但 DriverSection 指向的内存已经不在链上系统在恢复链表时访问到了不可用地址。解决方法是卸载前先把链表节点重新接回去并且确保摘链和恢复链两个动作都用同一个锁保护不要在DISPATCH_LEVEL下做半截操作。如果驱动里还有分页代码恢复链之后要先停掉所有可能触达驱动代码的线程再走卸载流程。5.2 现象驱动不在\Driver\里却被设备树找到用匿名方式创建驱动对象后!object \Driver里看不到它以为隐藏成功了。结果换!object \Device一看设备对象整整齐齐摆在设备树里而且!devobj能顺着设备对象反查到驱动对象。原因是在内核里设备对象和驱动对象是强引用关系只要设备存在驱动对象就能被逆向找到。解决方法是接受这个现实设备对象是驱动的功能载体隐藏驱动对象而不隐藏设备对象只能挡住最表层的目录枚举挡不住设备树遍历。在安全评估报告里这两者必须分开标注不能混为一谈。5.3 现象TP 这类组件给出「未知驱动」提示在测试机里加载了一个断链驱动后安全组件的日志没有报「发现隐藏驱动」而是报「未知驱动」。区别很关键隐藏驱动往往留有明确的模块痕迹或者字符串特征而「未知」说明它在白名单里找不到任何身份信息。原因就是交叉枚举起了作用模块链里查不到设备对象树里查得到注册表服务项里又对不上号这类组件只把结果标记为未知等后续特征匹配再定性。解决思路是从开发侧做身份信息建设比如补全驱动签名、服务描述、产品名而不是靠隐藏把自己变成一个更大的黑匣子。5.4 现象重启后隐藏驱动消失或无法加载断链和目录摘除都只对当前启动会话有效。重启后系统按服务注册表项重新加载驱动\Driver\目录自动重建断链状态完全丢失如果驱动没有签名或者签名测试模式没开可能直接加载失败。原因很朴素系统加载驱动的过程是标准的隐藏动作发生在加载之后不改变启动加载的默认行为。解决方法是把隐藏动作做成开机执行的控制逻辑每次启动后由驱动自身或配套服务重新应用隐藏状态。生产环境不要依赖这种方式一旦开机阶段蓝屏系统进不去后悔药都没有。5.5 现象调试器一挂上断链驱动立刻崩溃用 WinDbg 内核调试时lm命令会遍历PsLoadedModuleList。断链后这条链被跳过了一个节点调试器的模块遍历会读到不完整的链表状态轻则列出不一致的模块列表重则直接让系统触发CRITICAL_STRUCTURE_CORRUPTION或者KERNEL_SECURITY_CHECK_FAILURE。解决方法是调试阶段不要同时做断链先验证干净的驱动逻辑再在最后一轮实验里加隐藏动作。调试器和隐藏动作冲突是必然的不是配置问题不要浪费时间在调试参数上。6. 验证隐藏效果用 WinDbg 和交叉枚举确认驱动是否真的「看不见」做到最后验证比实现更重要。一个驱动到底有没有被隐藏靠的不是自信而是几个固定步骤的交叉确认。检查项命令判断依据驱动对象目录!object \Driver是否还能看到目标驱动名设备对象!object \Device设备是否仍然存在设备对象详情!devobj 地址能否反查到驱动对象指针模块链表dt nt!_LDR_DATA_TABLE_ENTRY!list目标模块是否还在模块链里驱动对象真的被隐藏时!object \Driver里找不到名字但!object \Device里一定找得到设备。所以验证的第二步是确认设备对象是否存在——设备在驱动对象就在只是绕了一层。第三步是!devobj反查设备对象的 Driver 字段指向驱动对象如果这个指针还在说明驱动对象对象引用没有释放只是名字被摘了。第四步查模块链表最狠PsLoadedModuleList是系统模块枚举的底线链里在就直接暴露。我把这套验证跑通后形成了一个固定习惯任何可见性控制实验结束前先跑!object \Driver和!object \Device做正反对照再跑一次dt nt!_LDR_DATA_TABLE_ENTRY检查模块链完整性最后用!verify或系统自带的驱动验证器过一遍保证没有把系统结构改坏。驱动隐藏这件事难点从来不是把链摘掉而是摘完之后系统还稳定、自己能恢复、别人查不到。希望这次的排查路径能帮你少走几步弯路。本文还有配套的精品资源点击获取