常见的用户模式函数截获方法包括内联代码挂钩、IAT 挂钩和硬件断点挂钩。这些方法很有效,但它们需要修改 .text 部分、使用 NtProtectVirtualMemory 更改内存保护或自定义异常处理程序,所有这些都可能是“干扰”。这篇文章描述了一种在不修改内存保护的情况下秘密挂接函数的方法。通过覆盖嵌套在目标函数中的全局指针或虚拟表条目,可以挂钩函数而不会引起怀疑,因为其中许多内存区域已经启用了写入权限。这是我多年来一直使用的方法,效果很好。
原文链接:https://www.x86matthew.com/view_post?id=stealth_hook
如果我们能够截获嵌套在目标函数中的子函数,我们就可以操纵原始函数的执行流和返回值。通过从嵌套函数遍历堆栈,我们可以找到原始目标函数的返回地址。然后,我们可以用新值覆盖原始返回地址,强制程序在继续使用原始代码之前执行进一步的指令。这种函数挂钩方法不需要对可执行代码进行任何修改,也不需要任何内存保护更改,因此很难使用传统技术进行检测。
此方法背后的概念相对简单,但手动查找合适的挂钩点可能既困难又耗时,尤其是在大型函数中。为了简化此过程,我开发了一个工具,可以自动执行此过程,并为任何给定函数快速找到合适的钩点(如果存在)。这使得在实践中实现此方法变得容易,并且同时支持 32 位和 64 位代码。
该程序通过扫描指向目标函数中指令的可写指针来构建引用列表。它通过安装自定义异常处理程序并单步执行函数(一次一条指令)来实现此目的。对于每条指令,程序会扫描每个加载的模块,以查看是否存在指向当前指令的可写指针。异常处理程序仅由开发人员用于初始发现潜在的挂钩点,最终的挂钩代码不需要安装自定义异常处理程序。
在 WoW64 进程中运行时,我们无法单步进入本机 64 位代码 – 在这种情况下,程序在 Wow64Transition 函数中捕获从 32 位到 64 位模式的转换,并在返回地址设置硬件断点。单步执行暂时禁用,直到目标函数返回到 32 位模式,此时它将恢复并继续监视函数。
创建引用列表后,程序将一次覆盖列表中的每个指针,然后再次运行目标函数。如果在运行期间执行覆盖的指针,则表明它是一个成功的挂钩点,程序将以此方式标记它。对列表中的所有指针重复此过程,直到测试完所有潜在的挂钩点。
该程序还计算并显示堆栈增量,这是需要向下移动到堆栈中的字节数或der 覆盖它标识的每个挂钩点的原始返回地址。
重要的是要考虑指向正在挂钩的子函数的指针可能被程序中的其他函数调用的可能性。在这种情况下,必须注意确保调用方是目标函数,而不是可能使用相同子函数的其他函数。
使用此方法的一个潜在缺点是目标函数中的代码流可能因目标模块的不同版本而异 – 这可能会使可靠地拦截函数调用变得更加困难。为了使用此方法提高钩子的可靠性,我建议正确枚举堆栈帧,而不是使用固定堆栈增量。现代控制流强制方法将来可能会有效地防止这些类型的钩子。
为了演示,我们将使用该工具在 CreateFileA 函数中搜索挂钩点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
StealthHook - x86matthew www.x86matthew.com Searching for hooking points... Instruction 0x777B3440 referenced at KERNELBASE.dll!0x7785FA7C (sect: .data, virt_addr: 0x1DFA7C, stack delta: 0x30) Instruction 0x77783AB0 referenced at KERNELBASE.dll!0x7785F650 (sect: .data, virt_addr: 0x1DF650, stack delta: 0x100) Found 2 potential hooking points, testing... Overwriting reference: 0x7785FA7C... Calling target function... Hook caught successfully! Overwriting reference: 0x7785F650... Calling target function... Hook caught successfully! Finished - found 2 successful hooking points |
正如我们在上面看到的,该工具发现了两个适合挂钩 CreateFileA 的可写全局指针。通过查看调试符号,我们可以看到这两个函数分别是_pfnEightBitStringToUnicodeString和feclient_EfsClientFreeProtectorList。由于feclient_EfsClientFreeProtectorList函数不常用(与_pfnEightBitStringToUnicodeString不同),因此在这种情况下使用这是最佳选择。
下面的代码展示了如何使用上面发现的信息在 32 位测试程序中钩住和操作 CreateFileA 调用的返回值:
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 |
#include <stdio.h> #include <windows.h> DWORD dwGlobal_OrigCreateFileReturnAddr = 0; DWORD dwGlobal_OrigReferenceAddr = 0; void __declspec(naked) ModifyReturnValue() { // the original return address for the CreateFile call redirects to here _asm { // CreateFile complete - overwrite return value mov eax, 0x12345678 // continue original execution flow (ecx is safe to overwrite at this point) mov ecx, dwGlobal_OrigCreateFileReturnAddr jmp ecx } } void __declspec(naked) HookStub() { // the hooked global pointer nested within CreateFile redirects to here _asm { // store original CreateFile return address mov eax, dword ptr [esp + 0x100] mov dwGlobal_OrigCreateFileReturnAddr, eax // overwrite the CreateFile return address lea eax, ModifyReturnValue mov dword ptr [esp + 0x100], eax // continue original execution flow mov eax, dwGlobal_OrigReferenceAddr jmp eax } } DWORD InstallHook() { BYTE *pModuleBase = NULL; BYTE *pHookAddr = NULL; // get base address of kernelbase.dll pModuleBase = (BYTE*)GetModuleHandle("kernelbase.dll"); if(pModuleBase == NULL) { return 1; } // get ptr to function reference pHookAddr = pModuleBase + 0x1DF650; // store original value dwGlobal_OrigReferenceAddr = *(DWORD*)pHookAddr; // overwrite ptr to call HookStub *(DWORD*)pHookAddr = (DWORD)HookStub; return 0; } int main() { HANDLE hFile = NULL; // create temporary file (without hook) printf("Creating file #1...\n"); hFile = CreateFile("temp_file_1.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); printf("hFile: 0x%X\n\n", hFile); // install hook printf("Installing hook...\n\n"); if(InstallHook() != 0) { return 1; } // create temporary file (with hook) printf("Creating file #2...\n"); hFile = CreateFile("temp_file_2.txt", GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); printf("hFile: 0x%X\n\n", hFile); return 0; } |
该测试程序的结果如下所示:
1 2 3 4 5 6 7 |
Creating file #1... hFile: 0xDC Installing hook... Creating file #2... hFile: 0x12345678 |
输出显示第一个创建文件调用正常完成。安装钩子后的第二个调用返回了我们的钩子值 0x12345678。
该工具的完整源代码如下:
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 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 |
#include <stdio.h> #include <windows.h> #define DEBUG_REGISTER_EXEC_DR0 0x1 #define DEBUG_REGISTER_EXEC_DR1 0x4 #define DEBUG_REGISTER_EXEC_DR2 0x10 #define DEBUG_REGISTER_EXEC_DR3 0x40 #define SINGLE_STEP_FLAG 0x100 #define MAXIMUM_STORED_ADDRESS_COUNT 1024 #define OVERWRITE_REFERENCE_ADDRESS_VALUE 1 #if _WIN64 #define NATIVE_VALUE ULONGLONG #define CURRENT_EXCEPTION_STACK_PTR ExceptionInfo->ContextRecord->Rsp #define CURRENT_EXCEPTION_INSTRUCTION_PTR ExceptionInfo->ContextRecord->Rip #else #define NATIVE_VALUE DWORD #define CURRENT_EXCEPTION_STACK_PTR ExceptionInfo->ContextRecord->Esp #define CURRENT_EXCEPTION_INSTRUCTION_PTR ExceptionInfo->ContextRecord->Eip #endif struct UNICODE_STRING { USHORT Length; USHORT MaximumLength; PWSTR Buffer; }; struct PEB_LDR_DATA { DWORD Length; DWORD Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; }; struct LDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; LIST_ENTRY InMemoryOrderLinks; LIST_ENTRY InInitializationOrderLinks; PVOID DllBase; PVOID EntryPoint; PVOID SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; PVOID Reserved5[3]; PVOID Reserved6; ULONG TimeDateStamp; }; struct PEB { BYTE Reserved1[2]; BYTE BeingDebugged; BYTE Reserved2[1]; PVOID Reserved3[1]; PVOID ImageBaseAddress; PEB_LDR_DATA *Ldr; // ..... }; DWORD dwGlobal_TraceStarted = 0; DWORD dwGlobal_AddressCount = 0; DWORD dwGlobal_SuccessfulHookCount = 0; DWORD dwGlobal_CurrHookExecuted = 0; NATIVE_VALUE dwGlobal_Wow64TransitionStub = 0; NATIVE_VALUE dwGlobal_InitialStackPtr = 0; NATIVE_VALUE dwGlobal_OriginalReferenceValue = 0; NATIVE_VALUE dwGlobal_AddressList[MAXIMUM_STORED_ADDRESS_COUNT]; BYTE *pGlobal_ExeBase = NULL; DWORD ExecuteTargetFunction(); DWORD ScanModuleForAddress(BYTE *pModuleBase, char *pModuleName, NATIVE_VALUE dwAddr, NATIVE_VALUE dwStackPtr) { IMAGE_DOS_HEADER *pImageDosHeader = NULL; IMAGE_NT_HEADERS *pImageNtHeader = NULL; IMAGE_SECTION_HEADER *pCurrSectionHeader = NULL; DWORD dwReadOffset = 0; BYTE *pCurrPtr = NULL; MEMORY_BASIC_INFORMATION MemoryBasicInfo; DWORD dwStackDelta = 0; // get dos header pImageDosHeader = (IMAGE_DOS_HEADER *)pModuleBase; if(pImageDosHeader->e_magic != 0x5A4D) { return 1; } // get nt header pImageNtHeader = (IMAGE_NT_HEADERS *)(pModuleBase + pImageDosHeader->e_lfanew); if(pImageNtHeader->Signature != IMAGE_NT_SIGNATURE) { return 1; } // loop through all sections for(DWORD i = 0; i < pImageNtHeader->FileHeader.NumberOfSections; i++) { // get current section header pCurrSectionHeader = (IMAGE_SECTION_HEADER *)((BYTE *)&pImageNtHeader->OptionalHeader + pImageNtHeader->FileHeader.SizeOfOptionalHeader + (i * sizeof(IMAGE_SECTION_HEADER))); // ignore executable sections if(pCurrSectionHeader->Characteristics & IMAGE_SCN_MEM_EXECUTE) { continue; } // scan current section for the target address dwReadOffset = pCurrSectionHeader->VirtualAddress; for(DWORD ii = 0; ii < pCurrSectionHeader->Misc.VirtualSize / sizeof(NATIVE_VALUE); ii++) { // check if the current value contains the target address pCurrPtr = pModuleBase + dwReadOffset; if(*(NATIVE_VALUE *)pCurrPtr == dwAddr) { // found target address - check memory protection memset((void *)&MemoryBasicInfo, 0, sizeof(MemoryBasicInfo)); if(VirtualQuery(pCurrPtr, &MemoryBasicInfo, sizeof(MemoryBasicInfo)) != 0) { // check if the current region is writable if(MemoryBasicInfo.Protect == PAGE_EXECUTE_READWRITE || MemoryBasicInfo.Protect == PAGE_READWRITE) { // ensure the address list is not full if(dwGlobal_AddressCount >= MAXIMUM_STORED_ADDRESS_COUNT) { printf("Error: Address list is full\n"); return 1; } // store current address in list dwGlobal_AddressList[dwGlobal_AddressCount] = (NATIVE_VALUE)pCurrPtr; dwGlobal_AddressCount++; // calculate stack delta dwStackDelta = (DWORD)(dwGlobal_InitialStackPtr - dwStackPtr); printf("Instruction 0x%p referenced at %s!0x%p (sect: %s, virt_addr: 0x%X, stack delta: 0x%X)\n", (void*)dwAddr, pModuleName, (void*)pCurrPtr, pCurrSectionHeader->Name, dwReadOffset, dwStackDelta); } } } // increase read offset dwReadOffset += sizeof(NATIVE_VALUE); } } return 0; } DWORD ScanAllModulesForAddress(NATIVE_VALUE dwAddr, NATIVE_VALUE dwStackPtr) { DWORD dwPEB = 0; PEB *pPEB = NULL; LDR_DATA_TABLE_ENTRY *pCurrEntry = NULL; LIST_ENTRY *pCurrListEntry = NULL; DWORD dwEntryOffset = 0; char szModuleName[512]; DWORD dwStringLength = 0; // get PEB ptr #if _WIN64 pPEB = (PEB *)__readgsqword(0x60); #else pPEB = (PEB *)__readfsdword(0x30); #endif // get InMemoryOrderLinks offset in structure dwEntryOffset = (DWORD)((BYTE *)&pCurrEntry->InLoadOrderLinks - (BYTE *)pCurrEntry); // get first link pCurrListEntry = pPEB->Ldr->InLoadOrderModuleList.Flink; // enumerate all modules for(;;) { // get ptr to current module entry pCurrEntry = (LDR_DATA_TABLE_ENTRY *)((BYTE *)pCurrListEntry - dwEntryOffset); // check if this is the final entry if(pCurrEntry->DllBase == 0) { // end of module list break; } // ignore main exe module if(pCurrEntry->DllBase != pGlobal_ExeBase) { // convert module name to ansi dwStringLength = pCurrEntry->BaseDllName.Length / sizeof(wchar_t); if(dwStringLength > sizeof(szModuleName) - 1) { dwStringLength = sizeof(szModuleName) - 1; } memset(szModuleName, 0, sizeof(szModuleName)); wcstombs(szModuleName, pCurrEntry->BaseDllName.Buffer, dwStringLength); // scan current module ScanModuleForAddress((BYTE *)pCurrEntry->DllBase, szModuleName, dwAddr, dwStackPtr); } // get next module entry in list pCurrListEntry = pCurrListEntry->Flink; } return 0; } LONG WINAPI ExceptionHandler(EXCEPTION_POINTERS *ExceptionInfo) { NATIVE_VALUE dwReturnAddress = 0; // check exception code if(ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_SINGLE_STEP) { if(dwGlobal_TraceStarted == 0) { // trace not started - ensure the current eip is the target function if(CURRENT_EXCEPTION_INSTRUCTION_PTR != ExceptionInfo->ContextRecord->Dr0) { return EXCEPTION_CONTINUE_SEARCH; } // store original stack pointer dwGlobal_InitialStackPtr = CURRENT_EXCEPTION_STACK_PTR; // set hardware breakpoint on the original return address ExceptionInfo->ContextRecord->Dr1 = *(NATIVE_VALUE *)dwGlobal_InitialStackPtr; // initial trace started dwGlobal_TraceStarted = 1; } // set debug control field ExceptionInfo->ContextRecord->Dr7 = DEBUG_REGISTER_EXEC_DR1; // check current instruction pointer if(CURRENT_EXCEPTION_INSTRUCTION_PTR == dwGlobal_Wow64TransitionStub) { // we have hit the wow64 transition stub - don't single step here, set a breakpoint on the current return address instead dwReturnAddress = *(NATIVE_VALUE *)CURRENT_EXCEPTION_STACK_PTR; ExceptionInfo->ContextRecord->Dr0 = dwReturnAddress; ExceptionInfo->ContextRecord->Dr7 |= DEBUG_REGISTER_EXEC_DR0; } else if(CURRENT_EXCEPTION_INSTRUCTION_PTR == ExceptionInfo->ContextRecord->Dr1) { // we have reached the original return address - remove all breakpoints ExceptionInfo->ContextRecord->Dr7 = 0; } else { // scan all modules for the current instruction pointer ScanAllModulesForAddress(CURRENT_EXCEPTION_INSTRUCTION_PTR, CURRENT_EXCEPTION_STACK_PTR); // single step ExceptionInfo->ContextRecord->EFlags |= SINGLE_STEP_FLAG; } // continue execution return EXCEPTION_CONTINUE_EXECUTION; } else if(ExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { // access violation - check if the eip matches the expected value if(CURRENT_EXCEPTION_INSTRUCTION_PTR != OVERWRITE_REFERENCE_ADDRESS_VALUE) { return EXCEPTION_CONTINUE_SEARCH; } // caught current hook successfully dwGlobal_CurrHookExecuted = 1; // restore correct instruction pointer CURRENT_EXCEPTION_INSTRUCTION_PTR = dwGlobal_OriginalReferenceValue; // continue execution return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } DWORD InitialiseTracer() { NATIVE_VALUE dwWow64Transition = 0; PVOID(WINAPI * RtlAddVectoredExceptionHandler)(DWORD dwFirstHandler, void *pExceptionHandler) = NULL; HMODULE hNtdllBase = NULL; // store exe base pGlobal_ExeBase = (BYTE *)GetModuleHandleA(NULL); if(pGlobal_ExeBase == NULL) { return 1; } // get ntdll base hNtdllBase = GetModuleHandleA("ntdll.dll"); if(hNtdllBase == NULL) { return 1; } // get RtlAddVectoredExceptionHandler function ptr RtlAddVectoredExceptionHandler = (void *(WINAPI*)(unsigned long, void *))GetProcAddress(hNtdllBase, "RtlAddVectoredExceptionHandler"); if(RtlAddVectoredExceptionHandler == NULL) { return 1; } // add exception handler if(RtlAddVectoredExceptionHandler(1, (void *)ExceptionHandler) == NULL) { return 1; } // find Wow64Transition export dwWow64Transition = (NATIVE_VALUE)GetProcAddress(hNtdllBase, "Wow64Transition"); if(dwWow64Transition != 0) { // get Wow64Transition stub address dwGlobal_Wow64TransitionStub = *(NATIVE_VALUE *)dwWow64Transition; } return 0; } DWORD BeginTrace(BYTE *pTargetFunction) { CONTEXT DebugThreadContext; // reset values dwGlobal_TraceStarted = 0; dwGlobal_SuccessfulHookCount = 0; dwGlobal_AddressCount = 0; // set initial debug context - hardware breakpoint on target function memset((void *)&DebugThreadContext, 0, sizeof(DebugThreadContext)); DebugThreadContext.ContextFlags = CONTEXT_DEBUG_REGISTERS; DebugThreadContext.Dr0 = (NATIVE_VALUE)pTargetFunction; DebugThreadContext.Dr7 = DEBUG_REGISTER_EXEC_DR0; if(SetThreadContext(GetCurrentThread(), &DebugThreadContext) == 0) { return 1; } // execute the target function ExecuteTargetFunction(); return 0; } DWORD TestHooks() { // attempt to hook the target function at all referenced instructions found earlier for(DWORD i = 0; i < dwGlobal_AddressCount; i++) { printf("\nOverwriting reference: 0x%p...\n", (void*)dwGlobal_AddressList[i]); // reset flag dwGlobal_CurrHookExecuted = 0; // store original value dwGlobal_OriginalReferenceValue = *(NATIVE_VALUE *)dwGlobal_AddressList[i]; // overwrite referenced value with placeholder value *(NATIVE_VALUE *)dwGlobal_AddressList[i] = OVERWRITE_REFERENCE_ADDRESS_VALUE; printf("Calling target function...\n"); // execute target function ExecuteTargetFunction(); // restore original value *(NATIVE_VALUE *)dwGlobal_AddressList[i] = dwGlobal_OriginalReferenceValue; // check if the hook was executed if(dwGlobal_CurrHookExecuted == 0) { // hook wasn't executed - ignore printf("Failed to catch hook\n"); } else { // hook was executed - this address can be used to hook the target function printf("Hook caught successfully!\n"); dwGlobal_SuccessfulHookCount++; } } return 0; } DWORD ExecuteTargetFunction() { // call the target function CreateFileA("temp_file.txt", GENERIC_WRITE, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); return 0; } // www.x86matthew.com int main() { printf("StealthHook - x86matthew\n"); printf("www.x86matthew.com\n\n"); // initialise tracer if(InitialiseTracer() != 0) { return 1; } printf("Searching for hooking points...\n\n"); // begin trace if(BeginTrace((BYTE *)CreateFileA) != 0) { return 1; } // check if any referenced addresses were found if(dwGlobal_AddressCount == 0) { // none found printf("No potential hooking points found\n"); } else { printf("\nFound %u potential hooking points, testing...\n", dwGlobal_AddressCount); // test all of the potential hooks if(TestHooks() != 0) { return 1; } } // finished printf("\nFinished - found %u successful hooking points\n\n", dwGlobal_SuccessfulHookCount); return 0; } |