Ring3與Ring0之間的通信方式(Windows內核學習筆記)

我們寫一個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內核安全與驅動開發》

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章