逆向工程核心原理-33

逆向工程核心原理-第33章-隐藏进程

回顾

中间有一周没有看这本书了前面的知识有些忘了, 所以在这里先把前面的东西缕一缕(copy)

调试器钩取API

  • 使用DebugActiveProcess()建立起调试器被调试者的关系
  • 通过DLL共享虚拟内存地址相同的特性获取到目标API的起始地址
  • 修改该API的起始第一个字节为0xCC, 即INT3断点指令机器码
  • 我们在调用该API时, 就会遇上这个断点指令从而将控制权交给了调试器
  • 在调试器中通过ReadProcessMemory()和WriteProcessMemory()实现调试器和被调试者之间的数据交互, 从而达到修改数据的操作
  • 然后脱钩, 即将0xCC改回原来的内容(通过全局变量来实现), 并将EIP减一回到断点前, 如果不脱勾的话, 当控制流从调试器转回被调试者时又会遇到INT3指令再次回到调试器钩取, 陷入死循环.
  • 再根据需要决定是否要继续挂钩

通过DLL修改IAT钩取API

还是要重复: DLL注入之所以能过如此灵活的使用于多个场景, 重要的不是在DllMain中做了什么操作, 而是在于它可以让我们可以轻易进入一个目标的虚拟空间, 并对其进行读写.

  • 首先时Inject程序将我们的DLL注入到目标进程中
  • DllMain获取了我们目标API的地址(便于后面的IAT替换和恢复), 同时也轻易得到了我们的钩取函数MyFun()的地址
  • 随后进入的IAT修改函数通过进程句柄的转换得到了进程的基址, 然后通过对PE头的结构体信息利用找到了IDT(导入表, 描述的是导入的全部DLL), 然后通过DLL的名称遍历筛选找到了目标API所在的DLL
  • 同样通过结构体IID的成员信息找到了对应的IAT, 通过函数名称遍历筛选找到了我们的目标API在IAT表中的对应的ITD结构体, 并修改其内容: 将其中描述函数起始地址的成员修改为我们的钩取函数MyFun()的地址
  • 当我们决定脱钩的时候仍然调用的是IAT修改函数, 经过同样的过程将原来的API地址恢复到IAT中

33.1 技术图表

Untitled

我们所用到的技术仍然是DLL注入, 其作用在上面的回顾和上一章(好像)都提到过

33.2 API代码修改技术的原理

跟上一章的修改IAT技术实现钩取有相似之处, 所以理解起来会相对的简单, 但是也要注意区分二者的区别

  • IAT通过修改IAT值来实现API钩取
  • API代码修改技术则是通过将API前5个字符修改为JMP XXXXX来钩取API(这又有点像是调试钩取API技术了, 只不过调试是只修改一个字节为0xCC), 从而将控制转移给钩取函数.

33.2.1 钩取之前

正常调用API

Untitled

钩取后

Untitled

这里看起来可能有点绕, 我们分步分析

  • 00422CF7调用了48C69C地址上的内容作为调用地址, 48C69C的内容是7C93D92E, 也就是API
  • 控制流来到了7C93D92E, 而API的开头内容就是JMP 10001120, 所以我们不会执行API的内容, 而是跳至目标地址
  • 跳转至10001120 这个地址就是我们的Hooking函数, 在这里我们先使用脱钩函数(这里的目的跟前面调试钩取的脱钩思路是一样的)
  • 在Hooking函数中我们先调用unhook()脱钩, 因为后面还要调用到目标函数, 如果不脱勾, API开始的内容又是JMP变成了类似无穷递归的操作(这跟上面调试脱钩是一样的道理)
  • 然后在对数据进行了一系列我们想要的操作以后, 我们调用原来的API实现其功能
  • 随后调用hook()重新挂钩
  • 最后我们使用show参数取消挂钩, 然后在DllMain中使用另一个标签直接脱钩.

33.3 进程隐藏

进程隐藏最常用的ntdll.ZwQuerySystemInformation()API钩取技术

33.3.1 进程隐藏工作原理

想要隐藏进程首先想到的方法是让目标进程隐身, 但就像是我们现实生活中一样, 隐身是不可能实现的. 所以我们用相反的方法实现隐藏进程, 那就是让其他的进程都变成瞎子, 所有的进程都看不见我们的目标进程, 那它也就相当于隐身了.

33.3.2 相关API

进程间要检测其存在, 需要调用相关API, 这种API相当于其他进程的”眼睛”

CreateToolhelp32Snapshot() & EnumProcess()

1
2
3
4
5
6
7
8
9
10
11
12
//作用是得到系统进程快照的句柄
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags, //指定获取系统进程快照的类型
DWORD th32ProcessID //指定要获取进程快照的ID, 获取系统内所有进程快照时是0
);

//作用是检索系统中每个进程对象的进程标识符
BOOL EnumProcesses(
DWORD* pProcessIds, //指向接收进程标识符列表的数组的指针
DWORD cb, //pProcesslds数组的大小, 以字节为单位
DWORD* pBytesReturned //pProcesslds数组中返回的字节数
)

上面的两个APi均在内部调用了ntdll.ZwQuerySystemInformation()API

ntdll.ZwQuerySystemInformation()

1
2
3
4
5
6
7
//检索指定的系统信息
NTSTATUS ZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass, //要检索的系统信息的类型
PVOID SystemInformation, //指向接收请求信息的缓冲区的指针
ULONG SystemInformationLength, //描述上面参数所指向的缓冲区的大小, 以字节为单位
PULONG ReturnLength //指向函数写入请求信息的实际大小的位置的可选指针
)

ZwQuerySystemInformation()所获取的是进程信息的链表, 所以我们要做的就是从除目标函数意外的所有函数中钩取ZwQuerySystemInformation(), 在获取的链表中删去关于目标函数的节点. 这样我们刺瞎了其他进程的眼睛(当然, 有点不恰当, 因为其他进程只是看不见目标进程, 其他的进程仍然可以看到)

33.3.3 隐藏技术的问题

问题一: 要钩取的进程个数

能看到我们目标进程不只有检索进程的那些查找工具, 任何函数都可以调用ZwQuerySystemInformation()来实现查看进程

问题二: 新创建的进程

如果用户再运行一个任务管理器, 如果没有被钩取的话, 我们隐藏进程的目的仍然没有达到.

解决方法: 全局钩取

为了解决上面的问题, 我们隐藏test.exe进程时需要钩取系统中运行的所有进程的ZwQuerySystemInformation()API, 并且对后面将要启动运行的所有进程进行相同的操作. 这就是全局钩取的概念.

33.4 练习 #1 (HideProc.exe, stealth.dll)

  • HideProc.exe负责将stealth.dll文件注入到所有运行中的进程.
  • stealth.dll负责钩取进程的ntdll.ZwQuerySystemInformation()

33.4.1 运行notepad.exe, procexp.exe, taskmgr.exe

首先将HideProc.exe跟stealth.dll放入同一个文件夹

Untitled

随后打开notepad.exe文件

Untitled

使用procxp查看进程

Untitled

此时procxp是可以检测到notepad的

然后我们开始钩取API

Untitled

再次使用procxp查看进程

Untitled

此时procxp不能检测到notepad

我们查看有哪些进程是被注入了stealth.dll的

Untitled

可以看到我们用来检索notepad的procxp被注入了stealth.dll, 所以才没有检索到notepad进程

接下来我们停止隐藏进程

Untitled

再次使用procxo查看进程

Untitled

此时notepad再一次出现了

33.5 源代码分析

33.5.1 HideProc.cpp

HideProc.exe负责向运行中的所有进程注入/卸载指定DLL文件, 它再原有InjectDll.exe的基础上添加了向所有进程注入DLL的功能, 可以认为是InjectDll.exe程序的加强版

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
#define _CRT_SECURE_NO_WARNINGS
#include <Windows.h>
#include <tlhelp32.h>

BOOL InjectAllProcess(int nMode, LPCTSTR szDllPath)
{
DWORD dwPID = 0;//进程PID
HANDLE hSnapShot = INVALID_HANDLE_VALUE;//初始化系统快照句柄
PROCESSENTRY32 pe;
/*
* 作用是描述快照中的进程列表中的条目结构体
typedef struct tagPROCESSENTRY32 {
DWORD dwSize; //该结构的大小
DWORD cntUsage; //弃用, 始终为0
DWORD th32ProcessID; //该进程的PID
ULONG_PTR th32DefaultHeapID; //弃用, 始终为0
DWORD th32ModuleID; //弃用, 始终为0
DWORD cntThreads; //进程启动的执行线程数
DWORD th32ParentProcessID; //父进程PID, 即PPID
LONG pcPriClassBase; //此进程创建的任何线程的基本优先级
DWORD dwFlags; //弃用, 始终为0
CHAR szExeFile[MAX_PATH]; //进程的可执行文件的名称
} PROCESSENTRY32;
*/
//获取系统快照
pe.dwSize = sizeof(PROCESSENTRY32);//获取描述进程的结构体的内存大小, 并给dwSize成员, 防止报错
hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPALL, NULL);//获取系统快照

//查找进程
Process32First(hSnapShot, &pe);//首先获取第一个进程信息结构体
do
{
dwPID = pe.th32ProcessID;//从进程信息结构体中获取对应进程的PID

//鉴于系统安全性的考虑, 对于PID小于100的系统进程不执行DLL注入操作
if (dwPID < 100)
continue;

if (nMode == INJECTION_MODE)//根据这个传入的参数来决定是注入还是卸载
InjectDll(dwPID, szDllPath);//注入DLL
else
EjectDll(dwPID, szDllPath);//卸载DLL
} while (Process32Next(hSnapShot, &pe));//获取下一个进程信息结构体

CloseHandle(hSnapShot);//关闭系统快照句柄

return TRUE;//返回值为真
}

这个函数是对全部(除了特殊的进程)进程注入或者是卸载stealth.dll

我们来到下一个源码分析

33.5.2 stealth.cpp

实际的API钩取操作由Stealth.dll文件负责

SetProcName()导出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// global variable (in sharing memory)
#pragma comment (linker, "/SECTION:.SHARE,RWS")//当第一个参数为linker表示链接器选项,
///SECTION表示更改节的属性, RWS表示Read可读, W表示Write可写, S表示Shared共享
#pragma data_seg(".SHARE")
TCHAR g_szProcName[MAX_PATH] = { 0 };//建立g_szProcName缓冲区
#pragma data_seg()

//export function 导出函数
#ifdef __cplusplus//__cplusplus表示这是第一段C++代码, 这样的意义是: 如果这是一段cpp代码, 就加入extern"C"{和}处理其中的代码
extern "C" {//这个的作用是告诉链接器, 这是一个用C语言写的代码, 要用C语言的方式去链接它, 而前提条件是上面的"如果这是一段C++代码"
#endif
__declspec(dllexport) void SetProcName(LPCTSTR szProcName)//导出函数, 作用是将隐藏的进程名称保存再g_szProcName数组中
{ //这个函数在HideProc()中被调用执行
_tcscpy_s(g_szProcName, szProcName);
}
#ifdef __cplusplus
}
#endif

下面我们看DllMain函数

DllMain

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
BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{

char szCurProc[MAX_PATH] = { 0, };
char * p = NULL;

//异常处理: 若当前进程为HideProc.exe, 则终止, 不进行钩取操作
GetModuleFileNameA(NULL, szCurProc, MAX_PATH);//查找当前进程的文件名
p = strrchr(szCurProc, '\\');//获取路径中文件名的前一个地址
if ((p != NULL) && !_stricmp(p + 1, "HideProc.exe"))//用前一个地址来找到文件名并跟HideProc作比较
return TRUE;//如果是HideProc就返回为真直接退出, 不对HideProc进程钩取API
switch (ul_reason_for_call)
{
//API钩取
case DLL_PROCESS_ATTACH:
hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
(PROC)NewZwQuerySystemInformation, g_pOrgBytes);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
//API脱钩
case DLL_PROCESS_DETACH:
unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes);

break;
}
return TRUE;
}

可以看到在进入了DllMain后还会通过进程文件名来对进程进行一次筛选, 如果是HideProc.exe注入程序本身就会跳过注入.

ATTCH附加则调用hook_by_code实现API钩取, DETACH脱离则调用unhook_by_code()实现API脱钩

下面是对两个函数进行解析

hook_by_code()

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
//           目标API所在DLL的名称指针 目标API的名称指针   钩取函数的地址  存储原有代码的内存的缓冲区指针
BOOL hook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew, PBYTE pOrgBytes)
{
FARPROC pfnOrg;//用于存储目标API的地址
DWORD dwOldProtect, dwAddress;
byte pBuf[5] = { 0xE9, 0, };
PBYTE pByte;

//获取要钩取的API地址
pfnOrg = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName),//通过名称调用两个Getxxx函数获取API的地址
szFuncName);
pByte = (PBYTE)pfnOrg;//转换类型: 从一个WINAPI类型的指针转换成了字节指针, 方便后面对内存进行操作

//若已经被钩取, 则返回FALSE
if (pByte[0] = 0xE9)//E9就是JMP的字节码, 正常的API哪会上来就跳转, 所以通过首地址的内容判断是否已被钩取
return FALSE;

//为了修改5个字节, 先向内存添加"写"属性
VirtualProtect((LPVOID)pfnOrg, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);//改变内存保护属性(权限), 这里是将其设为可读可写, 同时将旧保护属性存储在dwOldProtect结构体中

//备份原有代码(5字节)
memcpy(pOrgBytes, pfnOrg, 5);

//计算JMP地址(E9 XXXXXXXX)
dwAddress = (DWORD)pfnNew - (DWORD)pfnOrg - 5;//这里的跳转字节码用的是相对地址, 其值为 = 函数地址跟下条指令之间的距离, 所以还需要减5表示去掉指令本身的长度
memcpy(&pBuf[1], &dwAddress, 4);//添加上这四个字节的内容pBuf就是一条完整的跳转指令了

//钩子: 修改5个字节
memcpy(pfnOrg, pBuf, 5);//将我们的jmp字节码修改到API的开头

//恢复内存属性
VirtualProtect((LPVOID)pfnOrg, 5, dwOldProtect, &dwOldProtect);//参三一个选项, 只是接下来的造作方式

return TRUE;
}

大致思路

  • 获取目标API首地址
  • 查看是否已经注入
  • 修改内存属性
  • 计算JMP地址
  • 挂钩(将JMP修改到API首地址)
  • 恢复内存属性

unhook_by_code()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BOOL unhook_by_code(LPCSTR szDllName, LPCSTR szFuncName, PBYTE pOrgBytes)//比钩取函数少了一个钩取函数地址
{
FARPROC pFunc;
DWORD dwOldProtect;
PBYTE pByte;

//获取API地址
pFunc = GetProcAddress(GetModuleHandleA(szDllName), szFuncName);//通过参一参二两个字符串指针得到了API首地址
pByte = (PBYTE)pFunc;//转换类型: 从一个WINAPI类型的指针转换成了字节指针, 方便后面对内存进行操作

if (pByte[0] != 0xE9)//如果开头不是JMP指令, 说明还没有挂钩, 所以不用脱钩直接退出
return FALSE;

//向内存添加"写"属性, 为恢复代码做准备
VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

//脱钩
memcpy(pFunc, pOrgBytes, 5);

//恢复内存属性
VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

return TRUE;
}

unhook_by_code()跟hook_by_code几乎是一模一样

  • 获取目标API首地址
  • 查看是否有注入
  • 修改内存属性
  • 脱钩
  • 恢复内存属性

NewZwQuerySystemInformation()

最后, 分析钩取函数NewZwQuerySystemInformation(), 在这之前再次看看ntdll.ZwQuerySystemInformation()API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
NTSTATUS WINAPI ZwQuerySystemInformation(
__in SYSTEM_INFORMATION_CLASS SystemInformationClass,//要检索的系统信息的类型
__inout PVOID SystemInformation,//指向接收请求信息的缓冲区的指针
__in ULONG SystemInformationLength,//描述上面参数所指向的缓冲区的大小, 以字节为单位
__out_opt PULONG ReturnLength//指向函数写入请求信息的实际大小的位置的可选指针
);

//系统进程信息结构体
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;//当前节点的地址加上NextEntryOffset就是下一个节点的起始地址
ULONG NumberOfThreads;//描述该进程中的线程数
byte Reserved1[48];//保留
PVOID Reserved2[3];//保留, 类型为指针, 根据下面的源代码1号元素存有进程名字符串指针
HANDLE UniqueProcessId;//PID
PVOID Reserved3;//保留
ULONG HandleCount;//描述进程正在使用的句柄总是
byte Reserved4[4];//保留
Pvoid Reserved5[11];//保留
SIZE_T PeakPagefileUsage;//描述进程正在使用页面文件存储的字节数
SIZE_T PrivatePageCount;//描述分配给进程使用的内存页数
LARGE_INTEGER Reserved6[6];//保留
} SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

下面是我们的NewZwQuerySystemInformation()

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
NTSTATUS WINAPI NewZwQuerySystemInformation(
SYSTEM_INFORMATION_CLASS SystemInformationClass,
PVOID SystemInformation//进程信息单项链表
ULONG SystemInformationLength,//表示的就是unsigned long
PULONG ReturnLength//表示是unsigned long的指针
)
{
NTSTATUS status;
FARPROC pFunc;
PSYSTEM_PROCESS_INFORMATION pCur, pPrev;
char szProcName[MAX_PATH] = { 0, };

//在开始前先脱钩
unhook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION, g_pOrgBytes);

//调用原始API
pFunc = GetProcAddress(GetModuleHandleA(DEF_NTDLL), DEF_ZWQUERYSYSTEMINFORMATION);

status = ((PFZWQUERYSYSTEMINFORMATION)pFunc)//通过函数起始地址调用原来的API
(SystemInformationClass, SystemInformation,
SystemInformationLength, ReturnLength);//返回值: NTSTATUS成功或者错误代码

if (status != STATUS_SUCCESS)//如果调用该API失败, 则直接跳转到结尾
goto __NTQUERYSYSTEMINFORMATION_END;

//仅针对SystemProcessInformation类型操作
if (SystemInformationClass == SystemProcessInformation)//确定获取的系统信息是进程信息
{
//通过类型转换获取到pCur单项链表的头
pCur = (PSYSTEM_PROCESS_INFORMATION)SystemInformation;

while (TRUE)
{
if (pCur->Reserved2[1] != NULL)//Reserved2[1]存有进程名字符串指针
{ //g_szProcName是目标进程名的字符串指针, 这里是根据名称比较来确定是否是需要隐藏的进程节点
if (!_tcsicmp((PWSTR)pCur->Reserved2[1], g_szProcName))
{
if (pCur->NextEntryOffset == 0)//如果是最后的节点就直接删除并让上一个节点变成末节点
pPrev->NextEntryOffset = 0;
else//如果是中间的节点就要让上一个节点跳过这个节点
pPrev->NextEntryOffset += pCur->NextEntryOffset;
}
else
pPrev = pCur;//如果不是我们要找的节点的话我们就换下一个
}

if (pCur->NextEntryOffset == 0)
break;

pCur = (PSYSTEM_PROCESS_INFORMATION)((ULONG)pCur + pCur->NextEntryOffset);//获取链表的下一项
}
}
__NTQUERYSYSTEMINFORMATION_END:
//函数终止前, 再次执行API钩取操作,为下一次调用做准备
hook_by_code(DEF_NTDLL, DEF_ZWQUERYSYSTEMINFORMATION,
(PROC)NewZwQuerySystemInformation, g_pOrgBytes);

return status;
}

大致流程:

  • 首先脱钩, 为了后面能正常使用函数, 而不是无限递归
  • 然后通过Loadxxx获取到了隐藏API的地址
  • 通过地址正常调用了API
  • 获取进程信息链表
  • 通过名称字符串对比找到了隐藏进程所在节点, 并删去, 这时我们的隐藏进程对于当前进程来说就是不存在的了
  • 重新挂钩

我之前对于ZwQuerySystemInformation()的理解有点误区, 那就是我认为进程信息的链表是全局的, 也就是说只用需改一次就可以了, 但是实际上并不是, ZwQuerySystemInformation()的工作原理是获取当前的进程信息组成局部链表, 然后给其他的API使用, 如果下一次没有过去的话, ZwQuerySystemInformation()又会重新获取新的进程信息链表, 这时该链表中又有我们想要隐藏的进程信息了, 所以我们每次调用ZwQuerySystemInformation()时都需要重复钩取.

33.6 全局API钩取

全局钩取的对象有两个:

  • 当前运行的所有进程
  • 将来要运行的所有进程

我们前面只完成了第一个目标, 接下来我们将在此基础上完成第二个目标.

33.6.1 Kernel32.CreateProcess()API

Kernel32.CreateProcess()API用来创建新进程.其他创建进程的API内部都调用了这个API

1
2
3
4
5
6
7
8
9
10
11
12
BOOL WINAPI CreateProcess(
__in_opt LPCTSTR lpApplicationName,//要执行的模块的完整的路径名和文件名
__inout_opt LPTSTR lpCommandLine,//要执行的命令行
__in_opt LPSECURITY_ATTRIBUTES lpProcessAttributes,//指向**进程**的安全描述符结构体的指针
__in_opt LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向**线程**的安全描述符结构体的指针
__in BOOL bInheritHandles,//为TRUE则进程中的每个可继承句柄都有子进程继承, 为FALSE则相反
__in DWORD dwCreationFlags,//控制优先级和进程创建的标志
__in_opt LPVOID lpEnvironment,//指向新进程的环境快指针
__in_opt LPCTSTR lpCurrentDirectory,//进程当前目录的完整路径
__in LPSTARTUPINFO lpStartupInfo,//指向STARTUPINFOA或STARTUPINFOEXA结构体, 这两个结构体都是描述新进程的窗口, 桌面, 标准句柄和属性的
__out LPPROCESS_INFORMATION lpProcessInformation//指向PROCESS_INFORMATION结构体指针, 该结构体接收有关新进程的标识信息
);

因此, 向当前运行的所有进程注入stealth.dll后, 如果在stealth.dll中将CreateProcess()API也一起钩取, 那么以后运行的进程也会自动注入stealth.dll文件.

但是要考虑以下方面:

  • 钩取CreateProcess()API时, 还要分别钩取CreateProcessA(), CreateProcessW()这两个API
  • CreateProcessA(), CreateProcessW()中又分别调用了CreateProcessInternalA(), CreateProcessInternalW()这两个函数.
  • 钩取函数(NewCreateProcess)要钩取子进程的API. 因此, 极短时间内, 子进程可能在未钩取的状态下运行.

对于上面的问题, 有一个一次性解决的方法, 那就是钩取比CreateProcess()更低级的API

33.6.2 Ntdll.ZwResumeThread()API

1
2
3
4
ZwResumeThread(
IN HANDLE ThreadHandle,
OUT PULONG SuspendCount OPTIONAL
)

ZwResumeThread()是在进程创建后, 主线程运行前被调用执行. 所以只用钩取这个函数, 即可在不运行子进程代码的状态下钩取API.

在读书的时候读到这里会有点晕, 首先对于子进程, 我们的目标是钩取子进程的API, 而钩取API所要做的事就是注入DLL, 而我们想要达成注入DLL到子进程中所要做的事是在父进程中钩取创建子进程的API, 然后在钩取函数中调用Loadxxx. 这样捋下来就是: 我们要在父进程中额外钩取API, 这一个钩取函数的行为是在子进程中再一次注入DLL钩取API, 同时子进程中也会发生同样的操作.

33.7 练习#2 (HideProc2.exe, Stealth2.dll)

33.7.1 复制stealth2.dll文件到%SYSTEM%文件夹中

为了把stealth2.dll文件注入所有运行进程, 首先要把stealth2.dll文件复制到%SYSTEM%文件夹

33.7.2 运行HideProc2.exe -hide

Untitled

33.7.3 运行ProcExp.exe

这时再运行ProcExp看是否是全局钩取

Untitled

可以看到即使是钩取后的进程也依然被钩取了

33.7.4 运行HideProc2.exe -show

Untitled

在取消钩取后又可以看到notepad了

33.8 源代码分析

33.8.1 HideProc2.cpp

和HideProc.cpp相比, HideProc2.cpp只是减少了参数个数, 内容跟HideProc差不多, 所以可以参照上面的内容

33.8.2 stealth2.cpp

DllMain.cpp

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
BOOL APIENTRY DllMain( HMODULE hModule, DWORD  ul_reason_for_call, LPVOID lpReserved)
{
char szCurProc[MAX_PATH] = { 0, };
char* p = NULL;

//异常处理使注入不会发生在HideProc2.exe进程
GetModuleFileNameA(NULL, szCurProc, MAX_PATH);//获取当前进程的文件路径(这里已经开始钩取了, 对每个进程注入DLL是HideProc的功能)
p = strrchr(szCurProc, '\\');
if ((p != NULL) && !_stricmp(p + 1, "HideProc2.exe"))
return TRUE;

//改变privilege
SetPrivilege(SE_DEBUG_NAME, TURE);

switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
//钩取
hook_by_code("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA, g_pOrgCPA);//对创建进程的API进行钩取

hook_by_code("kernel32.dll", "CreateProcessW", (PROC)NewCreateProcessW, g_pOrgCPW);//对创建进程的API进行钩取

hook_by_code("ntdll.dll", "ZwQuerySystemInformation", (PROC)NewZwoQuerySystemInformation, g_pOrgZwQSI);//对检索进程的API进行钩取

break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
//脱钩
unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);

unhook_by_code("kernel32.dll", "CreateProcessW", g_pOrgCPW);

unhook_by_code("ntdll.dll", "ZwQuerySystemInformation", g_pOrgZwQSI);

break;
}
return TRUE;
}

NewCreateProcessA()

针对创建进程API的钩取函数的源代码

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
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
BOOL bRet;
FARPROC pFunc;

//脱钩(针对当前进程)
unhook_by_code("kernel32.dll", "CreateProcessA", g_pOrgCPA);

//获取原函数的指针
pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
//通过指针调用了该函数(已经脱钩), 生成子进程, 若成功则返回一个非零值, 失败则相反
bRet = ((PFCREATEPROCESSA)pFunc)(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);//这个结构体负责存储子进程的信息, 在下面的作用是获取子进程PID

//向生成的子进程注入stealth2.dll(针对子进程)
if (bRet)//如果成功创建子进程
InjectDll2(lpProcessInformation->hProcess, Str_MODULE_NAME);//参一的结构体赋值是在上面的函数调用中实现的, 这个结构体是最后一个参数

//钩取(针对当前进程)
hook_by_code("kernel32.dll", "CreateProcessA", (PROC)NewCreateProcessA, g_pOrgCPA);

return bRet;
}

大致思路:

  • 在当前进程中对创建进程API脱钩, 防止无限递归
  • 然后调用脱钩后的原API创建子进程
  • 通过PID对该子进程进程DLL注入, 钩取了子进程的检索进程函数创建进程函数
  • 在当前进程中对创建进程挂钩

33.9 利用”热补丁”技术钩取API

33.9.1 API代码修改技术

由于线程问题, 在对API进行写的操作的同时, 运行代码会引发错误, 所以需要跟安全的API钩取技术.

33.9.2 “热补丁”(修改7个字节代码)

我们观察API的结构

Untitled

可以发现又7个字节的空余, 这些专门用于热补丁的

思路(二次跳转)

  • 前面5个字节用于JMP
  • 后面2个字节用于JMP SHORT, 跳转的目的就是前面的5字节JMP

跟代码修改JMP之间的区别在于, 这个API是可以正常运行原有功能的, 因为下面的指令都没有被破坏, 然后如果想要正常调用不需要再脱钩挂钩, 直接起始地址 + 2 就可调用了.

33.11 源代码分析

stealth3.cpp

hook_by_hotpatch()

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
BOOL hook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName, PROC pfnNew)
{
FARPROC pFunc;
DWORD dwOldProtect, dwAddress;
BYTE pBuf[5] = { 0xE9 , 0 };//第二次跳转, 调转到hook函数
byte pBuf2[2] = { 0xEB, 0xF9 };//第一次跳转的字节码, 因为相对地址是固定的, 所以可以直接确定机器码
PBYTE pByte;

//获取目标API的首地址
pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
//类型转换
pByte = (PBYTE)pFunc;
//检查是否已经挂钩, 如果挂钩内存内容就是0xEB短跳转
if (pByte[0] == 0xEB)
return FALSE;
//改变那7个字节的保护属性
VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, PAGE_EXECUTE_READWRITE, &dwOldProtect);

//改变上面的五个字节
dwAddress = (DWORD)pfnNew - (DWORD)pFunc;//计算出要跳转的偏移, 这里不用减5是因为跳转后的下一条指令就是函数的开头
memcpy(&pBuf[1], &dwAddress, 4);//将地址赋给数组
memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);//将数组赋值给内存

memcpy(pFunc, pBuf2, 2);//将API开头两个字节修改

VirtualProtect((LPVOID)((DWORD)pFunc - 5), 7, dwOldProtect, &dwOldProtect);

return TRUE;
}

跟之前的钩取函数差不多, 只是修改了7个字节

unhook_by_hotpatch()

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
BOOL unhook_by_hotpatch(LPCSTR szDllName, LPCSTR szFuncName)
{
FARPROC pFunc;
DWORD dwOldProtect;
PBYTE pByte;
byte pBuf[5] = { 0x90, 0x90, 0x90, 0x90, 0x90 };//恢复的API开头之前的5个NOP
byte pBuf2[2] = { 0x8b, 0xFF };//恢复的API开头两个字节
//获取需要恢复的API地址
pFunc = (FARPROC)GetProcAddress(GetModuleHandleA(szDllName), szFuncName);
//类型转换
pByte = (PBYTE)pFunc;
//判断是否挂钩
if (pByte[0] != 0xEB)
return FALSE;
//修改内存保护属性
VirtualProtect((LPVOID)pFunc, 5, PAGE_EXECUTE_READWRITE, &dwOldProtect);

//恢复前五个字节
memcpy((LPVOID)((DWORD)pFunc - 5), pBuf, 5);

//恢复MOV EDI, EDI
memcpy(pFunc, pBuf2, 2);

VirtualProtect((LPVOID)pFunc, 5, dwOldProtect, &dwOldProtect);

return TRUE;
}

修改后的NewCreateProcessA()

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
BOOL CreateProcessA(
LPCSTR lpApplicationName,
LPSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCSTR lpCurrentDirectory,
LPSTARTUPINFOA lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
)
{
BOOL bRet;
FARPROC pFunc;

//获取原函数的指针
pFunc = GetProcAddress(GetModuleHandleA("kernel32.dll"), "CreateProcessA");
//原API的地址现在设置为: 起始地址 + 2
pFunc = (FARPROC)((DWORD)pFunc + 2);
//通过指针调用了该函数(已经脱钩), 生成子进程, 若成功则返回一个非零值, 失败则相反
bRet = ((PFCREATEPROCESSA)pFunc)(
lpApplicationName,
lpCommandLine,
lpProcessAttributes,
lpThreadAttributes,
bInheritHandles,
dwCreationFlags,
lpEnvironment,
lpCurrentDirectory,
lpStartupInfo,
lpProcessInformation);//这个结构体负责存储子进程的信息, 在下面的作用是获取子进程PID

//向生成的子进程注入stealth2.dll(针对子进程)
if (bRet)//如果成功创建子进程
InjectDll2(lpProcessInformation->hProcess, Str_MODULE_NAME);//参一的结构体赋值是在上面的函数调用中实现的, 这个结构体是最后一个参数

return bRet;
}

跟之前的HOOK函数相比

  • 省去了hook和unhook
  • 多出了一个 起始地址 + 2的操作

注意使用热补丁之前要看看API是否支持热补丁

文章作者: LamのCrow
文章链接: http://example.com/2022/04/27/逆向工程核心原理-第33章-隐藏进程 95cea6f6be2c470783b612eb9258314c/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 LamのCrow