驅動篇——常規的0環與3環通訊
- 2021 年 11 月 6 日
- 筆記
- 羽夏看Win系統內核
寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎回饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並聲明我的個人資訊和本人部落格地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統內核——簡述 ,方便學習本教程。
看此教程之前,問個問題,你明確學驅動的目的了嗎?你的開發環境準備好了嗎?上一節的內容學會了嗎? 沒有的話就不要繼續了,請重新學習前面驅動篇的教程內容繼續。
🔒 華麗的分割線 🔒
練習及參考
本次答案均為參考,可以與我的答案不一致,但必須成功通過。
1️⃣ 遍歷內核模組,輸出模組名稱,基址以及大小。
🔒 點擊查看答案 🔒
此題目不難,就是一個循環雙向鏈表的遍歷,程式碼見下面的摺疊,效果如下:
🔒 點擊查看程式碼 🔒
#include <ntddk.h>
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸載成功!!!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
DbgPrint("\n=====驅動遍歷 By 寂靜的羽夏 cnblog=====\n");
LIST_ENTRY* lethis = (LIST_ENTRY*)DriverObject->DriverSection;
LIST_ENTRY* item = lethis;
DRIVER_OBJECT obj;
while (1)
{
PUNICODE_STRING name = (PUNICODE_STRING)(((UINT32)item) + 0x2c);
UINT32 DllBase = *(UINT32*)(((UINT32)item) + 0x18);
UINT32 ImgSize= *(UINT32*)(((UINT32)item) + 0x20);
DbgPrint("DriverName : %wZ\nDllBase : %x\nImgSize : %x\n======\n", name, DllBase, ImgSize);
item = item->Blink;
if (item == lethis)
{
break;
}
}
return STATUS_SUCCESS;
}
2️⃣ 編寫一個函數,找到一個未導出的函數,並調用。(例子:找到PspTerminateProcess
,通過調用這個函數結束記事本進程)
🔒 點擊查看答案 🔒
根據PE
的知識,我們可以通過基址+偏移的方式定位該函數,這個是最簡潔的方式。當然可以通過特徵碼的方式,不過效率低,特徵碼找不好還不準確。
我們先在WinDbg
找找這個函數在哪裡:
kd> x nt!_PspTerminateProcess
805c9da4 nt!PspTerminateProcess (_PspTerminateProcess@8)
這個函數是在內核文件導出,分頁不同,導出的函數不同,位置也可能不同,下面是在2-9-9-12
分頁模式下做的實驗,如果在10-10-12
分頁可能函數的位置不同:
我們只需要獲取函數偏移,獲取基地址,加起來即是函數地址,然後調用即可,程式碼見摺疊,必要位置具有注釋。
好了,我們嘗試一下能不能終止進程,先在WinDbg
找到EPROCESS
結構體的地址:
Failed to get VadRoot
PROCESS 89cb7918 SessionId: 0 Cid: 0454 Peb: 7ffdf000 ParentCid: 05fc
DirBase: 12d002e0 ObjectTable: e1072a18 HandleCount: 44.
Image: notepad.exe
89cb7918
就是我們需要的地址,修改調用PspTerminateProcess
的第一個參數的數值,然後編譯。在虛擬機進行註冊啟動驅動效果如下:
由於這個函數很底層,可以幹掉很多流氓軟體,甚至殺軟都不放過。比如火絨(已將該情況上報給火絨官方):
🔒 點擊查看程式碼 🔒
#include <ntddk.h>
typedef NTSTATUS (__stdcall *PspTerminateProcess)(INT32,INT32);
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
DbgPrint("卸載成功!!!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
UNICODE_STRING ntkrnl;
RtlInitUnicodeString(&ntkrnl, L"ntoskrnl.exe"); //有意思的是即使是 2-9-9-12 分頁,還是這個名字
LIST_ENTRY* lethis = (LIST_ENTRY*)DriverObject->DriverSection;
LIST_ENTRY* item = lethis;
DRIVER_OBJECT obj;
UINT32 DllBase = 0;
while (1)
{
PUNICODE_STRING name = (PUNICODE_STRING)(((UINT32)item) + 0x2c);
if (!RtlCompareUnicodeString(name,&ntkrnl,FALSE))
{
DllBase = *(UINT32*)(((UINT32)item) + 0x18);
break;
}
item = item->Blink;
if (item == lethis)
{
break;
}
}
if (DllBase)
{
PspTerminateProcess pspTerminateProcess = (PspTerminateProcess)(DllBase + 0xF1DA4); //0xF1DA4 就是偏移
pspTerminateProcess(0x89b56c98, 0); //第一個參數根據自己的填
}
return STATUS_SUCCESS;
}
3️⃣ 通過斷鏈實現隱藏驅動模組。
🔒 點擊查看答案 🔒
此題目不難,就是一個鏈表斷鏈,效果如下:
PCHunter
這個ARK
工具仍能發現我們的模組,指明為隱藏驅動。但是你用普通的API
試試,你絕對發現不了它。
🔒 點擊查看程式碼 🔒
#include <ntddk.h>
LIST_ENTRY* lethis;
LIST_ENTRY* fle;
LIST_ENTRY* ble;
NTSTATUS UnloadDriver(PDRIVER_OBJECT DriverObject)
{
fle->Blink = lethis;
ble->Flink = lethis;
DbgPrint("卸載成功!!!");
}
NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath)
{
DriverObject->DriverUnload = UnloadDriver;
lethis = (LIST_ENTRY*)DriverObject->DriverSection;
fle = lethis->Flink;
ble = lethis->Blink;
fle->Blink = lethis->Blink;
ble->Flink = lethis->Flink;
DbgPrint("載入並隱藏成功!!!");
return STATUS_SUCCESS;
}
設備對象
我們在開發窗口程式的時候,消息被封裝成一個結構體:MSG
。在內核開發時,消息被封裝成另外一個結構體:IRP
,英文全稱:I/O Request Package
。在窗口程式中,能夠接收消息的只能是窗口對象。在內核中,能夠接收IRP
消息的只能是設備對象。示意圖如下所示:
常規通訊流程
為了實現3環程式與驅動程式正常的通訊功能,微軟提供了一系列的API
。我們可以通過它來實現常規的通訊。我們的硬碟、鍵盤、顯示卡想要工作,在Windows
平台都需要用此實現通訊,來實現想要的功能。下面我來介紹具體流程。
創建設備對象
如果MSG
需要傳遞,就必須創建一個窗體,因為只有窗體才有消息隊列這個東西,才嗯那個接收消息。如果想要驅動實現通訊,就必須有一個設備對象。我們可以用下面的程式碼實現創建設備:
//創建設備名稱
UNICODE_STRING Devicename;
RtlInitUnicodeString(&Devicename,L"\\Device\\MyDevice");
//創建設備
IoCreateDevice(
pDriver, //當前設備所屬的驅動對象
0,
&Devicename, //設備對象的名稱
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,
&pDeviceObj //設備對象指針
);
設置交互數據的方式
既然設備對象創建好了,我們需要規定一個「協議」,就是3環程式與驅動交互的協議。具體有如下幾個方式:
緩衝區方式讀寫(DO_BUFFERED_IO
) :作業系統將應用程式提供緩衝區的數據複製到內核模式下的地址中。
直接方式讀寫(DO_DIRECT_IO
) :作業系統會將用戶模式下的緩衝區鎖住。然後作業系統將這段緩衝區在內核模式地址再次映射一遍。這樣,用戶模式的緩衝區和內核模式的緩衝區指向的是同一區域的物理記憶體。缺點就是要單獨佔用物理頁面。
其他方式讀寫(在調用IoCreateDevice
創建設備後對pDevObj->Flags
即不設置DO_BUFFERED_IO
也不設置DO_DIRECT_IO
此時就是其他方式。在使用其他方式讀寫設備時,派遣函數直接讀寫應用程式提供的緩衝區地址。在驅動程式中,直接操作應用程式的緩衝區地址是很危險的。只有驅動程式與應用程式運行在相同執行緒上下文的情況下,才能使用這種方式。如果CPU
中的任務切換了,即CR3
切換掉了,在高2GB
的驅動仍在使用該方式讀取低2GB
記憶體,導致讀到的數據和實際不符,導致錯誤,故強烈不推薦此方式。
用程式碼設置交互數據的方式舉例如下:
pDeviceObj->Flags |= DO_BUFFERED_IO;
創建符號鏈接
設備對象創建好了,通訊方式也約定好了,但3環的程式仍找不到你的驅動對象。設備名稱的作用是給內核對象用的,如果要在3環訪問,必須要有符號鏈接。其實就是一個別名,沒有這個別名,在3環不可見。用程式碼實現如下:
//創建符號鏈接名稱
RtlInitUnicodeString(&SymbolicLinkName,L"\\??\\MyTestDriver");
//創建符號鏈接
IoCreateSymbolicLink(&SymbolicLinkName,&Devicename);
有些細節需要特別注意:內核模式下,符號鏈接是以\??\
開頭的,如C盤就是\??\C:
。而在用戶模式下,則是以\\.\
開頭的,如C盤就是\\.\C:
。
IRP
前面的程式碼都寫好的,驅動與3環的通訊的基礎就搭建好了。但是,如果真正實現通訊,還得需要註冊派遣函數。
如上圖所示,我們在編寫Win32
窗體程式時。假設我在窗體點擊了滑鼠,作業系統就會產生一個消息,用MSG
這個結構體封裝一下,派發給窗體對象。目標窗體對象接受到後發現它是滑鼠單擊消息。窗體對象中註冊了很多回調函數:滑鼠點擊回調、滑鼠雙擊回調、鍵盤鍵按下回調等等。然後進一步處理是單擊,就調用單擊回調函數。同理,我們在3環調用CreateFile
函數,作業系統就會產生一個IRP
派發給設備對象,目標設備對象處理方式和窗體消息沒啥差別。接下來我們看看IRP
的類型:
當應用層通過CreateFile
、ReadFile
、WriteFile
、CloseHandle
等函數打開、從設備讀取數據、向設備寫入數據、關閉設備的時候,會使作業系統分別產生出IRP_MJ_CREATE
、IRP_MJ_READ
、IRP_MJ_WRITE
、IRP_MJ_CLOSE
等不同的IRP
。值得注意的是,我們之前使用CreateFile
這個東西只是為了創建文件,其實它的本質是與設備對象創建訪問,我們3環程式想要通過符號鏈接與驅動建立通訊,就必須通過這個函數。
當然IRP
不止上面的這幾種,我們再給出常見的IRP
:
IRP類型 | 來源 |
---|---|
IRP_MJ_DEVICE_CONTROL | 使用 DeviceControl 函數時產生 |
IRP_MJ_POWER | 在作業系統處理電源消息時產生 |
IRP_MJ_SHUTDOWN | 關閉系統前時產生 |
我們最常用的IRP
有IRP_MJ_DEVICE_CONTROL
、IRP_MJ_CREATE
和IRP_MJ_CLOSE
,以實現交互、創建訪問、關閉訪問的功能。
派遣函數
了解了上面的東西,我們如何註冊派遣函數呢?我們再看一下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;
有沒有注意到MajorFunction
這個成員,它是一個數組,具有28個,我們的派遣函數都會在這裡面,如何註冊我們用如下程式碼形式:
//設置卸載函數
pDriverObject->DriverUnload = 卸載函數;
//設置派遣函數
pDriverObject->MajorFunction[IRP_MJ_CREATE] = 派遣函數1;
pDriverObject->MajorFunction[IRP_MJ_CLOSE] = 派遣函數2;
pDriverObject->MajorFunction[IRP_MJ_WRITE] = 派遣函數3;
pDriverObject->MajorFunction[IRP_MJ_READ] = 派遣函數4;
pDriverObject->MajorFunction[IRP_MJ_CLEANUP] = 派遣函數5;
pDriverObject->MajorFunction[IRP_MJ_SET_INFORMATION] = 派遣函數6;
pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = 派遣函數7;
pDriverObject->MajorFunction[IRP_MJ_SHUTDOWN] = 派遣函數8;
pDriverObject->MajorFunction[IRP_MJ_SYSTEM_CONTROL] = 派遣函數9;
派遣函數的格式
回調函數都有自己的格式,派遣函數也不例外,它的格式如下:
NTSTATUS MyDispatchFunction(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
//處理自己的業務……
//設置返回狀態
pIrp->IoStatus.Status = STATUS_SUCCESS; //GetLastError 函數得到的就是該值
pIrp->IoStatus.Information = 0; //返回給3環多少數據 沒有填0
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return STATUS_SUCCESS;
}
本節練習
本節的答案將會在下一節進行講解,務必把本節練習做完後看下一個講解內容。不要偷懶,實驗是學習本教程的捷徑。
俗話說得好,光說不練假把式,如下是本節相關的練習。如果練習沒做好,就不要看下一節教程了,越到後面,不做練習的話容易夾生了,開始還明白,後來就真的一點都不明白了。本節練習不多,請保質保量的完成。
1️⃣ 實現一個工具,利用未導出的函數PspTerminateProcess
殺死軟體(驅動的載入可不用程式碼實現,使用本教程工具進行載入)。
下一篇
驅動篇——Hook