本文首发于蚁景网安
进程遍历方法 在实现进程隐藏时,首先需要明确遍历进程的方法。
CreateToolhelp32Snapshot
函数用于创建进程的镜像,当第二个参数为0
时则是创建所有进程的镜像,那么就可以达到遍历所有进程的效果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> #include <Windows.h> #include <TlHelp32.h> int main () { setlocale (LC_ALL, "zh_CN.UTF-8" ); HANDLE hSnapshot = CreateToolhelp32Snapshot (TH32CS_SNAPPROCESS, 0 ); if (hSnapshot == INVALID_HANDLE_VALUE) { std::cout << "Create Error" << std::endl; exit (-1 ); } PROCESSENTRY32 pi; pi.dwSize = sizeof (PROCESSENTRY32); BOOL bRet = Process32First (hSnapshot, &pi); while (bRet) { wprintf (L"进程路径:%s\t进程号:%d\n" , pi.szExeFile, pi.th32ProcessID); bRet = Process32Next (hSnapshot, &pi); } }
EnumProcesses EnumProcesses
用于将所有进程号的收集。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> #include <Windows.h> #include <Psapi.h> int main () { setlocale (LC_ALL, "zh_CN.UTF-8" ); DWORD processes[1024 ], dwResult, size; unsigned int i; if (!EnumProcesses (processes, sizeof (processes), &dwResult)) { std::cout << "Enum Error" << std::endl; } size = dwResult / sizeof (DWORD); for (i = 0 ; i < size; i++) { if (processes[i] != 0 ) { TCHAR szProcessName[MAX_PATH] = { 0 }; HANDLE hProcess = OpenProcess (PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, FALSE, processes[i]); if (hProcess != NULL ) { HMODULE hMod; DWORD dwNeeded; if (EnumProcessModules (hProcess, &hMod, sizeof (hMod), &dwNeeded)) { GetModuleBaseName (hProcess, hMod, szProcessName, sizeof (szProcessName) / sizeof (TCHAR)); } wprintf (L"进程路径:%s\t进程号:%d\n" , szProcessName, processes[i]); } } } }
ZwQuerySystemInfomation ZwQuerySystemInfomation
函数是CreateToolhelp32Snapshot
函数与EnumProcesses
函数底层调用的函数,也用于遍历进程信息。代码参考https://cloud.tencent.com/developer/article/1454933
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 #include <iostream> #include <Windows.h> #include <ntstatus.h> #include <winternl.h> #pragma comment(lib, "ntdll.lib" ) typedef NTSTATUS (WINAPI* NTQUERYSYSTEMINFORMATION) ( IN SYSTEM_INFORMATION_CLASS SystemInformationClass, IN OUT PVOID SystemInformation, IN ULONG SystemInformationLength, OUT PULONG ReturnLength ) ;int main () { setlocale (LC_ALL, "zh_CN.UTF-8" ); HINSTANCE ntdll_dll = GetModuleHandle (L"ntdll.dll" ); if (ntdll_dll == NULL ) { std::cout << "Get Module Error" << std::endl; exit (-1 ); } NTQUERYSYSTEMINFORMATION ZwQuerySystemInformation = NULL ; ZwQuerySystemInformation = (NTQUERYSYSTEMINFORMATION)GetProcAddress (ntdll_dll, "ZwQuerySystemInformation" ); if (ZwQuerySystemInformation != NULL ) { SYSTEM_BASIC_INFORMATION sbi = { 0 }; NTSTATUS status = ZwQuerySystemInformation (SystemBasicInformation, (PVOID)&sbi, sizeof (sbi), NULL ); if (status == STATUS_SUCCESS) { wprintf (L"处理器个数:%d\r\n" , sbi.NumberOfProcessors); } else { wprintf (L"ZwQuerySystemInfomation Error\n" ); } DWORD dwNeedSize = 0 ; BYTE* pBuffer = NULL ; wprintf (L"\t----所有进程信息----\t\n" ); PSYSTEM_PROCESS_INFORMATION psp = NULL ; status = ZwQuerySystemInformation (SystemProcessInformation, NULL , 0 , &dwNeedSize); if (status == STATUS_INFO_LENGTH_MISMATCH) { pBuffer = new BYTE[dwNeedSize]; status = ZwQuerySystemInformation (SystemProcessInformation, (PVOID)pBuffer, dwNeedSize, NULL ); if (status == STATUS_SUCCESS) { psp = (PSYSTEM_PROCESS_INFORMATION)pBuffer; wprintf (L"\tPID\t线程数\t工作集大小\t进程名\n" ); do { wprintf (L"\t%d" , psp->UniqueProcessId); wprintf (L"\t%d" , psp->NumberOfThreads); wprintf (L"\t%d" , psp->WorkingSetSize / 1024 ); wprintf (L"\t%s\n" , psp->ImageName.Buffer); psp = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)psp + psp->NextEntryOffset); } while (psp->NextEntryOffset != 0 ); delete []pBuffer; pBuffer = NULL ; } else if (status == STATUS_UNSUCCESSFUL) { wprintf (L"\n STATUS_UNSUCCESSFUL" ); } else if (status == STATUS_NOT_IMPLEMENTED) { wprintf (L"\n STATUS_NOT_IMPLEMENTED" ); } else if (status == STATUS_INVALID_INFO_CLASS) { wprintf (L"\n STATUS_INVALID_INFO_CLASS" ); } else if (status == STATUS_INFO_LENGTH_MISMATCH) { wprintf (L"\n STATUS_INFO_LENGTH_MISMATCH" ); } } } }
进程隐藏 通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32Snapshot
、EnumProcesses
以及ZwQuerySystemInfomation
函数
但是CreateToolhelp32Snapshot
与EnumProcesses
函数底层都是调用了ZwQuerySystemInfomation
函数,因此我们只需要钩取该函数即可。
由于测试环境是Win11
,因此需要判断在Win11
情况下底层是否还是调用了ZwQuerySystemInfomation
函数。
可以看到在Win11
下还是会调用ZwQuerySystemInfomation
函数,在用户态下该函数的名称为NtQuerySystemInformation
函数。
这里采用内联钩取的方式对ZwQuerySystemInfomation
进行钩取处理,具体怎么钩取在浅谈内联钩取原理与实现 已经介绍过了,这里就不详细说明了。这里对自定义的ZwQuerySystemInfomation
函数进行说明。
首先第一步需要进行脱钩处理,因为后续需要用到初始的ZwQuerySystemInfomation
函数,紧接着获取待钩取函数的地址即可。
1 2 3 4 5 6 7 8 9 ... UnHook ("ntdll.dll" , "ZwQuerySystemInformation" , g_pOrgBytes); HMODULE hModule = GetModuleHandleA ("ntdll.dll" ); PROC pfnOld = GetProcAddress (hModule, "ZwQuerySystemInformation" ); NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength); ...
为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION
结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION
结构体,然后进行删除即可,效果如下图。
通过单链表中删除节点的操作,取出目标进程的结构体。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ... pCur = (PSYSTEM_PROCESS_INFORMATION)(SystemInformation); while (true ) { if (!lstrcmpi (pCur->ImageName.Buffer, L"test.exe" )) { if (pCur->NextEntryOffset == 0 ) pPrev->NextEntryOffset = 0 ; else pPrev->NextEntryOffset += pCur->NextEntryOffset; } else pPrev = pCur; if (pCur->NextEntryOffset == 0 ) break ; pCur = (PSYSTEM_PROCESS_INFORMATION)((PBYTE)pCur + pCur->NextEntryOffset); } ...
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/inlineHook.c
但是采用内联钩取的方法去钩取任务管理器就会出现一个问题,这里将断点取消,利用内联钩取的方式去隐藏进程。
首先利用bl
命令查看断点
紧着利用 bc [ID]
删除断点
在注入之后任务管理器会在拷贝的时候发生异常
在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限
在windbg
可以用使用!vprot + address
查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。
但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。
因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库Detours
,可以在钩取过程中确保线程安全。
Detours 项目地址:https://github.com/microsoft/Detours
环境配置 参考:https://www.cnblogs.com/linxmouse/p/14168712.html
使用vcpkg
下载
1 2 3 vcpkg.exe install detours:x86 -windows vcpkg.exe install detours:x64 -windows vcpkg.exe integrate install
实例 挂钩
利用Detours
挂钩非常简单,只需要根据下列顺序,并且将自定义函数的地址与被挂钩的地址即可完成挂钩处理。
1 2 3 4 5 6 7 8 9 10 11 12 ... DetourRestoreAfterWith (); DetourTransactionBegin (); DetourUpdateThread (GetCurrentThread ()); DetourAttach (&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx); error = DetourTransactionCommit (); ...
脱钩
然后根据顺序完成脱钩即可。
1 2 3 4 5 6 7 8 9 10 ... DetourTransactionBegin (); DetourUpdateThread (GetCurrentThread ()); DetourDetach (&(PVOID&)TrueZwQuerySystemInformation, ZwQuerySystemInformationEx); error = DetourTransactionCommit (); ...
挂钩的原理 从上述可以看到,Detours
是通过事务确保了在DLL
加载与卸载时后的原子性,但是如何确保多线程安全呢?后续通过调试去发现。
可以利用x ntdl!ZwQuerySystemInformation
查看函数地址,可以看到函数的未被挂钩前的情况如下图。
挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。
该地址里面又是一个jmp
指令,并且完成间接寻址的跳转。
该地址是自定义函数ZwQuerySystemInformationEx
,因此该间接跳转是跳转到的自定义函数内部。
跳转到TrueZwQuerySystemInformation
内部发现ZwQuerySystemInformation
函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。
该跳转到ZwQuerySystemInformation
函数内部紧接着完成ZwQuerySystemInformation
函数的调用。
综上所述,整体流程如下图。实际上Detours
实际上使用的是热补丁的思路,但是Detours
并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。
完整代码:https://github.com/h0pe-ay/HookTechnology/blob/main/ProcessHidden/detoursHook.c
参考链接 https://learn.microsoft.com/zh-cn/windows/win32/psapi/enumerating-all-processes
https://cloud.tencent.com/developer/article/1454933
https://www.cnblogs.com/linxmouse/p/14168712.html
《逆向工程核心原理》