驅動篇——核心程式設計基礎

寂靜的羽夏發表於2021-11-03

寫在前面

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

你如果是從中間插過來看的,請仔細閱讀 羽夏看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不能訪問分頁記憶體。

下一篇

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

相關文章