前面認識了 WinDbg 與 Kernel Debugging 的基本概念之後,今天要開始實際進入 Kernel Debugging。
過去在分析一般 User Mode 惡意程式時,我們經常會從 Windows API 下手。例如看到 CreateFile、WriteFile,就可以推測程式可能正在建立或寫入檔案;看到 Registry API,則可能進一步追蹤 persistence 或設定修改。
但如果真正執行這些行為的程式碼不在 User Mode,而是在 Kernel Mode 呢?
這時候只盯著 User-Space API 就可能漏掉真正重要的行為。
今天會透過一個簡單的案例,看看一個 User-Space Application 如何載入 Driver、取得 Device Handle,再透過 DeviceIoControl 將資料送進 Kernel,最後找出 Driver 中真正負責處理這個 Request 的 Function。
【聲明】由於時間問題本日文章由 AI 生成QQ
這次的範例是一個會從 Kernel Space 寫入檔案的程式。
對惡意程式而言,把某些操作搬進 Kernel Mode 的其中一個好處,就是能讓行為更難被發現。
例如平常分析一支程式時,如果懷疑它會建立或修改檔案,我們很自然會尋找:
CreateFile
WriteFile
等 User-Space API。
但如果檔案操作實際發生在 Kernel Mode,分析者就可能在 User-Space Code 裡完全找不到這些典型跡象。
而 Kernel Mode 也不能像一般 User-Space Application 一樣直接使用我們熟悉的 Win32 API,因此 Kernel 中會使用對應的 Native API,例如:
NtCreateFile
NtWriteFile
在今天的案例中,我們就要一路從 User-Space Application 往下追,找出真正負責寫入檔案的 Kernel Driver。
整體流程可以先想成:
User-Space Application
↓
載入 Kernel Driver
↓
取得 Device Handle
↓
DeviceIoControl
↓
Kernel Driver
↓
IRP_MJ_DEVICE_CONTROL
↓
Driver Dispatch Routine
↓
ZwCreateFile / ZwWriteFile
↓
在 Kernel Space 中寫入檔案
接下來就從 User-Space Code 開始。
第一步先不要急著進 WinDbg。
我們先使用 IDA Pro 分析 User-Space Application,確認它究竟如何與 Driver 互動。
首先可以在程式裡看到 CreateServiceA:
push esi ; lpPassword
push esi ; lpServiceStartName
push esi ; lpDependencies
push esi ; lpdwTagId
push esi ; lpLoadOrderGroup
push [ebp+lpBinaryPathName] ; lpBinaryPathName
push 1 ; dwErrorControl
push 3 ; dwStartType
push 1 ; dwServiceType
push 0F01FFh ; dwDesiredAccess
push [ebp+lpDisplayName] ; lpDisplayName
push [ebp+lpDisplayName] ; lpServiceName
push [ebp+hSCManager] ; hSCManager
call ds:CreateServiceA
這裡最值得注意的是:
dwServiceType = 0x01
dwServiceType 用來指定 Service 的類型,而 0x01 代表:
SERVICE_KERNEL_DRIVER
因此看到這裡,就知道程式並不是單純建立一般的 Windows Service,而是在建立一個 Kernel Driver Service。
這也是分析時非常重要的線索。
如果惡意程式透過 Service Control Manager 載入 .sys Driver,那麼後續真正重要的惡意行為就不一定存在於目前分析的 EXE 裡,而可能被搬進 Driver。
Driver 載入之後,User-Space Application 還需要一個方法與它溝通。
接著會看到:
xor eax, eax
push eax ; hTemplateFile
push 80h ; dwFlagsAndAttributes
push 2 ; dwCreationDisposition
push eax ; lpSecurityAttributes
push eax ; dwShareMode
push ebx ; dwDesiredAccess
push edi ; lpFileName
call esi ; CreateFileA
這裡雖然使用的是 CreateFileA,但它不一定真的代表「打開一般檔案」。
這次 EDI 中存放的名稱是:
\\.\FileWriterDevice
也就是 Driver 所建立的 Device Object。
因此這裡的:
CreateFileA("\\.\FileWriterDevice", ...)
真正目的其實是:
取得 Kernel Driver 所建立之 Device Object 的 Handle。
這個概念很重要。
User-Space Application 並不是直接「呼叫 Driver 裡的一個函式」,而是先取得 Device 的 Handle,之後再透過 Windows 提供的 I/O 機制與 Driver 溝通。
所以看到:
CreateFile("\\.\Something")
時,不要看到 CreateFile 就立刻認為它是在開啟硬碟上的檔案。
\\.\ 後面的名稱很可能指向某個 Device。
拿到 Device Handle 之後,接下來就會看到非常重要的:
DeviceIoControl
例如:
push 0 ; lpOverlapped
...
push 64h ; nOutBufferSize
push edi ; lpOutBuffer
...
push eax ; nInBufferSize
push esi ; lpInBuffer
push 9C402408h ; dwIoControlCode
push [ebp+hObject]; hDevice
call DeviceIoControl
DeviceIoControl 是 User-Space Application 與 Driver 溝通的重要方式之一。
這裡幾個特別值得注意的參數是:
hDevice
dwIoControlCode
lpInBuffer
nInBufferSize
lpOutBuffer
nOutBufferSize
其中:
就是剛才透過:
CreateFileA("\\.\FileWriterDevice")
取得的 Device Handle。
這是一個 I/O Control Code(IOCTL)。
它告訴 Driver:
「這一次送進來的是哪一種類型的 Request?」
在這個案例中可以看到:
0x9C402408
指向 User-Space Application 要傳送給 Driver 的資料。
因此目前為止,我們已經可以建立出 User Mode 這一側的流程:
CreateService
↓
載入 Kernel Driver
↓
CreateFile("\\.\FileWriterDevice")
↓
取得 Device Handle
↓
DeviceIoControl
↓
把 Request / Data 傳給 Kernel Driver
但是現在還有一個最大的問題:
DeviceIoControl 進入 Kernel 之後,到底會執行 Driver 裡的哪個 Function?
這就要進入 Kernel Mode 繼續追。
接下來切換到 WinDbg。
我們要動態分析剛才 DeviceIoControl 所觸發的 Kernel Code。
如果 WinDbg 已經 Attach Kernel Debugger,而且開啟 Verbose Output,Kernel Module 被載入時就會顯示相關訊息。
例如:
ModLoad: f7b0d000 f7b0e780 FileWriter.sys
代表:
FileWriter.sys
剛剛被載入 Kernel。
如果正在執行一個可疑程式,接著突然出現一個新的 Kernel Module,那這個 Driver 當然就是值得優先調查的對象。
不過在 VMware 環境下有一個例外:KMixer.sys 可能經常被載入與卸載,這屬於正常現象,不能單憑這個行為判斷它是惡意 Driver。
知道 Driver 名稱之後,可以使用:
!drvobj FileWriter
取得 Driver Object。
WinDbg 可能會得到:
kd> !drvobj FileWriter
Driver object (827e3698) is for:
\Driver\FileWriter
Device Object list:
826eb030
這裡最重要的是:
Driver Object = 0x827e3698
Windows Kernel 內部會使用各種 Object 與 Data Structure 管理系統資源,而 Driver 也有自己的:
_DRIVER_OBJECT
只要找到 Driver Object 的 Address,就可以進一步查看 Driver 的內部結構。
如果 !drvobj 找不到 Driver,也可以改用:
!object \Driver
列出 \Driver Namespace 中的 Driver Objects,再從裡面尋找可疑目標。
取得:
0x827e3698
後,可以使用:
dt nt!_DRIVER_OBJECT 0x827e3698
讓 WinDbg 按照 _DRIVER_OBJECT Structure 解讀這塊 Memory。
輸出會類似:
+0x000 Type
+0x002 Size
+0x004 DeviceObject
+0x008 Flags
+0x00c DriverStart
+0x010 DriverSize
...
+0x02c DriverInit
+0x030 DriverStartIo
+0x034 DriverUnload
+0x038 MajorFunction
其中我們現在最關心的是:
+0x038 MajorFunction
因為 MajorFunction 指向 Driver 的 Major Function Table。
這裡是整個案例最重要的觀念之一。
User-Space Application 對 Device 發出 Request 之後,Windows 必須知道:
「這個 Request 應該交給 Driver 裡的哪個 Function?」
Driver Object 裡的:
MajorFunction[]
就是用來處理這件事情。
可以把它想像成一張 Function Pointer Table:
MajorFunction[0]
MajorFunction[1]
MajorFunction[2]
MajorFunction[3]
...
不同 Index 對應不同類型的 I/O Request。
這些 Request 的名稱通常以:
IRP_MJ_
開頭。
例如:
IRP_MJ_READ
IRP_MJ_WRITE
IRP_MJ_DEVICE_CONTROL
我們前面看到 User-Space Application 使用:
DeviceIoControl
因此現在要找的就是:
IRP_MJ_DEVICE_CONTROL
對應的 Function。
IRP_MJ_DEVICE_CONTROL 的 Index 是:
0xE
而 _DRIVER_OBJECT 中 MajorFunction Table 的起點位於:
0x38
這個案例是 32-bit Windows,因此每一個 Function Pointer 大小為:
4 bytes
所以目標 Function Pointer 的位置可以算成:
DriverObject
+ MajorFunction Offset
+ IRP_MJ_DEVICE_CONTROL × Pointer Size
代入數值:
0x827e3698
+ 0x38
+ 0xE * 4
因此 WinDbg 可以直接輸入:
dd 827e3698+0x38+e*4 L1
這裡:
dd
代表以 DWORD 顯示 Memory;
L1
則代表只顯示一個 DWORD。
結果:
827e3708 f7b0da66
也就是:
IRP_MJ_DEVICE_CONTROL Handler
=
0xF7B0DA66
現在我們終於知道:
User Mode 呼叫
DeviceIoControl之後,Driver 中真正負責處理 Request 的 Function 在0xF7B0DA66。
找到 Address 後,可以使用:
u f7b0da66
反組譯該 Address:
FileWriter+0xa66:
f7b0da66 6a68 push 68h
f7b0da68 6838d9b0f7 push offset FileWriter+0x938
f7b0da6d e822faffff call FileWriter+0x494
這一步可以幫助確認剛才的 Address 計算是否正確。
如果 u 之後看到完全不像正常 Code 的內容,就應該重新檢查:
Driver Object Address
MajorFunction Offset
IRP_MJ_DEVICE_CONTROL Index
Pointer Size
是不是其中某一步算錯。
現在已經找到 Driver 中真正處理 DeviceIoControl 的 Function,可以選擇:
方法一:直接在 WinDbg 對該 Address 下 Breakpoint
方法二:把 Driver 丟進 IDA Pro 做 Static Analysis
實際分析時,通常可以先在 IDA Pro 中了解 Function 的整體結構,再回到 WinDbg 動態確認特定行為。
也就是:
IDA Pro
↓
先了解 Driver Function 的整體邏輯
WinDbg
↓
需要確認 Runtime Behavior 時再動態追蹤
這和之前分析 User-Space Malware 時「Static + Dynamic Analysis 搭配使用」其實是相同的概念。
接著在 IDA Pro 分析剛才找到的 Driver Function,可以看到:
push offset aDosdevicesCSec
...
call RtlInitUnicodeString
...
call ZwCreateFile
...
call ZwWriteFile
其中檔案名稱是:
\DosDevices\C:\secretfile.txt
所以這個 Driver 最後確實會從 Kernel Space 建立並寫入:
C:\secretfile.txt
這裡還會看到:
RtlInitUnicodeString
Windows Kernel 經常使用:
UNICODE_STRING
Structure 表示字串。
因此程式先使用:
RtlInitUnicodeString
建立 Kernel 使用的 Unicode String,再將它交給後面的 File Operation。
這和我們平常在 User Mode 看到的一般 Wide Character String 不完全相同。
另一個值得注意的地方是:
\DosDevices\C:\secretfile.txt
而不是單純:
C:\secretfile.txt
因為在 Kernel 中建立檔案時,需要指定完整的 Object Name,包含對應的 Root Device。
因此常會看到熟悉的 Windows Path 前面再加上:
\DosDevices
最後形成:
\DosDevices\C:\secretfile.txt
到這裡不要誤以為:
User Mode → Kernel Driver
只有 DeviceIoControl 這一條路。
User-Space Application 對 Device Handle 使用:
CreateFile
ReadFile
WriteFile
DeviceIoControl
等操作,都可能讓 Driver 收到不同種類的 Request。
例如:
ReadFile
↓
IRP_MJ_READ
IRP_MJ_READ 的 Index 是:
0x03
因此如果今天不是想找 DeviceIoControl Handler,而是想找 Driver 處理 Read Request 的 Function,就會改成:
DriverObject + 0x38 + 0x03 * 4
而不是:
DriverObject + 0x38 + 0x0E * 4
所以真正需要理解的並不是背:
0x38 + 0xe * 4
而是:
Driver Object
↓
MajorFunction[]
↓
根據 IRP_MJ_xxx 選擇 Index
↓
找到對應的 Dispatch Routine
理解這層關係之後,才能追蹤不同類型的 Driver Request。
剛才的案例比較簡單,因為 WinDbg 直接告訴我們:
FileWriter.sys
被載入了。
因此可以直接:
!drvobj FileWriter
找到 Driver Object。
但實際 Malware Analysis 不一定每次都這麼順利。
有時候我們可能只知道 User-Space Application 正在連線到某個 Device,卻不知道背後究竟是哪個 Driver。
這時就可以反過來:
User Application
↓
Device Object
↓
Driver Object
還記得前面 User-Space Code 裡看到:
CreateFileA("\\.\FileWriterDevice")
嗎?
這表示我們已經知道 Device 的名稱:
FileWriterDevice
因此可以在 WinDbg 使用:
!devobj FileWriterDevice
例如:
kd> !devobj FileWriterDevice
Device object (826eb030) is for:
Rootkit \Driver\FileWriter DriverObject 827e3698
從這裡就能得到:
Device Object
0x826eb030
↓
Driver Object
0x827e3698
也就是:
FileWriterDevice
↓
\Driver\FileWriter
取得 Driver Object Address 後,就可以再次回到前面的分析方法:
dt nt!_DRIVER_OBJECT
↓
MajorFunction
↓
IRP_MJ_xxx
↓
Dispatch Routine
另一種情況則完全相反。
假設我們已經知道:
FileWriter.sys
是一個可疑 Driver。
但是不知道:
到底是哪一個 User-Space Process 正在使用它?
前面的:
!devobj
會提供 Device Object Address。
例如:
0x826eb030
接著可以使用:
!devhandles 826eb030
WinDbg 就會搜尋各個 Process 的 Handle Table,找出哪些 Process 持有這個 Device 的 Handle。
這個操作可能花比較久,因為它必須遍歷 Process 的 Handle Table。
最後在案例中可以找到:
Image: FileWriterApp.exe
代表:
FileWriterApp.exe
持有這個 Device Object 的 Handle。
於是整條關係就串起來了:
FileWriterApp.exe
↓
CreateFile("\\.\FileWriterDevice")
↓
FileWriterDevice
↓
FileWriter.sys
↓
DeviceIoControl
↓
IRP_MJ_DEVICE_CONTROL
↓
Driver Dispatch Routine
↓
ZwCreateFile
↓
ZwWriteFile
↓
C:\secretfile.txt
這也是 Kernel Malware Analysis 很重要的一個思維:
不要只分析 Driver 本身,而是要把 User-Space Component、Device Object、Driver Object 與 Kernel Behavior 串在一起。
今天這個案例最重要的其實不是記住幾個 WinDbg Command,而是開始建立「User Mode 到 Kernel Mode」的分析思路。
以前看到:
CreateFile
WriteFile
可能只會想到檔案操作。
但現在看到:
CreateFile("\\.\DeviceName")
就應該多想到一件事情:
它可能是在取得 Device Handle。
接著看到:
DeviceIoControl
也不只是知道「它正在和 Driver 溝通」,而是可以繼續往 Kernel 裡追:
DeviceIoControl
↓
Driver Object
↓
MajorFunction[]
↓
IRP_MJ_DEVICE_CONTROL
↓
Dispatch Routine
最後真正找到 Kernel 中執行的程式碼。
今天使用到的幾個 WinDbg Command,也可以用它們在分析流程中的角色來理解:
| Command | 用途 |
|---|---|
!drvobj |
從 Driver Name 找 Driver Object |
!object \Driver |
瀏覽 \Driver Namespace |
dt nt!_DRIVER_OBJECT |
查看 Driver Object Structure |
dd |
查看 Memory 中的 DWORD |
u |
將指定 Address 的 Machine Code 反組譯 |
!devobj |
查看 Device Object,並找出對應 Driver |
!devhandles |
找出哪些 User-Space Process 持有 Device Handle |
把這些 Command 串起來,實際上就是:
User-Space Code
↓
找 Device
↓
找 Driver
↓
找 Driver Object
↓
找 Major Function Table
↓
找 Dispatch Routine
↓
分析 Kernel Code
↓
再反查使用 Driver 的 Process
這也是為什麼學 Malware Analysis 最後會逐漸碰到 Operating System Internal。
當惡意程式還待在 User Mode 時,我們可以靠 Win32 API、Process、Registry、File System 等線索理解它。
但一旦行為進入 Kernel Mode,就需要開始理解 Windows 如何管理:
Driver
Device
IRP
Driver Object
Function Pointer
Kernel Object
才能真正知道程式到底做了什麼。
