初探Windows調試原理和反附加手段
最近粗淺的研究了一下Windows應用層相關調試API和對應調試原理,以達到實現反附加的功能。本文內容主要參考《軟件調試》和網絡上相關優秀文章,并且主要側重在應用層調試附加方面,關于內核層面因為水平有限本文沒有詳細展現。
用戶態調試基本流程
首先我們先用調試API編寫一個最簡單的附加調試器:
int main(int argc,TCHAR *argv[])
{
DWORD dwPID;
BOOL waitEvent = TRUE;
if (argc > 1) {
dwPID = atoi(argv[1]);
}
else {
printf("usage: MyDebugger.exe dwPID");
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 實現
我們通過查找NT5的源碼可以看到DebugActiveProcess 具體實現代碼,主要就是調用了DbgUiConnectToDbg ,ProcessIdToHandle和DbgUiDebugActiveProcess這三個函數。


DbgUiConnectToDbg
首先判斷TEB->DbgSsReserved[1]是否保存著調試對象的句柄,如果存在則直接返回函數如果不存在就進行初始化并調用NtCreateDebugObject創建調試對象。


ProcessIdToHandle
如果是偽句柄則調用CsrGetProcessId獲取csrss.exe的PID,然后調用NtOpenProcess獲得對應進程句柄,給后續調用做準備。


DbgUiDebugActiveProcess
此函數首先傳入進程和調試對象的句柄進入內核,然后調用DbgUiIssueRemoteBreakin函數創建線程開始地址為DbgUiRemoteBreakin的遠程線程讓被調試進程斷下來,如果遠程線程設置失敗則調用DbgUiStopDebugging停止調試。
這個地方創建了遠程線程是在將來反附加檢測的主要檢測點。



調試子系統
Windows的調試子系統使用調試事件驅動,這個和窗體的消息驅動是很類似的。在調試子系統中使用WaitForDebugEvent在用戶態等待調試事件,當調試器處理調試事件時,被調試進程會被掛起,所以調試器處理完畢后要調用ContinueDebugEvent使被掛起的被調試進程繼續運行。
下圖是張銀奎老師在軟件調試縱橫談中的調試模型,在用戶空間部分就是我們此前通過源碼分析的用戶態附加調試API DebugActiveProcess 的基本流程,在系統空間維護著一個DebugObject的鏈表并且通過Dbgk*例程采集和傳遞調試事件。

反附加手段
根據此前內容知道,當調試器附加一個進程的時候是調用DebugActiveProcess函數,該函數內部調用了DbgUiDebugActiveProcess,此函數內部會調用DbgUiIssueRemoteBreakin函數,最后內部則會通過RtlCreateUserThread在被調試進程內創建一個線程,線程的起始地址是DbgUiRemoteBreakin。

在被調試進程內DbgUiRemoteBreakin會判斷PEB中的BeingDebugged標志位,如果在調試則調用DbgBreakPoint函數,調試器附加后被調試進程就中斷在DbgBreakPoint函數內。

根據上述流程我們可以知道在被調試進程(我們的程序)會創建新線程,線程起始函數是DbgUiRemoteBreakin。所以我們可以Hook DbgUiRemoteBreakin 直接調用 ExitProcess 結束我們的程序。

反反附加插件
本文以ScyllaHide為例子,因為ScyllaHide是開源項目可以直接分析源碼,插件加載后我們此前設置的HOOK會被還原。

對于SharpOD,他會在插件加載的時候Hook調試器的DebugActiveProcess和CreateProcessInternalW,在DebugActiveProcessDetour 會在被調試進程中添加ShellCode,并HOOK LdrInitializeThunk 跳轉到ShellCode中斷,并在附加進程的調試事件到達后還原LdrInitializeThunk 。


x64dbg的Titan Engine
對于新版x64dbg會內嵌Titan Engine(2020年11月12日版本后添加),所以SharpOD插件對于反反附加對新版本x64dbg起不到作用。在Titan Engine內部自己實現了DebugActiveProcess一套流程。

并且沒有調用DbgUiIssueRemoteBreakin讓程序被調試器附加的時候斷下。

總結
本文初步學習了Windows用戶層基本的調試原理和模型,并給出了一個簡單的反附加方案,同時分析了目前常見的反反附加插件。不過同時存在不知道如何繞過SharpOD的反反附加檢測等問題,還需要進一步學習研究。因為本人水平有限,如果存在錯誤還望各位前輩指正。