驅動篇——內核編程基礎

寫在前面

  此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看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環的應用程式和內核對應的函數舉例如下,具體使用請查看MSDNWDK的幫助文檔:

普通程式 內核中
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級別有四個:PassiveAPCDispatchDIRQLPASSIVE_LEVEL是最低級別,沒有被屏蔽的中斷,執行緒執行用戶模式,可以訪問分頁記憶體。APC_LEVEL只有APC級別的中斷被屏蔽,可以訪問分頁記憶體。當有APC發生時,處理器提升到APC級別,就屏蔽掉其它APCDISPATCH_LEVEL可以屏蔽DPC(延遲過程) 和更低的中斷,不能訪問分頁記憶體。因為只能處理分頁記憶體,所以在這個級別,能夠訪問的API大大減少。對於我們內核安全來講,了解這些就夠了,如下是IRQL的示意圖:

  在進行內核程式編寫的時候,尤其注意IRQL這個東西。有很多的藍屏因此而起。

本節練習

本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。

  俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。

1️⃣ 編寫驅動,申請一塊記憶體,並在記憶體中存儲GDT表的所有數據。然後在DebugView中顯示出來,最後釋放記憶體。

2️⃣ 編寫驅動,實現如下功能:
<1> 初始化一個字元串;
<2> 拷貝一個字元串;
<3> 比較兩個字元串是否相等;
<4> ANSI_STRINGUNICODE_STRING字元串相互轉換;

3️⃣ 思考題:為什麼DISPATCH_LEVEL不能訪問分頁記憶體。

下一篇

  驅動篇——內核空間與內核模組