應用與內核通信
5.2 應用方面的編程
5.2.1 基本的功能需求
本節例子將簡單實現一個通信需求: 應用程序可以隨時發送字符串給內核驅動,而內核驅動將把這些字符串保存在自己的緩衝區中: 同時應用程序也可以隨時發送請求將這些保存在內核驅動緩衝區中的字符讀取出來。
這看似非常簡單,但是它實際上給進程之間的通信提供了一種通道。因爲一個普通的運行在 r3 級別的進程,是無法主動跟其他進程進行通信的。但是通過這種渠道,一個進程將字符串存儲在內核中,另一個進程可以從內核中讀取該字符串,這就完成了進程之間的通信。類似的,還可以傳遞更復雜的數據結構。
5.2.2 在應用程序中打開與關閉設備
在應用程序紅打開設備和打開文件並沒什麼不同,除了設備的路徑有一些特殊以外。打開設備使用API CreateFile 。文件的路徑就是符號鏈接的路徑,但是符號鏈接的路徑在應用層看來,是以 “\\.\”開頭的。並且這些“\”在C語言中要使用“\\”來轉義。示例代碼:
#define CWK_DEV_SYM L"\\\\.\\slbkcdo_3948d33e"
int _tmain(int argc, _TCHAR* argv[]) //_tmain 是 main的別名
{
HANDLE device = NULL;
ULONG ret_len;
int ret = 0;
char *msg = {"Hello driver, this is a message from app.\r\n"};
// 打開設備.每次要操作驅動的時候,先以此爲例子打開設備
device=CreateFile(CWK_DEV_SYM,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
if (device == INVALID_HANDLE_VALUE)
{
printf("coworker demo: Open device failed.\r\n");
return -1;
}
else
printf("coworker demo: Open device successfully.\r\n");
if(!DeviceIoControl(device, CWK_DVC_SEND_STR, msg, strlen(msg) + 1, NULL, 0, &ret_len, 0))
{
printf("coworker demo: Send message failed.\r\n");
ret = -2;
}
else
printf("coworker demo: Send message successfully.\r\n");
CloseHandle(device);
return ret;
}
CreateFile 中最重要的參數就是第一個: 用一個字符串來表示設備的路徑。後面的參數可以在MSDN上詳細瞭解。這裏有翻譯後的詳細解釋
如果打開設備失敗,會返回 INVALID_HANDLE_VALUE,不是 NULL。這是一個容易出錯的地方。在結束的時候需要關閉設備,直接調用 CloseHandle(device) 即可。
5.2.3 設備控制請求
設備控制請求可以進行輸入和輸出。輸入和輸出都可以利用一個簡單的自定義結構和長度緩衝區。可以根據自己的需要來涉及非常複雜的通信協議。這裏介紹一個簡單的設計: 定義一個叫做 “發送字符串” 的功能號。每個設備控制請求都有一個功能號,以便區分不同的設備控制請求。
// 從應用層給驅動發送一個字符串
#define CWK_DVC_SEND_STR \
(UNLONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0X911,METHOD_BUFFERED,FILE_WRITE_DATA)
這裏給出CTL_CODE宏定義的解釋。 CTL_CODE 共有四個參數,第一個參數爲設備類型。這裏生成的設備與任何硬件都沒有關係所以填寫 FILE_DEVICE_UNKNOWN。第二個參數是生成這個功能號的核心數字,其中0x0~0x7ff已經被微軟預留,同時這個數字也不能大於0xfff。第三個參數是指定使用緩衝區讀寫的方式。最後一個參數是這個操作需要的權限,因爲是發送字符串,因此需要寫到緩衝區,寫權限。相應的可以再定義讀字符串:
// 從應用層給驅動發送一個字符串
#define CWK_DVC_RECV_STR \
(UNLONG)CTL_CODE(FILE_DEVICE_UNKNOWN,0X912,METHOD_BUFFERED,FILE_READ_DATA)
#define CWK_DEV_SYM L"\\\\.\\slbkcdo_3948d33e"
int _tmain(int argc, _TCHAR* argv[]) //_tmain 是 main的別名
{
HANDLE device = NULL;
ULONG ret_len;
int ret = 0;
char *msg = {"Hello driver, this is a message from app.\r\n"};
// 打開設備.每次要操作驅動的時候,先以此爲例子打開設備
device=CreateFile(CWK_DEV_SYM,GENERIC_READ|GENERIC_WRITE,0,0,OPEN_EXISTING,FILE_ATTRIBUTE_SYSTEM,0);
if (device == INVALID_HANDLE_VALUE)
{
printf("coworker demo: Open device failed.\r\n");
return -1;
}
else
printf("coworker demo: Open device successfully.\r\n");
if(!DeviceIoControl(device, CWK_DVC_SEND_STR, msg, strlen(msg) + 1, NULL, 0, &ret_len, 0))
{
printf("coworker demo: Send message failed.\r\n");
ret = -2;
}
else
printf("coworker demo: Send message successfully.\r\n");
CloseHandle(device);
return ret;
}
結合代碼,首先使用 CreateFile 打開設備,然後使用 DeviceIoControl 發送請求。msg 是要發送的字符串。
DeviceControl 的函數原型爲:
BOOL WINAPI DeviceIoControl(
_In_ HANDLE hDevice,
_In_ DWORD dwIoControlCode,
_In_opt_ LPVOID lpInBuffer,
_In_ DWORD nInBufferSize,
_Out_opt_ LPVOID lpOutBuffer,
_In_ DWORD nOutBufferSize,
_Out_opt_ LPDWORD lpBytesReturned,
_Inout_opt_ LPOVERLAPPED lpOverlapped
);
hDevice [in]
需要執行操作的設備句柄。該設備通常是卷,目錄,文件或流,使用 CreateFile 函數打開獲取設備句柄。具體的見備註
dwIoControlCode [in]
操作的控制代碼,該值標識要執行的特定操作以及執行該操作的設備的類型,有關控制代碼的列表,請參考備註。每個控制代碼的文檔都提供了lpInBuffer,nInBufferSize,lpOutBuffer和nOutBufferSize參數的使用細節。
lpInBuffer [in, optional]
(可選)指向輸入緩衝區的指針。這些數據的格式取決於dwIoControlCode參數的值。如果dwIoControlCode指定不需要輸入數據的操作,則此參數可以爲NULL。
nInBufferSize [in]
輸入緩衝區以字節爲單位的大小。單位爲字節。
lpOutBuffer [out, optional]
(可選)指向輸出緩衝區的指針。這些數據的格式取決於dwIoControlCode參數的值。如果dwIoControlCode指定不返回數據的操作,則此參數可以爲NULL。
nOutBufferSize [in]
輸出緩衝區以字節爲單位的大小。單位爲字節。
lpBytesReturned [out, optional]
(可選)指向一個變量的指針,該變量接收存儲在輸出緩衝區中的數據的大小。如果輸出緩衝區太小,無法接收任何數據,則GetLastError返回ERROR_INSUFFICIENT_BUFFER,錯誤代碼122(0x7a),此時lpBytesReturned是零。
5.2.4 內核中的對應處理
目前,在應用中調用 DeviceIoControl 一定會返回錯誤,因爲內核驅動中還沒處理。所以現在我們回到內核編程中來修改。在處理打開和關閉 IRP 時,比較簡單,直接返回成功即可。但是在處理設備控制請求時,還有如下的任務要完成。
- 獲得功能號。
- 如果有輸入緩衝區,則必須獲得輸入緩衝區的指針以及長度。
- 如果有輸出緩衝區,則必須獲得輸出緩衝區的指針以及長度。
…
if(irpsp->MajorFunction == IRP_MJ_DEVICE_CONTROL)
{
// 獲得緩衝區
PVOID buffer = irp->AssociatedIrp.SystemBuffer;
// 獲得輸入緩衝區的產長度
ULONG inlen = irpsp->Parameters.DeviceIoControl.InputBufferLength;
// 獲得輸出緩衝區的長度
ULONG inlen = irpsp->Parameters.DeviceIoControl.OutputBufferLength;
...
}
注意,緩衝區是 irp->AsspcoatedIrp.SystemBuffer 的前提是,這是一個緩衝方式的設備控制請求。這一點在應用層中有設置。完整代碼示例:
//Download by www.cctry.com
///
/// @file coworker_sys.c
/// @author tanwen
/// @date 2012-5-28
///
#include <ntifs.h>
#include <wdmsec.h>
PDEVICE_OBJECT g_cdo = NULL;
const GUID CWK_GUID_CLASS_MYCDO =
{0x17a0d1e0L, 0x3249, 0x12e1, {0x92,0x16, 0x45, 0x1a, 0x21, 0x30, 0x29, 0x06}};
#define CWK_CDO_SYB_NAME L"\\??\\slbkcdo_3948d33e"
// 從應用層給驅動發送一個字符串。
#define CWK_DVC_SEND_STR \
(ULONG)CTL_CODE( \
FILE_DEVICE_UNKNOWN, \
0x911,METHOD_BUFFERED, \
FILE_WRITE_DATA)
// 從驅動讀取一個字符串
#define CWK_DVC_RECV_STR \
(ULONG)CTL_CODE( \
FILE_DEVICE_UNKNOWN, \
0x912,METHOD_BUFFERED, \
FILE_READ_DATA)
void cwkUnload(PDRIVER_OBJECT driver)
{
UNICODE_STRING cdo_syb = RTL_CONSTANT_STRING(CWK_CDO_SYB_NAME);
ASSERT(g_cdo != NULL);
IoDeleteSymbolicLink(&cdo_syb);
IoDeleteDevice(g_cdo);
}
NTSTATUS cwkDispatch(
IN PDEVICE_OBJECT dev,
IN PIRP irp)
{
PIO_STACK_LOCATION irpsp = IoGetCurrentIrpStackLocation(irp);
NTSTATUS status = STATUS_SUCCESS;
ULONG ret_len = 0;
while(dev == g_cdo)
{
// 如果這個請求不是發給g_cdo的,那就非常奇怪了。
// 因爲這個驅動只生成過這一個設備。所以可以直接
// 返回失敗。
if(irpsp->MajorFunction == IRP_MJ_CREATE || irpsp->MajorFunction == IRP_MJ_CLOSE)
{
// 生成和關閉請求,這個一律簡單地返回成功就可以
// 了。就是無論何時打開和關閉都可以成功。
break;
}
if(irpsp->MajorFunction == IRP_MJ_DEVICE_CONTROL)
{
// 處理DeviceIoControl。
PVOID buffer = irp->AssociatedIrp.SystemBuffer;
ULONG inlen = irpsp->Parameters.DeviceIoControl.InputBufferLength;
ULONG outlen = irpsp->Parameters.DeviceIoControl.OutputBufferLength;
ULONG len;
switch(irpsp->Parameters.DeviceIoControl.IoControlCode)
{
case CWK_DVC_SEND_STR:
ASSERT(buffer != NULL);
ASSERT(inlen > 0);
ASSERT(outlen == 0);
DbgPrint((char *)buffer);
// 已經打印過了,那麼現在就可以認爲這個請求已經成功。
break;
case CWK_DVC_RECV_STR:
default:
// 到這裏的請求都是不接受的請求。未知的請求一律返回非法參數錯誤。
status = STATUS_INVALID_PARAMETER;
break;
}
}
break;
}
// 到這裏的請求都是不接受的請求。未知的請求一律返回非法參數錯誤。
irp->IoStatus.Information = ret_len;
irp->IoStatus.Status = status;
IoCompleteRequest(irp,IO_NO_INCREMENT);
return status;
}
NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
{
NTSTATUS status;
ULONG i;
UCHAR mem[256] = { 0 };
// 生成一個控制設備。然後生成符號鏈接。
UNICODE_STRING sddl = RTL_CONSTANT_STRING(L"D:P(A;;GA;;;WD)");
UNICODE_STRING cdo_name = RTL_CONSTANT_STRING(L"\\Device\\cwk_3948d33e");
UNICODE_STRING cdo_syb = RTL_CONSTANT_STRING(CWK_CDO_SYB_NAME);
KdBreakPoint();
// 生成一個控制設備對象。
status = IoCreateDeviceSecure(
driver,
0,&cdo_name,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,&sddl,
(LPCGUID)&CWK_GUID_CLASS_MYCDO,
&g_cdo);
if(!NT_SUCCESS(status))
return status;
// 生成符號鏈接.
IoDeleteSymbolicLink(&cdo_syb);
status = IoCreateSymbolicLink(&cdo_syb,&cdo_name);
if(!NT_SUCCESS(status))
{
IoDeleteDevice(g_cdo);
return status;
}
// 所有的分發函數都設置成一樣的。
for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++)
{
driver->MajorFunction[i] = cwkDispatch;
}
// 支持動態卸載。
driver->DriverUnload = cwkUnload;
// 清除控制設備的初始化標記。
g_cdo->Flags &= ~DO_DEVICE_INITIALIZING;
return STATUS_SUCCESS;
}
使用了 while 代替 if,這是希望在發生錯誤的時候,可以直接使用 break 跳到返回到的地方。關於緩衝區的處理,直接使用 DbgPrint((char)buffer)*。這樣處理的方式非常簡單,但是有一個缺點,就是沒有規定緩衝區的最大長度。換句話說,緩衝區的長度是由用戶態的應用程序發起調用時決定的。很容易受到緩衝區溢出攻擊。
5.2.5 實際演示
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-SnT71ou9-1587381472093)(assets/1587379173688.png)]
應用與內核通信成功。但是中途也遇到幾個問題:
- “const char”類型的值不能用於初始化“char”類型的實體*。解決方法: VS2017後期版本、VS2019版,對於直接利用char * 類型聲明變量時會產生““const char*”類型的值不能用於初始化“char”類型的實體”的錯誤。可以使用強制轉換的方法。
char* msg = (char*) "hello world" ;
繼續學習驅動編程