寫在前面
此系列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統內核的復雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閑錢,可以打賞支持我的創作。如想轉載,請把我的轉載信息附在文章后面,並聲明我的個人信息和本人博客地址即可,但必須事先通知我。
你如果是從中間插過來看的,請仔細閱讀 羽夏看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
殺死軟件(驅動的加載可不用代碼實現,使用本教程工具進行加載)。