驅動與應用程序的通信是非常有必要的,內核中執行代碼後需要將其動態顯示給應用層,但驅動程序與應用層畢竟不在一個地址空間內,爲了實現內核與應用層數據交互則必須有通信的方法,微軟爲我們提供了三種通信方式,如下先來介紹通過ReadFile
系列函數實現的通信模式。
長話短說,不說沒用的概念,首先系統中支持
的通信模式
可以總結爲三種。
- 緩衝區方式讀寫(DO_BUFFERED_IO)
- 直接方式讀寫(DO_DIRECT_IO)
- 其他方式讀寫
而通過ReadFile,WriteFile
系列函數實現的通信機制則屬於緩衝區通信
模式,在該模式下操作系統會將應用層中的數據複製
到內核中,此時應用層調用ReadFile,WriteFile
函數進行讀寫時,在驅動內會自動觸發 IRP_MJ_READ
與 IRP_MJ_WRITE
這兩個派遣函數,在派遣函數內則可以對收到的數據進行各類處理。
首先需要實現初始化各類派遣函數這麼一個案例,如下代碼則是通用的一種初始化派遣函數的基本框架,分別處理了IRP_MJ_CREATE
創建派遣,以及IRP_MJ_CLOSE
關閉的派遣,此外函數DriverDefaultHandle
的作用時初始化其他派遣用的,也就是將除去CREATE/CLOSE
這兩個派遣之外,其他的全部賦值成初始值的意思,當然不增加此段代碼也是無妨,並不影響代碼的實際執行。
#include <ntifs.h>
// 卸載驅動執行
VOID UnDriver(PDRIVER_OBJECT pDriver)
{
PDEVICE_OBJECT pDev; // 用來取得要刪除設備對象
UNICODE_STRING SymLinkName; // 局部變量symLinkName
pDev = pDriver->DeviceObject;
IoDeleteDevice(pDev); // 調用IoDeleteDevice用於刪除設備
RtlInitUnicodeString(&SymLinkName, L"\\??\\LySharkDriver"); // 初始化字符串將symLinkName定義成需要刪除的符號鏈接名稱
IoDeleteSymbolicLink(&SymLinkName); // 調用IoDeleteSymbolicLink刪除符號鏈接
DbgPrint("驅動卸載完畢...");
}
// 創建設備連接
// LyShark.com
NTSTATUS CreateDriverObject(IN PDRIVER_OBJECT pDriver)
{
NTSTATUS Status;
PDEVICE_OBJECT pDevObj;
UNICODE_STRING DriverName;
UNICODE_STRING SymLinkName;
// 創建設備名稱字符串
RtlInitUnicodeString(&DriverName, L"\\Device\\LySharkDriver");
Status = IoCreateDevice(pDriver, 0, &DriverName, FILE_DEVICE_UNKNOWN, 0, TRUE, &pDevObj);
// 指定通信方式爲緩衝區
pDevObj->Flags |= DO_BUFFERED_IO;
// 創建符號鏈接
RtlInitUnicodeString(&SymLinkName, L"\\??\\LySharkDriver");
Status = IoCreateSymbolicLink(&SymLinkName, &DriverName);
return STATUS_SUCCESS;
}
// 創建回調函數
NTSTATUS DispatchCreate(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
pIrp->IoStatus.Status = STATUS_SUCCESS; // 返回成功
DbgPrint("派遣函數 IRP_MJ_CREATE 執行 \n");
IoCompleteRequest(pIrp, IO_NO_INCREMENT); // 指示完成此IRP
return STATUS_SUCCESS; // 返回成功
}
// 關閉回調函數
NTSTATUS DispatchClose(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
pIrp->IoStatus.Status = STATUS_SUCCESS; // 返回成功
DbgPrint("派遣函數 IRP_MJ_CLOSE 執行 \n");
IoCompleteRequest(pIrp, IO_NO_INCREMENT); // 指示完成此IRP
return STATUS_SUCCESS; // 返回成功
}
// 默認派遣函數
NTSTATUS DriverDefaultHandle(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
NTSTATUS status = STATUS_SUCCESS;
pIrp->IoStatus.Status = status;
pIrp->IoStatus.Information = 0;
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return status;
}
// 入口函數
// By: LyShark
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark \n");
// 調用創建設備
CreateDriverObject(pDriver);
pDriver->DriverUnload = UnDriver; // 卸載函數
pDriver->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; // 創建派遣函數
pDriver->MajorFunction[IRP_MJ_CLOSE] = DispatchClose; // 關閉派遣函數
// 初始化其他派遣
for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DbgPrint("初始化派遣: %d \n", i);
pDriver->MajorFunction[i] = DriverDefaultHandle;
}
DbgPrint("驅動加載完成...");
return STATUS_SUCCESS;
}
代碼運行效果如下:
通用框架有了,接下來就是讓該驅動支持使用ReadWrite
的方式實現通信,首先我們需要在DriverEntry
處增加兩個派遣處理函數的初始化。
// 入口函數
// By: LyShark
NTSTATUS DriverEntry(PDRIVER_OBJECT pDriver, PUNICODE_STRING RegistryPath)
{
DbgPrint("hello lyshark \n");
// 調用創建設備
CreateDriverObject(pDriver);
// 初始化其他派遣
for (ULONG i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++)
{
DbgPrint("初始化派遣: %d \n", i);
pDriver->MajorFunction[i] = DriverDefaultHandle;
}
pDriver->DriverUnload = UnDriver; // 卸載函數
pDriver->MajorFunction[IRP_MJ_CREATE] = DispatchCreate; // 創建派遣函數
pDriver->MajorFunction[IRP_MJ_CLOSE] = DispatchClose; // 關閉派遣函數
// 增加派遣處理
pDriver->MajorFunction[IRP_MJ_READ] = DispatchRead; // 讀取派遣函數
pDriver->MajorFunction[IRP_MJ_WRITE] = DispatchWrite; // 寫入派遣函數
DbgPrint("驅動加載完成...");
return STATUS_SUCCESS;
}
接着,我們需要分別實現這兩個派遣處理函數,如下DispatchRead
負責讀取時觸發,與之對應DispatchWrite
負責寫入觸發。
- 引言:
- 對於讀取請求
I/O管理器
分配一個與用戶模式的緩衝區大小相同的系統緩衝區SystemBuffer
,當完成請求時I/O管理器將驅動程序已經提供的數據從系統緩衝區複製到用戶緩衝區。 - 對於寫入請求,會分配一個系統緩衝區並將
SystemBuffer
設置爲地址,用戶緩衝區的內容會被複制到系統緩衝區,但是不設置UserBuffer
緩衝。
通過IoGetCurrentIrpStackLocation(pIrp)
接收讀寫請求長度,偏移等基本參數,AssociatedIrp.SystemBuffer
則是讀寫緩衝區,IoStatus.Information
是輸出緩衝字節數,Parameters.Read.Length
是讀取寫入的字節數。
// 讀取回調函數
NTSTATUS DispatchRead(PDEVICE_OBJECT pDevObj, PIRP pIrp)
{
NTSTATUS Status = STATUS_SUCCESS;
PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(pIrp);
ULONG ulReadLength = Stack->Parameters.Read.Length;
char szBuf[128] = "hello lyshark";
pIrp->IoStatus.Status = Status;
pIrp->IoStatus.Information = ulReadLength;
DbgPrint("讀取長度:%d \n", ulReadLength);
// 取出字符串前5個字節返回給R3層
memcpy(pIrp->AssociatedIrp.SystemBuffer, szBuf, ulReadLength);
IoCompleteRequest(pIrp, IO_NO_INCREMENT);
return Status;
}
// 接收傳入回調函數
// By: LyShark
NTSTATUS DispatchWrite(struct _DEVICE_OBJECT *DeviceObject, struct _IRP *Irp)
{
NTSTATUS Status = STATUS_SUCCESS;
PIO_STACK_LOCATION Stack = IoGetCurrentIrpStackLocation(Irp);
ULONG ulWriteLength = Stack->Parameters.Write.Length;
PVOID ulWriteData = Irp->AssociatedIrp.SystemBuffer;
// 輸出傳入字符串
DbgPrint("傳入長度: %d 傳入數據: %s \n", ulWriteLength, ulWriteData);
IoCompleteRequest(Irp, IO_NO_INCREMENT);
return Status;
}
如上部分都是在講解驅動層面的讀寫派遣,應用層還沒有介紹,在應用層我們只需要調用ReadFile
函數當調用該函數時驅動中會使用DispatchRead
派遣例程來處理這個請求,同理調用WriteFile
函數則觸發的是DispatchWrite
派遣例程。
我們首先從內核中讀出前五個字節並放入緩衝區內,輸出該緩衝區內的數據,然後在調用寫入,將hello lyshark
寫回到內核裏裏面,這段代碼可以這樣來寫。
#include <iostream>
#include <Windows.h>
#include <winioctl.h>
int main(int argc, char *argv[])
{
HANDLE hDevice = CreateFileA("\\\\.\\LySharkDriver", GENERIC_READ | GENERIC_WRITE, 0,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (hDevice == INVALID_HANDLE_VALUE)
{
CloseHandle(hDevice);
return 0;
}
// 從內核讀取數據到本地
char buffer[128] = { 0 };
ULONG length;
// 讀入到buffer長度爲5
// By:lyshark.com
ReadFile(hDevice, buffer, 5, &length, 0);
for (int i = 0; i < (int)length; i++)
{
printf("讀取字節: %c", buffer[i]);
}
// 寫入數據到內核
char write_buffer[128] = "hello lyshark";
ULONG write_length;
WriteFile(hDevice, write_buffer, strlen(write_buffer), &write_length, 0);
system("pause");
CloseHandle(hDevice);
return 0;
}
使用驅動工具安裝我們的驅動,然後運行該應用層程序,實現通信,效果如下所示: