windows环境下的调试器探究
字数 1589 2025-08-07 08:22:39

Windows环境下的调试器探究

0x00 前言

在Windows环境中,调试器主要通过三种方式触发异常:软件断点、内存断点和硬件断点。本文将详细分析这三种方式的原理,并通过实际代码实现调试器的核心功能。

0x01 软件断点

原理分析

软件断点通过将目标指令替换为0xCC(即int 3指令)实现:

  1. CPU检测到INT 3指令
  2. 查IDT表找到对应的中断处理函数
  3. 调用CommonDispatchException
  4. 通过KiDispatchException分发异常
  5. DbgkForwardException收集并发送调试事件

关键点:

  • 调试器会修改目标指令为0xCC
  • 异常处理流程会经过内核到用户态的转换
  • 如果没有内核调试器存在,3环调试器才能接收到异常
  • 调试进程会被挂起(DbgkpSuspendProcess

实现代码关键部分

// 设置软件断点
VOID SetInt3BreakPoint(LPVOID addr)
{
    CHAR int3 = 0xCC;
    ReadProcessMemory(hDebuggeeProcess, addr, &OriginalCode, 1, NULL);
    WriteProcessMemory(hDebuggeeProcess, addr, &int3, 1, NULL);
}

// 处理INT 3异常
BOOL Int3ExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
    // 1. 恢复原始指令
    if(!bIsSystemInt3) {
        WriteProcessMemory(hDebuggeeProcess, 
                         pExceptionInfo->ExceptionRecord.ExceptionAddress, 
                         &OriginalCode, 1, NULL);
    }
    
    // 2. 修正EIP(INT 3异常EIP会停在断点+1的位置)
    Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(hDebuggeeThread, &Context);
    Context.Eip--;
    SetThreadContext(hDebuggeeThread, &Context);
    
    // 3. 显示断点信息并等待用户命令
    printf("Int 3断点 : 0x%p \r\n", pExceptionInfo->ExceptionRecord.ExceptionAddress);
    // ...
}

调试循环框架

BOOL ExceptionTest()
{
    // 1. 创建调试进程
    CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, 
                 DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, 
                 NULL, NULL, &startupInfo, &pInfo);
    
    // 2. 调试循环
    while(nIsContinue) {
        WaitForDebugEvent(&debugEvent, INFINITE);
        
        switch(debugEvent.dwDebugEventCode) {
            case EXCEPTION_DEBUG_EVENT:
                ExceptionHandler(&debugEvent);
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
                break;
            // 其他事件处理...
        }
        
        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
    }
}

0x02 内存断点

原理分析

内存断点分为两种类型:

  1. 内存访问:内存被读写时产生中断
  2. 内存写入:内存被写入时产生中断

实现原理:

  • 使用VirtualProtectEx修改内存页属性
  • 内存访问断点:设置为PAGE_NOACCESS(PTE的P位=0)
  • 内存写入断点:设置为PAGE_EXECUTE_READ(PTE的P位=1,R/W位=0)

异常处理流程:

  1. CPU访问错误的内存地址,触发页异常
  2. 查IDT表找到中断处理函数(nt!_KiTrap0E
  3. CommonDispatchException
  4. KiDispatchException
  5. DbgkForwardException收集并发送调试事件

实现代码关键部分

// 设置内存断点
VOID SetMemBreakPoint(PCHAR pAddress)
{
    // 访问断点
    VirtualProtectEx(hDebuggeeProcess, pAddress, 1, 
                    PAGE_NOACCESS, &dwOriginalProtect);
    // 写入断点
    // VirtualProtectEx(hDebuggeeProcess, pAddress, 1, 
    //                 PAGE_EXECUTE_READ, &dwOriginalProtect);
}

// 处理访问异常
BOOL AccessExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
    // 1. 获取异常信息
    DWORD dwAccessFlag = pExceptionInfo->ExceptionRecord.ExceptionInformation[0];
    DWORD dwAccessAddr = pExceptionInfo->ExceptionRecord.ExceptionInformation[1];
    
    // 2. 恢复内存属性
    VirtualProtectEx(hDebuggeeProcess, (VOID*)dwAccessAddr, 1, 
                    dwOriginalProtect, &dwProtect);
    
    // 3. 显示异常信息
    printf("内存断点 : dwAccessFlag - %x dwAccessAddr - %x \n", 
          dwAccessFlag, dwAccessAddr);
    
    // 4. 获取上下文(EIP不需要修正)
    Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(hDebuggeeThread, &Context);
    printf("Eip: 0x%p \n", Context.Eip);
    
    // ...
}

0x03 硬件断点

原理分析

硬件断点依赖于CPU的调试寄存器(Dr0-Dr7):

  • Dr0-Dr3:存储断点地址(最多4个硬件断点)
  • Dr6:调试状态寄存器
  • Dr7:调试控制寄存器

Dr7关键位:

  • L0/G0 ~ L3/G3:控制Dr0-Dr3是否有效(局部/全局)
  • LENx(16-19位):断点长度(00=1字节,01=2字节,11=4字节)
  • R/Wx(16-17位):断点类型(00=执行,01=写入,11=访问)

异常处理流程:

  1. CPU检测到当前线性地址与调试寄存器中的地址匹配
  2. 查IDT表找到中断处理函数(nt!_KiTrap01
  3. CommonDispatchException
  4. KiDispatchException
  5. DbgkForwardException收集并发送调试事件

实现代码关键部分

// 设置硬件断点
VOID SetHardBreakPoint(PVOID pAddress)
{
    // 1. 获取线程上下文
    Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(hDebuggeeThread, &Context);
    
    // 2. 设置断点位置
    Context.Dr0 = (DWORD)pAddress;
    Context.Dr7 |= 1;  // 启用Dr0
    
    // 3. 设置断点类型和长度
    Context.Dr7 &= 0xfff0ffff;  // 执行断点(16-17位=00),1字节(18-19位=00)
    
    // 4. 设置线程上下文
    SetThreadContext(hDebuggeeThread, &Context);
}

// 处理单步异常(硬件断点)
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO *pExceptionInfo)
{
    // 1. 获取线程上下文
    Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(hDebuggeeThread, &Context);
    
    // 2. 判断异常原因
    if(Context.Dr6 & 0xF) {  // B0-B3不为空,硬件断点
        printf("硬件断点:%x 0x%p \n", Context.Dr7&0x00030000, Context.Dr0);
        // 清除断点
        Context.Dr0 = 0;
        Context.Dr7 &= 0xfffffffe;
    } else {  // 单步异常
        printf("单步:0x%p \n", Context.Eip);
        Context.Dr7 &= 0xfffffeff;
    }
    
    SetThreadContext(hDebuggeeThread, &Context);
    // ...
}

综合调试器实现

完整调试循环框架:

int main(int argc, char* argv[])
{
    // 1. 创建调试进程
    STARTUPINFO startupInfo = {0};
    PROCESS_INFORMATION pInfo = {0};
    CreateProcess(DEBUGGEE, NULL, NULL, NULL, TRUE, 
                 DEBUG_PROCESS || DEBUG_ONLY_THIS_PROCESS, 
                 NULL, NULL, &startupInfo, &pInfo);
    
    hDebuggeeProcess = pInfo.hProcess;
    
    // 2. 调试循环
    while(nIsContinue) {
        WaitForDebugEvent(&debugEvent, INFINITE);
        
        switch(debugEvent.dwDebugEventCode) {
            case EXCEPTION_DEBUG_EVENT:
                ExceptionHandler(&debugEvent);
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                // 选择设置哪种断点
                SetInt3BreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
                // SetMemBreakPoint((PCHAR)debugEvent.u.CreateProcessInfo.lpStartAddress);
                break;
            // 其他事件处理...
        }
        
        ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
    }
    return 0;
}

关键点总结

  1. 软件断点

    • 修改指令为0xCC(INT 3)
    • 需要修正EIP(EIP会停在断点+1的位置)
    • 系统断点和用户断点需要区分处理
  2. 内存断点

    • 通过修改内存页属性触发异常
    • 访问断点(PAGE_NOACCESS)和写入断点(PAGE_EXECUTE_READ)
    • 异常信息中包含访问类型(读/写)和访问地址
  3. 硬件断点

    • 使用CPU调试寄存器(Dr0-Dr7)
    • 最多4个硬件断点(Dr0-Dr3)
    • 通过Dr6判断哪个寄存器触发了异常
    • 单步异常和硬件断点异常共用异常类型
  4. 调试框架

    • 使用DEBUG_PROCESS标志创建调试进程
    • WaitForDebugEventContinueDebugEvent构成主循环
    • 处理多种调试事件(异常、进程/线程创建、DLL加载等)
  5. 线程上下文

    • 使用GetThreadContextSetThreadContext访问/修改寄存器
    • 硬件断点需要在线程上下文中设置
    • 上下文标志CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS获取完整信息

通过这三种断点技术的组合,可以实现功能强大的调试器,满足不同的调试需求。

Windows环境下的调试器探究 0x00 前言 在Windows环境中,调试器主要通过三种方式触发异常:软件断点、内存断点和硬件断点。本文将详细分析这三种方式的原理,并通过实际代码实现调试器的核心功能。 0x01 软件断点 原理分析 软件断点通过将目标指令替换为 0xCC (即 int 3 指令)实现: CPU检测到 INT 3 指令 查IDT表找到对应的中断处理函数 调用 CommonDispatchException 通过 KiDispatchException 分发异常 DbgkForwardException 收集并发送调试事件 关键点: 调试器会修改目标指令为 0xCC 异常处理流程会经过内核到用户态的转换 如果没有内核调试器存在,3环调试器才能接收到异常 调试进程会被挂起( DbgkpSuspendProcess ) 实现代码关键部分 调试循环框架 0x02 内存断点 原理分析 内存断点分为两种类型: 内存访问:内存被读写时产生中断 内存写入:内存被写入时产生中断 实现原理: 使用 VirtualProtectEx 修改内存页属性 内存访问断点:设置为 PAGE_NOACCESS (PTE的P位=0) 内存写入断点:设置为 PAGE_EXECUTE_READ (PTE的P位=1,R/W位=0) 异常处理流程: CPU访问错误的内存地址,触发页异常 查IDT表找到中断处理函数( nt!_KiTrap0E ) CommonDispatchException KiDispatchException DbgkForwardException 收集并发送调试事件 实现代码关键部分 0x03 硬件断点 原理分析 硬件断点依赖于CPU的调试寄存器(Dr0-Dr7): Dr0-Dr3:存储断点地址(最多4个硬件断点) Dr6:调试状态寄存器 Dr7:调试控制寄存器 Dr7关键位: L0/G0 ~ L3/G3:控制Dr0-Dr3是否有效(局部/全局) LENx(16-19位):断点长度(00=1字节,01=2字节,11=4字节) R/Wx(16-17位):断点类型(00=执行,01=写入,11=访问) 异常处理流程: CPU检测到当前线性地址与调试寄存器中的地址匹配 查IDT表找到中断处理函数( nt!_KiTrap01 ) CommonDispatchException KiDispatchException DbgkForwardException 收集并发送调试事件 实现代码关键部分 综合调试器实现 完整调试循环框架: 关键点总结 软件断点 : 修改指令为 0xCC (INT 3) 需要修正EIP(EIP会停在断点+1的位置) 系统断点和用户断点需要区分处理 内存断点 : 通过修改内存页属性触发异常 访问断点(PAGE_ NOACCESS)和写入断点(PAGE_ EXECUTE_ READ) 异常信息中包含访问类型(读/写)和访问地址 硬件断点 : 使用CPU调试寄存器(Dr0-Dr7) 最多4个硬件断点(Dr0-Dr3) 通过Dr6判断哪个寄存器触发了异常 单步异常和硬件断点异常共用异常类型 调试框架 : 使用 DEBUG_PROCESS 标志创建调试进程 WaitForDebugEvent 和 ContinueDebugEvent 构成主循环 处理多种调试事件(异常、进程/线程创建、DLL加载等) 线程上下文 : 使用 GetThreadContext 和 SetThreadContext 访问/修改寄存器 硬件断点需要在线程上下文中设置 上下文标志 CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS 获取完整信息 通过这三种断点技术的组合,可以实现功能强大的调试器,满足不同的调试需求。