驅動篇——常規的0環與3環通訊

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

寫在前面

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

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。

  看此教程之前,問個問題,你明確學驅動的目的了嗎?你的開發環境準備好了嗎?上一節的內容學會了嗎? 沒有的話就不要繼續了,請重新學習前面驅動篇的教程內容繼續。


? 華麗的分割線 ?


練習及參考

本次答案均為參考,可以與我的答案不一致,但必須成功通過。

1️⃣ 遍歷核心模組,輸出模組名稱,基址以及大小。

? 點選檢視答案 ?


  此題目不難,就是一個迴圈雙向連結串列的遍歷,程式碼見下面的摺疊,效果如下:

驅動篇——常規的0環與3環通訊


? 點選檢視程式碼 ?
#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的第一個引數的數值,然後編譯。在虛擬機器進行註冊啟動驅動效果如下:

驅動篇——常規的0環與3環通訊

  由於這個函式很底層,可以幹掉很多流氓軟體,甚至殺軟都不放過。比如火絨(已將該情況上報給火絨官方,亂搞後果自負):

驅動篇——常規的0環與3環通訊


? 點選檢視程式碼 ?
#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️⃣ 通過斷鏈實現隱藏驅動模組。

? 點選檢視答案 ?


  此題目不難,就是一個連結串列斷鏈,效果如下:

驅動篇——常規的0環與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訊息的只能是裝置物件。示意圖如下所示:

驅動篇——常規的0環與3環通訊

常規通訊流程

  為了實現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環的通訊的基礎就搭建好了。但是,如果真正實現通訊,還得需要註冊派遣函式。

驅動篇——常規的0環與3環通訊

  如上圖所示,我們在編寫Win32窗體程式時。假設我在窗體點選了滑鼠,作業系統就會產生一個訊息,用MSG這個結構體封裝一下,派發給窗體物件。目標窗體物件接受到後發現它是滑鼠單擊訊息。窗體物件中註冊了很多回撥函式:滑鼠點選回撥、滑鼠雙擊回撥、鍵盤鍵按下回撥等等。然後進一步處理是單擊,就呼叫單擊回撥函式。同理,我們在3環呼叫CreateFile函式,作業系統就會產生一個IRP派發給裝置物件,目標裝置物件處理方式和窗體訊息沒啥差別。接下來我們看看IRP的型別:
  當應用層通過CreateFileReadFileWriteFileCloseHandle等函式開啟、從裝置讀取資料、向裝置寫入資料、關閉裝置的時候,會使作業系統分別產生出IRP_MJ_CREATEIRP_MJ_READIRP_MJ_WRITEIRP_MJ_CLOSE等不同的IRP。值得注意的是,我們之前使用CreateFile這個東西只是為了建立檔案,其實它的本質是與裝置物件建立訪問,我們3環程式想要通過符號連結與驅動建立通訊,就必須通過這個函式。
  當然IRP不止上面的這幾種,我們再給出常見的IRP

IRP型別 來源
IRP_MJ_DEVICE_CONTROL 使用 DeviceControl 函式時產生
IRP_MJ_POWER 在作業系統處理電源訊息時產生
IRP_MJ_SHUTDOWN 關閉系統前時產生

  我們最常用的IRPIRP_MJ_DEVICE_CONTROLIRP_MJ_CREATEIRP_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

相關文章