驅動篇——內核空間與內核模塊
- 2021 年 11 月 6 日
- 筆記
- 羽夏看Win系統內核
寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章後面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。
看此教程之前,問個問題,你明確學驅動的目的了嗎?你的開發環境準備好了嗎?上一節的內容學會了嗎? 沒有的話就不要繼續了,請重新學習前面驅動篇的教程內容繼續。
🔒 華麗的分割線 🔒
練習及參考
本次答案均為參考,可以與我的答案不一致,但必須成功通過。
1️⃣ 編寫驅動,申請一塊內存,並在內存中存儲GDT
表的所有數據。然後在DebugView
中顯示出來,最後釋放內存。
🔒 點擊查看代碼 🔒
#include <ntddk.h>
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
/*讀取 GDT 表*/
char gdt[6];
_asm
{
sgdt gdt;
}
short limit = *(short*)gdt;
UINT32* base = *(UINT32*)&gdt[2];
UINT32* mem = (UINT32*)ExAllocatePoolWithTag(NonPagedPool, limit, NULL);
if (!mem)
{
DbgPrint("申請內存失敗!!\n");
}
else
{
RtlMoveMemory(mem, base, limit);
DbgPrint("\n=====打印申請的內存存的數據=====\n");
for (int i = 0; i < limit / 4; i+=2)
{
DbgPrint("%0.8x`%0.8x\n", mem[i+1], mem[i]);
}
ExFreePool(mem);
}
return STATUS_SUCCESS;
}
2️⃣ 編寫驅動,實現如下功能:
<1> 初始化一個字符串;
<2> 拷貝一個字符串;
<3> 比較兩個字符串是否相等;
<4> ANSI_STRING
與UNICODE_STRING
字符串相互轉換;
🔒 點擊查看代碼 🔒
#include <ntddk.h>
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
UNICODE_STRING unicode_str;
ANSI_STRING ansi_str;
RtlInitAnsiString(&ansi_str, "WingSummer CNBLOG");
RtlInitUnicodeString(&unicode_str, L"CNBLOG ONLY");
DbgPrint("ANSI : %Z\n", &ansi_str);
DbgPrint("Unicode : %wZ\n", &unicode_str);
ANSI_STRING astr;
RtlInitAnsiString(&astr, "Wingsummer CNBLOG");
BOOLEAN b0 = RtlCompareString(&ansi_str, &astr, TRUE);
BOOLEAN b1 = RtlCompareString(&ansi_str, &astr, FALSE);
DbgPrint("RtlCompareString : %d %d", b0, b1);
UNICODE_STRING ustr;
RtlInitUnicodeString(&ustr, L"Hello CNBLOG");
b0 = RtlCompareUnicodeString(&unicode_str, &ustr, FALSE);
DbgPrint("RtlCompareUnicodeString : %d", b0);
ANSI_STRING astr0;
UNICODE_STRING ustr0 = {0}; //主要是為了讓 IntelliSense 閉嘴
RtlAnsiStringToUnicodeString(&ustr0, &ansi_str, TRUE);
RtlUnicodeStringToAnsiString(&astr0, &ustr, TRUE);
DbgPrint("%wZ\n%Z", &unicode_str, &astr);
RtlFreeAnsiString(&astr0);
RtlFreeUnicodeString(&ustr0);
return STATUS_SUCCESS;
}
3️⃣ 思考題:為什麼DISPATCH_LEVEL
不能訪問分頁內存。
🔒 點擊查看答案 🔒
說到這個問題,我們回過頭來看看上篇文章的一個圖:
從圖上可以看出,假設我們的驅動運行在IRQL
為DISPATCH_LEVEL
,並使用分頁內存,且使用的分頁內存的物理頁被操作系統回收放到文件了。然後驅動程序需要訪問這個內存,必定會觸發缺頁異常,正常的話會進入異常處理程序。
然而不幸的是,這個異常處理程序處在APC_LEVEL
這層IRQL
,結果你現在的IRQL
是DISPATCH_LEVEL
,很明顯異常處理程序無法打斷它,結果無法運行。既然異常處理不了,那就進入熟悉的藍屏環節。
綜上所述,這就是為什麼DISPATCH_LEVEL
不能訪問分頁內存。
內核空間與內核模塊
在之前的教程里,我們介紹了普通的應用程序的4GB
內存只有低2GB
可能被自身使用,而高2GB
共用。示意圖如下所示:
硬件種類繁多,不可能做一個兼容所有硬件的內核,所以,微軟提供規定的接口格式,讓硬件驅動人員安裝規定的格式編寫驅動程序
。在內核中,這些驅動程序每一個都是一個模塊,稱為內核模塊
,都可以加載到內核中,都遵守PE
結構。但本質上講,任意一個sys
文件與內核文件沒有區別。每個驅動都是一個模塊。就和在3環的程序加載dll
一樣,加載一個貼上一個。如下圖所示:
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;
我們如何通過調試信息輸出呢,我們如下示例:
DbgPrint("DRIVER_OBJECT 對象地址:%x \r\n",driver);
DbgPrint("驅動名稱:%ws \r\n",driver->DriverName.Buffer);
DbgPrint("模塊基址:%x \r\n",driver->DriverStart);
DbgPrint("模塊大小:%x \r\n",driver->DriverSize);
遍歷內核模塊與隱藏
上篇教程我們講過DriverSection
,它是一個存儲目前所有已加載的驅動程序信息相關的LDR_DATA_TABLE_ENTRY
結構體的雙向循環鏈表。並且我們用WinDbg
手動遍歷了一下。那麼我們用代碼遍歷也是可以的。這裡就留個作業了,具體代碼留在下一篇進行講解。
如果我們把這個項目從DriverSection
的InLoadOrderLinks
摘掉後,是不是可以實現隱藏呢?會不會對我加載後的驅動有沒有影響?既然驅動已經加載到內存當中,就算你去除了它的存在信息,對它的功能也沒有實際影響。可以實現隱藏,不過只是對付一些API
,但它還是被揪出來的,如何進一步隱藏將會在後面的教程介紹。
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。
1️⃣ 遍歷內核模塊,輸出模塊名稱,基址以及大小。
2️⃣ 編寫一個函數,找到一個未導出的函數,並調用。(例子:找到PspTerminateProcess
,通過調用這個函數結束記事本進程)
3️⃣ 通過斷鏈實現隱藏驅動模塊。
下一篇
驅動篇——常規的0環與3環通信