调试器的原理

软件断点

  • int3指令
  • 机器码为1字节,即0xCC
  • 没有数量限制
  • 局限性
  1. 属于代码类断点,即可以让CPU执行到代码段内的某个地址时停下来,不适用于数据段和I/O空间
  2. 对于在ROM(只读存储器)中执行的程序(比如BIOS或者其他固件程序),无法动态增加软件断点。因为目标内存是只读的无法动态写入断点指令。这时就要使用我们后面要介绍的硬件断点。

实验
是用windbg给MessageBoxW下断点,windbg只有在程序运行的时候才会把断点的地方改为int3

0:011> u user32!MessageBoxW
USER32!MessageBoxW:
7594f280 8bff            mov     edi,edi
7594f282 55              push    ebp
7594f283 8bec            mov     ebp,esp
7594f285 833d941c977500  cmp     dword ptr [USER32!gfEMIEnable (75971c94)],0
7594f28c 7422            je      USER32!MessageBoxW+0x30 (7594f2b0)
7594f28e 64a118000000    mov     eax,dword ptr fs:[00000018h]
7594f294 ba9c219775      mov     edx,offset USER32!gdwEMIThreadID (7597219c)
7594f299 8b4824          mov     ecx,dword ptr [eax+24h]
0:011> bp user32!MessageBoxW

查看下断点的地方是否变成0xCC
我们再启一个windbg附加来观察,同一个进程不能被2个调试器附加,所以我们使用Non-invasive模式,非入侵模式
在这里插入图片描述

0:000> u user32!MessageBoxW
USER32!MessageBoxW:
7594f280 cc              int     3
7594f281 ff558b          call    dword ptr [ebp-75h]
7594f284 ec              in      al,dx
7594f285 833d941c977500  cmp     dword ptr [USER32!gfEMIEnable (75971c94)],0
7594f28c 7422            je      USER32!MessageBoxW+0x30 (7594f2b0)
7594f28e 64a118000000    mov     eax,dword ptr fs:[00000018h]
7594f294 ba9c219775      mov     edx,offset USER32!gdwEMIThreadID (7597219c)
7594f299 8b4824          mov     ecx,dword ptr [eax+24h]

硬件断点

  • 基于CPU的调试寄存器
  • 可以对代码、数据访问和IO访问设置断点
  • 断点被触发时,CPU产生的是1号异常
  • 受调试寄存器的数量限制
  • Windbg的ba命令设置的便是硬件断点
  • 在多处理器系统中,硬件断点是与CPU相关的,也就是说针对一个CPU设置的硬件断点并适用于其它CPU

Intel 80306以上的CPU给我们提供了调试寄存器用于软件调试,硬件断点是通过设置调试寄存器实现的。
在这里插入图片描述
图为Intel手册提供的32位操作系统下8个调试寄存器的图示,DR0-DR3为设置断点的地址,DR4和DR5为保留。

DR6为调试异常产生后显示的一些信息,DR7保存了断点是否启用、断点类型和长度等信息。

我们在使用硬件断点的时候,就是要设置调试寄存器,将断点的位置设置到DR0-DR3中,断点的长度设置到DR7的LEN0-LEN3中,将断点的类型设置到DR7的RW0-RW3中,将是否启用断点设置到DR7的L0-L3中。

设置硬件断点需要的DR0-DR3很简单,就是下断点的地址,DR7寄存器很复杂,位段信息结构体如下:

typedef struct _DBG_REG7
{
    /*
    // 局部断点(L0~3)与全局断点(G0~3)的标记位
    */
    unsigned L0 : 1;  // 对Dr0保存的地址启用 局部断点
    unsigned G0 : 1;  // 对Dr0保存的地址启用 全局断点
    unsigned L1 : 1;  // 对Dr1保存的地址启用 局部断点
    unsigned G1 : 1;  // 对Dr1保存的地址启用 全局断点
    unsigned L2 : 1;  // 对Dr2保存的地址启用 局部断点
    unsigned G2 : 1;  // 对Dr2保存的地址启用 全局断点
    unsigned L3 : 1;  // 对Dr3保存的地址启用 局部断点
    unsigned G3 : 1;  // 对Dr3保存的地址启用 全局断点
                      /*
                      // 【以弃用】用于降低CPU频率,以方便准确检测断点异常
                      */
    unsigned LE : 1;
    unsigned GE : 1;
    /*
    // 保留字段
    */
    unsigned Reserve1 : 3;
    /*
    // 保护调试寄存器标志位,如果此位为1,则有指令修改条是寄存器时会触发异常
    */
    unsigned GD : 1;
    /*
    // 保留字段
    */
    unsigned Reserve2 : 2;
 
    unsigned RW0 : 2;  // 设定Dr0指向地址的断点类型
    unsigned LEN0 : 2;  // 设定Dr0指向地址的断点长度
    unsigned RW1 : 2;  // 设定Dr1指向地址的断点类型
    unsigned LEN1 : 2;  // 设定Dr1指向地址的断点长度
    unsigned RW2 : 2;  // 设定Dr2指向地址的断点类型
    unsigned LEN2 : 2;  // 设定Dr2指向地址的断点长度
    unsigned RW3 : 2;  // 设定Dr3指向地址的断点类型
    unsigned LEN3 : 2;  // 设定Dr3指向地址的断点长度
}DBG_REG7, *PDBG_REG7;

需要注意的是,设置硬件断点时,断点的长度、类型和地址是有要求的。
在这里插入图片描述
如图所示,保存DR0-DR3地址所指向位置的断点类型(RW0-RW3)与断点长度(LEN0-LEN3),状态描述如下:
​ 00:执行 01:写入 11:读写
​ 00:1字节 01:2字节 11:4字节

设置硬件执行断点时,长度只能为1

设置读写断点时,如果长度为1,地址不需要对齐,如果长度为2,则地址必须是2的整数倍,如果长度为4,则地址必须是4的整数倍。

实现
实现硬件断点,首先要获取当前线程环境

//获取线程环境
CONTEXT g_Context = { 0 };
g_Context.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(hThread, &g_Context);

在CONTEXT结构体中,存放了诸多当前线程环境的信息,以下是从winnt.h文件中找到的CONTEXT结构体

typedef struct _CONTEXT {
 
    //
    // The flags values within this flag control the contents of
    // a CONTEXT record.
    //
    // If the context record is used as an input parameter, then
    // for each portion of the context record controlled by a flag
    // whose value is set, it is assumed that that portion of the
    // context record contains valid context. If the context record
    // is being used to modify a threads context, then only that
    // portion of the threads context will be modified.
    //
    // If the context record is used as an IN OUT parameter to capture
    // the context of a thread, then only those portions of the thread's
    // context corresponding to set flags will be returned.
    //
    // The context record is never used as an OUT only parameter.
    //
 
    DWORD ContextFlags;
 
    //
    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
    // included in CONTEXT_FULL.
    //
 
    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;
 
    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
    //
 
    FLOATING_SAVE_AREA FloatSave;
 
    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_SEGMENTS.
    //
 
    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;
 
    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_INTEGER.
    //
 
    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;
 
    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_CONTROL.
    //
 
    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;
 
    //
    // This section is specified/returned if the ContextFlags word
    // contains the flag CONTEXT_EXTENDED_REGISTERS.
    // The format and contexts are processor specific
    //
 
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
 
} CONTEXT;

从CONTEXT结构体中我们可以看到存放了调试寄存器 Dr0-Dr3和Dr6、Dr7,通过设置这些寄存器我们可以实现硬件断点。

已经获取了当前线程环境,接下来就是设置调试寄存器

//传入下断点的地址、类型、长度
void SetHardBP(DWORD addr, BreakPointHard type, BreakPointLen len)
{
    //利用上文中的DR7寄存器位段信息
    DBG_REG7 *pDr7 = (DBG_REG7 *)&g_Context.Dr7;
 
    if (len == 1)
    {
        //两字节的对齐粒度
        addr = addr - addr % 2;
    }
    else if (len == 3)
    {
        //四字节的对齐粒度
        addr = addr - addr % 4;
    }
 
    if (pDr7->L0 == 0)
    {
        g_Context.Dr0 = addr;   //利用Dr0寄存器存放地址
        pDr7->RW0 = type;       //Dr7寄存器中的RW0设置类型
        pDr7->LEN0 = len;        //Dr7寄存器中的LEN0设置长度
        pDr7->L0 = 1;            //Dr7寄存器中的L0启用断点
    }
    else if (pDr7->L1 == 0)
    {
        g_Context.Dr1 = addr;
        pDr7->RW1 = type;
        pDr7->LEN1 = len;
        pDr7->L1 = 1;
    }
    else if (pDr7->L2 == 0)
    {
        g_Context.Dr2 = addr;
        pDr7->RW2 = type;
        pDr7->LEN2 = len;
        pDr7->L2 = 1;
    }
    else if (pDr7->L3 == 0)
    {
        g_Context.Dr3 = addr;
        pDr7->RW3 = type;
        pDr7->LEN3 = len;
        pDr7->L3 = 1;
    }
}

调试寄存器的信息设置好之后,我们要将当前环境保存

//设置当前环境
SetThreadContext(hThread, &g_Context);

实现单步调试

当TF=1,CPU会发生中断,标志寄存器会自动压栈,在中断服务程序中,中断服务程序会改变(人为的改变)栈内TF位的设置TF=0,最后弹出标志寄存器给CPU,
所以不会产生你所说的反复中断(有些程序调试的时候需要单步中断,这时候只要把TF设置成1)

//设置TF标志位
void SetTrapFlag() {
    CONTEXT context = {0};
    GetDebuggeeContext(&context);
    context.EFlags |= 0x100;
    SetDebuggeeContext(&context);
 
}

实现步出

步出则是在ebp+4的地址设置断点,ebp+4保存的就是该函数的返回地址,也是上一个函数call指令的下一条指令

BOOL MoveOut()
{
    // 获取ebp
    CONTEXT    Context = {0};
    GetDebuggeeContext(&Context);
    // 获取ebp+4处保存的返回地址
    SIZE_T    addr = 0;
    if(!ReadDebuggeeMemory(Context.Ebp + 4,sizeof(addr),(LPVOID)&addr))
    {
        return FALSE;
    }
    // 设置一次性断点
    SetCCBreakPointAt(addr,SOFTTYPE_ONCE);
    return TRUE;
}

步过的实现

步过则是获得下一条指令长度,在下下条指令下断点

//步过,获得eip下一条指令的长度,越过这条指令下断点,这样就不会进入call里面
BOOL MoveOver()
{
    CONTEXT    Context = {0};
    GetDebuggeeContext(&Context);
    SIZE_T addr = GetCoodeLen(Context.Eip) + Context.Eip;
    SetCCBreakPointAt(addr,SOFTTYPE_ONCE);
    return TRUE;
}

用户态调试基本流程

编写一个最简单的附加调试器

int main(int argc,TCHAR *argv[])
{
    DWORD dwPID;
       BOOL waitEvent = TRUE;
    if (argc > 1) {
        dwPID = atoi(argv[1]);
    }
    else {
        printf("usage: MyDebugger.exe dwPID\n");
        exit(0);
    }
 
    DebugActiveProcess(dwPID);
    while (waitEvent)
    {
        DEBUG_EVENT MyDebugInfo;
        waitEvent = WaitForDebugEvent(&MyDebugInfo, INFINITE); // Waiting
        switch (MyDebugInfo.dwDebugEventCode)
        {
            case EXIT_PROCESS_DEBUG_EVENT:
                waitEvent = FALSE
                break;
        }
        if (waitEvent) {
            ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_CONTINUE);
        }
    }
    return 0;
}

DebugActiveProcess 这个调试API对目标PID进程进行调试附加操作,如果我们要在程序创建的时候就对程序进行调试可以在Debugger中执行CreateProcess并将第6个参数传入DEBUG_ONLY_THIS_PROCESS,这样设置之后,子进程发生的调试事件会通知给父进程处理。

CreateProcess(path, // 可执行模块路径
    NULL, // 命令行
    NULL, // 安全描述符
    NULL, // 线程属性是否可继承
    FALSE, // 否从调用进程处继承了句柄
    DEBUG_ONLY_THIS_PROCESS, // 以“只”调试的方式启动
    NULL, // 新进程的环境块
    NULL, // 新进程的当前工作路径(当前目录)
    &stcStartupInfo, // 指定进程的主窗口特性
    &stcProcInfo)) // 接收新进程的识别信息

DEBUG_EVENT中的dwDebugEventCode表示调试信息的种类,对于DEBUG_EVENT详细的介绍可以查看MSDN,简单来说就是用共用体来存储具体的数据。

typedef struct _DEBUG_EVENT {
    DWORD dwDebugEventCode;
    DWORD dwProcessId;
    DWORD dwThreadId;
    union {
        EXCEPTION_DEBUG_INFO Exception;
        CREATE_THREAD_DEBUG_INFO CreateThread;
        CREATE_PROCESS_DEBUG_INFO        
        CreateProcessInfo;
        EXIT_THREAD_DEBUG_INFO ExitThread;
        EXIT_PROCESS_DEBUG_INFO ExitProcess;
        LOAD_DLL_DEBUG_INFO LoadDll;
        UNLOAD_DLL_DEBUG_INFO UnloadDll;
        OUTPUT_DEBUG_STRING_INFO DebugString;
        RIP_INFO RipInfo;
    } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

用户态 DebugActiveProcess 实现

DebugActiveProcess 具体实现代码,主要就是调用了DbgUiConnectToDbg ,ProcessIdToHandle和DbgUiDebugActiveProcess这三个函数
在这里插入图片描述
DbgUiConnectToDbg
首先判断TEB->DbgSsReserved[1]是否保存着调试对象的句柄,如果存在则直接返回函数如果不存在就进行初始化并调用NtCreateDebugObject创建调试对象
在这里插入图片描述
ProcessIdToHandle
如果是伪句柄则调用CsrGetProcessId获取csrss.exe的PID,然后调用NtOpenProcess获得对应进程句柄,给后续调用做准备。
在这里插入图片描述
DbgUiDebugActiveProcess
此函数首先传入进程和调试对象的句柄进入内核,然后调用DbgUiIssueRemoteBreakin函数创建线程开始地址为DbgUiRemoteBreakin的远程线程让被调试进程断下来,如果远程线程设置失败则调用DbgUiStopDebugging停止调试。
在这里插入图片描述
全览图
在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值