ProcecssHide

本文首发于蚁景网安

进程遍历方法

在实现进程隐藏时,首先需要明确遍历进程的方法。

CreateToolhelp32Snapshot

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");
//创建进程镜像,参数0代表创建所有进程的镜像
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
if (hSnapshot == INVALID_HANDLE_VALUE)
{
std::cout << "Create Error" << std::endl;
exit(-1);
}

/*
* typedef struct tagPROCESSENTRY32 {
* DWORD dwSize;               进程信息结构体大小,首次调用之前必须初始化
* DWORD cntUsage;              引用进程的次数,引用次数为0时,则进程结束
* DWORD th32ProcessID;           进程的ID
* ULONG_PTR th32DefaultHeapID;      进程默认堆的标识符,除工具使用对我们没用
* DWORD th32ModuleID; 进程模块的标识符
* DWORD cntThreads;            进程启动的执行线程数
* DWORD th32ParentProcessID; 父进程ID
* LONG pcPriClassBase;          进程线程的基本优先级
* DWORD dwFlags;              保留
* TCHAR szExeFile[MAX_PATH]; 进程的路径
* } PROCESSENTRY32;
* typedef PROCESSENTRY32 *PPROCESSENTRY32;
*/
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++)
{
//判断进程号是否为0
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");
}
}
}
}

进程隐藏

通过上述分析可以知道遍历进程的方式有三种,分别是利用CreateToolhelp32SnapshotEnumProcesses以及ZwQuerySystemInfomation函数

但是CreateToolhelp32SnapshotEnumProcesses函数底层都是调用了ZwQuerySystemInfomation函数,因此我们只需要钩取该函数即可。

由于测试环境是Win11,因此需要判断在Win11情况下底层是否还是调用了ZwQuerySystemInfomation函数。

可以看到在Win11下还是会调用ZwQuerySystemInfomation函数,在用户态下该函数的名称为NtQuerySystemInformation函数。

image-20240521112821914

这里采用内联钩取的方式对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");
//调用原始的ZwQuerySystemInfomation函数
NTSTATUS status = ((NTQUERYSYSTEMINFORMATION)pfnOld)(SystemInformationClass, SystemInformation, SystemInformationLength, ReturnLength);
...

为了隐藏指定进程,我们需要遍历进程信息,找到目标进程并且删除该进程信息实现隐藏的效果。这里需要知道的是进程信息都存储在SYSTEM_PROCESS_INFORMATION结构体中,该结构体是通过单链表对进程信息进行链接。因此我们通过匹配进程名称找到对应的SYSTEM_PROCESS_INFORMATION结构体,然后进行删除即可,效果如下图。

image-20240521114544753

通过单链表中删除节点的操作,取出目标进程的结构体。代码如下

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命令查看断点

image-20240521163453039

紧着利用 bc [ID]删除断点

image-20240521163650956

在注入之后任务管理器会在拷贝的时候发生异常

image-20240521164244394

在经过一番调试后发现,由于多线程共同执行导致原本需要可写权限的段被修改为只读权限

windbg可以用使用!vprot + address查看指定地址的权限,可以看到由于程序往只读权限的地址进行拷贝处理,所以导致了异常。

image-20240521164709861

但是在执行拷贝阶段是先修改了该地址为可写权限,那么导致该原因的情况就是其他线程执行了权限恢复后切换到该线程中进行写,所以导致了这个问题。

image-20240521165018268

因此内联钩取是存在多线程安全的问题,此时可以使用微软自己构建的钩取库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
...
//用于确保在 DLL 注入或加载时,恢复被 Detours 修改的进程镜像,保持稳定性
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查看函数地址,可以看到函数的未被挂钩前的情况如下图。

image-20240521201600606

挂钩之后原始的指令被修改为一个跳转指令把前八个字节覆盖掉,剩余的3字节用垃圾指令填充。

image-20240521201738346

该地址里面又是一个jmp指令,并且完成间接寻址的跳转。

image-20240521203443934

该地址是自定义函数ZwQuerySystemInformationEx,因此该间接跳转是跳转到的自定义函数内部。

image-20240521203616163

跳转到TrueZwQuerySystemInformation内部发现ZwQuerySystemInformation函数内部的八字节指令被移动到该函数内部。紧接着又完成一个跳转。

image-20240521203949688

该跳转到ZwQuerySystemInformation函数内部紧接着完成ZwQuerySystemInformation函数的调用。

image-20240521204307767

综上所述,整体流程如下图。实际上Detours实际上使用的是热补丁的思路,但是Detours并不是直接在原始的函数空间中进行补丁,而是开辟了一段临时空间,将指令存储在里面。因此在挂钩后不需要进行脱钩处理就可以调用原始函数。因此就不存在多线程中挂钩与脱钩的冲突。

image-20240521205625185

完整代码: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

《逆向工程核心原理》


ProcecssHide
https://h0pe-ay.github.io/ProcecssHide/
作者
hope
发布于
2024年7月15日
许可协议