StealthHook – 一种在不修改内存保护的情况下挂接函数的方法

常见的用户模式函数截获方法包括内联代码挂钩、IAT 挂钩和硬件断点挂钩。这些方法很有效,但它们需要修改 .text 部分、使用 NtProtectVirtualMemory 更改内存保护或自定义异常处理程序,所有这些都可能是“干扰”。这篇文章描述了一种在不修改内存保护的情况下秘密挂接函数的方法。通过覆盖嵌套在目标函数中的全局指针或虚拟表条目,可以挂钩函数而不会引起怀疑,因为其中许多内存区域已经启用了写入权限。这是我多年来一直使用的方法,效果很好。

原文链接:https://www.x86matthew.com/view_post?id=stealth_hook

如果我们能够截获嵌套在目标函数中的子函数,我们就可以操纵原始函数的执行流和返回值。通过从嵌套函数遍历堆栈,我们可以找到原始目标函数的返回地址。然后,我们可以用新值覆盖原始返回地址,强制程序在继续使用原始代码之前执行进一步的指令。这种函数挂钩方法不需要对可执行代码进行任何修改,也不需要任何内存保护更改,因此很难使用传统技术进行检测。

此方法背后的概念相对简单,但手动查找合适的挂钩点可能既困难又耗时,尤其是在大型函数中。为了简化此过程,我开发了一个工具,可以自动执行此过程,并为任何给定函数快速找到合适的钩点(如果存在)。这使得在实践中实现此方法变得容易,并且同时支持 32 位和 64 位代码。

该程序通过扫描指向目标函数中指令的可写指针来构建引用列表。它通过安装自定义异常处理程序并单步执行函数(一次一条指令)来实现此目的。对于每条指令,程序会扫描每个加载的模块,以查看是否存在指向当前指令的可写指针。异常处理程序仅由开发人员用于初始发现潜在的挂钩点,最终的挂钩代码不需要安装自定义异常处理程序。

在 WoW64 进程中运行时,我们无法单步进入本机 64 位代码 – 在这种情况下,程序在 Wow64Transition 函数中捕获从 32 位到 64 位模式的转换,并在返回地址设置硬件断点。单步执行暂时禁用,直到目标函数返回到 32 位模式,此时它将恢复并继续监视函数。

创建引用列表后,程序将一次覆盖列表中的每个指针,然后再次运行目标函数。如果在运行期间执行覆盖的指针,则表明它是一个成功的挂钩点,程序将以此方式标记它。对列表中的所有指针重复此过程,直到测试完所有潜在的挂钩点。

该程序还计算并显示堆栈增量,这是需要向下移动到堆栈中的字节数或der 覆盖它标识的每个挂钩点的原始返回地址。

重要的是要考虑指向正在挂钩的子函数的指针可能被程序中的其他函数调用的可能性。在这种情况下,必须注意确保调用方是目标函数,而不是可能使用相同子函数的其他函数。

使用此方法的一个潜在缺点是目标函数中的代码流可能因目标模块的不同版本而异 – 这可能会使可靠地拦截函数调用变得更加困难。为了使用此方法提高钩子的可靠性,我建议正确枚举堆栈帧,而不是使用固定堆栈增量。现代控制流强制方法将来可能会有效地防止这些类型的钩子。

为了演示,我们将使用该工具在 CreateFileA 函数中搜索挂钩点:

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 调用的返回值:

#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;
}

该测试程序的结果如下所示:

Creating file #1...
hFile: 0xDC

Installing hook...

Creating file #2...
hFile: 0x12345678

输出显示第一个创建文件调用正常完成。安装钩子后的第二个调用返回了我们的钩子值 0x12345678。

该工具的完整源代码如下:

#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;
}

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注