我們寫一個Ring0的代碼,有時候需要和Ring3進行交互,比如我們做一個進程防殺的驅動保護,那我們可以通過Ring3的進程,發送請求告訴驅動層,究竟哪些進程屬於白名單,哪些屬於黑名單,而在通信過程中,涉及到了應用層與內核層之間通信的一些數據傳輸方法,我在這裏簡單做一個學習筆記:
如果一個驅動要和應用程序通信,首先需要生成一個設備對象(DeviceObject),設備對象可以通過某種方式在內核中暴露出來給用戶層,應用層就可以像操作文件一樣操作它。而在內核層創建一個設備對象,Windows也提供給了我們底層函數接口:
- IoCreateDevice
可以用這個底層函數創建一個與Ring3進行通信的控制設備對象。使用這個函數需要注意,它生成的設備對象具有默認的安全屬性,需要有管理員權限的進程纔可以打開這個設備對象。對於我們用來通信的控制設備來說,肯定是需要一個設備名稱的,上面我們還提到,設備名是無法直接被用戶層所打開的,需要一些特殊的操作,而這個操作具體就是,我們爲當前設備對象創建一個符號鏈接名,用來被Ring3層打開。而這些名稱的定義也有一個確定的規範:
#define DEVICE_OBJECT_NAME L"\\Device\\Kt_DeviceObjectName" //驅動之間用的 命名規範就是這樣,最後'\\'後面的字符串是可以自己定義的
#define DEVICE_LINK_NAME L"\\DosDevices\\Kt_DeviceLinkName" //Ring3和Ring0之間通信
RtlInitUnicodeString(&DeviceObjectName, DEVICE_OBJECT_NAME);
//創建與Ring3層通信的控制設備對象 也稱之爲CDO Control Device Object
Status = IoCreateDevice(
DriverObject,
0,
&DeviceObjectName,
FILE_DEVICE_UNKNOWN,
FILE_DEVICE_SECURE_OPEN,
FALSE,//是否獨佔,若獨佔,在某一時刻只能被打開一個句柄
&DeviceObject
);
if (!NT_SUCCESS(Status))
{
return Status;
}
RtlInitUnicodeString(&DeviceLinkName, DEVICE_LINK_NAME);
//爲了和Ring3層進行通信,創建一個符號鏈接名
Status = IoCreateSymbolicLink(
&DeviceLinkName,
&DeviceObjectName
);
if (!NT_SUCCESS(Status))
{
//如果符號鏈接名創建失敗,則直接over
IoDeleteDevice(DeviceObject);
return Status;
}
驅動卸載時候,記得銷燬設備鏈接名,刪除創建的設備對象。
應用層需要連接符號鏈接名時,使用文件操作的API CreateFile函數即可:
//通過Ring0的設備對象的設備鏈接名進行打開獲取設備對象句柄
DeviceHandle = CreateFile(
_T("\\\\.\\Kt_DeviceLinkName"),//需要轉義字符\\.\Kt_DeviceLinkName
GENERIC_READ | GENERIC_WRITE,
FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL,
OPEN_EXISTING,
FILE_FLAG_NO_BUFFERING,
NULL);
一旦連接設備成功,則需要像設備發送設備請求:
//CreateFile失敗函數返回不是NULL,而是INVALID_HANDLE_VALUE
if (DeviceHandle != INVALID_HANDLE_VALUE)
{
//向設備請求IO
BOOL IsOk = DeviceIoControl(
DeviceHandle,
MY_IOCTL_CODE, //自定義的某種IO請求方式
InputBuffer,
InputBufferLength,
OutBuffer,
OutBufferLength,
&v1,
NULL);
}
經過前面的一些鋪墊,接下來我們進入今日正題,來看一下自定義控制碼的方法:
#define MY_IOCTL_CODE \
CTL_CODE \
( \
FILE_DEVICE_UNKNOWN,\ //未知的類型
0x911, \ //生成功能號的核心數字,並且不大於0xfff,0x000~0x7ff被微軟預留
METHOD_NEITHER, \ //數據傳輸的方式,重點
FILE_ANY_ACCESS \ //文件操作的權限
)
我們可以用上面這種方式,來設置一個自己的設備控制碼,CTL_CODE是一個SDK的宏:
#define CTL_CODE( DeviceType, Function, Method, Access ) ( \
((DeviceType) << 16) | ((Access) << 14) | ((Function) << 2) | (Method) \
)
其中用來區分數據是以何種方式傳輸到內核層的參數,是第三參數,其總共有三種方式(直接輸入和輸出歸結爲直接方式),分別是:
#define METHOD_BUFFERED 0 //緩衝區
#define METHOD_IN_DIRECT 1 //直接輸入方式
#define METHOD_OUT_DIRECT 2 //直接輸出方式
#define METHOD_NEITHER 3 //原始方式
METHOD_BUFFERED:
若使用這種方式傳輸數據,那麼我們的數據將會通過PIRP->AssociatedIrp.SystemBuffer來進行用戶層輸入與輸出數據的緩衝,Ring0將數據進行拷貝,而不是直接對Ring3層地址進行訪問,所以這種方式比較安全。
METHOD_IN_DIRECT/METHOD_OUT_DIRECT:
使用這種方式傳輸數據,我們的輸入也將會通過PIRP->AssociatedIrp.SystemBuffer來進行用戶層輸入數據的緩衝,而輸出數據是以MDL映射的方式,鎖定用戶區的內存,直到Ring0完成I/O請求之後,Ring3層纔可以訪問這塊內存,也算是相對安全的一種方式。IN和OUT的區別是對於打開設備的權限,當只讀打開,使用METHOD_IN_DIRECT成功,METHOD_OUT_DIRECT失敗。如果讀寫權限,則都可以。
METHOD_NEITHER:
使用這種方式傳輸數據,我們通過PIO_STACK_LOCATION->Parameters.DeviceIoControl.Type3InputBuffer獲取用戶層輸入地址,輸出數據地址通過PIRP->UserBuffer來存放。使用這種方式時候,驅動可以直接對用戶層地址進行讀寫,所以一定要注意對用戶區提供的地址進行檢查(小心藍屏),看看參數是否合法。使用ProbeForRead和ProbeForWrite函數來進行地址校驗。
還有一篇博文寫的特別好,圖文並茂,給出鏈接:
https://www.cnblogs.com/lsh123/p/7354573.html
“世人見我恆殊調,聞餘大言皆冷笑。”–李白
參考書籍:
《Windows內核安全與驅動開發》