Buffer I/Oによるドライバとアプリケーション間のデータ転送をやってみる
アプリケーションからI/O要求を行い,デバイスドライバにデータを転送する場合,一時的にデータを保存するためのバッファが必要になる. Windowsではデータバッファにアクセスするための3つの方法が用意されている.
- Buffer I/O
- Direct I/O
- Neither I/O
ここでは,最も使用方法が単純なBuffer I/Oを使ってデバイスドライバとアプリケーション間のデータ転送をやってみる.
単純なドライバ
Visual Studioによる警告無しにコンパイル可能な,ドライバの最小の構成は以下だろう.
#include <ntddk.h> extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(DriverObject); UNREFERENCED_PARAMETER(RegistryPath); /* do something */ return STATUS_SUCCESS; }
デバイスドライバがシステムにロードされた時の最初のルーチンとして,DriverEntryが呼び出される.
ただし,Unload処理などを一切を記述していないため,このまま使用するのは避けるべきだ.
またこのままではアプリケーションからこのドライバにアクセスすることはできない.
アプリケーションからドライバにアクセスするにはシンボリックリンクを介する必要がある.
extenr "C"について補足すると,C++では関数名がそのままシンボル名になるわけではなく,引数や戻り値の型情報を付加した情報をリンカーに渡すようになっている. したがってCとC++で書かれたコードを混在させる場合,名前修飾される前の関数名をシンボルとして使用するにはこのようにすればよいということである.
デバイスとシンボリックリンクの作成
ドライバはWindowsのカーネル空間上にロードされるため,ユーザー空間上のアプリケーションから直接アクセスすることはできない.
Windowsでは,ユーザー空間のアプリケーションがドライバを操作するのにシンボリックリンクを使用する.
まず,ドライバ側でデバイスオブジェクトと,そのデバイスオブジェクトへのシンボリックリンクを作成する.
アプリケーションは,このシンボリックリンクを読み書きすることでデバイスを操作する.
この操作に該当するコードは以下のようになる.
// Create Device Object(Driver Instance) status = IoCreateDevice(DriverObject, 0, &DeviceName, FILE_DEVICE_UNKNOWN, FILE_DEVICE_SECURE_OPEN, FALSE, &DeviceObject); if (!NT_SUCCESS(status)) { DbgPrint("[+]IoCreateDeivce failed\n"); return status; } DbgPrint("[+]IoCreateDeivce success\n"); // Create Symblic Link status = IoCreateSymbolicLink(&SymbolicLinkName, &DeviceName); if (!NT_SUCCESS(status)) { DbgPrint("[+]IoCreateSymbolicLink failed\n"); return status; } DbgPrint("[+]IoCreateSymbolicLink success\n");
デバイスとシンボリックリンクを確認するにはWinObjを使用する. 以下は,デバイスIRPTestDeviceとそのシンボリックリンクIRPTestDeviceをWinObjで見た様子.

ディスパッチハンドラの作成
ユーザー空間のアプリケーションがデバイスにアクセスするには必ずNT ExecutiveのI/Oマネージャを通過する必要がある. アプリケーションはWindows APIを介してI/OマネージャにI/O操作を要求し,I/OマネージャがデバイスにIRP(I/O Request Packet)を送信する.

引用元:http://www.windowsbugcheck.com/p/lets-start-with-question-what-is-driver.html
IRPはI/Oマネージャによって作成される構造体であり,そのIO_STACK_LOCATIONには関数コードが格納されている.ドライバはこの関数コードを判定し,適当なディスパッチルーチンを実行する.
必要要件ではないが,ここではIRP_MJ_DEVICE_CONTROLを除いたすべてのIRPを同一の関数へ流すことにする.
この操作に該当するコードは以下のようになる.
// Modify IRP handler for (auto i = 0; i < IRP_MJ_MAXIMUM_FUNCTION; i++) { DriverObject->MajorFunction[i] = DispatchTestFunction; } DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = SioctlDeviceControl; DbgPrint("[+]MajorFunction modified\n");
関数コードIRP_MJ_DEVICE_CONTROLを受けて呼び出されるディスパッチハンドラSioctlDeviceControlについては後述する.
Unload処理の作成
デバイスオブジェクトの終了処理はシステムの安定さに不可欠だ.
シンボリックリンクの削除とデバイスオブジェクトの削除が主な内容になる.
この操作に該当するコードは以下のようになる.
void Unload(PDRIVER_OBJECT dob) { UNREFERENCED_PARAMETER(dob); IoDeleteSymbolicLink(&SymbolicLinkName); DbgPrint("[-]SymbolicLink deleted\n"); IoDeleteDevice(DeviceObject); DbgPrint("[-]DeviceObject deleted\n"); DbgPrint("[-]Driver unloaded!\n"); }
アプリケーションの準備
ここまででドライバ側に必要な処理はほとんど済んだ.
次はこのドライバを操作するアプリケーションの準備だ.
アプリケーションは,カーネル空間のデバイスオブジェクトとシンボリックリンクを介して通信する必要があることは既に述べた.
これには以下のようにすればよい.
まず,シンボリックリンクをCreateFileでオープンし,そのシンボリックリンクファイルのハンドルを得る.
アプリケーション側のコードの雛形は以下のようになる.
#include<stdio.h> #include<Windows.h> #include <cstdlib> #pragma comment(lib,"Kernel32.lib") LPCWSTR SymbolicLinkName = L"\\\\.\\IRPTestDevice"; // IRP code that will call our Buffer I/O functionality #define DEVICE_SEND CTL_CODE(FILE_DEVICE_UNKNOWN, 0x815, METHOD_BUFFERED, FILE_WRITE_DATA) int main() { // Get device handle HANDLE hDevice = CreateFile(SymbolicLinkName, GENERIC_READ | GENERIC_WRITE, 0, nullptr, OPEN_EXISTING, 0, nullptr); if (hDevice == INVALID_HANDLE_VALUE) { printf("[-]Cannot open device\n"); return 0; } /* do something */ CloseHandle(hDevice); return 0; }
Buffer I/Oの動作
さて,ここまでアプリケーションとデバイスオブジェクトのシンボリックリンクを介した操作方法を見てきた.
次にBuffer I/Oを見ていく.
Buffer I/Oによる動作の流れは以下のようになる.

引用元:https://docs.microsoft.com/ja-jp/windows-hardware/drivers/kernel/using-buffered-i-o
アプリケーションからのI/O要求を受けて,I/Oマネージャはデータバッファと同じ大きさのシステムバッファを非ページプールに確保する.
ドライバはこのシステムバッファに対して種々の操作を行う.
ドライバによるバッファへの処理が終了すると,I/Oマネージャはシステムバッファの中身をアプリケーションのバッファへコピーする.
デバイスからアプリケーションへのIRP送信
実際にBuffer I/Oを介したドライバとアプリケーション間のデータ転送をやってみる.
これにはDeviceIoControlを使用した制御コードの直接送信を利用する. アプリケーション側に以下のようなコードを加える.
WCHAR message[256] = L"send sample from application"; ULONG returnLength = 0; LPVOID returnBuff[256] = { 0 }; BOOLEAN call_result = DeviceIoControl( hDevice, METHOD_BUFFERED, message, (wcslen(message) + 1) * 2, returnBuff, sizeof(returnBuff), &returnLength, 0); printf("[+]Execute DeviceIoControl \n"); if (!call_result) { printf("[-] Error sending IRP to driver: %s \n", GetLastError()); return 0; } wprintf(L"[+]returnBuff %s\n", returnBuff); printf("[+]returnLength %d\n", returnLength);
DeviceIoControlを使うことで,ハンドルを持っているデバイスオブジェクトへ直接任意の制御コードを送信することができる.
デバイスオブジェクトは関数コードIRP_MJ_DEVICE_CONTROLを受け取り,対応するディスパッチャへ処理が移る. このディスパッチャ内で制御コードを判定すればよいだろう.
この制御コードを受けてBuffer I/Oにとるデータ処理を行うドライバ側のコードは以下のようになる.
NTSTATUS SioctlDeviceControl(PDEVICE_OBJECT DeviceObject, PIRP Irp) { DbgPrint("[+]DispatchDeviceControl called\n"); UNREFERENCED_PARAMETER(DeviceObject); PIO_STACK_LOCATION irpSp;// Pointer to current stack location NTSTATUS ntStatus = STATUS_SUCCESS;// Assume success ULONG inBufLength; // Input buffer length ULONG outBufLength; // Output buffer length PCHAR inBuf, outBuf; // pointer to Input and output buffer PWCHAR data = L"This String is from Device Driver !!!"; size_t datalen = (wcslen(data) + 1) * 2 ;//Length of data including null UNREFERENCED_PARAMETER(DeviceObject); PAGED_CODE(); // Causing a BSOD when code is executed in a non-pagepool irpSp = IoGetCurrentIrpStackLocation(Irp); inBufLength = irpSp->Parameters.DeviceIoControl.InputBufferLength; outBufLength = irpSp->Parameters.DeviceIoControl.OutputBufferLength; if (!inBufLength || !outBufLength) { ntStatus = STATUS_INVALID_PARAMETER; goto End; } switch (irpSp->Parameters.DeviceIoControl.IoControlCode) { case METHOD_BUFFERED: DbgPrint("[+]Called IOCTL_SIOCTL_METHOD_BUFFERED\n"); PrintIrpInfo(Irp); inBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer; outBuf = (PCHAR)Irp->AssociatedIrp.SystemBuffer; DbgPrint("\tData from User :"); PrintChars(inBuf, inBufLength); RtlCopyBytes(outBuf, data, outBufLength); DbgPrint("\tData to User : "); PrintChars(outBuf, datalen); Irp->IoStatus.Information = (outBufLength < datalen ? outBufLength : datalen); break; default: ntStatus = STATUS_INVALID_DEVICE_REQUEST; DbgPrint("ERROR: unrecognized IOCTL %x\n", irpSp->Parameters.DeviceIoControl.IoControlCode); break; } End: Irp->IoStatus.Status = ntStatus; IoCompleteRequest(Irp, IO_NO_INCREMENT); return ntStatus; }
ここまでの全体の流れをまとめると以下のようになる.
- アプリケーションが, DeviceIoControlを使用してシンボリックリンクへアクセスすることにより,I/Oマネージャがドライバに対してIRPを送信する.
- IRPを受信したドライバはIRPの関数コードを判定し,適切なディスパッチルーチンを呼び出す.
- DeviceIoControlを使用したIRPの関数コードはIRP_MJ_DEVICE_CONTROLであり,上記コードのSioctlDeviceControlへ処理が移る.
- SioctlDeviceControlでは制御コードを判定して処理を行う.
- もし制御コードがMETHOD_BUFFEREDであった場合,アプリケーションが用意したバッファと同じサイズのバッファが非ページシステムバッファに作成される.
- 任意の処理を行った後,この非ページシステムバッファがアプリケーション側のバッファに反映される.
デモ
コード全体は以下を参照のこと. ドライバ側で入力値検査を行っていない点に注意.
以上.