简介面向Windows平台的C进程隐藏技术资料包聚焦应用层与驱动级隐藏实现适合系统安全研究人员、逆向工程学习者以及有隐蔽执行需求的开发者。内容基于C源码与Visual Studio工程展开覆盖钩子拦截、注册表修改、系统API调用、驱动层交互等常见技术路线并附有调试符号、驱动信息文件、数字证书及使用说明方便直接编译和对照分析。压缩包共33个文件以C/C源代码、工程配置文件、驱动信息文件、调试符号、日志、图片说明等类型为主整体大小588KB体量轻但结构完整包含32位与64位工程及调试输出目录。目前已有1465人浏览学习。通过研读核心源码与配套工程可深入理解进程枚举与反枚举的对抗思路掌握从应用层到驱动层的隐藏实现细节为后续系统安全研究、恶意代码分析与检测对抗打下基础。1. 进程隐藏的最后一公里为什么驱动隐藏仍是硬骨头C进程隐藏在安全研究和EDR对抗里最难的从来不是写代码而是搞明白你藏在哪一层。最近我拿到一个名为 HideProcess完结篇.zip 的 C 源码包里面同时涉及隐藏进程、隐藏驱动而且驱动隐藏进程的部分直接写进了内核回调。这篇笔记拆给你看这个包解决什么问题编译环境怎么搭驱动怎么加载哪个环节最容易蓝屏以及怎么验证隐藏真的生效。适合两类人一类是把进程隐藏当恶意样本分析题的蓝队人员另一类是准备在虚拟机里做底层对抗验证的红队学员。新手跟着步骤走能跑通熟手可以直接跳去第4章看参数边界或者直奔第5章的避坑清单。2. 驱动还是API Hook先想清楚隐藏层级再决定要不要下这个包拿到包先别急着解压编译先问自己隐藏动作放在 Ring3 还是 Ring0这个选择决定后续所有工作。HideProcess完结篇这个包的主线是驱动隐藏进程但你不把用户态那几条路看明白根本理解不了为什么驱动方案值得做。2.1 用户态隐藏的三种常见手法及其失效边界最入门的是在 Ring3 层做手脚。常见做法有三种替换进程枚举 API、伪造进程身份、从进程链表断链。这三种我都在实验环境里跑过结论是在 Windows 10 1903 往后的系统上它们都很难骗过正经的 EDR。第一种API Hook。通过 SetWindowsHookEx 或直接 patch IAT把 NtQuerySystemInformation、CreateToolhelp32Snapshot 的输出过滤掉目标进程。写起来也简单一个 DLL 注入就能看到效果。问题也最明显EDR 自身枚举进程时虽然也走系统调用但它会在回调里校验返回结果和当前线程栈。只要你 hook 的是 NtQuerySystemInformationEDR 检查栈就能发现你的 DLL 做了 inline patch。而且有一部分安全产品会直接通过未公开的内核接口读 ActiveProcessLinks根本不经过 API 层。所以 API Hook 只适合用在任务管理器这种轻量检测面前。第二种伪装成系统进程。改进程名、改 PEB 里的 ProcessParameters、伪造 CommandLine 和父进程 PID。任务管理器里确实会显示成 svchost.exe看起来很安全。但 Process Explorer 一开看路径、签名、会话 ID 和父进程执行时间立刻穿帮。系统进程的规律太明显你伪装成它反而比不伪装更扎眼。更麻烦的是很多系统进程启动时间都早于用户登录伪装的进程只要出现的时间不对分分钟暴露。第三种用户态断链。用 NtSuspendProcess 把进程挂起后从某个用户态进程链表中摘除。这个方法在早期版本还能用一阵现在的内核本身有多个进程视图哪怕你用自己的链表工具把目标摘掉内核的调度器和句柄表还会保留它。只要有一个枚举视角绕开你的链表就暴露了。这三种手法的共同根病隐藏动作发生在用户态内核不知道谁直接查内核都会穿帮。你观察到的现象往往是任务管理器里消失的进程在安全软件或几个诊断命令下又回来了。这不是手法 bug而是层级决定的。为了判断你到底需要哪一层可以按三个视角一起看观察视角API Hook伪装系统进程用户态断链任务管理器隐藏显示但可信隐藏Process Explorer隐藏明显可疑可能隐藏EDR/内核枚举全部可发现全部可发现全部可发现结论很直接如果你只是让任务管理器看不到用户态足够如果你面对的是带内核回调的 EDR必须往下看驱动。2.2 内核态隐藏DKOM、SSDT Hook、对象回调的三条分岔路到了内核态思路要反过来让内核本身不暴露这个进程。这里有三条主流路线HideProcess 这个包走的是对象回调但前两条也要能区分不然你看到网上满屏的 DKOM 代码会产生混淆。DKOM 式隐藏直接修改 EPROCESS 的 ActiveProcessLinks把要隐藏的进程从全局双向链表里摘除。这是最狠也最脆的方案。狠在它让绝大多数用户态枚举器失效脆在 Windows 内核对进程的很多查询是通过 PID 到结构的哈希表或 HandleTable 来做的不依赖链表。你只是把链表摘了进程句柄表还完整EDR 换个 API 照样能发现。再加上不同 build 的 EPROCESS 偏移差异稍有不慎回调没跑就蓝屏。这个方案现在拿来考古合适做对抗很容易翻车。SSDT Hook 是另一个老方案替换 NtQuerySystemInformation 或 NtQueryDirectoryFile 的内核函数入口地址让它把目标进程的条目过滤掉。PatchGuard 上线之后这个方案已经很难站稳。你驱动加载后如果改了 SSDT几轮校验下来直接蓝屏。更别说还有多核环境下每 CPU 都要同步修改少了哪个核心都会漏。对象回调是当前相对常见的方案也是这个包主要采用的思路。Windows 为进程创建、镜像加载和对象句柄操作提供了一组回调接口EDR 自己也是用 ObRegisterCallbacks 和 PsSetCreateProcessNotifyRoutine 来感知新进程的。隐藏的思路不是拆链表而是把自己注册到回调链的头部然后对特定进程的句柄操作直接返回 STATUS_ACCESS_DENIED。这样 EDR 在打开进程对象做查询时拿不到 handle自然枚举失败。这个方案不破坏 EPROCESS 链表稳定性比 DKOM 高也不会触发 PatchGuard 的安全校验。2.3 这个包解决的是哪一段从文件清单反推实现我拆包时的习惯是先把目录结构过一遍从命名推断作者设计。HideProcess完结篇 这个包的产物大概可以分成三个部分用户态控制端、内核驱动工程、编译所需脚本和说明文档。控制端负责接收 PID、加载驱动、下发隐藏生效或还原的命令驱动端则负责注册回调、在 IOCTL 分发里解出 PID并把当前进程从枚举路径里藏掉。如果你解压后看到里面还有一个带 x64 字样的文件夹说明工程按 64 位编译32 位系统基本不考虑。这种结构在现在的 Windows 环境里很正常毕竟内核驱动和 EDR 的对抗都默认跑在 x64 上。把这些先看清楚再往下搭编译环境你就知道改哪里、重编哪里不至于一把梭把整个工程都重编了。3. 编译驱动的环境搭法WDK、VS和签名一个都不能缺驱动编译环境和普通 C 项目完全不同。你写的是 Ring0 代码编译产物是 .sys必须要有 WDK 和对应版本的 VS。这个过程中最常见的问题不是代码 bug而是环境没配对。3.1 VS版本与WDK版本对应关系WDK 版本和 VS 版本强绑定装错版本之后的症状很奇怪工程能打开但编译一堆 SDK 宏找不到或者报Platform Toolset not found。我踩过两次才把对应关系记熟。VS 版本WDK 版本目标平台VS201910.0.19041.0WDK for Windows 10 2004Windows 10 / Server 2016VS202210.0.22621.0WDK for Windows 11 22H2Windows 11 / Server 2022如果你的目标测试机是 Windows 10 20H2用 VS2019 加 10.0.19041 这套最不容易出问题。要是目标机是 Windows 11 22H2就换 VS2022 那套。头文件里很多宏在不同版本有差异比如 OB_FLT_REGISTRATION_VERSION 的数值变化会让老代码直接编译不过。建议做两个独立的虚拟机一个装 Windows 10 配 VS2019 环境一个装 Windows 11 配 VS2022而不是在同一主机上反复切换版本。有些时候你装的 VS 是 2022但 WDK 自带的 SDK 版本不对编译时会报Windows SDK 版本 10.0.19041.0 未找到。解决办法是在 VS Installer 的单个组件里勾选对应的 Windows SDK 包。装完以后打开工程属性在常规里把 Windows SDK 版本改成你当前已有的版本把平台工具集改成 KernelModeDriver1022621 或 KernelModeDriver1019041。很多人卡在这一步其实关键就这两项。3.2 最小可用的INF文件能加载就算成功驱动代码再漂亮没有 INF 文件Windows 不知道它是什么服务。如果你从源码重新建工程要写一个最简 INF。我复现时习惯先删掉设备相关段只留服务安装的核心内容[Version] Signature $WINDOWS NT$ Class System ClassGuid {4d36e97d-e325-11ce-bfc1-08002be10318} Provider %ProviderName% DriverVer 07/20/2024,1.0.0.0 CatalogFile hide_process.cat [DestinationDirs] DefaultDestDir 12 [DefaultInstall] CopyFilesDrv_Dir [DefaultInstall.Services] AddServicehpsvc,%SPSVCINST_ASSOCSERVICE%,svc_install [Drv_Dir] hideprocess.sys,,, [svc_install] DisplayName Hide Process Driver ServiceType 2 ; SERVICE_FILE_SYSTEM_DRIVER StartType 1 ; SERVICE_SYSTEM_START ErrorControl 0 ServiceBinary %12%\hideprocess.sys [Strings] ProviderNameTestLabServiceType 2 表示文件系统驱动类型适合这种内核驱动StartType 1 表示系统启动时加载。如果驱动入口里自己创建了设备对象而不是依赖系统加载器创建设备StartType 改成 3手动更灵活测试时用 sc start 手动启动即可。%12% 代表 System32\drivers 目录。注意 INF 文件里不能出现缩进混乱分号是注释DefaultDestDir 12 表示把驱动文件复制到系统驱动目录。很多新手抄这个 INF 后安装时提示哈希不存在或者未找到签名。这通常不是 INF 语法问题而是签名问题。第 3.3 节讲测试签名第 5 章也有专门条目。3.3 测试签名两行命令但要在调试模式下x64 Windows 默认不会加载未签名的 .sys。如果签名有问题sc start 会直接报 0xc0000428。在测试阶段最简单的方式是开启测试签名模式bcdedit /set testsigning on bcdedit /set nointegritychecks on shutdown /r /t 0我一般只设置 testsigning onnointegritychecks 针对某些旧系统新内核不一定生效。在 UEFI Secure Boot 打开的情况下这两条命令会执行成功但重启后 testsigning 会被自动关闭。排查方法很直接重启后进命令行敲 bcdedit看 testsigning 是不是 Yes。如果不是进 BIOS 关 Secure Boot 再试。如果你不想开启系统测试模式还可以用临时加载方式开机进入高级启动菜单选禁用驱动程序强制签名然后立即加载驱动。但这种方式只对某次开机有效重新开机就失效了。长期开发测试我更建议直接关 Secure Boot 开 testsigning省得每次踩坑。3.4 编译驱动的三个常见坑宏定义、内存模型和初始化除了环境匹配还会遇到几个编译期和链接期的问题。第一个坑是没定义AMD64。64 位驱动代码如果没在预处理器里看到 x64 宏可能会把 ULONG_PTR 当成 4 字节导致结构体尺寸计算错误链接期出现 unresolved external symbol。解决办法是在工程属性里把预处理器定义的AMD64显式加上。64 位驱动必须用 -D_AMD64_ 编译否则系统 API 的原型对不上。第二个坑是回调函数里用了分页内存。WDK 的 PAGED_CODE 宏在非分页上下文里执行时就会触发检查。回调函数会被系统调用在任意 IRQL 下执行所以回调里不能放分页内存访问。实际调试中看到 DRIVER_IRQL_NOT_LESS_OR_EQUAL 这个蓝屏码多半就是这里的问题。可以把回调单独放在非分页池并在函数注释里写明 IRQL 限制。第三个坑是驱动入口 DriverEntry 里用了 UNICODE_STRING 却忘了初始化。每次写 DeviceName 和 SymbolicLinkName 的时候容易漏掉这一步。需要先 RtlInitUnicodeString再 IoCreateDevice。不初始化字符串长度字段IoCreateDevice 拿到的是一堆垃圾数据设备名根本注册不上。这个错误不会蓝屏但会导致后面对 DeviceIoControl 总是打开失败。4. 把用户态控制端改成自己能用PID传参、驱动加载和通信驱动生成之后控制端的可测性决定了你后续调试快不快。HideProcess 这类包往往默认把 PID 写死在源码里不改成动态参数你每换个目标进程都得重新编译。下面这段是控制端比较稳的写法。4.1 从写死PID到命令行动态传参用户态入口通常长这个样子我习惯用 _wtoi 解析宽字符参数并在发起隐藏前先验证目标进程是否存在int wmain(int argc, wchar_t* argv[]) { if (argc 2) { wprintf(L用法: HideProcessCtrl.exe PID\n); return -1; } DWORD targetPid (DWORD)_wtoi(argv[1]); if (targetPid 0) { wprintf(L无效PID\n); return -2; } HANDLE hProc OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, FALSE, targetPid); if (!hProc) { wprintf(L无法打开进程错误码%lu\n, GetLastError()); return -3; } CloseHandle(hProc); wprintf(L目标PID: %lu\n, targetPid); return HideByPid(targetPid); }OpenProcess 在这里有两个作用第一是验证 PID 对应进程确实存在第二是提前发现权限不足因为驱动隐藏一个不存在的 PID 时会静默失败你不会知道是自己传错了还是驱动没响应。PROCESS_QUERY_LIMITED_INFORMATION 权限比 PROCESS_ALL_ACCESS 小得多在监控视角里没那么刺眼但这只是控制端的准备动作真正调节器是驱动里的 IOCTL 命令。还有一个小技巧如果测试用例的 PID 每次随机可以在批处理里用系统命令取 PID再传给控制端。不要在 C 里写死路径否则进程路径一旦变化索引失效。4.2 控制端与驱动通信的骨架CreateFile和DeviceIoControl和驱动建立通信的方式是打开驱动在 Ring0 创建的设备对象。设备名一般写成 \.\HideProcessDevice在 C 里转义后是 L\\.\HideProcessDevice。下面是完整骨架HANDLE hDev CreateFileW( L\\\\.\\HideProcessDevice, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); if (hDev INVALID_HANDLE_VALUE) { wprintf(L打开设备失败: %lu\n, GetLastError()); return FALSE; } ULONG inBuf (ULONG)pid; DWORD bytesRet 0; BOOL ok DeviceIoControl( hDev, IOCTL_HIDE_PROCESS, inBuf, sizeof(inBuf), nullptr, 0, bytesRet, nullptr); CloseHandle(hDev);CreateFileW 的第一个参数有四种转义层级\.\DeviceName 在 C 字符串里必须写成 L\\.\DeviceName。很多人漏掉一个反斜杠导致句柄永远是 INVALID_HANDLE_VALUE错误码是 ERROR_FILE_NOT_FOUND。DeviceIoControl 的第二个参数是控制码控制码由驱动端定义双方一致即可。控制端接收到返回值只是发送成功不代表驱动已经执行了隐藏逻辑。如果驱动在 IOCTL 处理里因为访问违规导致崩溃DeviceIoControl 可能返回成功但驱动可能已经挂了。所以我在通信骨架里会额外加一个 GetLastError 检查并在 IOCTL 返回后主动自查一次而不是只看 DeviceIoControl 的返回值。4.3 加载驱动用服务控制管理器比sc命令更好封装sc create 和 sc start 能处理大多数场景但要写进 C 控制端里还是用 Windows 服务管理 API 更方便。注册服务的代码段第 3 章已经给过这里补上停止和删除服务SC_HANDLE scm OpenSCManagerW(nullptr, nullptr, SC_MANAGER_ALL_ACCESS); SC_HANDLE svc OpenServiceW(scm, LHideProcessDrv, SERVICE_ALL_ACCESS); if (svc) { SERVICE_STATUS ss; ControlService(svc, SERVICE_CONTROL_STOP, ss); DeleteService(svc); CloseHandle(svc); } CloseHandle(scm);这里的关键是用 SERVICE_CONTROL_STOP 让驱动执行 DriverUnload然后 DeleteService 删注册表里的服务记录。频繁测试时你会遇到一个坑上次加载失败后服务对象还在但内核驱动没有真正加载成功。这时候 StartService 会返回 ERROR_SERVICE_ALREADY_RUNNING实际上驱动已经死掉。你必须在重启机器后 sc query 看状态如果状态是 STOPPED再 start。很多包自带的批处理只写了 start不写 stop。我一般会自己补一个 run_test.bat 和 clean_test.bat把加载、启动、运行、卸载四步分开方便反复测试。4.4 驱动端要有的IOCTL分发和回调注册既然控制端的 DeviceIoControl 已经到位驱动端必须有对应的分发函数。下面是去掉头文件依赖的最小骨架NTSTATUS DrvDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { PIO_STACK_LOCATION irpSp IoGetCurrentIrpStackLocation(Irp); ULONG code irpSp-Parameters.DeviceIoControl.IoControlCode; DWORD pid 0; if (code IOCTL_HIDE_PROCESS) { if (irpSp-Parameters.DeviceIoControl.InputBufferLength sizeof(DWORD)) { return STATUS_BUFFER_TOO_SMALL; } pid *(DWORD*)Irp-AssociatedIrp.SystemBuffer; // 在这里把 pid 保存到全局数组或直接传给回调 } Irp-IoStatus.Status STATUS_SUCCESS; Irp-IoStatus.Information 0; IoCompleteRequest(Irp, IO_NO_INCREMENT); return STATUS_SUCCESS; }这里容易被忽略的是SystemBuffer 在 METHOD_BUFFERED 模式下才有效。如果驱动创建设备时没有设置 DO_BUFFERED_IOSystemBuffer 可能是空指针。建议在 DriverEntry 里设置 DeviceObject-Flags | DO_BUFFERED_IO。输入缓冲区长度检查也不能省否则用户态传一个短 buffer读越界直接触发蓝屏。驱动回调注册一般放在 DriverEntry 里OB_CALLBACK_REGISTRATION obReg {0}; obReg.Version OB_FLT_REGISTRATION_VERSION; obReg.OperationCount 1; obReg.Altitude 0x8001; obReg.RegistrationContext (PVOID)targetPid; obReg.OperationRegistration[0].ObjectType PsProcessType; obReg.OperationRegistration[0].Operations OB_OPERATION_HANDLE_CREATE | OB_OPERATION_HANDLE_DUPLICATE; obReg.OperationRegistration[0].PreOperation HideProcessPreCallback; NTSTATUS status ObRegisterCallbacks(obReg, obHandle);Altitude 是回调注册时的海拔高度数值越高优先级越靠前。EDR 的回调海拔通常在四位数以上这个包要抢在前面就得设一个足够高的值。这里 0x8001 已经是较高位但实际产品会用字符串形式传入类型是 UNICODE_STRING很多新手把数字当整数传编译通过但运行报错。Operations 字段必须包含句柄创建和复制两种操作否则只拦了一部分路径。5. 实测避坑蓝屏、无效PID、回调失效的排查清单这章是我从多次复现这个包中总结的高频问题。按现象、原因、解决写你可以直接当排障手册用。5.1 蓝屏ObRegisterCallbacks 回调里不能做的事现象驱动加载完后几秒钟系统蓝屏错误码集中在 SYSTEM_SERVICE_EXCEPTION 或 KMODE_EXCEPTION_NOT_HANDLED。原因回调的 PreOperation 函数在系统分发上下文里被调用这个上下文 IRQL 不低并且不允许做引发分页的操作。很多人为了拿到 PID 直接访问 EPROCESS 偏移或者调用 IoGetDeviceObjectPointer 这种可能触碰磁盘的函数直接触发蓝屏。另一个常见错误是在回调里调用 ObDereferenceObject导致引用计数错误。解决回调里只做三件事拿 PsGetCurrentProcessId 比较 PID、拿 ObGetObjectType 判断类型、返回 OB_PREOP_SUCCESS。所有复杂逻辑都移到工作线程或延迟调用里。如果必须从回调查 EPROCESS 偏移请在 DriverEntry 时用 RtlGetVersion 判断系统版本再把编译期写死的偏移做一次验证不要每次进回调都动态算。5.2 隐藏后进程仍被EDR发现藏的是列表不是句柄现象隐藏成功后任务管理器看不到目标进程但 EDR 控制台还是能看到。原因回调拦的是句柄创建和复制可是 EDR 如果在进程启动阶段已经拿到了 Process 对象句柄后续它通过这个句柄查询状态你就拦不到了。另一个原因是很多 EDR 用 NtQueryInformationProcess 加 ProcessHandleInformation 遍历全系统句柄表这个路径不一定过 ObRegisterCallbacks因为它读的是句柄表快照不需要重新打开目标进程。解决把隐藏动作提前到进程创建回调里做在进程出生时直接由回调在句柄创建前拒绝。用户态控制端传 PID 只能拦已经有句柄的进程所以测试驱动时最好让目标进程由驱动加载的进程创建。如果做对抗验证需要额外写一个句柄清理函数从系统句柄表删除目标 Process 句柄这在 Ring0 用 ZwQuerySystemInformation(SystemHandleInformation) 配合 ObDuplicateObject 实现比单纯回调复杂得多。5.3 加载失败0xc0000428 和 0x80070005现象sc start HideProcessDrv 返回普通权限不够或者直接报 0xc0000428。原因0xc0000428 是数字签名验证失败常见于没开 testsigning 却加载测试签名驱动。0x80070005 是访问拒绝多半是 sys 文件拷在用户目录而不是在 System32\drivers 下系统不信任这个路径也可能被杀软锁住了文件。解决确认驱动文件放在 C:\Windows\System32\drivers\hideprocess.sys在管理员命令行下运行 bcdedit /set testsigning on重点启动再确认当前系统处于测试签名模式。如果还失败检查是否开了 Secure Boot。测试驱动一定要把 Secure Boot 关掉不然 bcdedit 的命令执行成功也会被 UEFI 回退。如果你用的是自签名证书还要把证书导入到受信任的根证书颁发机构和受信任的发布者两个存储区缺一个都会报签名校验失败。5.4 控制端打开设备失败ERROR_FILE_NOT_FOUND现象DeviceIoControl 之前CreateFile 返回 INVALID_HANDLE_VALUEGetLastError 等于 2 或 3。原因设备名没对上或者驱动没成功创建设备对象。控制端写的是 \.\HideProcessDevice而驱动端注册的设备名少了前缀或者设备创建成功但符号链接没创建用户态无法得到设备路径。解决打开调试器在驱动里加断点看 DriverEntry 走到哪一步。或者在驱动入口里用 IoCreateSymbolicLink 建立符号链接用户态才能用 \.\HideProcessDevice 访问。注意 IoCreateSymbolicLink 的 DosDevices 名称一般要写成 L\DosDevices\HideProcessDevice控制端才能解析成 \.\HideProcessDevice。很多驱动代码只调 IoCreateDevice 不调 IoCreateSymbolicLink这是打开失败最典型的原因。5.5 隐藏后无法还原PID不在枚举范围现象用还原命令让进程重新出现有时候能成功有时候不能。原因驱动里保存目标 PID 是一个数组但还原时只把数组中对应的隐藏标记清空没有恢复之前被拒绝的句柄。进程枚举路径被恢复后以前被拦的句柄已经丢掉需要重新从系统服务端创建才能被外部看到。如果还原命令是发到错误的控制码也会出现这种静默失败。解决在驱动里用链表保存 {PID, 启用隐藏} 结构体还原逻辑遍历链表把所有匹配项移除并重新触发一次进程枚举。如果还原命令发错控制码先在调试器里设置 IOCTL 入口断点看实际收到的控制码是什么再回控制端比对。如果你希望隐藏后进程还能被指定进程看到就要在回调里做更细的对象类型和句柄来源判断而不是一刀切。6. 进阶验证用Process Explorer和自有枚举程序确认隐藏是否生效驱动跑起来控制端也返回成功是不是真的藏住了别急验证驱动层隐藏是否生效角度比你想的多。第一个办法是开 Process Explorer按 PID 排序确认目标进程不在列表里。我一般还会勾选 View 菜单里的 Show Process Tree如果进程还在树里但列表里没有说明是断链方案HideProcess 这类回调方案理论上进程树里也不会有。第二个办法是写一个 Ring3 枚举程序用不同 API 对比结果。用 CreateToolhelp32Snapshot 和 NtQuerySystemInformation 分别枚举一次如果两个结果都不含目标进程说明隐藏点在更底层如果一个有目标而另一个没有说明隐藏只覆盖了其中一条路径。驱动隐藏的目标应该是两条路径同时失效。第三个办法是直接用 System Informer 看句柄表目标进程的 Process 句柄如果打开时返回 STATUS_ACCESS_DENIED说明回调确实在句柄源端拦截了。这一步能区分枚举器没查它和底层确实打开不了。最后一个小技巧在驱动回调里加一个计数器变量每次有进程被拦截就加 1然后透过用户态一个查询 IOCTL 把它读出来。这样你能确切知道隐藏模块在第几次回调里拦到了目标而不是靠猜。隐藏的进程是否真的消失最终要看的是所有路径一致而不是只看任务管理器。从那以后我每次做这类驱动隐藏测试都会强制走一遍 Process Explorer、双向枚举和句柄表三条路。虽然多花十分钟但能区分真的藏住还是只藏住了任务管理器。希望这篇拆解帮到你下载、编译、改 PID、测回调按这个顺序走能少踩一半的坑。本文还有配套的精品资源点击获取