編者按:這是一篇講解Windows內核驅動逆向分析之套路方法的好文,可以幫助大家奠定逆向分析木馬驅動文件的基礎,具有很強的指導性。通過此文,理清思路,掌握固定模式的逆向分析套路,方便大家按圖索驥。
文中作者(安全客)舉例分析的是一個非常簡單標準的驅動;下圖是我用Ghidra9.1分析mimikatz.sys,這個驅動要複雜一些,但總體思路是一樣的。
文章很長,希望你能認真地看完,也不枉我的一番心血。
(文章來源:安全客)
一、工作流程:
1、跟隨DriverEntry的第一個參數,即驅動程序對象,直至找到指示MajorFunction表的偏移量。
2、在MajorFunction[0xe]處查找偏移量,標記DeviceIoControl IRP Handler。
3、跟隨這個函數的第二個參數PIRP,直至找到PIRP->Tail +0x40,將其標記為CurrentStackLocation。
4、從CurrentStackLocation查找偏移量+0x18,這就是我們想尋找的IOCTL。
在很多情況下,我們會跳過第3步和第4步,並藉助反編譯器進行一連串的DWORD比較。如果為了方便,我們往往會尋找對IofCompleteRequest的調用,然後從調用向上滾動,以查找DWORD比較。
二、概述
多年來,許多惡意組織紛紛致力於針對Windows內核模式軟體驅動程序進行攻擊,特別是針對第三方發布的驅動程序開展攻擊。在這些漏洞中,一個比較常見和有據可查的就是CAPCOM.sys任意函數執行、Win32k.sys本地特權提升以及EternalBlue池損壞漏洞。攻擊者在驅動程序中得到了一些新的攻擊維度,無論是通過傳統的漏洞利用原語,還是濫用合法的驅動程序功能,這些都無法在用戶模式中實現。
隨著Windows安全性的不斷發展,研究內核模式驅動程序中的漏洞利用對於我們的攻防技術而言變得越來越重要。為了輔助分析這些漏洞,我認為比較重要的一件事是,我們需要在研究中探尋內核漏洞,並找到一些值得關注並且可以濫用的功能。
在本文中,我將首先介紹驅動程序的工作原理,說明所需的先驗知識,隨後將進入反彙編領域,逐步查找潛在的易受攻擊的內部函數。
目標識別與選擇
通常情況下,我們要首先分析的,就是基礎工作站和伺服器映像上到底加載了哪些驅動程序。如果我們能在這些核心驅動程序中發現漏洞,那麼其影響將會比較廣泛。同時,這也在對抗過程中帶來了一個好處,也就是不需要再投放和加載新的驅動程序,從而降低被發現的概率。為此,我將手動查看註冊表中的驅動程序(HKLM\System\ControlSetServices,其中Type為0x1,ImagePath包含*.sys的條目),或使用類似於DriverQuery的工具通過C2來運行。
在選擇目標時,我們需要考慮綜合因素,因為沒有某一種特定類型的驅動程序是比較容易受到攻擊的。儘管如此,但我們傾向於將目標放在由安全廠商發布的驅動程序、由主板廠商發布的任何內容以及性能監控軟體。並且,我們傾向於忽略掉微軟的驅動程序,因為我們通常沒有太多的時間對其進行深入研究。
三、驅動內部原理分析
如果大家以前沒有開發過內核模式軟體驅動程序,那麼可能會發現,它看起來要比實際複雜得多。在開始進行逆向之前,必須首先了解三個重要概念:DriverEntry、IRP Handler和IOCTL。
3.1DriverEntry
與C/C++語言中的main()函數非常相似,驅動程序必須指定入口點DriverEntry。
DriverEntry要負責很多工作,例如創建設備對象、創建用於與驅動程序和核心函數(IRP Handler、卸載函數、回調例程等)進行通信的符號連結。
DriverEntry首先使用到IoCreateDevice()或IoCreateDeviceSecure()的調用來創建設備對象,後者通常用於將安全描述符應用於設備對象,以限制對本地管理員和NT AUTHORITYSYSTEM的訪問。
接下來,DriveEntry將IoCreateSymbolicLink()與先前創建的設備對象一起使用,以建立符號連結,該連結將允許用戶模式進程與驅動程序進行通信。
其代碼如下:
NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {
UNREFERENCED_PARAMETER(RegistryPath);
NTSTATUS status;
// Create the device object
UNICODE_STRING devName = RTL_CONSTANT_STRING(L"\Device\MyDevice");
PDEVICE_OBJECT DeviceObject;
NTSTATUS status = IoCreateDevice(DriverObject, 0, &devName, FILE_DEVICE_UNKNOWN, 0, FALSE, &DeviceObject);
if (!NT_SUCCESS(status)) {
KdPrint(("Failed to create device object");
return status;
}
// Create the symbolic link
UNICODE_STRING symLink = RTL_CONSTANT_STRING(L"\??\MySymlink");
status = IoCreateSymbolicLink(&symLink, &devName);
if (!NT_SUCCESS(status)) {
KdPrint(("Failed to create symbolic link"));
IoDeleteDevice(DeviceObject);
return status;
}
return status;
}
最後,DriverEntry還定義了IRP Handler的函數。
3.2IRP Handler
中斷請求包(IRP)本質上只是驅動程序的一條指令。這些數據包允許驅動程序通過提供函數所需的相關信息來執行特定的主要函數。主要函數的代碼較多,但其中最常見的是IRP_MJ_CREATE、IRP_MJ_CLOSE和IRP_MJ_DEVICE_CONTROL。
這些與用戶模式函數相關:
IRP_MJ_CREATE → CreateFileIRP_MJ_CLOSE → CloseFileIRP_MJ_DEVICE_CONTROL → DeviceIoControl
在用戶模式下執行以下代碼時,驅動程序將收到具有主要函數代碼IRP_MJ_CREATE的IRP,並將執行MyCreateCloseFunction函數:
hDevice = CreateFile(L"\\.\MyDevice", GENERIC_WRITE|GENERIC_READ, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
在幾乎所有情況下,對我們來說最重要的主要函數是IRP_MJ_DEVICE_CONTROL,該函數用於發送請求,以從用戶模式執行特定的內部函數。這些請求中包括一個IO控制代碼,該代碼負責通知驅動程序具體的操作,還包含一個向驅動程序發送數據和從驅動程序接收數據的緩衝區。
3.3IOCTL
IO控制代碼(IOCTL)是我們的主要搜尋目標,因為其中包含我們需要知道的很多重要細節。它是以DWORD表示,每一個32位都表示有關請求的詳細信息,包括設備類型、需要的訪問、函數代碼和傳輸類型。微軟提供了一個可視化的圖表來分解這些欄位:
1、傳輸類型:定義將數據傳遞到驅動程序的方式,具體的類型可以是METHOD_BUFFERED、METHOD_IN_DIRECT、METHOD_OUT_DIRECT或METHOD_NEITHER。
2、函數代碼:驅動程序要執行的內部函數。這部分應該是以0x800開始,但實際上我們會發現,很多都是從0x0開始的。其中的自定義位(Custom bit)用於定義廠商分配的值。
3、設備類型:在IoCreateDevice(Secure)()指定的驅動程序設備對象類型。在Wdm.h和Ntddk.h中,定義了許多設備類型,但對於軟體驅動程序而言,最常見的一種就是FILE_DEVICE_UNKNOWN (0x22)。其中的通用位(Common bit)用於定義廠商分配的值。
驅動程序標頭定義示例如下:
#define MYDRIVER_IOCTL_DOSOMETHING CTL_CODE(0x8000, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS)
我們完全可以自行對這些值進行解碼,但如果大家覺得解碼過程過於繁瑣,可以使用OSR的在線解碼器。並且,我們發現!ioctldecode Windbg擴展可能對這一過程非常有幫助。當我們編寫與目標驅動程序接口的應用程式時,這些細節將尤為重要。在反彙編程序中,它們仍然會以十六進位表示。
3.4 組合
我知道,上述的過程可能太過複雜。但是,我們可以對其進行簡化,並類比於發送網絡數據包的過程來對其進行分析。我們可以使用所需的任何細節來構造數據包,然後將其發送到伺服器進行處理,使用該數據包執行某些操作。而對於伺服器端,要不然會忽略我們的數據包,要不然會返回一些結果。接下來,我們對IOCTL的發送和處理方式進行簡化並說明:
1、用戶模式應用程式獲取符號連結上的句柄。
2、用戶模式應用程式使用DeviceIoControl()將所需的IOCTL和輸入/輸出緩衝區發送到符號連結。
3、符號連結指向驅動程序的設備對象,並允許驅動程序接收用戶模式應用程式的數據包(IRP)。
4、驅動程序看到該數據包來自DeviceIoControl(),因此將其傳遞給已定義的內部函數MyCtlFunction()。
5、MyCtlFunction()將函數代碼0x800映射到內部函數SomeFunction()。
6、SomeFunction()執行。
7、IRP已經完成,其狀態以及驅動程序在用戶模式應用程式中提供的輸出緩衝區中包含的為用戶提供的所有內容都將傳回給用戶。
注意:在這裡我們並不是說IRP已經完成,但需要關注的是,一旦SomeFunction()執行,上述時間就可以發生,並且將得到函數返回的狀態代碼,這表明操作的結束。
四、驅動程序反編譯
現在,我們已經對需要探尋的關鍵結構有了一定了解,是時候開始深入研究目標驅動程序了。我們會清晰展現出如何在Ghidra中執行此操作,IDA中的方法與之完全相同。
一旦我們將要定位的驅動程序下載到我們的分析環境中,就應該開始尋找IRP Handler,該處理程序會使我們明確潛在的關鍵函數。
4.1 設置
由於Ghidra中並沒有包含太多分析驅動程序所需的符號,所以我們需要找到一種方法,以某種方式將其進行導入。幸運的是,感謝0x6d696368(Mich)此前所做的研究工作,幫助我們簡化了這一過程。
Ghidra支持Ghidra數據類型存檔(GDT)格式的數據類型,這些數據類型是打包的二進位文件,其中包含從所選標頭衍生出的符號,這些符號可能是自定義的,也可能是微軟提供的。有關這些文件的文檔並不多,並且確實需要進行一些手工修改,但幸運的是,Mich已經完成了這部分工作。
我們在他的GitHub項目中找到了針對Ntddk.h和Wdm.h的預編譯GDT,即ntddk_64.gdt。我們需要在運行Ghidra的系統上下載該文件。
要導入並開始使用GDT文件,我們需要打開要分析的驅動程序,單擊「Data Type Manager」的下拉箭頭,然後選擇「Open File Archive」。
然後,選擇先前下載的ntddk_64.gdt文件並打開。
在「Data Type Manager」窗口中,目前有一個新的條目「ntddk_64」。右鍵單擊該條目,然後選擇「Apply Function Data Types」,隨後將會更新反編譯器,並且可以看到許多函數籤名的變化。
4.2 查找DriverEntry
現在,我們已經對數據類型進行了排序,接下來需要確定驅動程序對象。這個過程相對簡單,因為它是DriverEntry的第一個參數。首先,在Ghidra中打開驅動程序,並進行初始的自動分析。在「Symbol Tree」窗口下,展開「Exports」項目,就可以找到有一個名為entry的函數。
注意:在某些情況下,可能還會有一個GsDriverEntry函數,看起來像是對兩個未命名函數的調用。這是開發人員使用/GS編譯器標誌並設置棧Cookie的結果。其中的一個函數是真正的驅動程序入口,因此我們需要檢查其中較長的一個函數。
4.3 查找IRP Handler
我們需要查找的第一個內容是驅動程序對象的一系列偏移量。這些都與nt!_DRIVER_OBJECT結構的屬性有關。其中,我們最感興趣的一個是MajorFunction表(+0x70)。
使用我們新應用的符號,就變得容易很多。因為我們知道,DriverEntry的第一個參數是指向驅動程序對象的指針,所以我們可以在反編譯器中單擊該參數,然後按CTRL+L來調出數據類型選擇器,搜索PDRIVER_OBJECT,然後單擊「OK」,這樣將更改參數的類型以對應其真實類型。
注意:我希望將參數名稱更改為DriverObject,以在執行該函數時為我提供一些幫助。要執行此操作,需要單擊參數,然後按「L」,然後輸入要使用的名稱。
現在,我們就有了適當的類型,是時候開始尋找MajorFunction表的偏移量了。有時候,我們可能會在DriverEntry函數中看到這個權限,但有時可以看到驅動程序對象作為參數傳遞給另一個內部函數。
接下來,我們查找DriverObject變量的出現。使用滑鼠可以輕鬆完成查找工作,只需要在變量上單擊滑鼠滾輪,反編譯器中就可以突出顯示該變量的所有實例。在我們使用的示例中,沒有看到對驅動程序對象的偏移量的引用,但發現它被傳遞到另一個函數。
我們跳到FUN_00011060這個函數,然後將第一個參數重新輸入到PDRIVER_OBJECT中,因為我們知道DriverEntry將其作為唯一參數顯示。
然後,再次開始從DriverObject變量中搜索對偏移量的引用。我們正在尋找的是:
在vanilla Ghidra中,我們將這些視圖視為DriverObject的詳細偏移量,但由於我們已經應用了NTDDK數據類型,因此現在它變得更為整潔。現在,我們已經找到了標記了MajorFunction表的DriverObject偏移量,索引的位置是(0, 2, 0xe)?這些偏移量都是在WDM標頭(wdm.h)中定義,代表IRP主要函數代碼。
在我們的示例中,驅動程序處理3個主要函數代碼:IRP_MJ_CREATE、IRP_MJ_CLOSE和IRP_MJ_DEVICE_CONTROL。其中,前兩個我們並不關注,但第三個IRP_MJ_DEVICE_CONTROL非常關鍵,因為在該偏移量(0x104bc)處定義的函數使用了DeviceIoControl及其包含的I/O控制代碼(IOCTL)來處理從用戶模式發出的請求。
接下來,讓我們深入研究該函數。我們查看MajorFunction[0xe]的偏移量,將會看到驅動程序中偏移量為0x104bc的函數。該函數的第二個參數以及所有設備I/O控制IRP Handler是指向IRP的指針。我們可以再次使用CTLR+L,將第二個參數重新命名為PIRP(或者其他自定義名稱)。
IRP結構非常複雜,即使有了我們新的類型定義,也無法弄清楚所有內容。在其中,我們首先要尋找的是IOCTL。這部分在反編譯器中將以DWORD來表示,但我們需要知道它們將分配給哪個變量。為了弄明白這一點,我們要依靠我們的老朋友:WinDbg。
我們可以看到,IRP的第一個偏移量是IRP->Tail + 0x40。
接下來,我們深入研究一下IRP結構。
我們可以看到Tail是從偏移量+0x78開始,但是0x40位元組又是什麼呢?藉助WinDbg,我們可以看到CurrentStackLocation是位於Irp->Tail偏移量+0x40的位置,但僅僅顯示為一個指針。
微軟似乎暗示,這是指向_IO_STACK_LOCATION結構的指針。因此,在反編譯器中,我們可以將lVar2重命名為CurrentStackLocation。
在這個新變量之後,我們希望找到對偏移量+0x18(即IOCTL)的引用。
如果希望,還可以將該變量重命名為便於識別的名稱。
現在,我們已經找到了包含IOCTL的變量,我們看到,它與一系列DWORD進行了比較。
這些比較是驅動程序檢查這些IOCTL是否屬於其可以處理的範圍。在每次比較之後,可能會發生內部函數調用。當特定的IOCTL從用戶模式發送到驅動程序時,將會執行這些操作。在上面的示例中,驅動程序收到IOCTL 0x8000204c時,將執行FUN_0000944c(某些類型的列印函數)和FUN_000100d0。
五、函數逆向
既然我們已經知道驅動程序收到IOCTL時哪些函數會在內部執行,我們就可以開始逆向這些函數,以找到有趣的功能。由於各個驅動程序之間的差異很大,因此我們的分析中就沒有包含這部分內容。
在這裡,我的常用思路是,在這些函數中查找有趣的API調用,確定輸入所需的內容,然後使用簡單的用戶模式客戶端(根據目標來複製並修改的通用模板)來發送IRP。在分析EDR驅動程序時,我還希望了解它們具體的功能,例如進程對象處理程序回調。在此過程中,我找到了一些不錯的驅動程序漏洞,可以激發出我們的一些靈感。
需要注意的一件重要事情,特別是在使用Ghidra時,需要注意這個變量聲明:
如果我們在WinDbg中查看此內容,我們可以發現,在這個偏移量的位置是指向MasterIrp的指針。
實際上,我們看到的是與IRP->SystemBuffer的併集,該變量實際上是METHOD_BUFFERED數據結構。這也就是為什麼我們經常會看到它作為參數傳遞給內部函數的原因。在對內部函數進行逆向的過程中,請確保將其視為輸入/輸出緩衝區。
假期的時間充裕,總是能做很多事情!