1. 程式人生 > >64位核心開發第一講,驅動框架.

64位核心開發第一講,驅動框架.

驅動框架介紹

1.應用程式3環到0環的框架

1.1 3環到0環的驅動框架.

首先是我們的3環API

API -> 封裝資料跟命令 ->呼叫kerner32或者ntdll的函式 ->進行封裝,傳送給IRP結構體 ->呼叫驅動

這裡接觸了一個新的概念.IRP .IRP結構體其實是3環的資料以及命令.進行封裝傳送到0環的時候.儲存在這個結構體裡面. 0環通過讀取進而呼叫0環的 NT函式來執行.

如我們呼叫ReadFile.那麼會直接呼叫我們寫的驅動的派遣函式
DispathRead
其中有0x1B(27)個分發派遣函式. 以及一個DriverUnLoad函式.

我們的資料都存放在 IRP中.我們如果要完成例程,那麼就設定IRP中的.

IOstatus即可.我們的驅動是分層驅動.如果不設定.他還會呼叫其它的驅動.

1.2 NT驅動框架

上面我們說了,3環的API會呼叫0環.其中資料以及命令資訊會放在IRP結構體中.

那麼如果我們呼叫 CreateFile. 那麼則會產生一個IRP_MJ_CREATE
我們核心層則會呼叫DispathCreate()來進行設定.

如下:

Nt模型,函式 訊息
DriverEntry 單執行緒環境,程式入口點.
DispatchCreate IRP_MJ_CREATE
DispatchRead IRP_MJ_READ
DispatchWrite IRP_MJ_WRITE
DisPatchchClose IRP_MJ_CLOSE FileObject核心物件
DispatchClean IRP_MJ_CLEANUP HANDLE為控制代碼
DisPatchControl irp_mj_device_control
DriverUnLoad 單執行緒環境,程式解除安裝.

檔案控制代碼為0.那麼系統就會發送IRP_MJ_CLEANUP
FileOBject核心物件.如果對檔案的核心物件沒有在操作了(包括核心)
則會發送IRP_MJ_CLOSE. 大部分情況這兩種都會同時發生的.

WDM模型
WDM是網絡卡等.它引入了兩個新的函式
WDMAddDevice()

wdmpnp()
連結即可.

應用框架
Sfilter/Minifilter 檔案過濾框架.可以使用Nt模型.
TDI/NDIS/WFP 基於NT模型加的新的框架.防火牆用的
DISPERF 磁碟基於Nt模型.產生的磁碟過濾框架
HOOK

二丶編寫自己的最簡單的 NT模型驅動.



#include <ntddk.h>   //很多驅動的結構體函式的宣告呀.都包含在這裡面

#define DEVICE_NAME L"\\device\\IBinaryFirst"  // 驅動的裝置的名字       格式為 \device\你自定義的名字. \\是代表轉義 在source中要一樣.

//#define LINK_NAME L"\\DosDevices\IBinaryFirst" // 驅動的符號連線名 格式\dosdevices\自定義的名字  也可以\\??\\自定義的名字
#define LINK_NAME L"\\DosDevices\\IBinaryFirst"


/*
控制碼,應用層,核心層通用.

*/
#define IOCTRL_BASE 0x800
#define MYIOCTRL_CODE(i)\
    CTL_CODE(FILE_DEVICE_UNKNOWN,IOCTRL_BASE+i,METHOD_BUFFERED,FILE_ANY_ACCESS)
//驅動裝置型別 裝置控制碼數值  定義R3跟R0的通訊方式.是指定Device.  我們的許可權.

/*
METHOD_BUFFERED  以快取方式讀取
METHOD_IN_DIRECT 只讀,只有開啟裝置的時候 IoControl將會成功 METHOD_OUT_DIRECT 則會失敗
METHOD_OUT_DIRECT 讀寫方式的時候.兩種方式都會成功 都在MDL中拿資料

METHOD_NEITHER  在 type3InputBuffer拿資料 IN的資料. stack->Parameters.DeviceIoControl.Type3InputBuffer
傳送給R3 在 pIrp->UserBuffer裡面.
3中通訊方式.
*/

#define CTL_HELLO MYIOCTRL_CODE(0) //控制碼為0則是HELLO.

NTSTATUS DispatchCommon(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
NTSTATUS DispatchCreate(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
NTSTATUS DispatchRead(PDEVICE_OBJECT  pDeviceObject,PIRP pIrp);
NTSTATUS DispatchWrite(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
NTSTATUS DispatchClose(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
NTSTATUS DispatchClean(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
NTSTATUS DispatchControl(PDEVICE_OBJECT pDeviceObject,PIRP pIrp);
VOID     DriverUnLoad(PDRIVER_OBJECT pDriverObject);

NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObject,PUNICODE_STRING pRegPath)
{
  
    UNICODE_STRING uDeviceName = {0};
    UNICODE_STRING uLinkName = {0};
    NTSTATUS ntStatus = 0;
    PDEVICE_OBJECT pDeviceObject = NULL;
    ULONG i = 0;
    
    pDriverObject->DriverUnload = DriverUnLoad;
    
    DbgPrint("Load Driver Sucess");
    
    
    
    RtlInitUnicodeString(&uDeviceName,DEVICE_NAME); //初始化驅動裝置名字
    RtlInitUnicodeString(&uLinkName,LINK_NAME);     //初始化3環宇0環通訊的裝置名字
    
    
    ntStatus = IoCreateDevice(pDriverObject,0,&uDeviceName,FILE_DEVICE_UNKNOWN,0,FALSE,&pDeviceObject);//建立裝置物件
    /*
    引數1: 驅動物件 
    引數2: 裝置擴充套件,建立完裝置物件之後,申請的一段額外記憶體.可以儲存裝置物件的上下文的一些資料
    引數3: 裝置名字,傳入函式,需要傳地址 
    引數4: 裝置型別.普通的驅動設定為FILE_DEVICE_UNKNOWN
    引數5: 裝置的屬性
    
    引數6: 裝置物件是用來傳入IRP請求的.是讓我們應用層開啟它. R3 傳送IRP -> 裝置物件(我們自己建立的)
    引數6的意思就是 如果為TRUE 只能一個程序開啟,獨佔開啟.FALSE是可以多個程序開啟的.
    引數7: 建立好的裝置物件通過最後一個引數傳出. 注意是2級指標.
    */
    
    
    
    DbgPrint("IoCreateDevice load.....\r\n");
    if (!NT_SUCCESS(ntStatus))
    {
        //判斷是否設定成功
        DbgPrint(L"IoCreateDevice Failed \r\n");
        return 0;
    }
    
    //設定通訊的方式
    pDeviceObject->Flags |= DO_BUFFERED_IO; 
    /*
    R3 -> IRP ->核心. 通過IRP傳送給核心層.
    三種通訊方式
    1.快取方式:
      DO_BUFFERED_IO 最安全的一個通訊方式.(資料的交換)基於快取
      核心中會專門會分配跟R3的 Buffer一樣的快取. 核心層從這個空間讀取
      這個就是 DO_BUFFERED. 處理完畢之後.在放到分配的快取區中.那麼IO管理器
      在拷拷貝給應用層.完成資料互動.
     2.直接IO方式
       DO_DIRECT_IO
       R3 有一塊資料. 會使用MDL方式. 會將R3傳送的資料.對映到實體記憶體中.
       並且鎖住. 
       就相當於 R3的資料地址 對映到核心中實體地址. R3往核心中寫資料其實也是
       往核心資料讀取. 這個通訊完全就是在核心中對映的實體記憶體中進行的.
     3.虛擬地址直接傳送到R0
     第三種方式是虛擬地址 直接傳送到R0. 前提條件.程序不能切換.必須處在
     同一個執行緒上下文.
     這樣不安全所以我們要對這塊記憶體進行檢查才可以.
     ProbeFroWrite
     ProbeFroRead 
     
    */
    
    DbgPrint("IoCreateSymbolicLink load.... \r\n");
    ntStatus = IoCreateSymbolicLink(&uLinkName,&uDeviceName); //建立符號連結名字.
    if (!NT_SUCCESS(ntStatus))
    {
        //建立失敗,我們就要刪除
        IoDeleteDevice(pDeviceObject);
        DbgPrint("IoCreateSymbolicLink Error");
        return 0;
    }
    DbgPrint("IoCreateSymbolicLink Sucess");
    //初始化分發派遣函式.
    
    
    for (i = 0; i < IRP_MJ_MAXIMUM_FUNCTION +1;i++)
    {
        //分發函式有0x1b個(27)我們不注意的可以進行設定通用的分發函式.分發函式都是一樣的.
        pDriverObject->MajorFunction[i] = DispatchCommon;
    }
    
    pDriverObject->MajorFunction[IRP_MJ_CREATE] = DispatchCreate;
    pDriverObject->MajorFunction[IRP_MJ_READ] = DispatchRead;
    pDriverObject->MajorFunction[IRP_MJ_WRITE]= DispatchWrite;
    pDriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchControl;
    pDriverObject->MajorFunction[IRP_MJ_CLEANUP]=DispatchClean;
    pDriverObject->MajorFunction[IRP_MJ_CLOSE] = DispatchClose;
    
    
    DbgPrint("驅動安裝成功IBinary \r\n");
    //設定驅動解除安裝
    
    
    
    return STATUS_SUCCESS;  
}


NTSTATUS DispatchCommon(PDEVICE_OBJECT pDeviceObject,PIRP pIrp)
{
    pIrp->IoStatus.Status = STATUS_SUCCESS; //IRP記錄這次操作與否的.
    pIrp->IoStatus.Information = 0;         //Information用來記錄實際傳輸的位元組數的.
    
    //提交請求.
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    return STATUS_SUCCESS;                  //上面的 STATUS_SUCCESS是給R3看的.現在的返回時給IO管理器系統的
}
NTSTATUS DispatchCreate(PDEVICE_OBJECT pDeviceObject,PIRP pIrp)
{
    
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    
    //提交請求.
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    
    return STATUS_SUCCESS;
    
}
NTSTATUS DispatchRead(PDEVICE_OBJECT  pDeviceObject,PIRP pIrp)
{
    
    PVOID pReadBuffer = NULL;
    ULONG uReadLength = 0;
    PIO_STACK_LOCATION pStack = NULL;
    ULONG uMin = 0;
    ULONG uHelloStr = 0;
    
    uHelloStr = (wcslen(L"Hello World") + 1) * sizeof(WCHAR);
    pReadBuffer = pIrp->AssociatedIrp.SystemBuffer; //緩衝區通訊方式.則是這個值
    //獲取IRP堆疊.我們說過3環呼叫0環.需要封裝在IRP結構中.windows是分層驅動.所以IRP頭部是共用的.其餘的是棧傳遞.
    
    pStack = IoGetCurrentIrpStackLocation(pIrp);
    uReadLength = pStack->Parameters.Read.Length;
    uMin = uReadLength > uHelloStr ? uHelloStr : uReadLength;
    
    RtlCopyMemory(pReadBuffer,L"Hello World",uMin); //拷貝到緩衝區中給3環.
    
    
    
    
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = uMin;
    
    //提交請求.
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    
    return STATUS_SUCCESS;
    
}
NTSTATUS DispatchWrite(PDEVICE_OBJECT pDeviceObject,PIRP pIrp)
{
    PVOID pWriteBuffer = NULL;
    ULONG uWriteLength = 0;
    PIO_STACK_LOCATION pIrpStack = NULL;
    PVOID pBuffer = NULL;
    //獲取IRP堆疊
    pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
    
    //獲取寫的長度.
    uWriteLength = pIrpStack->Parameters.Write.Length;
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    
    
    //申請記憶體.
    pBuffer = ExAllocatePoolWithTag(PagedPool,uWriteLength,'TSET');
    /*
    PagedPool 在分頁中分配記憶體 CPU無分頁才能在分頁中分配. Dispathch級別則不能使用分頁記憶體.
    NoPagePool非分頁中分配.
    優先順序最低的才能使用分頁記憶體.
    
    引數2: 長度
    引數3: 標記. 不能超過4個位元組. 單引號引起來. 引數3是用來跟蹤我們分配的記憶體的.
    注意是低位優先, 記憶體中看到的是 TEST.
    */
    if (NULL == pBuffer)
    {
        pIrp->IoStatus.Status = STATUS_INSUFFICIENT_RESOURCES;
        pIrp->IoStatus.Information = 0;
        IoCompleteRequest(pIrp,IO_NO_INCREMENT);
        return STATUS_INSUFFICIENT_RESOURCES;
    }
    //提交請求.
    
    memset(pBuffer,0,uWriteLength);
    //拷貝到0環緩衝區
    RtlCopyMemory(pBuffer,pWriteBuffer,uWriteLength);
    ExFreePool(pBuffer);
    pBuffer = NULL;
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = uWriteLength;
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    
    return STATUS_SUCCESS;
}
NTSTATUS DispatchClose(PDEVICE_OBJECT pDeviceObject,PIRP pIrp)
{
    //控制 其它互動都通過控制碼傳送.
    
    
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    
    //提交請求.
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    
    return STATUS_SUCCESS;
}
NTSTATUS DispatchClean(PDEVICE_OBJECT pDeviceObject,PIRP pIrp)
{
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    
    //提交請求.
    IoCompleteRequest(pIrp,IO_NO_INCREMENT); 
    
    return STATUS_SUCCESS;
}
NTSTATUS DispatchControl(PDEVICE_OBJECT pDeviceObject,PIRP pIrp)
{
    
    //核心中共享 SystemBuffer 有時間差.先讀在寫.
    PIO_STACK_LOCATION pIrpStack;
    PVOID InPutBuffer = NULL;
    PVOID OutPutBuffer = NULL;
    ULONG uInPutLength = 0;
    ULONG uOutPutBufferLength = 0;
    ULONG IoCtrl = 0;
    
    InPutBuffer = OutPutBuffer = pIrp->AssociatedIrp.SystemBuffer;
    pIrpStack = IoGetCurrentIrpStackLocation(pIrp);
    
    //uOutPutBufferLength = pIrpStack->Parameters.DeviceIoControl.OutPutBufferLength;
    //uInPutLength = pIrpStack->Parameters.DeviceIoControl.InPutBufferLength;
    
    
    IoCtrl = pIrpStack->Parameters.DeviceIoControl.IoControlCode; //獲取控制碼.
    
/*
    switch(IoCtrl)
    {
    case CTL_HELLO:
        KdPrint("Hello World");
        break;
    default:
        break;  
    }
*/
    pIrp->IoStatus.Status = STATUS_SUCCESS;
    pIrp->IoStatus.Information = 0;
    
    //提交請求.
    IoCompleteRequest(pIrp,IO_NO_INCREMENT);
    
    return STATUS_SUCCESS;
}

//驅動解除安裝

VOID  DriverUnLoad(PDRIVER_OBJECT pDriverObject)
{
  DbgPrint("Unload MyDrive\n");
}


根據上面程式碼我們可以做個解析
1.DriverEntry();這個是NT驅動的入口點.兩個引數. 驅動物件.以及登錄檔路勁.
2.使用IoCreateDevice函式建立了一個驅動裝置物件.這樣當r3使用ReadFile等函式會傳送給裝置物件.
3.使用IoCreateSymbolicLink();建立符號連結.此時我們R3呼叫CreateFile則可以進行連結了.
4.最後註冊派遣函式即可.
5.在派遣函式中寫入你的操作.如讀取操作.我們將資料返還給R3.

1.3 IRP 結構

上面我們看的IRP有頭部.
可以看到 IOSTATUS .裡面儲存了狀態.以及實際Buffer位元組.
SystemBuffer.這個是快取IO.就是我們現在用的. 核心中開闢空間儲存3環.再從裡面讀取.最後再給這個緩衝區設定.進行輸出.

MdlAddress 這個則是直接IO.我們上面程式碼的註釋中說了.直接IO是
3環的緩衝區地址,對映到0環的物理聶村. 進而0環讀取實體記憶體進行操作.

UserBuffer
UserBuffer是自定義的.其中UserBuffer是傳出的.而內部還有一個Buffer是用來讀取的.

n以後就是IRP的棧. 在我們檔案驅動與磁碟驅動.那麼共享IRP頭部.

磁碟裝置則會使用0層的.
因為驅動是分層的.

而在棧中有一個很重要的聯合體.

Read Write DeviceControl...等等.不同結構體對應不同的IRP請求.

所以在Read派遣函式中.獲取ReadIrp的堆疊.

二丶編譯驅動.

我用的是WDK7600.可以使用XP進行測試.
編譯的時候需要使用WDK的 命令列.
當你安裝WDK7600之後再開始選單中則會看到.

開啟之後切換到你的編寫程式碼的目錄.直接輸入build進行編譯即可.
注意你的驅動程式碼字尾名要為.c的檔案.這樣不會編譯錯誤.
cpp有名字粉碎.你需要使用 extern C 表示這個函式名不會名稱粉碎.

在編譯的時候我們還需要提供一個sources 檔案.

內容為:

TARGETNAME= IBinaryFirst     //編譯的驅動名字.

TARGETTYPE=DRIVER            //編譯的型別為驅動
  
SOURCES= IBinaryFirst.c     //你驅動的程式碼檔案

這是我的:
TARGETNAME=IBinaryFirst 
TARGETTYPE=DRIVER       
SOURCES=IBinaryFirst.c      

編譯之後如下.

3.載入驅動.

載入驅動有專門的的API進行操作.我以前寫過.
可以看之前的文章.

https://www.cnblogs.com/iBinary/p/8280912.html

現在我們直接用工具載入了.

可以看到載入成功.寬字元打印出錯.不影響.

4.ring3操作核心.進行讀取.

可以看到我們的 HelloWorld已經正常讀取了.

ring3下完整程式碼.


// Ring3.cpp : Defines the entry point for the console application.
//


#include <windows.H>
#include <stdio.h>

int main(int argc, char* argv[])
{
    HANDLE hFile = CreateFile(TEXT("\\\\?\\IBinaryFirst"),
        GENERIC_WRITE | GENERIC_READ,
        0,
        NULL,
        OPEN_EXISTING,
        FILE_ATTRIBUTE_NORMAL,
        NULL);
    if (hFile == INVALID_HANDLE_VALUE)
    {
        printf("CreateFile ErrorCode:%d\n", GetLastError());
        return 0;
    }
    system("pause");
    TCHAR szBuff[0X100];
    DWORD dwBytes = 0;
    if (!ReadFile(hFile, szBuff, sizeof(szBuff)/sizeof(szBuff[0]), &dwBytes, NULL))
    {
        CloseHandle(hFile);
        printf("ReadFile ErrorCode:%d\n", GetLastError());
        return 0;
    }
    printf("bytes:%d data:%ls\n", dwBytes, szBuff);
    system("pause");
    //WriteFile();
    //DeviceIoControl
    CloseHandle(hFile);
    system("pause");
    return 0;
}