简介:面向驱动开发者和逆向安全研究人员的驱动隐藏源码包,以HideDriver项目为主线,集中演示驱动断链隐藏、注册回调隐藏以及针对TP等动态调试工具的检测与规避思路,可帮助读者理解内核驱动如何绕过模块枚举、隐藏自身存在,并掌握反调试与自我防护的典型实现。资源共43个文件,压缩包大小5.88MB,工程结构完整,包括Visual Studio解决方案与源码、inf安装配置、头文件结构定义、pdb调试符号及构建日志,既可逐行阅读关键实现,也可打开工程直接编译,观察断链前后模块列表的变化。已有1541人学习,适合有一定内核或驱动基础的读者,结合项目中的模块结构体和入口逻辑分析,能够梳理出驱动断链隐藏从加载到生效的完整过程,对恶意软件分析、内核防护开发均有参考价值。
1. 隐藏驱动这事,不是玄学:先想清楚你要对抗的是谁
第一次用驱动断链隐藏时,我干过一件蠢事:把驱动里所有导出表清空,又改了 INF 里的服务名,然后兴致勃勃装上,结果用 Process Explorer 一扫,驱动照样躺在系统模块列表里。后来才明白,隐藏驱动不是把文件藏起来,而是要让操作系统的加载器“忘了”自己。这份 HideDriver 源码包,核心就是做这件事——它用 TP 驱动调试视角给你演示了驱动断链隐藏到底怎么实现。适合三类人:在内核驱动开发里被双机调试坑过的、做安全产品希望核心驱动不被人一眼拖走的、以及想搞懂 TP 这类追踪工具是从哪个接口抓到驱动加载的人。先放结论:断链隐藏并不神秘,只要把 Windows 内核里的PsLoadedModuleList链表上的自身节点摘掉,用户态工具就再也枚举不到它。
2. 驱动断链隐藏的原理拆解:PsLoadedModuleList 上摘一个节点
2.1 系统怎么记住“你加载过”这个驱动
Windows 内核维护了一张全局链表PsLoadedModuleList,每个加载的驱动都用LDR_DATA_TABLE_ENTRY结构挂在这条双向链表上。内核的模块管理器、调试器、以及 TP 这类动态追踪工具,要枚举驱动时都是顺着这个链表挨个翻。断链隐藏的思路很简单:把自己的节点从链表中摘除,让枚举者走到这里时直接“断线”。这份源码里的头文件SYSTEM_MODULE_STRUCT.h实际上就是在定义这个节点的内存结构。注意,断链摘除的是“模块链表”这个视图,驱动本身的文件、回调函数、对象目录还在系统里,只是常规 API 查不到它了。
2.2 摘链表的典型实现:CONTAINING_RECORD 的用法
源码包里的Driver1.c承担了核心逻辑,常见做法是先拿到自己驱动的映像基址,然后遍历链表,找到基址匹配的节点,最后改Flink和Blink指针实现双向摘除。以下代码是这个思路的简化示意:
// 摘除 PsLoadedModuleList 中自身驱动节点的核心逻辑 PLIST_ENTRY pHead = g_PsLoadedModuleList; // 全局链表头,运行时动态获取 PLIST_ENTRY pCurrent = pHead->Flink; // 从头节点的下一个开始遍历 while (pCurrent != pHead) // 环链表绕一圈 { PLDR_DATA_TABLE_ENTRY pEntry = CONTAINING_RECORD(pCurrent, LDR_DATA_TABLE_ENTRY, InLoadOrderLinks); if (pEntry->DllBase == g_MyDriverBase) // 基址一致即命中自身驱动 { pCurrent->Blink->Flink = pCurrent->Flink; // 让前一节点的正向指针跳过自己 pCurrent->Flink->Blink = pCurrent->Blink; // 让后一节点的反向指针跳过自己 // 注意此时不释放本节点内存,仅断开链接 break; } pCurrent = pCurrent->Flink; }逻辑说明:CONTAINING_RECORD根据链表节点的地址反推出结构体首地址,这是内核里最常用的容器操作。判断命中用的是DllBase,也就是 DriverEntry 传进来的DriverObject->DriverStart。修改链表指针这一段,正反双向都要改,只改一边会造成链表断裂,后续任何模块枚举都会蓝屏。这里的g_PsLoadedModuleList和g_MyDriverBase需要在 DriverEntry 里初始化,常见做法是调用MmGetSystemRoutineAddress拿PsLoadedModuleList的地址,而自己的基址直接用DriverObject->DriverStart,千万别用NtCurrentPeb()之类的用户态方法。参数上最值得你调整的是命中条件:如果你做了基址重定位或者伪装成微软驱动,就要把DllBase ==改成说明一下:实际源码里还包含 GET_MIPROCESSLOADERENTRY.h这个头文件,它提供了另一种获取模块加载入口的方法,其核心是绕过PsLoadedModuleList先用NtQuerySystemInformation枚举,再定位自己。两者你只要选一条跑通就行。
2.3 断链之后还有三个“尸体”要处理
只摘链表节点远远不够。你自己要确认三处:第一,驱动对象DriverObject的DriverSection字段还指向原来的 LDR 节点,需要在摘除前把它改成NULL,否则某些自旋带DeviceIoControl内核路径的工具仍然能拿到驱动对象。第二,\Driver\YourName对象目录里的符号链接还在,需要调用ZwDeleteFile或者直接断开符号链接。第三,驱动的回调函数(例如DriverUnload、派发函数)仍然注册在系统分发表里,TP 这类调试者不在乎你的加载记录,它直接监控IRP_MJ_DEVICE_CONTROL派发就能追踪你的 IOCTL 调用地址。所以断链的最好搭档是“把派发函数注册成共享分发表里的其他函数地址”,让调用看起像一般处理程序。这份源码包没有直接实现第三项,但给了你SYSTEM_MODULE_STRUCT.h这个结构,就是在提示你顺着这个方向去扩展。我建议你第一遍调试时,只做链表摘除这步,验证单点有效后再加后续处理,上来就全做会导致问题定位困难。
3. 把 HideDriver 工程真正跑起来:WDK 配置与双机调试
3.1 拿到源码包先确认工程结构
解压HideDriver_hidedriver_TP_驱动隐藏_驱动断链隐藏_隐藏驱动_源码.rar后,打开HideDriver.sln,至少在 Visual Studio 2019 + WDK 10 环境里能直接编译。你会看到x64和ARM Debug两个配置目录,这表示项目同时支持 64 位和 ARM 架构的驱动目标。源码包里的.vcxproj已经预设好了驱动程序项目类型,HideDriver.inf负责描述驱动的安装行为,Driver1.c就是主逻辑文件。全部源码加起来没有多少,但值得仔细读的是GET_MIPROCESSLOADERENTRY.h——它定义了一个内联函数来获取当前进程的模块加载入口,这个入口在摘链表时需要用来做二次定位。ARM Debug 配置在实际运行时需要部署到 ARM 开发板或对应虚拟机,通常你需要一台 Windows on ARM 设备,一般开发者在 x64 调试机上跑不了 ARM 驱动。
3.2 编译环境三件套:DDK、测试签名、目标机调试模式
编译前最常翻车的是 WDK 版本不匹配。常见做法是用 Visual Studio 的组件管理器装 WDK,然后在项目属性里把“目标平台版本”和“目标平台最低版本”均设为与 SDK 一致。以下是完整的部署步骤:
# 1. 主机侧:开启测试签名并设置调试传输 bcdedit /set testsigning on bcdedit /set debug on bcdedit /dbgsettings net hostip:192.168.5.10 port:53000 key:1.2.3.4 # 2. 部署引导程序到虚拟机/目标机(需要管理员权限) # 在 VS 的“驱动安装”设置里,把目标机指定为 ip:192.168.5.20 # 勾选“部署”后,VS 会自动拷贝 .sys 和 .inf 到目标机并创建服务 # 3. 在目标机上手工启动驱动服务(适合确认部署是否成功) sc start HideDriver sc query HideDriver逻辑说明:第一步的bcdedit全部在目标机执行,不是主机。testsigning on允许加载未签名的测试驱动,dbgsettings net指定网络内核调试器的位置,三个参数分别是主机的 IP、调试端口和键值。第二步需要你在 VS 的项目属性里填写目标机凭据,VS 会通过Run as administrator的方式在目标机上创服务。第三步的sc start用于验证,如果驱动在 DriverEntry 中断链成功,driverquery列表里立刻看不到HideDriver,但sc query仍然能查到服务存在——这就是断链的典型特征:服务注册表还有,模块链表没了。此时用 WinDbg 的lm命令也看不到模块,只有用!drvobj传入驱动对象地址才找得到。
3.3 拿到驱动对象的方式:从 DriverObject 逆推链指针
这里教你在 WinDbg 里验证是否断链成功,这是整个项目里最值得保存的操作:
dt nt!_OBJECT_HEADER ffffaa8000001000 Name !object \Driver\HideDriver dt nt!_DRIVER_OBJECT ffffaa8000001000 DriverSection如果第三步DriverSection的输出显示NULL,说明链已经摘除;如果还显示一个模块地址,说明你只改了PsLoadedModuleList的正向遍历,反向指针没处理。参数说明:!object输出对象的句柄和引用计数,若对象不存在,说明符号链接也被你删了,这是断链隐藏的完整状态。另外,建议你在 DriverEntry 里刻意加入一条标记,例如写一个注册表值HKLM\SYSTEM\CurrentControlSet\Services\HideDriver\Debug\Hooked,然后双击运行时用定时器读取该值来确认驱动仍在执行——因为断链后普通工具难以察觉驱动是否活着,这条标记是你自己的“后悔药”,调试完务必删掉。
3.4 ARM Debug 配置的坑:结构体对齐
项目里的 ARM Debug 配置与 x64 最大的差别是结构体对齐和指针截断。Windows on ARM 使用 ARM64 ABI,指针仍是 64 位,但LIST_ENTRY里的Flink/Blink在单核模拟下可能因为中断优先级问题出现总线错误。按照实际操作经验,编译 ARM Debug 前,你务必在Driver1.c顶部加上:
#pragma pack(push, 8) // 强制 8 字节对齐,与 Windows ARM64 的模块节一致 #include "SYSTEM_MODULE_STRUCT.h" #pragma pack(pop)否则头文件里定义的SYSTEM_MODULE_STRUCT按 4 字节对齐时,DllBase偏移和实际LDR_DATA_TABLE_ENTRY不匹配,断链时会把相邻节点内存改坏,目标机直接重启。这个智力损失我踩过三小时,换到pack(8)就稳定了。除了对齐,还要确认GET_MIPROCESSLOADERENTRY.h里没有用#pragma region之类的 VC 专用语法,ARM64 交叉编译时这些预处理指令被 MSVC 忽略掉了,影响不大,但你要留意。
4. 避坑:HideDriver 最容易翻车的四个场景
4.1 场景一:断链后驱动卸载蓝屏
现象:驱动在 DriverEntry 里断链成功,但当你用sc stop HideDriver触发驱动卸载时,目标机直接蓝屏,报错内容指向KeBugCheckEx(0xCE)。原因:DriverUnload里如果执行了常规的删除链表操作,或者在卸载时又被系统管理代码引用,系统无法找回已摘除的节点,会再次尝试向链表上登记,双重登记造成溢出。解决:不要在驱动的DriverUnload例程中恢复链表节点,正确的做法是在卸载前重新把节点插回链表,即手动执行“反摘除”——把摘除时的前后节点指针恢复回去。建议在 DriverUnload 里调用一个RestoreModuleListEntry()函数,实现方式与摘除完全相反。
4.2 场景二:符号链接没删导致驱动对象可寻
现象:断链后用driverquery -v看不到驱动,但用WinObj或\Device遍历还能看到HideDriver的设备名,攻击者能直接打开设备并发送 IOCTL。原因:你只摘了模块链表,忘了删除\Driver\HideDriver对象目录里的符号链接。解决:在 DriverEntry 里主动调用IoCreateSymbolicLink之前,先检查是否已有同名链接;更彻底的做法是把驱动的常量名改为与微软驱动相似的系统名,或者干脆不使用符号链接,设备和应用之间的通信通过设备对象名完成,这样常规目录工具默认不显示未命名设备。
4.3 场景三:TP 双击调试规避,直接掉进异常
现象:原文里说“TP 双击调试规避”,但实际运行中发现调试工具一旦通过SeDebugPrivilege枚举 System Process 里的线程,你的驱动虽然不可见,但驱动创建的线程栈里出现HideDriver模块的字符串名称———字符串引用被TP找到。原因:驱动对象本身的字符串属性(例如DriverRegistryPath)还会出现在内存池里,TP 扫描的是内存特征并非链表。解决:不要在驱动里保留任何明文字符串,把HideDriver改成Hd或其他缩写,然后用RtlInitUnicodeString时把它拼在非连续缓冲区里,避免整串暴露。另外一个关键点:TP 这类工具会遍历所有ETHREAD的Process指针来找出内核线程,断链不会影响线程挂靠关系,所以你创建的全部线程也要断链,或者在DriverEntry里加一个延迟初始化的线程,等 TP 打摘要完成后再启动。
4.4 场景四:ARM Debug 配置编译通过但运行卡死
现象:工程在ARM Debug编译生成.sys,部署到 ARM 目标机后,系统启动卡在滚动条。原因:SYSTEM_MODULE_STRUCT.h里的结构体在 ARM 上默认以 4 字节对齐,而 ARM64 内核模块节点是 8 字节对齐,导致CONTAINING_RECORD取出的指针不对,摘除操作把其他节点内存改坏。解决:前面提过的#pragma pack(push, 8)必须放到所有结构定义之前,包括#include的所有头文件。如果你用了GET_MIPROCESSLOADERENTRY.h,该文件也需要对结构体做同样的对齐改动,否则返回值是错的,整体白搭。
5. 验证演进与组合拳:确认藏住了,再把回调隐藏合并进来
断链隐藏验证有硬指标,不能凭感觉。你在目标机上用工具列表看不到自己的驱动,再切换到内核调试器执行dt nt!_LDR_DATA_TABLE_ENTRY查看整个链表,这一步确认前后节点没有被摘除链操作破坏。真正的核心验证手段是用 WinDbg 的!poolfind HideDriver扫描内存池,因为驱动映像的地址一定对齐为 4KB,!poolfind能按 TAG 在内存池里找回你的映像——所以除非你把驱动文件全部抹掉,不然这一步能扫到。把这段作为你的最后一道验证关卡,我通常在 VS 双机调试里用如下脚本一次性完成:
# 在 KD 监视断链后的驱动残留 dt nt!_LDR_DATA_TABLE_ENTRY <PsLoadedModuleList> InLoadOrderLinks !object \Driver\HideDriver !poolfind Hide参数说明:<PsLoadedModuleList>在实际调试时替换成wdf lms或者其他方式获取的链表地址,!poolfind Hide的后缀是你在驱动里自定义的池标签,一般是 4 个字符。如果!object返回“Object not found”但!poolfind还能找到驱动池,说明断链成功但镜像未释放——也就是你需要在驱动的隐藏逻辑结束前把链表恢复回来,否则卸载时蓝屏。
进阶的玩法是组合回调隐藏。断链后,系统的DriverObject派发函数仍然挂在IRP_MJ_*分发表里,被 TP 跟踪事件时你会暴露 CPU 时间。常见做法是把自己的IRP_MJ_DEVICE_CONTROL替换成系统已有驱动(比如\Driver\volmgr)的同序号函数地址,然后在回调里通过比较 IRP 地址来劫持控制流。这种方法的关键参数是DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL]的保存与替换顺序:先保存真地址,再赋值假地址。验证时用dt _DRIVER_OBJECT打印分发表,你会发现任何工具都看不出你的处理器。最后说一个我吃过血泪教训:断链一时爽,还原火葬场——从那以后我每次测试都强制走一遍RestoreModuleListEntry()再提交代码,确保即使蓝屏也能在下一次启动时把链表恢复干净。这个习惯保住了我很多个开发环境的稳定性,希望帮到你。
本文还有配套的精品资源,点击获取