iT邦幫忙

2026 iThome 鐵人賽

DAY 22
0
Security

《Nyahello!從零開始的 30 天 malware 學習日誌》系列 第 22

【Day 22】Kernel Debugging in Practice:實際追蹤 User Mode 與 Kernel Mode 的惡意程式行為

  • 分享至 

  • xImage
  •  

前言

前面認識了 WinDbg 與 Kernel Debugging 的基本概念之後,今天要開始實際進入 Kernel Debugging。

過去在分析一般 User Mode 惡意程式時,我們經常會從 Windows API 下手。例如看到 CreateFileWriteFile,就可以推測程式可能正在建立或寫入檔案;看到 Registry API,則可能進一步追蹤 persistence 或設定修改。

但如果真正執行這些行為的程式碼不在 User Mode,而是在 Kernel Mode 呢?

這時候只盯著 User-Space API 就可能漏掉真正重要的行為。

今天會透過一個簡單的案例,看看一個 User-Space Application 如何載入 Driver、取得 Device Handle,再透過 DeviceIoControl 將資料送進 Kernel,最後找出 Driver 中真正負責處理這個 Request 的 Function。

【聲明】由於時間問題本日文章由 AI 生成QQ


Kernel Debugging in Practice

這次的範例是一個會從 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 開始。


Looking at the User-Space Code

第一步先不要急著進 WinDbg。

我們先使用 IDA Pro 分析 User-Space Application,確認它究竟如何與 Driver 互動。

1. 使用 CreateService 建立 Kernel Driver Service

首先可以在程式裡看到 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。


2. 使用 CreateFile 取得 Device Object 的 Handle

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。


3. 使用 DeviceIoControl 將資料送進 Kernel

拿到 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

其中:

hDevice

就是剛才透過:

CreateFileA("\\.\FileWriterDevice")

取得的 Device Handle。

dwIoControlCode

這是一個 I/O Control Code(IOCTL)

它告訴 Driver:

「這一次送進來的是哪一種類型的 Request?」

在這個案例中可以看到:

0x9C402408

lpInBuffer

指向 User-Space Application 要傳送給 Driver 的資料。

因此目前為止,我們已經可以建立出 User Mode 這一側的流程:

CreateService
      ↓
載入 Kernel Driver
      ↓
CreateFile("\\.\FileWriterDevice")
      ↓
取得 Device Handle
      ↓
DeviceIoControl
      ↓
把 Request / Data 傳給 Kernel Driver

但是現在還有一個最大的問題:

DeviceIoControl 進入 Kernel 之後,到底會執行 Driver 裡的哪個 Function?

這就要進入 Kernel Mode 繼續追。


Looking at the Kernel-Mode Code

接下來切換到 WinDbg。

我們要動態分析剛才 DeviceIoControl 所觸發的 Kernel Code。


1. 找到被載入的 Driver

如果 WinDbg 已經 Attach Kernel Debugger,而且開啟 Verbose Output,Kernel Module 被載入時就會顯示相關訊息。

例如:

ModLoad: f7b0d000 f7b0e780 FileWriter.sys

代表:

FileWriter.sys

剛剛被載入 Kernel。

如果正在執行一個可疑程式,接著突然出現一個新的 Kernel Module,那這個 Driver 當然就是值得優先調查的對象。

不過在 VMware 環境下有一個例外:KMixer.sys 可能經常被載入與卸載,這屬於正常現象,不能單憑這個行為判斷它是惡意 Driver。


2. 使用 !drvobj 找 Driver Object

知道 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,再從裡面尋找可疑目標。


3. 使用 dt 查看 _DRIVER_OBJECT

取得:

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


4. 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。


5. 找出 IRP_MJ_DEVICE_CONTROL 的 Function

IRP_MJ_DEVICE_CONTROL 的 Index 是:

0xE

_DRIVER_OBJECTMajorFunction 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


6. 使用 u 確認 Function Address

找到 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

是不是其中某一步算錯。


7. 接下來用 IDA 還是 WinDbg?

現在已經找到 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 搭配使用」其實是相同的概念。


8. 找到 ZwCreateFile 與 ZwWriteFile

接著在 IDA Pro 分析剛才找到的 Driver Function,可以看到:

push offset aDosdevicesCSec
...
call RtlInitUnicodeString
...
call ZwCreateFile
...
call ZwWriteFile

其中檔案名稱是:

\DosDevices\C:\secretfile.txt

所以這個 Driver 最後確實會從 Kernel Space 建立並寫入:

C:\secretfile.txt

9. Kernel 中的 UNICODE_STRING

這裡還會看到:

RtlInitUnicodeString

Windows Kernel 經常使用:

UNICODE_STRING

Structure 表示字串。

因此程式先使用:

RtlInitUnicodeString

建立 Kernel 使用的 Unicode String,再將它交給後面的 File Operation。

這和我們平常在 User Mode 看到的一般 Wide Character String 不完全相同。


10. 為什麼路徑是 \DosDevices\C:\secretfile.txt?

另一個值得注意的地方是:

\DosDevices\C:\secretfile.txt

而不是單純:

C:\secretfile.txt

因為在 Kernel 中建立檔案時,需要指定完整的 Object Name,包含對應的 Root Device。

因此常會看到熟悉的 Windows Path 前面再加上:

\DosDevices

最後形成:

\DosDevices\C:\secretfile.txt

11. 不只有 DeviceIoControl 會進入 Driver

到這裡不要誤以為:

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。


Finding Driver Objects

剛才的案例比較簡單,因為 WinDbg 直接告訴我們:

FileWriter.sys

被載入了。

因此可以直接:

!drvobj FileWriter

找到 Driver Object。

但實際 Malware Analysis 不一定每次都這麼順利。

有時候我們可能只知道 User-Space Application 正在連線到某個 Device,卻不知道背後究竟是哪個 Driver。

這時就可以反過來:

User Application
      ↓
Device Object
      ↓
Driver Object

1. 從 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

2. 從 Driver 反查是哪個 Process 在使用

另一種情況則完全相反。

假設我們已經知道:

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

才能真正知道程式到底做了什麼。

參考資料

  • Practical Malware Analysis (Chapter 9)
  • chatGPT

本日貓味

https://ithelp.ithome.com.tw/upload/images/20260917/20183878fN6XEcNbU7.jpg


上一篇
【Day 21】WinDbg 基本介紹:從 User Mode Debugging 走向 Windows Kernel
下一篇
【Day 23】惡意程式分析研究資源整理:Conference、Blog、Podcast 與 Paper
系列文
《Nyahello!從零開始的 30 天 malware 學習日誌》28
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言