• kr 第三阶段(三)调试器


    附件下载链接

    调试框架

    • 特点:事件驱动,事件响应。 Win32 程序是消息驱动响应的基址,而在调试器则是事件驱动响应,有事件则处理,无事件则去做别的事。
      • 事件:整个调试框架是建立在异常的基础之上的基本单位。
      • 响应:通过事件循环体,持续的获取事件,处理事件。

    在这里插入图片描述

    创建调试会话

    创建进程

    创建进程主要是通过 CreateProcess 函数来实现的,该函数的原型如下:

    BOOL CreateProcess(
      LPCTSTR lpApplicationName,                 // name of executable module
      LPTSTR lpCommandLine,                      // command line string
      LPSECURITY_ATTRIBUTES lpProcessAttributes, // SD
      LPSECURITY_ATTRIBUTES lpThreadAttributes,  // SD
      BOOL bInheritHandles,                      // handle inheritance option
      DWORD dwCreationFlags,                     // creation flags
      LPVOID lpEnvironment,                      // new environment block
      LPCTSTR lpCurrentDirectory,                // current directory name
      LPSTARTUPINFO lpStartupInfo,               // startup information
      LPPROCESS_INFORMATION lpProcessInformation // process information);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    下面是 CreateProcess 函数的一些参数的说明:

    • lpApplicationName:指向可执行文件的路径或者文件名。
    • lpCommandLine:指向一个以 null 结尾的字符串,包含了命令行参数。
    • lpProcessAttributeslpThreadAttributes:用于指定进程和线程的安全性属性,默认为 NULL。
    • bInheritHandles:指示新进程是否继承父进程的句柄。
    • dwCreationFlags:指定进程的创建标志,例如是否创建一个新的控制台窗口、创建方式等。
    • lpEnvironment:指定新进程的环境变量,如果为 NULL,则继承父进程的环境变量。
    • lpCurrentDirectory:指定新进程的当前工作目录,如果为 NULL,则继承父进程的当前目录。
    • lpStartupInfo:指向一个 STARTUPINFO 结构体,用于指定新进程的一些启动参数,例如窗口大小、显示方式等。
    • lpProcessInformation:指向一个 PROCESS_INFORMATION 结构体,用于接收新进程的相关信息,例如进程句柄和线程句柄。
    • CreateProcess 函数成功执行时会返回非零值,否则返回零。

    针对 dwCreationFlags 参数,有 DEBUG_ONLY_THIS_PROCESSDEBUG_PROCESS 两个标志参数用于指定创建进程时的调试模式:

    • DEBUG_PROCESS:调试器会受到目标进程及由目标进程创建的所有子进程中发生的调试事件。
    • DEBUG_ONLY_THIS_PROCESS:调试器只收到目标进程的调试事件,对子进程不响应。

    调试器通常设置的都是 DEBUG_ONLY_THIS_PROCESS 标志位。

    附加

    如果想要通过附加调试某一进程需要使用 DebugActiveProcess 函数,该函数定义如下:

    BOOL DebugActiveProcess(
      DWORD dwProcessId   // process to be debugged
    );
    
    • 1
    • 2
    • 3
    • dwProcessId:要调试的目标进程的进程标识符(PID)。
    • 函数调用成功时,返回值为非零值;否则,返回值为零。
    • 该函数附加进程效果与创建进程使用 DEBUG_ONLY_THIS_PROCESS 效果类似。

    如果想脱离附加的进程可以使用 DebugActiveProcessStop 函数,该函数定义如下:

    BOOL DebugActiveProcessStop(
      DWORD dwProcessId
    );
    
    • 1
    • 2
    • 3
    • dwProcessId:要调试的目标进程的进程标识符(PID)。
    • 函数调用成功时,返回值为非零值;否则,返回值为零。

    然而有时脱离附加的进程会导致被附加的进程退出,这时候就需要在调用 DebugActiveProcessStop 函数之前调用 DebugSetProcessKillOnExit 来防止被调试进程退出,该函数定义如下:

    BOOL DebugSetProcessKillOnExit (  BOOL KillOnExit);
    
    • 1
    • KillOnExit:指定调试器在退出时是否终止被调试的进程。如果设置为 TRUE,调试器在退出时将终止被调试的进程;如果设置为 FALSE,调试器在退出时不会终止被调试的进程。
    • 函数调用成功时,返回值为非零值;否则,返回值为零。

    循环接受调试事件

    等待调试事件使用的是 WatiForDebugEvent 函数,该函数定义如下:

    BOOL WaitForDebugEvent(
      LPDEBUG_EVENT lpDebugEvent,  // debug event information
      DWORD dwMilliseconds         // time-out value
    );
    
    • 1
    • 2
    • 3
    • 4
    • lpDebugEvent:一个指向 DEBUG_EVENT 结构的指针,用于接收调试事件的信息。DEBUG_EVENT 结构包含了调试事件的类型和相关的数据,如调试进程、线程、异常等。
    • dwMilliseconds:等待调试事件的超时时间,以毫秒为单位。如果设置为 INFINITE (0xFFFFFFFF),则表示无限等待,直到有调试事件发生。如果设置为零,则表示不等待,立即返回。
    • 函数调用成功时,返回值为非零值;否则,返回值为零。

    接收调试事件的 DEBUG_EVENT 结构体定义如下:

    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; 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • dwDebugEventCode:表示调试事件的类型。可以是以下值之一:
      • EXCEPTION_DEBUG_EVENT:异常事件。
      • CREATE_THREAD_DEBUG_EVENT:线程创建事件。
      • CREATE_PROCESS_DEBUG_EVENT:进程创建事件。
      • EXIT_THREAD_DEBUG_EVENT:线程退出事件。
      • EXIT_PROCESS_DEBUG_EVENT:进程退出事件。
      • LOAD_DLL_DEBUG_EVENT:动态链接库加载事件。
      • UNLOAD_DLL_DEBUG_EVENT:动态链接库卸载事件。
      • OUTPUT_DEBUG_STRING_EVENT:输出调试字符串事件。
      • RIP_EVENT:RIP(调试错误)事件。
    • dwProcessId:调试事件所属的进程标识符(PID)。
    • dwThreadId:调试事件所属的线程标识符(TID)。
    • u:一个联合体,用于存储不同类型的调试事件数据。根据 dwDebugEventCode 的不同值,使用相应的字段来访问具体的调试事件数据。

    处理调试事件

    调试器具体代码实现。

    提交处理结果

    在处理调试事件时被调试进程是处于挂起的状态,因此提交处理结果是告诉被调试进程是否继续运行。提交处理结果的函数是 ContinueDebugEvent,该函数定义如下:

    BOOL ContinueDebugEvent(
      DWORD dwProcessId,       // process to continue
      DWORD dwThreadId,        // thread to continue
      DWORD dwContinueStatus   // continuation status
    );
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • dwProcessId:要继续运行的进程的标识符(PID)。
    • dwThreadId:要继续运行的线程的标识符(TID)。
    • dwContinueStatus:继续运行的状态。它可以是以下值之一:
      • DBG_CONTINUE:继续运行被调试进程。
      • DBG_EXCEPTION_NOT_HANDLED:异常未被处理,系统按照正常的异常处理流程派发异常。
    • 函数返回一个布尔值,表示操作是否成功。如果函数调用成功,返回值为非零值;如果函数调用失败,返回值为零。

    环境搭建

    所需工具都可以在附件中下载。

    由于汇编代码可读性差,后面改用 C++ 实现(感觉w老师自己都蚌埠住了)。

    汇编开发环境

    由于这里使用的是汇编来编写调试框架,因此需要安装 RadASM(IDE) 和 MASM32(开发环境)。

    • RadASM 直接下载 RadASM-XXX-FullPackage.zip 然后解压到安装目录即可。
    • MASM32 就点下载链接然后一路默认安装即可,注意盘符最好选 C 盘,因为我这里 RadASM 是在 C 盘找 MASM32 的。

    因为我们开发的是命令行式的调试器,因此创建项目时工程类型选择 Colsole App ,其他默认设置即可。

    反汇编引擎

    反汇编引擎这里我使用的是 Udis86 。该项目在 github 下载的源码缺少文件无法编译,因此我是从这里下载的源码进行编译。

    由于该项目使用汇编调用过于麻烦,因此我先实现了一个提供汇编的接口项目 MuUdis86Dll

    #include "pch.h"
    #include "udis86.h"
    
    #pragma comment(lib, "libudis86.lib")
    
    extern "C" {
        __declspec(dllexport) DWORD GetAsm(
                BYTE *pCode,
                DWORD nCodeLen,
                DWORD nEIP,
                CHAR *szAsmBuf,
                DWORD dwAsmBufLen) {
            ud u;
            ud_init(&u);
            ud_set_mode(&u, 32);
            ud_set_pc(&u, nEIP);
            ud_set_input_buffer(&u, pCode, nCodeLen);
            ud_set_syntax(&u, UD_SYN_INTEL);
            ud_disassemble(&u);
            uint32_t nLen = ud_insn_len(&u);
            strcpy_s(szAsmBuf, dwAsmBufLen, ud_insn_asm(&u));
            return nLen;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    之后只需要在汇编项目中导入该项目及接口即可调用 GetAsm 函数实现反汇编。注意这里我没有声明 GetAsm 的调用约定因此默认采用 __cdecl 调用约定,因此在汇编中需要再改接口声明中加 C 来声明调用约定。

    includelib MyUdis86Dll.lib
    
    GetAsm proto C pCode:LPBYTE,nCodeLen:DWORD,nEip:DWORD,szAsmBuf:LPSTR,dwAsmBufLen:DWORD
    
    • 1
    • 2
    • 3

    软件断点

    什么是软件断点

    软件断点即 CC 断点或 int3 断点,OD中的快捷键F2,使用率也是最多的断点类型。以调试方式创建的进程,必定会有一个系统断点。

    软件断点实现思路

    • 断得下来:需要再软件断点位置写入 int3(0xcc)
    • 走的过去:断点处的指令能够正常执行不受影响。
      • 触发 int3 异常后需要还原断点位置的指令。
      • 由于 int3 指令被执行了,因此还需要将 eip 寄存器减 1(int3 指令长度)。
    • 下次还来:下次执行到断点位置时还能断下来。
      • 由于触发 int3 异常后调试器将断点位置的指令恢复,因此在执行完断点位置的指令后需要再次在该位置写入 int3 指令。
      • 可以在触发 int3 异常时 TF 标志寄存器(EFLAGS 标志寄存器第 8 位)置位,这样在执行下一条指令的时候会触发单步异常。
      • 调试器在接收到该异常的时候可以恢复 int3 断点。

    代码实现

    创建断点实际上就是在断点位置写入 \xCC

        void HandleBpCmd(const std::vector<std::tstring> &args) {
            if (args.size() != 2) {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            } 
            PVOID lpBreakPoint = (PVOID) tcstoull(args[1]);
            if (m_BreakPointList.count(lpBreakPoint)) {
                return;
            }
            m_Process.ReadMemory(lpBreakPoint, &m_BreakPointList[lpBreakPoint], sizeof(m_BreakPointList[lpBreakPoint]));
            m_Process.WriteMemory(lpBreakPoint, (LPVOID) &INT3, sizeof(INT3));
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    如果调试事件 EXCEPTION_DEBUG_EVENT 到来且异常类型为断点异常 EXCEPTION_BREAKPOINT 那么:

    • 将 TF 标志寄存器置位。
    • 修正 eip 寄存器减 1 。
    • 恢复断点处的指令。
    • m_lpNeedRecoverBreakPoint 指向断点位置,以便后续 EXCEPTION_SINGLE_STEP 异常到来时恢复断点。
    • 进入命令行交互。
                case EXCEPTION_BREAKPOINT: {
                    if (m_bIsSysBreakPoint) {
                        std::cout << _T("EXCEPTION_BREAKPOINT") << std::endl;
                        m_bIsSysBreakPoint = FALSE;
                    } else {
                        CONTEXT ctx{};
                        ctx.ContextFlags = CONTEXT_CONTROL;
                        m_Process.GetContext(ctx);
                        ctx.Eip--;
                        if (m_BreakPointList.count((LPVOID) ctx.Eip)) {
                            ctx.EFlags |= 0x100;
                            m_lpNeedRecoverBreakPoint = (LPVOID) ctx.Eip;
                            m_Process.WriteMemory((LPVOID) ctx.Eip, &m_BreakPointList[(LPVOID) ctx.Eip], sizeof(m_BreakPointList[(LPVOID) ctx.Eip]));
                        } 
                        m_Process.SetContext(ctx);
                    }
                    return PareseCommandLine();
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    如果调试事件 EXCEPTION_DEBUG_EVENT 到来且异常类型为单步异常 EXCEPTION_SINGLE_STEPm_lpNeedRecoverBreakPoint 不为 NULL 则需要将断点回复。

                case EXCEPTION_SINGLE_STEP: {
                    if (m_lpNeedRecoverBreakPoint != NULL) {
                        m_Process.WriteMemory(m_lpNeedRecoverBreakPoint, (LPVOID) &INT3, sizeof(INT3));
                        m_lpNeedRecoverBreakPoint = NULL;
                    }
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    效果展示

    效果如下:

    skydbg> u 0x772d78f1
    772D78F1 mov dword [ebp-0x4], 0xfffffffe
    772D78F8 mov ecx, [ebp-0x10]
    772D78FB mov [fs:0x0], ecx
    772D7902 pop ecx
    772D7903 pop edi
    772D7904 pop esi
    772D7905 pop ebx
    772D7906 leave
    skydbg> bp 772D7902
    skydbg> g
    772D7902 pop ecx
    skydbg> u
    772D7902 pop ecx
    772D7903 pop edi
    772D7904 pop esi
    772D7905 pop ebx
    772D7906 leave
    772D7907 ret
    772D7908 int3
    772D7909 int3
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    单步

    单步命令实现思路

    单步指令分为单步步入(t 命令)和单步步过(p 命令),这两条命令只有在 call 指令处会有所不同。

    • 单步步入:逢 call 则入
      • 设置 TF 标志位即可
    • 单步步过:逢 call 则过
      • 如果当前指令不是 call 则与单步步入相同
      • 否则在 call 的下一条指令设置临时断点

    单步命令代码实现

    在这里插入图片描述
    为了能够让单步步入,单步步过,用户断点三者能够和谐相处,OnException 实现如上图所示。

    注意:单步异常到来时当前指令还没有执行,而断点异常到来时当前指令(int3)已经被执行。

        DWORD OnException(DEBUG_EVENT& de) {
            switch (de.u.Exception.ExceptionRecord.ExceptionCode) {
                case EXCEPTION_BREAKPOINT: {
                    if (m_bIsSysBreakPoint) {
                        std::cout << _T("EXCEPTION_BREAKPOINT") << std::endl;
                        m_bIsSysBreakPoint = FALSE;
                    } else {
                        CONTEXT ctx{};
                        ctx.ContextFlags = CONTEXT_CONTROL;
                        m_Process.GetContext(ctx);
                        ctx.Eip--;
                        if (m_BreakPointList.count((LPVOID) ctx.Eip)) {
                            ctx.EFlags |= 0x100;
                            m_lpNeedRecoverBreakPoint = (LPVOID) ctx.Eip;
                            m_Process.WriteMemory((LPVOID) ctx.Eip, &m_BreakPointList[(LPVOID) ctx.Eip], sizeof(m_BreakPointList[(LPVOID) ctx.Eip]));
                        } 
                        if ((LPVOID) ctx.Eip == m_lpSignelStepBreakPoint.first) {
                            if (!m_BreakPointList.count((LPVOID) ctx.Eip)) {
                                m_Process.WriteMemory((LPVOID) ctx.Eip, &m_lpSignelStepBreakPoint.second, sizeof(m_lpSignelStepBreakPoint.second));
                            }
                            m_lpSignelStepBreakPoint = {};
                            m_nSingleStepCountP--;
                        } 
                        m_Process.SetContext(ctx);
                    }
                    return PareseCommandLine();
                }
                case EXCEPTION_SINGLE_STEP: {
                    if (m_lpNeedRecoverBreakPoint != NULL) {
                        m_Process.WriteMemory(m_lpNeedRecoverBreakPoint, (LPVOID) &INT3, sizeof(INT3));
                        m_lpNeedRecoverBreakPoint = NULL;
                    }
                    if (m_nSingleStepCountT || m_nSingleStepCountP) {
                        if (m_nSingleStepCountT) { 
                            m_nSingleStepCountT--; 
                            assert(m_nSingleStepCountP == 0);
                        }
                        if (m_nSingleStepCountP) {
                            m_nSingleStepCountP--;
                            assert(m_nSingleStepCountT == 0);
                        }
                        CONTEXT ctx{};
                        ctx.ContextFlags = CONTEXT_CONTROL;
                        m_Process.GetContext(ctx);
                        if (m_BreakPointList.count((LPVOID)ctx.Eip)) {
                            ctx.EFlags |= 0x100;
                            m_lpNeedRecoverBreakPoint = (LPVOID) ctx.Eip;
                            m_Process.WriteMemory((LPVOID) ctx.Eip, &m_BreakPointList[(LPVOID) ctx.Eip], sizeof(m_BreakPointList[(LPVOID) ctx.Eip]));
                            m_Process.SetContext(ctx);
                        }
                        return PareseCommandLine();
                    }
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    针对单步步入命令只需要 TF 置位。

        void HandleTCmd(const std::vector<std::tstring> &args) {
            if (args.size() > 2) {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
            if (!args.empty()) {
                m_nSingleStepCountT = args.size() == 2 ? tcstoull(args[1]) : 1;
            }
            CONTEXT ctx{};
            ctx.ContextFlags = CONTEXT_CONTROL;
            m_Process.GetContext(ctx);
            ctx.EFlags |= 0x100;
            m_Process.SetContext(ctx);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    单步步过命令需要判断指令是否是 call 指令,如果不是 call 指令处理方式和单步步过相同,否则需要再 call 指令的下一条指令设置临时断点。

        void HandlePCmd(const std::vector<std::tstring>& args) {
            if (args.size() > 2) {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
            if (!args.empty()) {
                m_nSingleStepCountP = args.size() == 2 ? tcstoull(args[1]) : 1;
            }
    
            LPVOID lpEip = m_Process.GetPC();
            std::string Code, Asm;
            m_Process.ReadMemory(lpEip, Code, 16);
            DWORD dwCodeLen = m_Asm.GetOneAsm(Code, lpEip, Asm);
            if (Asm.starts_with("call")) {
                m_lpSignelStepBreakPoint.first = (LPVOID) ((SIZE_T) lpEip + dwCodeLen);
                m_Process.ReadMemory(m_lpSignelStepBreakPoint.first, &m_lpSignelStepBreakPoint.second, sizeof(m_lpSignelStepBreakPoint.second));
                m_Process.WriteMemory(m_lpSignelStepBreakPoint.first, (LPVOID) &INT3, sizeof(INT3));
            } else {
                CONTEXT ctx{};
                ctx.ContextFlags = CONTEXT_CONTROL;
                m_Process.GetContext(ctx);
                ctx.EFlags |= 0x100;
                m_Process.SetContext(ctx);
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    这里我实现单步支持 trace 功能,即一次可以执行多步并记录执行过的命令,因此对于单步步入和步过我都记录了这条命令的剩余执行次数。在用户命令交互函数 PareseCommandLine 中,如果单步命令还有剩余执行次数则直接调用对应命令的处理函数自动执行。

        DWORD PareseCommandLine() {
            ShowOneAsm(m_Process.GetPC());
            if (m_nSingleStepCountT) {
                assert(m_nSingleStepCountP == 0);
                HandleTCmd({});
                return DBG_CONTINUE;
            }
            if (m_nSingleStepCountP) {
                assert(m_nSingleStepCountT == 0);
                HandlePCmd({});
                return DBG_CONTINUE;
            }
            ...
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    效果展示

    首先 trace 功能能够不受断点影响,且断点不受 trace 功能影响。

    skydbg> u 0x772d78f1
    772D78F1 mov dword [ebp-0x4], 0xfffffffe
    772D78F8 mov ecx, [ebp-0x10]
    772D78FB mov [fs:0x0], ecx
    772D7902 pop ecx
    772D7903 pop edi
    772D7904 pop esi
    772D7905 pop ebx
    772D7906 leave
    skydbg> bp 772D7902
    skydbg> bp 772D7903
    skydbg> bp 772D7904
    skydbg> g
    772D7902 pop ecx
    skydbg> t 5
    772D7903 pop edi
    772D7904 pop esi
    772D7905 pop ebx
    772D7906 leave
    772D7907 ret
    skydbg> u 772D7902 6
    772D7902 int3
    772D7903 int3
    772D7904 int3
    772D7905 pop ebx
    772D7906 leave
    772D7907 ret
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    步过功能遇到 call 指令不会步入且临时断点自动去除。

    skydbg> p 5
    772D7905 pop ebx
    772D7906 leave
    772D7907 ret
    772D1D3E call 0x7728b6c4
    772D1D43 cmp byte [0x7734d1ae], 0x0
    skydbg> u 772D1D3E 3
    772D1D3E call 0x7728b6c4
    772D1D43 cmp byte [0x7734d1ae], 0x0
    772D1D4A jnz 0x772d1de5
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    利用单步异常实现反调试

    主要利用了调试器先于 SEH 接管单步异常来实现反调试的特性(实际上这里的单步异常换成其他异常也是可以的)。
    在这里插入图片描述

    硬件断点

    调试寄存器

    IA-32 处理器定义了 8 个调试寄存器,分别称为 DR0~DR7 。这个 8 个寄存器结构如下图所示:
    在这里插入图片描述

    • DR0~DR3:调试地址寄存器,用于保存 4 个硬件断点的地址。
    • DR4~DR5:保留未使用。
    • DR6:调试状态寄存器,用于在调试事件发生时向调试器报告详细信息。
      • B0~B3:如果其中任何一个置位,则表示是相应的 DR0~DR3 断点引发的调试陷阱。
      • BD:检测到访问调试寄存器,这一位与 DR7GD 位相联系,当 GD 位被置为 1,而且 CPU 发现了要修改调试寄存器(DR0~DR7)的指令时,CPU 会停止继续执行这条指令,把 BD 位设为 1,然后把执行权交给调试异常(#DB)处理程序。
      • BS:单步,这一位与标志寄存器的 TF 位相联系,如果该位为 1,则表示异常是由单步执行(single step)模式触发的。与导致调试异常的其他情况相比,单步情况的优先级最高,因此当此标志为 1 时,也可能有其他标志也为 1。
      • BT:任务切换,这一位与任务状态段(TSS)的 T 标志(调试陷阱标志,debug trap flag)相联系。当 CPU 在进行任务切换时,如果发现下一个任务的 TSS 的 T 标志为 1,则会设置 BT 位,并中断到调试中断处理程序。
      • DR6 寄存器的值建议在每次异常提交之前清除。
    • DR7:调试控制寄存器,用于进一步定义断点的中断条件。
      • R/W0~R/W3:读写域,分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来指定被监控地址的访问类型。
        • 00:执行断点
        • 01:写断点
        • 10:386 和 486 不支持此组合。对于以后的 CPU,可以通过把 CR4 寄存器的 DE(调试扩展)位设为 1 启用该组合,其含义为“当相应地址进行输入输出(即 I/O 读写)时中断”
        • 11:读写断点,但是从该地址读取指令除外。
      • LEN0~LEN3:长度域, 分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来指定被监控区域的长度。
        • 00:1 字节长
        • 01:2 字节长
        • 10:8 字节长(奔腾 4 或至强 CPU)或未定义(其他处理器)
        • 11:4 字节长
        • 注意:如果对应 R/Wn 为 0(即执行指令中断),那么这里的设置应该为 0 。
      • L0~L3:局部断点启用, 分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来启用或禁止对应断点的局部匹配。
        • 如果该位设为 1,当 CPU 在当前任务(线程,寄存器是线程独占的)中检测到满足所定义的断点条件时便中断,并且自动清除此位。
        • 如果该位设为 0,便禁止此断点。
      • G0~G3:全部断点启用,分别与 DR0~DR3 这 4 个调试地址寄存器相对应,用来全局启用或禁止对应的断点(实测没有用,要想真正设置区局断点需要遍历进程中的所有线程然后都设置 Ln,x64dbg 和 OllyDbg 都是这么实现的)。
        • 如果该位设为 1,当 CPU 在任何任务中检测到满足所定义的断点条件时便中断,不会自动清除此位。
        • 如果该位设为 0,便禁止此断点。
      • LEGE:启用局部或者全局(精确)断点。从 486 开始的 IA-32 处理器都忽略这两位的设置。此前这两位是用来启用或禁止数据断点匹配的。对于早期的处理器,当设置有数据断点时,需要启用本设置,这时CPU会降低执行速度,以监视和保证当有指令要访问符合断点条件的数据时产生调试异常。
      • GD:启用或禁止对调试寄存器的保护。当设为 1 时,如果 CPU 检测到将修改调试寄存器(DR0~DR7)的指令,CPU 会在执行这条指令前产生一个调试异常。

    硬件断点实现思路

    首先需要明确,对于硬件断点:

    • 如果是执行断点,那么程序会在断点所在指令执行前断下来。因为 CPU 需要先取指令才能执行指令,而取指令的时候程序就因为硬件断点断下来,此时指令没有执行。
    • 如果是内存访问断点,那么程序会在访问断点所在内存的指令执行完后断下来。因为只有执行访存指令时 CPU 才能去访问对应内存,因此程序因为硬件断点断下来时这条访存指令已经执行完了。

    与软件断点相同,硬件断点同样需要满足:

    • 断得下来:需要在目标线程对应的调试寄存器中设置硬件断点。(需要再执行到程序入口点的时候再下硬件断点,这样硬件断点对应的线程是主线程)
    • 走的过去:断点处的指令能够正常执行不受影响。
      • 如果是内存访问断点断下来的时候指令已经执行完了,这个条件已经满足。
      • 如果是执行断点那么触发硬件断点后需要删除内存断点。
    • 下次还来:下次执行到断点位置时还能断下来。
      • 如果是执行断点那么在断下来的时候需要设置 TF 标志寄存器并且记录硬件断点。
      • 下一次单步异常的时候重新设置硬件断点。

    硬件断点代码实现

    示例程序设置硬件断点的命令是 ba <条件> <长度> <地址> 。设置硬件断点有以下步骤:

    • 在调试地址寄存器中写入断点地址
    • 将对应的有效位 DR7.Ln 置位
    • 设置 DR7.R/Wn 断点条件
    • 设置 DR7.LENn 断点长度
        BOOL SetHBreakPoint(HBP &hbp) {
            int index = 0;
            while (m_HBreakPointList[index].InUse && index < 4) {
                index++;
            }
            if (index == 4) {
                std::tcout << _T("There are not enough hardware breakpoints.") << std::endl;
                return FALSE;
            }
            m_HBreakPointList[index] = hbp;
    
            CONTEXT ctx{};
            ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS;
            m_Process.GetContext(ctx);
            (&ctx.Dr0)[index] = (DWORD) hbp.Addr;
            ctx.Dr7 |= 1U << (index * 2);
            (ctx.Dr7 &= ~(0b1111u << (16 + index * 4))) |= (hbp.RL * 1U) << (16 + index * 4);
            ctx.Dr6 = 0;
            m_Process.SetContext(ctx);
            return TRUE;
        }
    
        void HandleBACmd(const std::vector<std::tstring> &args) {
            if (args.size() != 4) {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
    
            int index = 0;
            while (m_HBreakPointList[index].InUse && index < 4) {
                index++;
            }
    
            if (index == 4) {
                std::tcout << _T("There are not enough hardware breakpoints.") << std::endl;
                return;
            }
    
            HBP hbp{};
            if (args[1] == _T("e")) {
                hbp.RW = 0;
            } else if (args[1] == _T("r")) {
                hbp.RW = 1;
            } else if (args[1] == _T("w")) {
                hbp.RW = 3;
            } else {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
    
            if (args[2] == _T("1")) {
                hbp.LEN = 0;
            } else if (args[2] == _T("2")) {
                hbp.LEN = 1;
            } else if (args[2] == _T("4")) {
                hbp.LEN = 3;
            } else {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
            if (hbp.RW == 0) {
                hbp.LEN = 0;
            }
            hbp.InUse = TRUE;
            hbp.Addr = (LPVOID) tcstoull(args[3]);
            SetHBreakPoint(hbp);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67

    单步异常捕获到硬件断点后如果是硬件执行断点则:

    • 删除硬件断点
    • 记录硬件断点信息
    • 设置 TF 标志位以便下一次单步异常恢复硬件断点
                    if (m_NeedRecoverHBreakPoint.InUse) {
                        SetHBreakPoint(m_NeedRecoverHBreakPoint);
                        m_NeedRecoverHBreakPoint.InUse = FALSE;
                    }
                    ...
                        CONTEXT ctx{};
                        ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS | CONTEXT_CONTROL;
                        m_Process.GetContext(ctx);
                        if (ctx.Dr6 & 0b1111u) {
                            std::tcout << _T("Hit the Hardware BreakPoint" ) << std::endl;
                            unsigned long index;
                            _BitScanForward(&index, ctx.Dr6);
                            if (!(ctx.Dr7 >> (16 + index * 4) & 0b11)) {
                                m_NeedRecoverHBreakPoint = m_HBreakPointList[index];
                                ResetHBreakPoint(index);
                                ctx.ContextFlags = CONTEXT_CONTROL;
                                m_Process.GetContext(ctx);
                                ctx.EFlags |= 0x100;
                                m_Process.SetContext(ctx);
                            }
                            return PareseCommandLine();
                        } else {
                            return DBG_CONTINUE;
                        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    效果展示

    为了体现硬件断点的特性,这里用硬件断点调试下面的代码:

    #include
    #include
    
    DWORD g_Val;
    
    int main() {
        for (int i = 0; i < 10; i++) {
            g_Val = i;
            std::cout << "i: " << i << std::endl;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    硬件执行断点:

    skydbg> bp 004AC9C0
    skydbg> g
    004AC9C0 push ebp
    skydbg> ba e 1 4A802F
    skydbg> g
    Hit the Hardware BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 0
    Hit the Hardware BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 1
    Hit the Hardware BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 2
    Hit the Hardware BreakPoint
    004A802F mov [0x506f40], ecx
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    硬件访问断点:

    skydbg> bp 004AC9C0
    skydbg> g
    004AC9C0 push ebp
    skydbg> ba w 4 506F40
    skydbg> g
    Hit the Hardware BreakPoint
    004A8035 push 0x4a27f0
    skydbg> g
    i: 0
    Hit the Hardware BreakPoint
    004A8035 push 0x4a27f0
    skydbg> g
    i: 1
    Hit the Hardware BreakPoint
    004A8035 push 0x4a27f0
    skydbg> g
    i: 2
    Hit the Hardware BreakPoint
    004A8035 push 0x4a27f0
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    硬件断点的适用场景

    如果程序采用修改自身代码(SMC)的方式进行反调试,那么常规的软件断点会被修改导致失效,而硬件断点因为是在寄存器中记录因此不受影响。

    然而如果程序通过清空自身的调试寄存器的方式进行反调试,那么硬件断点同样失效。

    内存断点

    内存断点原理

    内存断点和硬件断点中的内存访问断点功能相似,不过原理不同。

    内存访问断点是通过内存访问异常来实现的。调试器将内存断点所在的内存页的属性设置为不可访问。一旦有代码访问到该内存页就会产生内存访问异常。调试器会判断产生内存访问异常的地址是否是调试器设置的内存页以及用户设置的内存断点,从而确定断点是否被命中。
    在这里插入图片描述

    内存断点实现思路

    注意:与硬件断点实现的内存访问断点不同,检测到内存访问异常时访存指令还没有执行。

    内存断点同样需要满足:

    • 断得下来:将断点所在内存页属性设置为不可访问。注意需要再下一条指令时设置,因为设置内存断点前可能是因为内存断点断下来的,如果在当前指令时设置会造成断点不满足“走的过去”条件。
    • 走的过去:断点处的指令能够正常执行不受影响。
      • 检测到内存断点时需要还原内存断点所在内存页的属性使得内存访问指令可以正常执行。
    • 下次还来:下次执行到断点位置时还能断下来。
      • 内存断点断下来的时候需要设置 TF 标志寄存器并且记录内存断点。
      • 下一次单步异常的时候重新设置内存断点所在内存页不可访问。

    内存断点代码实现

    设置内存断点的时候不能着急修改内存属性,而是设置 TF 标志位能单步异常的时候修改内存属性。

        void HandleBMCmd(const std::vector<std::tstring> &args) {
            if (args.size() != 4) {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
    
            MBP mbp = {};
            if (args[1] == _T("r")) {
                mbp.RW = FALSE;
            } else if (args[1] == _T("w")) {
                mbp.RW = TRUE;
            } else {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
    
            mbp.Len = tcstoull(args[2]);
            mbp.Addr = (LPVOID) tcstoull(args[3]);
    
            LPVOID page = (LPVOID) ((SIZE_T) mbp.Addr & ~0xFFFU);
            if ((LPVOID) (((SIZE_T) mbp.Addr + mbp.Len) & ~0xFFFU) != page) {
                std::tcout << _T("Invalid Command") << std::endl;
                return;
            }
    
            if (!m_MBreakPointList.count(page)) {
                m_lpNeedRecoverMBreakPointPage.insert(page);
                CONTEXT ctx{};
                ctx.ContextFlags = CONTEXT_CONTROL;
                m_Process.GetContext(ctx);
                ctx.EFlags |= 0x100;
                m_Process.SetContext(ctx);
                m_MBreakPointList[page].first = m_Process.GetPageProtect(page);
            }
            m_MBreakPointList[page].second.emplace_back(mbp);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    对于内存访问异常则检查是否是内存断点所在内存页,如果是内存断点所在内存页

    • 设置恢复内存页属性。
    • 遍历该内存页中的所有内存断点找到一个与异常相匹配的断点,进入命令交互模式。
                case EXCEPTION_ACCESS_VIOLATION: {
                    BOOL RW = de.u.Exception.ExceptionRecord.ExceptionInformation[0];
                    LPVOID lpAddr = (LPVOID) de.u.Exception.ExceptionRecord.ExceptionInformation[1];
                    LPVOID page = (LPVOID) ((SIZE_T) lpAddr & ~0xFFFU);
                    if (m_MBreakPointList.count(page)) {
                        DWORD dwOldProtect;
                        m_Process.VirtualProtectEx(page, 0x1000, m_MBreakPointList[page].first, &dwOldProtect);
                        m_lpNeedRecoverMBreakPointPage.insert(page);
                        CONTEXT ctx{};
                        ctx.ContextFlags = CONTEXT_CONTROL;
                        m_Process.GetContext(ctx);
                        ctx.EFlags |= 0x100;
                        m_Process.SetContext(ctx);
                        for (auto &hbp: m_MBreakPointList[page].second) {
                            if (lpAddr >= hbp.Addr && lpAddr < (LPVOID) ((SIZE_T) hbp.Addr + hbp.Len) && RW == hbp.RW) {
                                std::tcout << _T("Hit the Memory BreakPoint" ) << std::endl;
                                return PareseCommandLine();
                            }
                            return DBG_CONTINUE;
                        }
                    } else {
                        return DBG_CONTINUE;
                    }
                }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    在单步异常时如果有需要恢复内存断点的内存页则设置该内存页不可访问属性。

                    if (!m_lpNeedRecoverMBreakPointPage.empty()) {
                        for (auto page: m_lpNeedRecoverMBreakPointPage) {
                            DWORD dwOldProtect;
                            m_Process.VirtualProtectEx(page, 0x1000, PAGE_NOACCESS, &dwOldProtect);
                        }
                        m_lpNeedRecoverMBreakPointPage.clear();
                    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    由于一个内存断点会拦截过滤程序对一整个内存页的访问,会多次查询相关断点条件,因此需要优化断点查询策略。这里可以使用多棵平衡树分别维护不同权限的内存断点区间的合并和查询。

    效果展示

    可以重复设置内存断点,遇到访存指令可以正常触发内存断点。

    skydbg> bm w 4 0506F40
    skydbg> g
    Hit the Memory BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 0
    Hit the Memory BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 1
    Hit the Memory BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 2
    Hit the Memory BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> bm w 4 0506F40
    skydbg> g
    i: 3
    Hit the Memory BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg> g
    i: 4
    Hit the Memory BreakPoint
    004A802F mov [0x506f40], ecx
    skydbg>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    内存断点适用的场景

    在游戏逆向的时候首先通过 CE 找到关键数据,然后再调试器内下内存断点找到访问数据的代码,从而找到关键代码或者其他关键数据。

  • 相关阅读:
    Python 数据分析的敲门砖 jieba模块中文分词
    U9二次开发之BP定时任务插件开发
    java毕业设计面向大学生心理健康服务平台源码+lw文档+mybatis+系统+mysql数据库+调试
    c++_learning-并发与多线程
    Springboot毕业设计毕设作品,微信网上图书商城购物小程序设计与实现
    Java数组编程练习题(面试题)
    电脑重装系统后鼠标动不了该怎么解决
    jvm oom内存溢出,导出dump,使用mat进行问题分析
    asp.net core、c#关于路径的总结
    R语言dplyr包select函数筛选dataframe数据中以指定字符串开头的数据列(变量)
  • 原文地址:https://blog.csdn.net/qq_45323960/article/details/133443755