驅動開發:通過ReadFile與核心層通訊

2022-09-30 18:00:14

驅動與應用程式的通訊是非常有必要的,核心中執行程式碼後需要將其動態顯示給應用層,但驅動程式與應用層畢竟不在一個地址空間內,為了實現核心與應用層資料互動則必須有通訊的方法,微軟為我們提供了三種通訊方式,如下先來介紹通過ReadFile系列函數實現的通訊模式。

長話短說,不說沒用的概念,首先系統中支援通訊模式可以總結為三種。

  • 緩衝區方式讀寫(DO_BUFFERED_IO)
  • 直接方式讀寫(DO_DIRECT_IO)
  • 其他方式讀寫

而通過ReadFile,WriteFile系列函數實現的通訊機制則屬於緩衝區通訊模式,在該模式下作業系統會將應用層中的資料複製到核心中,此時應用層呼叫ReadFile,WriteFile函數進行讀寫時,在驅動內會自動觸發 IRP_MJ_READIRP_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;
}

使用驅動工具安裝我們的驅動,然後執行該應用層程式,實現通訊,效果如下所示: