應用與內核通信
文章目錄
5.3 阻塞、等待與安全設計
5.3.1 驅動主動通知應用
之前設計的功能需求中,還缺少一個應用從內核中讀取之前保存的字符串。通信是雙向的,從應用主動向內核發出請求是一件很容易的事——應用首先調用 **CreateFile**, 然後調用 **DeviceIoControl** ,就可以引發內核程序的反應。但是內核如何主動通知應用呢? 一個簡單的辦法: 使用內核獨用的同步事件來等待。
用戶態隨意調用 DeviceIoControl,在內核程序中的分發函數裏等待一個同步事件即可。這樣,在用戶態看來,程序就像是被阻塞了一樣。當內核想通知程序的時候,只要設置該同步事件即可。只要在用戶態的線程裏使用該 DeviceControl,就可以一邊持續工作,一邊等待內核的通知了。
5.3.2 通信接口的測試
在驅動程序的實際開發中,控制檯的用戶態應用程序往往作爲“測試者”。一個提供給用戶態通信接口的驅動程序尤其榮譽暴露出內核漏洞。從而給整個內核帶來被攻擊的風險。所以在發佈之前,需要用戶態程序對通信接口進行各種可能的全面測試,來覆蓋各種意外情況。
- 如果輸入緩衝區極大/極小會如何?
- 如果輸入緩衝區剛好等於規定的最大/最小長度會如何?
- 如果輸入的字符串沒有結束符會如何?
- 如果輸入的字符串長度爲0會如何。
- 只涉及邏輯關係簡單、具有可測試行的通信接口。
- 只實習通信中各種可能的情況做充分的測試。
本章規定的接口較爲簡答,只有兩個接口: 一個爲發送字符串接口(字符串從應用到內核),一個爲接受字符串接口(字符串從內核到應用)。
5.3.3 內核中的緩衝區鏈表結構
在內核中將使用一個雙向鏈表來保存所有已經輸入的字符串。每當應用層來取字符串的時候,每次返回其中一個,並從鏈表中刪除。保證一個先入先出的順序。
//定義一個鏈表來保存字符串
#define CWK_STR_LEN_MAX 512
typedef struct{
LIST_ENTRY list_entry;
char buf[CWK_STR_LEN_MAX];
}CWT_STR_NODE;
//還必須有一個自旋鎖來保證鏈表操作的安全性
KSPIN_LOCK g_cwk_lock;
//用一個事件來標識是否有字符串可以取
KEVENT g_cwk_enent;
//必須有一個鏈表頭
LIST_ENTRY g_cwk_str_list;
上面這個數據結構的好處是長度固定,但是非常浪費空間。但是我們作爲測試用例就無所謂。所有鏈表結構中的空間都是動態分配的。
// 分配內存並初始化一個鏈表節點
CWK_STR_NODE *cwkMallocStrNode()
{
CWK_STE_NODE *ret = ExAllocatePoolWithTag(
NonPagedPool,sizeof(CWK_STR_NODE).MEM_TAG);
if(ret == NULL)
return NULL;
return ret;
}
記得在刪除的時候要釋放內存。
5.3.4 輸入: 內核中的請求處理中的安全檢查
接下來回到從應用層發送字符串到內核。內核程序的分發函數的處理中,首先就是對輸入緩衝區的檢查,確保緩衝區的長度符號正確的要求。
注意不要把 ASSERT(斷言)和安全檢查混淆起來。
斷藥只有在檢查版本中才被編譯。斷言是爲了提示開發者,僅僅用於調試。斷言不能出現在發行版本中,因爲被編譯到發行版本中的斷言能夠導致系統藍屏。
安全檢查的原則之一是越簡單越好。當我們將輸入緩衝區的長度限制爲512字節時,這就意味着攻擊者可能使用的輸入緩衝區的長度範圍從無限大急劇地縮小到了512字節。
輸入緩衝區的長度即使是正確的,也必須要檢查輸入緩衝區的內容。不可以使用 strlen 函數來檢查輸入緩衝區的長度。因爲該函數並沒有限度,它從第一個字符串開始搜索直到找到結束符爲止。如果一直沒有結束符,就會一直找下去,這完全可能會導致異常。
所以我們使用 strnlen 來檢查字符串的長度。它有一個最大限度,超過這個限度就不會再往後搜索結束符了。
//安全的編程態度,使用strnlen而不是strlen來檢查長度
DbgPrint("strnlen = %d\r\n,strnlen(char *)buffer,inlen);
if(strnlen((char *)buffer,inlen) == inlen)
//字符串沾滿了緩衝區,並且中間沒有結束符,立刻返回錯誤
status = STATUS_INVALID_PARAMETER;
break;
//如果成功的找到了結束符,則可以認爲是輸入有效的,這時候才繼續處理。
str_node = cwMallocStrNode();
if(str_node == NULL)
{
//如果分配不到空間了,則返回資源不足
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
...
如果後續要拷貝字符串。按道理說使用 strcpy 來拷貝,也會和 strlen 有一樣的不安全因素。但是我們在分配空間之前已經確認了輸入的字符室友帶結束符的,所以後續可以放心的使用 strcpy 了。但是處於良好的習慣,我們還是使用 strncpy來拷貝字符串。
strncpy(str_node->buf,(char *)buffer,CWK_STR_LEN_MAX);
//插入到鏈表末尾,用鎖來保證安全性
ExInterlockedInsertTailList(&g_cwk_str_list,(PLIST_ENTRY)str_node,&g_cwk_lock);
//現在就可以認爲這個請求已經成功,因爲
//剛剛已經成功插入了一個字符串,那麼可以設置事件結束來表明隊列中已經有元素了
KeSetEvent(&g_cwk_event,0,TRUE);
break;
...
這裏使用了 ExInterlockerInsertTailLIst 來把新的字符串鏈表節點插入到緩衝區中,是因爲該驅動可以同時接受很多個進程來獲取或者存儲字符串,但是每次去除的字符串只發給第一個索取者。因此,這段代碼隨時都可能是有多個線程同時執行的,所以使用鎖來保證多線程安全性。
5.3.5 輸出處理與卸載清理
同樣,在處理接受字符串請求的時候,也就是應用層要i求從內核讀取字符串的時候,也要做類似的安全處理。
在對內存不敏感的時候,直接要求應用層給出足夠大的緩衝區來接受字符串。
//應用要求接受字符串。對此,要求輸出緩衝區要足夠長
if(outlen < CWK_STRL_LEN_MAX)
{
statsu = STATUS_INVALID_PARAMETER;
break;
}
......
這裏使用 while 循環來一直檢測鏈表裏是否有字符串可以返回,但是持續的詢問是多餘的,可以使用 sleep 來減少CPU的佔有率。
while(1)
{
//從鏈表頭取數據,用鎖來保證安全性
str_node = (CWK_STR_NODE *)ExInterlockedRemoveHeadList(&g_cwk_str_list,&g_cwk_lock);
if(str_node != NULL)
{
//一旦取得了字符串,就拷貝到緩衝區中
strncpy((char *)buffer,str_node-buf,CWK_STR_LEN_MAX);
ret_len = strnlen(str_node-buf,CWK_STR_LEN_MAX)+1;
ExFreePool(str_node);
break;
}
else
{
//如果鏈表爲空,則事件進行等待
KeWaitForSingleObject(&g_cwk_event,Executive,KernelMode,0,0);
}
}
break;
最後,不要忘了在卸載函數裏增加釋放所有的內存的代碼。
給出完整的內核態代碼供實驗:
//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)
// 定義一個鏈表用來保存字符串
#define CWK_STR_LEN_MAX 512
typedef struct {
LIST_ENTRY list_entry;
char buf[CWK_STR_LEN_MAX];
} CWK_STR_NODE;
// 還必須有一把自旋鎖來保證鏈表操作的安全性
KSPIN_LOCK g_cwk_lock;
// 一個事件來標識是否有字符串可以取
KEVENT g_cwk_event;
// 必須有個鏈表頭
LIST_ENTRY g_cwk_str_list;
#define MEM_TAG 'cwkr'
// 分配內存並初始化一個鏈表節點
CWK_STR_NODE *cwkMallocStrNode()
{
CWK_STR_NODE *ret = ExAllocatePoolWithTag(
NonPagedPool, sizeof(CWK_STR_NODE), MEM_TAG);
if(ret == NULL)
return NULL;
return ret;
}
void cwkUnload(PDRIVER_OBJECT driver)
{
UNICODE_STRING cdo_syb = RTL_CONSTANT_STRING(CWK_CDO_SYB_NAME);
CWK_STR_NODE *str_node;
ASSERT(g_cdo != NULL);
IoDeleteSymbolicLink(&cdo_syb);
IoDeleteDevice(g_cdo);
// 負責的編程態度:釋放分配過的所有內核內存。
while(TRUE)
{
str_node = (CWK_STR_NODE *)ExInterlockedRemoveHeadList(
&g_cwk_str_list, &g_cwk_lock);
// str_node = RemoveHeadList(&g_cwk_str_list);
if(str_node != NULL)
ExFreePool(str_node);
else
break;
};
}
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;
CWK_STR_NODE *str_node;
switch(irpsp->Parameters.DeviceIoControl.IoControlCode)
{
case CWK_DVC_SEND_STR:
ASSERT(buffer != NULL);
ASSERT(outlen == 0);
// 安全的編程態度之一:檢查輸入緩衝的長度對於長度超出預期的,果
// 斷返回錯誤。
if(inlen > CWK_STR_LEN_MAX)
{
status = STATUS_INVALID_PARAMETER;
break;
}
// 安全的編程態度之二:檢查字符串的長度,不要使用strlen!如果使
// 用strlen,一旦攻擊者故意輸入沒有結束符的字符串,會導致內核驅
// 動訪問非法內存空間而崩潰。
DbgPrint("strnlen = %d\r\n", strnlen((char *)buffer, inlen));
if(strnlen((char *)buffer, inlen) == inlen)
{
// 字符串佔滿了緩衝區,且中間沒有結束符。立刻返回錯誤。
status = STATUS_INVALID_PARAMETER;
break;
}
// 現在可以認爲輸入緩衝是安全而且不含惡意的。分配節點。
str_node = cwkMallocStrNode();
if(str_node == NULL)
{
// 如果分配不到空間了,返回資源不足的錯誤
status = STATUS_INSUFFICIENT_RESOURCES;
break;
}
// 前面已經檢查了緩衝區中的字符串的確長度合適而且含有結束符
// ,所以這裏用什麼函數來拷貝字符串對安全性而言並不非常重要。
strncpy(str_node->buf,(char *)buffer, CWK_STR_LEN_MAX);
// 插入到鏈表末尾。用鎖來保證安全性。
ExInterlockedInsertTailList(&g_cwk_str_list, (PLIST_ENTRY)str_node, &g_cwk_lock);
// InsertTailList(&g_cwk_str_list, (PLIST_ENTRY)str_node);
// 打印
// DbgPrint((char *)buffer);
// 那麼現在就可以認爲這個請求已經成功。因爲剛剛已經插入了一
// 個,那麼可以設置事件來表明隊列中已經有元素了。
KeSetEvent(&g_cwk_event, 0, TRUE);
break;
case CWK_DVC_RECV_STR:
ASSERT(buffer != NULL);
ASSERT(inlen == 0);
// 應用要求接收字符串。對此,安全上要求是輸出緩衝要足夠長。
if(outlen < CWK_STR_LEN_MAX)
{
status = STATUS_INVALID_PARAMETER;
break;
}
while(1)
{
// 從鏈表頭取出數據。用鎖來保證安全性。
str_node = (CWK_STR_NODE *)ExInterlockedRemoveHeadList(&g_cwk_str_list, &g_cwk_lock);
// str_node = RemoveHeadList(&g_cwk_str_list);
if(str_node != NULL)
{
// 這種情況下,取得了字符串。那就拷貝到輸出緩衝中。然後
// 整個請求就返回了成功。
strncpy((char *)buffer, str_node->buf, CWK_STR_LEN_MAX);
ret_len = strnlen(str_node->buf, CWK_STR_LEN_MAX) + 1;
ExFreePool(str_node);
break;
}
else
{
// 對於合法的要求,在緩衝鏈表爲空的情況下,等待事件進行
// 阻塞。也就是說,如果緩衝區中沒有字符串,就停下來等待
// 。這樣應用程序也會被阻塞住,DeviceIoControl是不會返回
// 的。但是一旦有就會返回。等於驅動“主動”通知了應用。
KeWaitForSingleObject(&g_cwk_event,Executive,KernelMode,0,0);
}
}
break;
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;
}
// 初始化事件、鎖、鏈表頭。
KeInitializeEvent(&g_cwk_event,SynchronizationEvent,TRUE);
KeInitializeSpinLock(&g_cwk_lock);
InitializeListHead(&g_cwk_str_list);
// 所有的分發函數都設置成一樣的。
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;
}
完整的用戶態代碼:
//Download by www.cctry.com
// coworker_user.cpp : 定義控制檯應用程序的入口點。
//
#include "stdafx.h"
#define CWK_DEV_SYM 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)
int _tmain(int argc, _TCHAR* argv[])
{
HANDLE device = NULL;
ULONG ret_len;
int ret = 0;
char *msg = {"Hello driver, this is a message from app.\r\n"};
char tst_msg[1024] = { 0 };
// 打開設備.每次要操作驅動的時候,先以此爲例子打開設備
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");
// 這裏開始,其實是對驅動的一系列測試。分配3個字符串:
// 1.長度爲0.應該可以正常輸入。
// 2.長度爲511字節,應該可以正常輸入。
// 3.長度爲512字節,應該返回失敗。
// 4.長度爲1024字節的字符串,但聲明緩衝區長度爲128,應該返回失敗。
// 5.第一次讀取,應該讀出msg的內容。
// 5.第一次讀取,應該讀出長度爲511字節的字符串。
// 6.第二次讀取,應該讀出長度爲0的字符串。
do {
memset(tst_msg, '\0', 1);
if(!DeviceIoControl(device, CWK_DVC_SEND_STR, tst_msg, 1, NULL, 0, &ret_len, 0))
{
ret = -3;
break;
}
else
{
printf("TEST1 PASS.\r\n");
}
memset(tst_msg, '\0', 512);
memset(tst_msg, 'a', 511);
if(!DeviceIoControl(device, CWK_DVC_SEND_STR, tst_msg, 512, NULL, 0, &ret_len, 0))
{
ret = -5;
break;
}
else
{
printf("TEST2 PASS.\r\n");
}
memset(tst_msg, '\0', 513);
memset(tst_msg, 'a', 512);
if(DeviceIoControl(device, CWK_DVC_SEND_STR, tst_msg, 513, NULL, 0, &ret_len, 0))
{
// 這個緩衝區已經過長,理應返回失敗。如果成功了則
// 認爲是錯誤。
ret = -5;
break;
}
else
{
printf("TEST3 PASS.\r\n");
}
memset(tst_msg, '\0', 1024);
memset(tst_msg, 'a', 1023);
if(DeviceIoControl(device, CWK_DVC_SEND_STR, tst_msg, 128, NULL, 0, &ret_len, 0))
{
// 這個緩衝區雖然不過長,但是字符串過長,理應返回失
// 敗。如果成功了則認爲是錯誤。
ret = -5;
break;
}
else
{
printf("TEST4 PASS.\r\n");
}
free(tst_msg);
// 現在開始測試輸出。第一個讀出的應該是msg.
if(DeviceIoControl(device, CWK_DVC_RECV_STR, NULL, 0, tst_msg, 1024, &ret_len, 0) == 0 || ret_len != strlen(msg) + 1)
{
ret = -6;
break;
}
else
{
printf("TEST5 PASS.\r\n");
}
// 第二個讀出的應該是長度爲0的空字符串。
if(DeviceIoControl(device, CWK_DVC_RECV_STR, NULL, 0, tst_msg, 1024, &ret_len, 0) == 0 || ret_len != 1)
{
ret = -6;
break;
}
else
{
printf("TEST6 PASS.\r\n");
}
// 第三個讀出的應該是長度爲511的全a字符串
if(DeviceIoControl(device, CWK_DVC_RECV_STR, NULL, 0, tst_msg, 1024, &ret_len, 0) != 0 || ret_len != 511 + 1)
{
ret = -6;
break;
}
else
{
printf("TEST7 PASS.\r\n");
}
} while(0);
CloseHandle(device);
return ret;
}
明日計劃
驅動編程第六章 64位和32位內核開發差異