應用與內核通信
5.1 內核方面的編程
5.1.1 生成控制設備
一個驅動要和應用程序通信,要利用設備對象。設備對象和分發函數構成了整個內核體系的基本框架。設備對象可以在內核中暴露出來給應用層,應用層可以像操作文件一樣操作它。一般而言,用於和應用程序通信的設備往往用來“控制”這個內核驅動,所以往往稱之爲“控制設備對象”。
生成設備可以使用函數 IoCreateDevice 。這個函數的原型如下:
NTSTATUS IoCreateDevice(
//可以從 DriverEntry 的參數中獲得
IN PDRIVER_OBJECT DriverObject,
// 設備擴展的大小
IN ULONG DeviceExtensionSize,
// 設備名,一般都提供一個
IN PUNICODE_STRING DeviceName OPTIONAL,
//設備類型, Windows已經規定了一系列設備類型
IN DEVICE_TYPE DeviceTpye,
//需要填寫的一組設備屬性
IN ULONG DeviceCharacteristics,
//設備是否爲獨佔設備。表示同一時刻是否只能被打開一個句柄。一般都不會設置爲獨佔,但是安全軟件顯然只希望被病毒控制,所以可能會設爲獨佔。
IN BOOLEAN Exclusive,
//返回結果
OUT PDEVICE_OBJECT *DeviceObject
);
使用 IoCreateDevice 生成的控制設備對初學者來說會遇到一個潛在問題。即安全屬性的問題。設備具有默認的安全屬性,會導致必須要有管理員權限的進程纔可以打開它。因此,我們可以使用另一個函數來強迫生成一個任何用戶都可以打開的設備。但是這樣不安全。
NTSTATUS IoCreateDeviceSecure(
//可以從 DriverEntry 的參數中獲得
IN PDRIVER_OBJECT DriverObject,
// 設備擴展的大小
IN ULONG DeviceExtensionSize,
// 設備名,一般都提供一個
IN PUNICODE_STRING DeviceName OPTIONAL,
//設備類型, Windows已經規定了一系列設備類型
IN DEVICE_TYPE DeviceTpye,
//需要填寫的一組設備屬性
IN ULONG DeviceCharacteristics,
//設備是否爲獨佔設備。表示同一時刻是否只能被打開一個句柄。一般都不會設置爲獨佔,但是安全軟件顯然只希望被病毒控制,所以可能會設爲獨佔。
IN BOOLEAN Exclusive,
//對設備對象的安全設置
IN PCUNICODE_STRING DefaultSDDLString,
//設備對象的GUID,全球唯一標識符
IN LPCGUID DeviceClassGuid,
//返回創建到的設備對象結果
OUT PDEVICE_OBJECT *DeviceObject
);
DefautSDDLString 的設置可以從 WDK 的幫助中拷貝一個可以支持任何用戶打開設備的字符串。DeviceClassGuid 理論上需要由微軟提供的函數 CoCreateGuid 來生成(只是調用該函數一次,得到一個設備的GUID,而不是每次執行時都生成一個)。
//接收返回結果
DEVICE_OBJECT g_cdo = NULL;
//生成可以被任何用戶操控的設備對象
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;
...
}
g_cdo是一個全局變量。一般而言,控制設備生成之後都保存在全局變量中。因爲一個驅動程序只有一個控制設備,簡單的保存在全局變量裏容易在其他函數中識別。
5.1.2 控制設備的名字和符號鏈接
設備對象可以是沒有名字的。但是控制設備需要有一個名字,這樣它纔會暴漏出來供其他程序打開與之通信。設備的名字可以在調用 IoCreateDevice 或 IoCreateDeviceSecure 時指定。此外,應用層是無法直接通過設備的名字來打開對象的,爲此必須要建立一個暴露給應用層的符號鏈接。符號鏈接就是記錄一個字符串對應到另一個字符串的一種簡單結構。生成符號連接的函數是:
NTSTATUS IoCreateSymbolicLink(
IN PUNICODE_STRING SymbolicLinkName,//符號連接名
IN PUNICODE_STRING DeviceName //設備名
);
一般而言這個函數都會成功。不過,如果一個符號連接的名字已經在系統裏存在了,那麼這個函數就會失敗。符號鏈接的名字是在Windows中全局存在的。
所以用符號連接是不太穩妥的方式,因爲存在重名的可能性。最穩妥的方法是使用 GUID 的方式來訪問設備。要進一步詳細瞭解設備對象的訪問方式,需要參考專業的硬件驅動開發書籍。
5.1.3 控制設備的刪除
既然在驅動中生成了控制設備及符號鏈接,那麼在驅動卸載時就應該刪除它們;否則符號鏈接就會一直存在。刪除示例代碼如下:
#define CWK_CDO_SYB_NAME L"\\??\\slbkcdo_3948d334" //符號鏈接名
NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path)
{
...
//在DriverEntry中,設置了 cwkUnload 即卸載函數
driver-DriverUnload = ckwUnload;
}
void cwkUnload(PDRIVER_OBJECT driver)
{
UNICODE_STRING cdo_syb = RTL_CONSTANT_STRING(SLBKCDO_SYB_NAME);
//如果沒有該設備對象則出錯
ASSERT(g_cdo != NULL);
//依次刪除
IoDeleteSymblicLink(&cdo_syb);
IoDeleteDevice(g_cdo)
}
5.1.4 分發函數
分發函數是一組用來處理髮送給設備對象的請求的函數。分發函數是設置在驅動對象上的。即每一個驅動都有一組自己的分發函數。Windows 的 IO 管理器在收到請求時,會根據請求發送的目標,也就是一個設備對象,來調用這個設備對象所叢書的驅動對象上對應的分發函數。
不同的分發函數處理不同的請求。在本章中,作者使用到了三種請求:
-
打開(Create): 在試圖訪問一個設備對象之前,必須先用打開請求“打開”它。只有得到成功的返回,纔可以發送其他的請求。
-
關閉(Close): 在結束訪問一個設備對象之後,發送關閉請求將它關閉。關閉之後就必須再次打開才能訪問。
-
設備控制(Device Control) : 設備控制請求是一種既可以用來輸入(從應用到內核)。 又可以用來輸出 (從內核到應用) 的請求。因此很適合本節的需求。
一個標準的分發函數原型如下:
NTSTATUS cwwDispatch( IN PDEVICE_OBJECT dev, IN PIRP irp )
其中的 dev 就是請求要發送給的目標對象: irp 則是代表請求內容的數據結構的指針。無論如何,分發函數必須先首先設置驅動對象,這個工作一般在 DriverEntry 中完成。
NTSTATUS DriverEntry(PDRIVER_OBJECT driver,PUNICODE_STRING reg_path) { ... // 所有的分發函數都設置成一樣的。 for(i=0;i<IRP_MJ_MAXIMUM_FUNCTION;i++) { driver->MajorFunction[i] = cwkDispatch; } ... }
在該代碼片段中,MajorFunction是一個函數指針數組,保存所有分發函數的指針。以上代碼都設置成同一種函數,實際中可以設置不同的分發函數。
5.1.5 請求的處理
在分發函數中處理請求的第一步是獲取當前棧空間。請求的棧空間結構是適應於 Windows 內核驅動中設備對象的棧結構的。但是這不是本書的重點。
- 打開請求的主功能號是 IRB_MJ_CREATE
- 關閉請求的主功能號是 IRP_MJ_CLSOE
- 設備請求控制的功能號是 IRP_MJ_DEVICE_CONTROL
請求的當前棧空間可以用 IoGetCurrentIrpStackLocation 取得,然後根據主功能號做出不同的處理。代碼如下:
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;
}
明日計劃
繼續學習驅動編程。