回顧一下我們學過的內容,從一開始拿到一個惡意程式,先到檢測平台觀察、使用基本靜態工具檢查資訊、使用基礎動態工具尋找網路連線、再到使用 IDA 深入分析,我們已經成功學會如何在這支程式中尋找蛛絲馬跡,並嘗試去拼湊他所執行及可能發生的惡意行為,於是在了解這些資訊以後,我們接下來要使用進階的動態分析工具,去實際觀察這個惡意程式執行起來每一步的動作,並透過修改去觀察不同的結果。
所謂的 Debugger,顧名思義,就是拿來 debug 的工具。簡單來說,他是一種用來測試或檢查另一個程式執行過程的軟體或硬體,可以協助軟體開發的過程,因為程式在剛寫好時通常都包含許多錯誤,在開發程式時,你會提供輸入給程式並觀察輸出結果,但並不會看到程式是如何產生這些輸出的。Debugger 可以讓你深入了解程式在執行期間實際做了哪些事情,因為他的設計目的,就是讓開發人員能夠觀察、測量以及控制程式執行時的內部狀態與執行流程。
Debugger 可以提供許多反組譯器難以甚至無法取得的資訊,反組譯器只能提供程式在執行第一條指令之前的靜態快照,也就是程式的原始樣貌,但是 Debugger 卻可以提供程式執行時的動態視角。例如,Debugger 可以顯示記憶體位址中的內容如何隨著程式執行而持續改變、測量與控制程式執行的能力,對於惡意程式分析而言具有極其重要的價值。
Debugger 可以讓我們查看每一個記憶體位置、每一個 CPU 暫存器,以及每一個函式參數的實際數值,此外,除錯器也允許我們在程式執行的任何時刻修改任何執行狀態。例如,只要你知道某個變數足夠的資訊,包括它在記憶體中的位置,你就可以在程式執行期間隨時改變該變數的值。
之後會介紹兩款 Debugger:
大多數軟體開發人員都熟悉 Source-Level Debugger,這種 Debugger 能讓程式設計師在撰寫程式時進行除錯,通常內建於整合開發環境(IDE)中,它可以設定中斷點(Breakpoint),當程式執行到特定的原始碼行時就會暫停,以便檢查程式內部變數的狀態,並且可以一次一行地逐步執行程式。
例設有一個 C 程式:
int a = 5;
int b = 3;
int c = a + b;
可以直接在
int c = a + b;
這一行設 Breakpoint
程式執行到這一行時就會停下來,可以直接看到:
a = 5
b = 3
c = ?
Assembly-Level Debugger,有時也稱為低階除錯器(Low-Level Debugger),操作的對象是組合語言程式碼,而不是原始碼。與 Source-Level Debugger 相同,我們也可以利用組合語言層級除錯器一次執行一條指令、設定中斷點使程式在特定的組合語言指令處停止,以及檢查記憶體中的內容。惡意程式分析人員大量使用 Assembly-Level Debugger,因為它們不需要取得程式的原始碼即可進行分析。
例如只有:
malware.exe
沒有:
malware.c
這樣就只能看到:
mov eax, 5
add eax, 3
call 401200
但是一樣可以:
只是看到的是組合語言,不是 C 程式碼。
在 User-Mode 下,Debugger 與被除錯的程式都執行在同一台電腦上。進行 User-Mode 除錯時,除錯的是單一的可執行程式,而作業系統會將它與其他執行中的程式彼此隔離。
例如:
notepad.exe
malware.exe
chrome.exe
這些都是 User-Mode 的程式
可以直接在同一台電腦上:
x64dbg
│
▼
malware.exe
兩者都在同一台 Windows 裡執行。
這也是平常使用:
的方式
與除錯 User-Mode 的程式相比,除錯 Kernel-Mode 的程式更加困難,因為 Kernel-Mode 通常需要使用兩台不同的系統。Kernel-Mode Debugging 需要在兩台系統上進行,因為整個系統只有一個核心(Kernel),如果核心在中 Breakpoint 處暫停,系統上的所有應用程式都將無法繼續執行。因此,其中一台系統負責執行被除錯的程式,而另一台系統則執行 Debugger,此外,作業系統必須事先設定成允許 Kernel-Mode Debugging,並且需要將兩台電腦彼此連接。
例如:
它們都在 Kernel-Mode 執行。
如果讓 Kernel 停在 Breakpoint:
Kernel
↓
停止
那整個 Windows 都會停住。
因為:
全部都需要 Kernel,所以不能像 User-Mode 一樣,在同一台電腦上 Debug。
所以需要使用兩台電腦,架構如下:
Debug PC
+----------------+
| WinDbg |
+----------------+
│
Serial / USB / Network
│
▼
+----------------+
| Target PC |
| Windows Kernel |
+----------------+
當 Target 停下來時:
Target
↓
Breakpoint
↓
整台 Windows 暫停
Debug PC 仍然正常運作,因此可以查看:
使用 Debugger 來分析程式有兩種方式:
第一種方式是在 Debugger 中直接啟動程式,當程式被載入到記憶體後,它會在 Entry Point 之前立即停止執行,此時,就有對整個程式完整的控制權。
另一種方式是將 Debugger Attach 到一個已經正在執行中的程式,當 Debugger 成功附加後,該程式的所有 Threads 都會被暫停,接著就可以開始進行除錯。如果要分析一個已經執行一段時間的程式,或是想分析一個受到惡意程式影響的 Process,這種方式會是很好的選擇。
單步執行是使用 Debugger 最基本的功能,也就是一次只執行一條指令。單步執行可以讓我們觀察程式內部所發生的一切事情,理論上可以利用單步執行來執行整個程式,但對於複雜的程式而言,這樣會浪費一堆時間。單步執行適合用來理解某一小段程式碼的細節,我們必須有選擇性地決定要分析哪些程式碼。分析時應該專注於整體流程,否則很容易迷失在過多的細節之中。
例如,下圖的反組譯程式碼展示了如何利用 Debugger 來協助理解某一段程式碼

這個程式會在一個迴圈中持續存取並修改某個資料位址,最後顯示的資料值看起來既不像 ASCII 文字,也不像任何可以辨識的資料,但是我們可以利用 Debugger 逐步執行這個迴圈,以了解這段程式碼究竟在做什麼。如果我們使用 WinDbg 或 x64dbg 對這個迴圈執行單步執行,就可以看到資料在每一次迴圈中都被修改。

例如,在上圖中,可以看到這個函式所修改的 13 個位元組,每經過一次迴圈就會改變一次,當 Debugger Attach 到程式後,就可以清楚看出,這個函式利用單一位元組的 XOR 運算來解碼出字串 LoadLibraryA。如果只依靠靜態分析,要辨識出這個字串將會困難得多。
如果使用 Stepping-Over,在跑的時候就會跳過去而不會跑進去看。
例如:
call A
如果按:
Step Over
Debugger 會:
call A
↓↓↓↓↓↓
A()
全部執行完
↓↓↓↓↓↓
停在
call 後面
例如:
call A
mov eax,1
按一次 Step Over:
直接停在:
mov eax,1
完全不看 A() 裡面的程式。
如果使用 Stepping-Into,在跑的時候就會進去看。
例如:
main()
{
A();
B();
}
其中:
A()
{
printf("Hello");
}
CPU 執行到:
call A
如果按 Step Into:
call A
│
▼
A()
push ebp
mov ebp,esp
...
會直接跑進 A() 裡。
也就是:
要看這個函式裡面到底做了什麼
今天對於 Debugger 有了基本的認知,之後在進行 Debug 的時候就可以知道要做甚麼並且對於操作步驟有更充分的理解!
