除錯篇——斷點與單步

寂靜的羽夏發表於2022-03-03

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


? 華麗的分割線 ?


概述

  在軟體除錯的過程中,最常用的就是斷點和單步了。有過除錯經驗的人知道,斷點有軟體斷點、硬體斷點、記憶體斷點;單步有單步步入和單步步過。下面我們來介紹它們的實現原理。

軟體斷點

  軟體斷點是實現的本質就是在下斷點的地方插入int3,也被稱之為CC斷點。下面我們來做個實驗:
  我們開啟一個偵錯程式除錯一個程式,然後在某條彙編下斷點,於此同時通過CE來檢視當前指令的位元組變化情況,如下是操作示例:

除錯篇——斷點與單步

  也就是說,偵錯程式幫我們遮蔽掉了0xCC的顯示,本質上還是int3,就憑這些我們就可以寫一個接管軟體斷點的簡單的偵錯程式了:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;

#define DBGBREAKPOINT

int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;
    DWORD buffer;
        
    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);

    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }

            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    isContinue = BreakPointHandler(&info);
                    break;
                }
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT"); 

#ifdef DBGBREAKPOINT

                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);

                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

#endif

                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,DBG_CONTINUE);   
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("建立程式失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
        
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
        
    WaitUserInput();
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        printf("COMMAND>> ");
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        default:
            break;
        }
    }
}

  我定義DBGBREAKPOINT這個巨集是為了方便之後實驗只需定義巨集來控制單個功能的測試。WaitUserInput函式裡面清空緩衝區的程式碼不清楚的話可以閱讀 羽夏閒談—— C 的 scanf 的高階用法 ,如下是我的實驗結果:

除錯篇——斷點與單步

  當然我們只是簡單的實現,一般的偵錯程式都會保留這個斷點,具體實現我就不實現了。

記憶體斷點

  記憶體斷點是通過修改頁屬性來製造異常出現接管的,既然是修改其他程式的,就需要呼叫下面的函式:

BOOL VirtualProtectEx(
  HANDLE hProcess,        // handle to process
  LPVOID lpAddress,       // region of committed pages
  SIZE_T dwSize,          // size of region
  DWORD flNewProtect,     // desired access protection
  PDWORD lpflOldProtect   // old protection
);

  記憶體斷點有被分為記憶體訪問斷點和記憶體寫入斷點,它們分別對應的第四個引數為PAGE_NOACCESSPAGE_EXECUTE_READPAGE_NOACCESS屬性會把對應的頁屬性的P位改為0,而PAGE_EXECUTE_READ對應的頁屬性的P位雖然是1,但R/W0。下面我們來實現一個簡單的記憶體訪問斷點:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;
DWORD orignExcuteAccess;
DWORD buffer;

//#define DBGBREAKPOINT
#define MemBREAKPOINT

int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;

    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);

    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }

            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    ret = BreakPointHandler(&info);
                    break;
                case EXCEPTION_ACCESS_VIOLATION:
                    ret = MemBreakPointHandler(&info);
                    break;
                }
                if (!ret)
                {
                    printf("出錯:%d\n",GetLastError());
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                isContinue = ret;
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT");  

#ifdef DBGBREAKPOINT

                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);

                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

#endif
    
#ifdef MemBREAKPOINT
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;   
                if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,PAGE_NOACCESS,&orignExcuteAccess))
                {
                    puts("記憶體執行斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                printf("記憶體訪問斷點的位置:0x%X\n",buffer);
#endif
                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");    
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }

            if (!isContinue)
            {
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,ret?DBG_CONTINUE:DBG_EXCEPTION_NOT_HANDLED);   
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("建立程式失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
        
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
        
    WaitUserInput();
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    DWORD ExceptionADDR = info->ExceptionRecord.ExceptionInformation[1];
    DWORD ExceptionAccess = info->ExceptionRecord.ExceptionInformation[0];
        
    if (ExceptionADDR>>12 == buffer>>12)    //這裡的判斷是不對的,但為了做示例足夠了
    {
        printf("記憶體執行斷點:%x %x\n",ExceptionADDR,   ExceptionAccess);
        WaitUserInput();
        if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,  orignExcuteAccess,&orignExcuteAccess))
        {
            printf("記憶體斷點修復失敗:%d\n",GetLastError());
        }
        return TRUE;
    }
    return FALSE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        printf("COMMAND>> ");

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        default:
            break;
        }
    }
}

  可以看出,該程式碼是基於軟體斷點實驗基礎上寫的。在WaitUserInput有一個小Bug,就是沒有輸入的話至少需要隨意輸入字元來繼續清空緩衝區操作,不過對於示例來說無傷大雅,如下是測試效果圖:

除錯篇——斷點與單步

  注意,我是簡單是實現記憶體斷點,為什麼這麼說呢?是因為這個函式一旦影響就是一個物理頁,所以你得做判斷。如果對資料下訪問斷點,如果多於1個位元組,你還得考慮在多個物理頁的情況,還得實現記憶體斷點的記錄功能。這些都是一個基本能用的偵錯程式所具備的功能。

硬體斷點

概述

  硬體斷點和上面的不同,它是基於硬體的,不依賴除錯程式,有自己的優勢,如果通過CRC校驗是不會被檢測到的。如下是與硬體斷點相關的暫存器結構:

除錯篇——斷點與單步

  Dr0 ~ Dr3用於設定硬體斷點,Dr4Dr5被保留了。由於只有4個斷點暫存器,所以最多隻能設定4個硬體除錯斷點。Dr7是最重要的暫存器,它比較複雜,我們來看看它的結構:

L0/G0 ~ L3/G3

  控制Dr0 ~ Dr3是否有效,區域性還是全域性。每次異常後,Lx都被清零,Gx不清零。

LEN0 ~ LEN3

  表示硬體斷點的長度。如果是0表示1個位元組;是1表示2個位元組;是3表示4個位元組。

R/W0 ~ R/W3

  指示斷點型別。如果是0表示執行斷點;是1表示寫入斷點;是3表示訪問斷點。

處理

  硬體除錯斷點產生的異常是STATUS_SINGLE_STEP,即單步異常。觸發異常後,B0 ~ B3對應的位會被置1,以此可以區分單步步入產生的單步異常,後續會詳細講解。

實驗

  如下是實驗程式碼:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;
DWORD orignExcuteAccess;
DWORD buffer;

//#define DBGBREAKPOINT
//#define MemBREAKPOINT
#define HardwareBREAKPOINT

int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;
        
    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);

    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }
   
            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    ret = BreakPointHandler(&info);
                    break;
                case EXCEPTION_ACCESS_VIOLATION:
                    ret = MemBreakPointHandler(&info);
                    break;
                case EXCEPTION_SINGLE_STEP:
                    ret = SingleStepHandler(&info);
                    break;
                default:
                    ret = FALSE;
                    break;
                }
                if (!ret)
                {
                    printf("出錯:%d\n",GetLastError());
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                isContinue = ret;
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT");  
    
#ifdef DBGBREAKPOINT
    
                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);

                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }

                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                    
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
    
#endif
    
#ifdef MemBREAKPOINT
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;   
                if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,PAGE_NOACCESS,&orignExcuteAccess))
                {
                    puts("記憶體執行斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                printf("記憶體訪問斷點的位置:0x%X\n",buffer);
#endif
    
#ifdef HardwareBREAKPOINT
                SuspendThread(pi.hThread);
                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);
                CONTEXT context;
                context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
                GetThreadContext(pi.hThread,&context);
                context.Dr0 = buffer;
                context.Dr7 |= 1;
                if (!SetThreadContext(pi.hThread,&context))
                {
                    puts("硬體斷點設定失敗!");
                }
                ResumeThread(pi.hThread);
#endif

                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");    
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }

            if (!isContinue)
            {
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,ret?DBG_CONTINUE:DBG_EXCEPTION_NOT_HANDLED);   
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("建立程式失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
        
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
        
    WaitUserInput();
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    DWORD ExceptionADDR = info->ExceptionRecord.ExceptionInformation[1];
    DWORD ExceptionAccess = info->ExceptionRecord.ExceptionInformation[0];
        
    if (ExceptionADDR>>12 == buffer>>12)    //這裡的判斷是不對的,但為了做示例足夠了
    {
        printf("記憶體執行斷點:%x %x\n",ExceptionADDR,ExceptionAccess);
        WaitUserInput();
        if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,orignExcuteAccess,&orignExcuteAccess))
        {
            printf("記憶體斷點修復失敗:%d\n",GetLastError());
        }
        return TRUE;
    }
    return FALSE;
}

BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info)
{
        
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
        
    if (context.Dr6&0xF)
    {
        puts("硬體斷點被觸發!");
        context.Dr7&=~1;
    }
    else
    {
        printf("單步中……:0x%X\n",context.Eip);
    }
        
    WaitUserInput();
    SetThreadContext(pi.hThread,&context);
    return TRUE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        printf("COMMAND>> ");

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        default:
            break;
        }
    }
}

  硬體斷點的實現十分簡單,就不贅述了,如下是實驗效果圖:

除錯篇——斷點與單步

單步步入

  在除錯中我們經常一條指令一條指令的進行除錯,這大大方便了我們查閱結果,CPU提供了這樣的基址,就是在Eflag中的TF位實現的,如下圖所示:

除錯篇——斷點與單步

  好我們繼續在之前的程式碼基礎上擴充套件單步功能:

#include "stdafx.h"
#include <windows.h>
#include <stdlib.h>

BOOL SysInt3 = TRUE;

void WaitUserInput();
BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info);
BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info);
const UCHAR Int3 = 0xCC;
UCHAR bInt3;
PROCESS_INFORMATION pi ;
DWORD orignExcuteAccess;
DWORD buffer;

#define DBGBREAKPOINT
//#define MemBREAKPOINT
//#define HardwareBREAKPOINT


int main(int argc, char* argv[])
{
    char filename[]= "C:\\WINDOWS\\NOTEPAD.EXE";
    STARTUPINFO si ={sizeof(STARTUPINFO)};
    DEBUG_EVENT dbgEvent;
    BOOL isContinue = TRUE;
    
    BOOL ret =CreateProcess(NULL,filename,NULL,NULL,FALSE,DEBUG_PROCESS|DEBUG_ONLY_THIS_PROCESS,NULL,NULL,&si,&pi);
    
    if (ret)
    {
        while (isContinue)
        {
            ret = WaitForDebugEvent(&dbgEvent,INFINITE);
            if (!ret)
            {
                printf("WaitForDebugEvent 出錯:%d",GetLastError());
                break;
            }
            
            EXCEPTION_DEBUG_INFO info = dbgEvent.u.Exception;

            switch (dbgEvent.dwDebugEventCode)
            {
            case EXCEPTION_DEBUG_EVENT:
                puts("EXCEPTION_DEBUG_EVENT");
                switch (info.ExceptionRecord.ExceptionCode)
                {
                case EXCEPTION_BREAKPOINT:
                    ret = BreakPointHandler(&info);
                    break;
                case EXCEPTION_ACCESS_VIOLATION:
                    ret = MemBreakPointHandler(&info);
                    break;
                case EXCEPTION_SINGLE_STEP:
                    ret = SingleStepHandler(&info);
                    break;
                default:
                    ret = FALSE;
                    break;
                }
                if (!ret)
                {
                    printf("出錯:%d\n",GetLastError());
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                isContinue = ret;
                break;
            case CREATE_THREAD_DEBUG_EVENT:
                puts("CREATE_THREAD_DEBUG_EVENT");
                break;
            case CREATE_PROCESS_DEBUG_EVENT:
                puts("CREATE_PROCESS_DEBUG_EVENT");        
                
#ifdef DBGBREAKPOINT
                
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;
                
                if (!ReadProcessMemory(pi.hProcess,(void*)buffer,&bInt3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                
                if (!WriteProcessMemory(pi.hProcess,(void*)buffer,(void*)&Int3,1,NULL))
                {
                    puts("軟體斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                
#endif
                
#ifdef MemBREAKPOINT
                buffer = (DWORD)dbgEvent.u.CreateProcessInfo.lpStartAddress;            
                if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,PAGE_NOACCESS,&orignExcuteAccess))
                {
                    puts("記憶體執行斷點設定失敗!");
                    isContinue = FALSE;
                    TerminateProcess(pi.hProcess,0);
                }
                printf("記憶體訪問斷點的位置:0x%X\n",buffer);
#endif
                
#ifdef HardwareBREAKPOINT
                SuspendThread(pi.hThread);
                puts("請輸入要下斷點的位置:");
                scanf("%x",&buffer);
                CONTEXT context;
                context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
                GetThreadContext(pi.hThread,&context);
                context.Dr0 = buffer;
                context.Dr7 |= 1;
                if (!SetThreadContext(pi.hThread,&context))
                {
                    puts("硬體斷點設定失敗!");
                }
                ResumeThread(pi.hThread);
#endif

                break;
            case EXIT_THREAD_DEBUG_EVENT:
                puts("EXIT_THREAD_DEBUG_EVENT");
                break;
            case EXIT_PROCESS_DEBUG_EVENT:
                puts("EXIT_PROCESS_DEBUG_EVENT");                
                break;
            case LOAD_DLL_DEBUG_EVENT:
                puts("LOAD_DLL_DEBUG_EVENT");
                break;     
            case UNLOAD_DLL_DEBUG_EVENT:
                puts("UNLOAD_DLL_DEBUG_EVENT");
                break;
            case OUTPUT_DEBUG_STRING_EVENT:
                puts("OUTPUT_DEBUG_STRING_EVENT");
                break;
            default:
                break;
            }
            
            if (!isContinue)
            {
                break;
            }

            isContinue = ContinueDebugEvent(dbgEvent.dwProcessId,dbgEvent.dwThreadId,ret?DBG_CONTINUE:DBG_EXCEPTION_NOT_HANDLED);            
        }
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
    }
    else
    {
        printf("建立程式失敗:%d\n",GetLastError());
    }
    system("pause");
    return 0;
}

BOOL BreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    if (SysInt3)
    {
        SysInt3 = FALSE;
        return TRUE;
    }
    
    EXCEPTION_RECORD record = info->ExceptionRecord;
    WriteProcessMemory(pi.hProcess,record.ExceptionAddress,&bInt3,1,NULL);
    printf(">> BreakPointHandler - Addr : 0x%X\n",record.ExceptionAddress);
    
    WaitUserInput();
    
    SuspendThread(pi.hThread);
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    GetThreadContext(pi.hThread,&context);
    context.Eip--;
    SetThreadContext(pi.hThread,&context);
    ResumeThread(pi.hThread);
    return TRUE;
}

BOOL MemBreakPointHandler(EXCEPTION_DEBUG_INFO* info)
{
    DWORD ExceptionADDR = info->ExceptionRecord.ExceptionInformation[1];
    DWORD ExceptionAccess = info->ExceptionRecord.ExceptionInformation[0];
    
    if (ExceptionADDR>>12 == buffer>>12)    //這裡的判斷是不對的,但為了做示例足夠了
    {
        printf("記憶體執行斷點:%x %x\n",ExceptionADDR,ExceptionAccess);
        WaitUserInput();
        if (!VirtualProtectEx(pi.hProcess,(void*)buffer,1,orignExcuteAccess,&orignExcuteAccess))
        {
            printf("記憶體斷點修復失敗:%d\n",GetLastError());
        }
        return TRUE;
    }
    return FALSE;
}

BOOL SingleStepHandler(EXCEPTION_DEBUG_INFO* info)
{
    
    CONTEXT context;
    context.ContextFlags = CONTEXT_FULL |CONTEXT_DEBUG_REGISTERS;
    SuspendThread(pi.hThread);
    GetThreadContext(pi.hThread,&context);
    
    if (context.Dr6&0xF)
    {
        puts("硬體斷點被觸發!");
        context.Dr7&=~1;
    }
    else
    {
        printf("單步中:0x%X\n",context.Eip);
        context.EFlags &= ~0x100;
    }    
    SetThreadContext(pi.hThread,&context);
    ResumeThread(pi.hThread);
    WaitUserInput();
    return TRUE;
}

void WaitUserInput()
{
    bool p = true;
    char cmd = 0;
    while(p)
    {
        
        /*************清空緩衝區**************/
        scanf("%*[^\n]");
        scanf("%*c");
        /*************************************/

        printf("COMMAND>> ");

        cmd = getchar();
        switch (cmd)
        {
        case 'g':
            p = false;
            break;
        case 't':
            p=false;
            SuspendThread(pi.hThread);
            CONTEXT context;
            context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
            GetThreadContext(pi.hThread,&context);
            context.EFlags |= 0x100;
            if (!SetThreadContext(pi.hThread,&context))
            {
                puts("單步設定失敗!");
            }
            ResumeThread(pi.hThread);
            break;
        default:
            break;
        }
    }
}

  單步是實現比較簡單,如下是效果圖:

除錯篇——斷點與單步

單步步過

  單步步過和單步步入不同,單步步入會逐個走指令,到達call執行會進入,而單步步過不會進入。單步步過是可以基於硬體斷點或者軟體斷點實現。至於實現過程我就不贅述了。

小結

  本篇文章主要介紹了偵錯程式斷點和單步的實現相關基本知識,如果要真正的實現一個偵錯程式,還需要大量的實現。在總結與提升篇,我還會詳細介紹所有斷點和單步異常的核心處理流程。

下一篇

  除錯篇——總結與提升

相關文章