初探Windows用戶態調試機制

咱們在感嘆Onlydbg強大與便利的同時,是否考慮過它實現的原理呢?服務器

做爲一個技術人員知其然必知其因此然,這纔是咱們追求的本心。數據結構

最近在學習張銀奎老師的《軟件調試》,獲益良多。熟悉Windows調試機制,對咱們深刻理解操做系統以及遊戲保護的原理有着莫大好處。框架

 

0X01函數

初探調試原理學習

 

 

調試系統的實現思路如圖所示:spa

調試器與被調試程序創建聯繫,程序像調試器發送調試信息,調試器暫停程序處理完調試信息後再恢復程序運行,如此周而復始。操作系統

下面咱們看看如何用操做系統提供的API去實現一個簡單的調試器。線程

 

複製代碼

//啓動要調試的進程或掛接調試器到已運行的進程上
CreateProcess(..., DEBUG_PROCESS, ...) or DebugActiveProcess(dwProcessId)

DEBUG_EVENT de;
BOOL bContinue = TRUE;
DWORD dwContinueStatus;

while(bContinue)
{
  bContinue = WaitForDebugEvent(&de, INFINITE);

  switch(de.dwDebugEventCode)
  {
  ...
  default:
    {
      dwContinueStatus = DBG_CONTINUE;
      break;
    }
  }

  ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}

複製代碼

 

 在調試器開始調試的時候,會啓動被調試程序的新進程或者掛接(attach)到一個已運行進程上,此時Win32系統會啓動調試接口的服務器端;而後調試器調用WaitForDebugEvent函數等待調試服務器端的調試事件被引起;調試器根據調試事件進行相應的處理;最後調用ContinueDebugEvent函數請求調試服務器繼續執行被調試進程,以等待並處理下一個調試事件。指針

 

0X02 調試

抽繭剝絲看調試機制

 

要想深刻了解Windows調試機制,對着三個函數的深刻分析是必不可少的。

 

1.DebugActiveProcess

複製代碼

BOOL WINAPI DebugActiveProcessSelf(
    _In_  DWORD dwProcessId
    )
{
    NTSTATUS    status;
    HANDLE        TargProcessHandle;

    status = DbgUiConnectToDbg();  //DebugObject
    if (!NT_SUCCESS(status))
    {
        BaseSetLastNTError(status);
        return false;
    }

    TargProcessHandle = GetTargProcessHandle(dwProcessId);
    if (TargProcessHandle == 0)
    {
        return false;
    }

    //調試目標進程
    status = DbgUiDebugActiveProcess(TargProcessHandle);
    
    //無論調試是否成功都關閉目標進程句柄
    ZwClose(TargProcessHandle);

    if (!NT_SUCCESS(status))
    {
        BaseSetLastNTError(status);
        return false;
    }

    return true;
}

複製代碼

 

DbgUiConnectToDbg函數內部主要調用ZwCreateDebugObject建立一個調試對象,並將調試對象句柄保存在調試器當前線程的TEB結構的DbgSsReserved[1]中。

其中TEB能夠經過FS:[0x18]得到,DbgSsReserved字段在不一樣操做系統版本中也不相同,在Win732位中處於TEB結構的0xF20中。那麼咱們能夠經過一下彙編獲得DbgSsReserved。

複製代碼

    __asm{
        push    eax
        mov        eax,FS:[0x18]
        lea        eax,[eax+0xF20]
        mov        DbgSsReserved,eax
        pop        eax
    }

複製代碼

那麼到底什麼是調試對象呢?

 

調試任務的順利進行在於調試器與調試程序二者間的事件交互,一開始的圖裏已經很好的表示了。既然是兩個進程間的交互,那麼一定涉及進程間通訊的問題,我在Windows進程通訊中已經總結的很明白了,進程間通訊靠的是全部進程共享高2G內核空間中的內核對象,

好比事件對象,管道對象等。由此能夠推斷出調試對象就是調試器與被調試程序間通信的橋樑! 調試對象保存在調試器TEB線程環境變量塊的DbgSsReserved[1]中,保存在被調試進程的DebugPort字段中。(這點下文作詳細分析)因此判斷一個進程是否被調試可

以看這個進程的DebugPort字段。遊戲保護其中的一種保護手段就是經過不斷抹除DebugPort,從而達到反調試的目的,因此咱們發現用OD沒法附加遊戲,固然咱們能夠經過端口移位的方法繞過這種保護方法,這裏暫且不作討論。

 

GetTargProcessHandle函數主要就是運用ZwOpenProcess函數得到了下進程句柄,在此不做分析,咱們下面主要看看最後這個DbgUiDebugActiveProcess函數。

 

複製代碼

NTSTATUS DbgUiDebugActiveProcess(HANDLE hTargProcess)
{
    NTSTATUS    status;
    HANDLE        hDebugObject;

    hDebugObject = (GetThreadDbgSsReserved())[1];
    status = ZwDebugActiveProcess(hTargProcess,hDebugObject);
    if (!NT_SUCCESS(status))
    {
        return status;
    }
    
    status = DbgUiIssueRemoteBreakin(hTargProcess);  //建立遠程線程 設置遠程斷點
    if (!NT_SUCCESS(status))
    {
        DbgUiStopDebugging(hTargProcess);
    }
    return status;
}

複製代碼

 

咱們先來看看DbgUiIssueRemoteBreakin函數

這個函數比較簡單的主要做用是建立遠程線程下遠程斷點,若是沒有斷點進行攔截,那還怎麼調試。

 

到此DebugActiveProcess函數在Ring3下分析的就差很少了,剩下咱們能夠看見把被調試程序和調試對象做爲參數調用系統函數ZwDebugActiveProcess

我結合上面所說的是否是很清晰這個系統調用在內核作了些什麼事情呢? 顯然在內核把調試對象放到被調試進程的Debugport字段中去了!

 

可是ZwDebugActiveProcess在內核中所作的事情可不止這麼一點哦,這個函數主要作三件事:

(1)取得被調試進程EPROCESS和調試對象的指針。

(2)向調試對象發送杜撰的調試事件。(當調試器附加到一個已經運行的進程時,爲了向調試器報告之前發生的但目前仍有意義的調試事件,調試子系統會「捏造」一些調試事件來模擬過去的調試事件,這樣的調試消息被稱爲杜撰的調試消息)。

(3)調用DbgSetprocessDebugObject將調試對象設置到被調試進程的Debug字段,並調用DbgkpmarkprocessPeb設置PEB中的BeingDebugged字段。

 

  我以爲學習新知識就應該從大致入手,千萬不能太摳細節,在有了清晰的框架後再逐漸瞭解細節的實現問題。看到這裏確定有了不少疑問,好比調試事件結構是什麼,它又是如何得到的,又是怎麼經過調試對象進行傳遞的?下面咱們再來一探究竟。

 

 調試事件的採起

 首先咱們應該明白什麼算調試事件:被調試進程建立了一個進程、建立了一個線程、加載了一個模塊......這些都是調試事件,那麼調試器又是如何知道的呢?

 在操做系統中有一組Dbgk開頭的一組函數它們就是採集例程。以建立線程爲例,咱們看一下調試消息傳遞過程。

 

 當咱們調用CreateThread函數時,函數創建了線程必要的內核對象和數據結構,作了必要的登記後,最終會調用PspUserthreadStartup函數,準備啓動該線    程。爲了支持調試,PspUserThreadStartup函數老是會調用DbgkCreateThread,以便採集調試事件。DbgkCreateThread函數會檢查本身的DebugPort字段是否爲空來判斷本身是否被調試,若是被調試,則採集調試信息調用DbgkpSendApiMessage函數向DebugPort發送消息。同理可得LoadLibrary會調用系統函數NtMapViewOfSection而後會調用採集函數DbgkMapViewOfMapSection,最後判斷本身是否被調試決定是否採集調試事件來調用DbgkpSendApiMessage。

 

咱們看到採集調試事件中最後都是調用DbgkpSendApiMessage,那麼這個函數到底作了些什麼呢?

咱們先來看看這個函數的定義

NTSTATUS DbgkpSendApiMessage(
    IN OUT PDBGKM_APIMSG ApiMsg,
    IN PVOID Port,
    IN BOOLEAN SuspendProcess)

 

其中ApiMsg用來描述消息的,Port用來指定要發送的端口,大多數時候就是EPROCESS結構的DebugPort字段的值,偶爾是進程中的異常端口,即ExceptionPort字段。

複製代碼

//消息結構
typedef struct _DBGKM_APIMSG { 
    PORT_MESSAGE h;                                //+0x0
    DBGKM_APINUMBER ApiNumber;                    //+0x18
    NTSTATUS ReturnedStatus;                    //+0x1c
    union { 
        DBGKM_EXCEPTION Exception;              //異常
        DBGKM_CREATE_THREAD CreateThread;       //建立線程
        DBGKM_CREATE_PROCESS CreateProcessInfo; //建立進程
        DBGKM_EXIT_THREAD ExitThread;           //線程退出
        DBGKM_EXIT_PROCESS ExitProcess;         //進程退出
        DBGKM_LOAD_DLL LoadDll;                 //映射DLL
        DBGKM_UNLOAD_DLL UnloadDll;             //反映射DLL
    } u;                                        //0x20
} DBGKM_APIMSG, *PDBGKM_APIMSG;

複製代碼

其中DBGKM_APINUMBER是個枚舉常量。

複製代碼

//枚舉類型,指定是哪一種事件
typedef enum _DBGKM_APINUMBER { 
    DbgKmExceptionApi, 
    DbgKmCreateThreadApi, 
    DbgKmCreateProcessApi, 
    DbgKmExitThreadApi, 
    DbgKmExitProcessApi, 
    DbgKmLoadDllApi, 
    DbgKmUnloadDllApi, 
    DbgKmMaxApiNumber 
} DBGKM_APINUMBER; 

複製代碼

 

上面說道DbgkpSendApiMessage把調試消息發送給調試對象,那麼調試對象又是如何管理這些調試消息的呢?

複製代碼

//調試對象
typedef struct _DEBUG_OBJECT
{
    KEVENT EventsPresent;
    FAST_MUTEX Mutex;
    LIST_ENTRY EventList;
    union
    {
        ULONG Flags;
        struct
        {
            UCHAR DebuggerInactive:1;
            UCHAR KillProcessOnExit:1;
        };
    };
} DEBUG_OBJECT, *PDEBUG_OBJECT;

複製代碼

這個就是調試對象的數據結構,裏面能夠清晰的看見有個LIST_ENTRY的雙向鏈表。

到這裏可能已經有點迷糊了,咱們須要個圖來整理整理。

這裏須要注意的是有個KEVENT的內核事件對象,咱們回憶下應用層有個WaitForDebugEvent函數在阻塞着,這個事件就是通知調試器有調試事件到達。

 

2.WaitForDebugEvent

BOOL WINAPI WaitForDebugEvent(
    _Out_  LPDEBUG_EVENT lpDebugEvent,
    _In_   DWORD dwMilliseconds
    )

 

WaitForDebugEvent用於等待和接收調試事件,收到調試事件後,調試器便根據事件的類型(事件ID)來分發和處理,並根據狀況決定是否要通知用戶並進入交互式調試。在處理調試事件的過程當中,被調試進程時處於掛起狀態的。處理調試事件後,調試器調用ContinueDebugEvent將處理結果回覆給調試子系統。到這裏細心的彷佛已經發現這個調試事件和內核中的調試事件的結構不同。

 

複製代碼

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;

複製代碼

 

在內核中調試事件使用DBGKM_APIMSG的結構來描述。在發送調試器時,調試API使用的是DEBUG_EVENT結構。因此之間一定有一個轉換過程。簡單的說,DBGKM_APIMSG轉換成DBGUI_WAIT_STATE_CHANGE而後在轉換成DEBUG_EVENT。

 

咱們再來畫張圖整理一下

 

 

 

 

 0X03 總結

相關文章
相關標籤/搜索