寫在前面
此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。
看此教程之前,問個問題,你明確學驅動的目的了嗎?你的開發環境準備好了嗎?上一節的內容學會了嗎? 沒有的話就不要繼續了,請重新學習前面驅動篇的教程內容繼續。
? 華麗的分割線 ?
核心 API 的使用
在應用層程式設計我們可以使用WINDOWS
提供的各種API
函式,只要匯入標頭檔案windows.h
就可以了。但是在核心程式設計的時候,微軟為核心程式提供了專用的API
,只要在程式中包含相應的標頭檔案就可以使用了,如:#include <ntddk.h>
,前提你必須安裝了WDK
。
遇到不會的函式或者不知道如何使用函式怎麼辦?在應用層程式設計的時候,我們通過MSDN
來了解函式的詳細資訊,在核心程式設計的時候,要使用WDK
自己的幫助文件。
然而WDK
說明文件中只包含了核心模組匯出的函式,對於未匯出的函式,則不能直接使用。如果要使用未匯出的函式,只要自己定義一個函式指標,並且為函式指標提供正確的函式地址就可以使用了。有兩種辦法都可以獲取為匯出的函式地址:特徵碼搜尋和解析核心PDB
檔案。對於第一種方法,每個函式不可能是一模一樣的,它們的硬編碼具有不同的特徵,通過這個特定的獨一無二的硬編碼可以搜到我想要的函式。對於最後一種方法,我們思考一下WinDbg
為什麼那麼強大。為什麼WinDbg
可以輕鬆分析一些結構體,或者函式名稱?本質原因它有符號檔案並且能夠解析它,也就是PDB
檔案。也就是為什麼我們之前要為它配備符號檔案路徑。
驅動基本資料型別
在核心程式設計的時候,強烈建議大家遵守WDK
的編碼習慣,建議不要這樣寫:unsigned long length;
,建議這樣寫:ULONG length
。
如下是WDK
習慣與我們常規的習慣:
WDK 習慣 | SDK 習慣 |
---|---|
ULONG | unsigned long |
PULONG | unsigned long* |
UCHAR | unsigned char |
PUCHAR | unsigned char* |
UINT | unsigned int |
PUNIT | unsigned int* |
VOID | void |
PVOID | void* |
函式返回值
大部分核心函式的返回值都是NTSTATUS
型別,如:
NTSTATUS PsCreateSystemThread();
NTSTATUS ZwOpenProcess();
NTSTATUS ZwOpenEvent();
這個值能說明函式執行的結果,比如:
#define STATUS_SUCCESS 0x00000000 //成功
#define STATUS_INVALID_PARAMETER 0xC000000D //引數無效
#define STATUS_BUFFER_OVERFLOW 0x80000005 //緩衝區長度不夠
當你呼叫的核心函式,如果返回的結果不是STATUS_SUCCESS
,就說明函式執行中遇到了問題,具體是什麼問題,可以在ntstatus.h
檔案中檢視。
核心異常處理
在核心中,一個小小的錯誤就可能導致藍屏,比如:讀寫一個無效的記憶體地址。為了讓自己的核心程式更加健壯,強烈建議大家在編寫核心程式時,使用異常處理,降低藍屏的可能性。不過錯誤大了該藍屏的還是藍屏。
Windows
提供了結構化異常處理機制,一般的編譯器都是支援的,如下:
__try{
//可能出錯的程式碼
}
__except(filter_value) {
//出錯時要執行的程式碼
}
出現異常時,可根據filter_value
的值來決定程式該如果執行,當filter_value
的值為:
1️⃣ EXCEPTION_EXECUTE_HANDLER(1)
:程式碼進入except
塊
2️⃣ EXCEPTION_CONTINUE_SEARCH(0)
:不處理異常,由上一層呼叫函式處理
3️⃣ EXCEPTION_CONTINUE_EXECUTION(-1)
:回去繼續執行錯誤處的程式碼
常用的核心記憶體函式
對記憶體的使用,主要就是:申請、設定、拷貝以及釋放。我們在編寫3環的應用程式和核心對應的函式舉例如下,具體使用請檢視MSDN
和WDK
的幫助文件:
普通程式 | 核心中 |
---|---|
malloc | ExAllocatePool2 |
memset | RtlFillMemory |
memcpy | RtlMoveMemory |
free | ExFreePool |
當然malloc
對應的核心函式有很多,但是有很多已經被廢棄掉了,下面是函式說明:
ExAllocatePool is obsolete and has been deprecated in Windows 10, version 2004. It has been replaced by ExAllocatePool2. For more information, see Updating deprecated > ExAllocatePool calls to ExAllocatePool2 and ExAllocatePool3.
When developing drivers for version of Windows prior to Windows 10, version 2004, use ExAllocatePoolZero.
核心字串
在編寫3環程式我們經常用:CHAR(char)
/WCHAR(wchar_t)
來分別表示宅字串和寬字串,用0表示結尾。但是在核心中,我們常用:ANSI_STRING
/UNICODE_STRING
來分別表示宅字串和寬字串。它們的結構如下:
ANSI_STRING
字串:
typedef struct _STRING
{
USHORT Length;
USHORT MaximumLength;
PCHAR Buffer;
}STRING;
UNICODE_STRING
字串:
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaxmumLength;
PWSTR Buffer;
} UNICODE_STRING;
為什麼核心要用這樣的字串呢?主要是為了安全考慮。我們初學C語言
的時候經常列印出燙燙燙
之類的字串,那是因為它列印沒用0結尾的字串的結果。如果核心出現了這個問題,很容易導致藍屏。故使用改結構體保證安全性。當然,處理這樣的字串核心就有專門處理的函式,接下來我將繼續介紹。
核心字串常用函式
字串常用的功能無非就是:建立、複製、比較以及轉換等等。它們的函式如下,具體使用請檢視WDK
的幫助文件:
ANSI_STRING | UNICODE_STRING |
---|---|
RtlInitAnsiString | RtlInitUnicodeString |
RtlCopyString | RtlCopyUnicodeString |
RtlCompareString | RtlCompareUnicoodeString |
RtlAnsiStringToUnicodeString | RtlUnicodeStringToAnsiString |
程式碼細節解析
上一篇教程我們用了一段程式碼,用來測試驅動是否能夠載入並執行,下面我們就來解析它,上次使用的程式碼如下:
#include <ntddk.h>
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Chapter Driver By WingSummer,Unloaded Successfully!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("Chapter Driver By WingSummer,Loaded Successfully!");
DriverObject->DriverUnload = UnloadDriver;
return STATUS_SUCCESS;
}
DriverEntry
DriverEntry
是驅動程式的入口,如果驅動載入成功後,就像Dll
載入成功呼叫DllMain
函式一樣,呼叫該函式。
PDRIVER_OBJECT
是指向DRIVER_OBJECT
結構體的指標。一個驅動檔案被載入後,它的完整資訊將會返回給我們。我們來看看DRIVER_OBJECT
這個結構體存了什麼,下面是標頭檔案裡面的定義:
typedef struct _DRIVER_OBJECT {
CSHORT Type;
CSHORT Size;
PDEVICE_OBJECT DeviceObject;
ULONG Flags;
PVOID DriverStart;
ULONG DriverSize;
PVOID DriverSection;
PDRIVER_EXTENSION DriverExtension;
UNICODE_STRING DriverName;
PUNICODE_STRING HardwareDatabase;
PFAST_IO_DISPATCH FastIoDispatch;
PDRIVER_INITIALIZE DriverInit;
PDRIVER_STARTIO DriverStartIo;
PDRIVER_UNLOAD DriverUnload;
PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION + 1];
} DRIVER_OBJECT;
既然是講解基礎,我們就挑最重要的幾個來講解。不過為了方便學習驅動,我們對上面的程式碼進行小小的修改:
#include <ntddk.h>
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("Chapter Driver By WingSummer,Unloaded Successfully!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DbgPrint("Chapter Driver By WingSummer,Loaded Successfully!");
DriverObject->DriverUnload = UnloadDriver;
DbgPrint("addr: %p", DriverObject);
return STATUS_SUCCESS;
}
然後編譯,讓虛擬機器載入這個驅動。如下圖所示,然後我們得到了它的首地址:
然後我們再dt
一下:
kd> dt _DRIVER_OBJECT 89B7FA20
ntdll!_DRIVER_OBJECT
+0x000 Type : 0n4
+0x002 Size : 0n168
+0x004 DeviceObject : (null)
+0x008 Flags : 0x12
+0x00c DriverStart : 0xbab50000 Void
+0x010 DriverSize : 0x6000
+0x014 DriverSection : 0x89936678 Void
+0x018 DriverExtension : 0x89b7fac8 _DRIVER_EXTENSION
+0x01c DriverName : _UNICODE_STRING "\Driver\HelloDriver"
+0x024 HardwareDatabase : 0x80671ae0 _UNICODE_STRING "\REGISTRY\MACHINE\HARDWARE\DESCRIPTION\SYSTEM"
+0x028 FastIoDispatch : (null)
+0x02c DriverInit : 0xbab54000 long HelloDriver!GsDriverEntry+0
+0x030 DriverStartIo : (null)
+0x034 DriverUnload : 0xbab51040 void HelloDriver!UnloadDriver+0
+0x038 MajorFunction : [28] 0x804f454a long nt!IopInvalidDeviceRequest+0
DriverStart
驅動物件載入後的起始地址。
DriverSize
驅動物件載入後的記憶體大小。
DriverSection
它是一個儲存目前所有已載入的驅動程式資訊相關的LDR_DATA_TABLE_ENTRY
結構體的雙向迴圈連結串列。通過這個東西來實現把它們全部串起來,通過這個我們也可以進行遍歷。我們通過WinDbg
來看看。我們先dt
一下我們自己編寫的驅動的DriverSection
:
kd> dt _LDR_DATA_TABLE_ENTRY 0x89936678
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x80554fc0 - 0x89b80d58 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xffffffff - 0xffffffff ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0x630069 - 0x0 ]
+0x018 DllBase : 0xbab50000 Void
+0x01c EntryPoint : 0xbab54000 Void
+0x020 SizeOfImage : 0x6000
+0x024 FullDllName : _UNICODE_STRING "\??\C:\Documents and Settings\wingsummer\桌面\HelloDriver.sys"
+0x02c BaseDllName : _UNICODE_STRING "HelloDriver.sys"
+0x034 Flags : 0x9104000
+0x038 LoadCount : 1
+0x03a TlsIndex : 0x49
+0x03c HashLinks : _LIST_ENTRY [ 0xffffffff - 0x1055c ]
+0x03c SectionPointer : 0xffffffff Void
+0x040 CheckSum : 0x1055c
+0x044 TimeDateStamp : 0xfffffffe
+0x044 LoadedImports : 0xfffffffe Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : 0x00650048 Void
然後我們繼續dt
下一個成員:
kd> dt _LDR_DATA_TABLE_ENTRY 0x89b80d58
ntdll!_LDR_DATA_TABLE_ENTRY
+0x000 InLoadOrderLinks : _LIST_ENTRY [ 0x89936678 - 0x89b45e98 ]
+0x008 InMemoryOrderLinks : _LIST_ENTRY [ 0xb8183850 - 0x1 ]
+0x010 InInitializationOrderLinks : _LIST_ENTRY [ 0xe - 0x0 ]
+0x018 DllBase : 0xb817e000 Void
+0x01c EntryPoint : 0xb81a6105 Void
+0x020 SizeOfImage : 0x2b000
+0x024 FullDllName : _UNICODE_STRING "\SystemRoot\system32\drivers\kmixer.sys"
+0x02c BaseDllName : _UNICODE_STRING "kmixer.sys"
+0x034 Flags : 0x9104000
+0x038 LoadCount : 1
+0x03a TlsIndex : 0x74
+0x03c HashLinks : _LIST_ENTRY [ 0xffffffff - 0x2f580 ]
+0x03c SectionPointer : 0xffffffff Void
+0x040 CheckSum : 0x2f580
+0x044 TimeDateStamp : 0xe1786190
+0x044 LoadedImports : 0xe1786190 Void
+0x048 EntryPointActivationContext : (null)
+0x04c PatchInformation : 0x006d006b Void
可以看出,我們可以通過這個連結串列實現遍歷驅動程式的資訊。
DriverName
指示驅動物件的名字,是一個_UNICODE_STRING
的結構體。
DriverUnload
驅動物件的解除安裝地址,如果存在則會呼叫它。它的定義:
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
其他
剩下的未介紹的成員,自己感興趣的自行繼續探索。
IRQL
IRQL
全稱Interrupt Request Level
,即中斷執行的優先順序。它是Windows
自己定義的一套優先順序方案,與CPU
無關,數值越大許可權越高。中斷包括了硬中斷和軟中斷,硬中斷是由硬體產生,而軟中斷則是完全虛擬出來的。處理器在一個IRQL
上執行執行緒程式碼,每個處理器的IRQL
決定了它如何處理中斷,以及允許接收哪些中斷。在同一處理器上,執行緒只能被更高階別IRQL
的執行緒能中斷。每個處理器都有自己的中斷IRQL
。常見的IRQL
級別有四個:Passive
、APC
、Dispatch
、DIRQL
。PASSIVE_LEVEL
是最低階別,沒有被遮蔽的中斷,執行緒執行使用者模式,可以訪問分頁記憶體。APC_LEVEL
只有APC
級別的中斷被遮蔽,可以訪問分頁記憶體。當有APC
發生時,處理器提升到APC
級別,就遮蔽掉其它APC
。DISPATCH_LEVEL
可以遮蔽DPC
(延遲過程) 和更低的中斷,不能訪問分頁記憶體。因為只能處理分頁記憶體,所以在這個級別,能夠訪問的API
大大減少。對於我們核心安全來講,瞭解這些就夠了,如下是IRQL
的示意圖:
在進行核心程式編寫的時候,尤其注意IRQL
這個東西。有很多的藍屏因此而起。
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。
1️⃣ 編寫驅動,申請一塊記憶體,並在記憶體中儲存GDT
表的所有資料。然後在DebugView
中顯示出來,最後釋放記憶體。
2️⃣ 編寫驅動,實現如下功能:
<1> 初始化一個字串;
<2> 拷貝一個字串;
<3> 比較兩個字串是否相等;
<4> ANSI_STRING
與UNICODE_STRING
字串相互轉換;
3️⃣ 思考題:為什麼DISPATCH_LEVEL
不能訪問分頁記憶體。
下一篇
驅動篇——核心空間與核心模組