《Windows內核安全與驅動編程》-第二章學習

數據類型

基本數據類型

  • unsigned long 重新定義爲 UlONG
  • unsigned char 重新定義爲 UCHAR
  • unsigned int 重新定義爲 UINT
  • void 重新定義爲 VOID
  • unsigned int * 重新定義爲 PUINT
  • unsigned long * 重新定義爲 PULONG
  • unsigned char * 重新定義爲 PUCHAR

返回狀態

絕大部分內核API的返回值是一個返回狀態。返回狀態的類型爲NTSTATUS

NTSTATUS  MyFunction()
{
    NTSTATUS status;
    //打開一個文件
    status = ZwCreateFile(...)'
    if(!NT_SUCCESS(status)){
    	//如果出錯則直接返回錯誤
        return status;
    }
    
}

使用NT_SUCCESS() 判斷返回值是否成功。遇到錯誤時,在WDK的頭文件中找定義尋找答案。

字符串

字符串有特殊的數據結構,該結構定義爲:

typedef struct _UNICODE_STRING{
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING *PUINCODE_STRING;

重要的數據結構

驅動對象

一個驅動對象代表一個驅動程序或一個內核模塊。驅動對象的結構如下:

typedef struct _DRIVER_OBJECT{	
	//結構的類型和大小 CSHORT 爲 short重定義
	CSHORT Type;
	CSHORT Size;
	
	//設備對象,一條設備對象的單鏈表
	PDEVICE_OBJECT DeviceObject;
	
	//這個內核模塊在內核空間中的開始地址和大小
	PVOID DriverStart;
	ULONG DriverSize;
	...
	//驅動的名字
	UNICODE_STRING DriverName;
	...
	//快速IO分發函數
	PFAST_IP_DISPATCH FastIoDispatch;
	...
	///驅動的卸載函數
	PDRIVER_UNLOAD DriverUnload;
	//普通分發函數
	PDRIVER_DISPATCH MajorFunction[IRP_MJ_MAXIMUM_FUNCTION+1];
}DRIVER_OBJECT;

內核模塊不生成一個進程,只填寫相應的回調函數供Windows調用。回調函數包括普通分發函數和快速IO分發函數。

設備對象

在內核世界裏,大部分“消息”都是以請求(IRP)方式傳遞,設備對象(DEVICE_OBJECT)是唯一可以接受請求的實體。

設備對象的數據結構爲:

typedef struct DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT)
_DEVICE_OBJECT{
    //和驅動對象一樣
    CSHORT Type;
    USHORT Size;
    
    //引用計數
    ULONG ReferenceCount;
    
    //這個設備所屬的驅動對象
    struce _DRIVER_OBJECT *DriverObject;
    
    //下一個設備對象。在一個驅動對象中有n個設備,這些設備用這個指針連接
    //作爲一個單項的鏈表
    struct _DEVICE_OBJECT *NextDevice;
    
    //設備類型
    DEVICE_TYPE DeviceType;
    
    //IRP棧大小
    HAR StackSize;
    
    ...
}DEVICE_OBJECT;

驅動對象生成多個設備對象。Windows向設備對象發送請求,這些請求被驅動對象的分發函數所捕獲。當內核向一個設備發送一個請求時,驅動對象的分發函數中的某一個會被調用。分發函數的原型如下:

//一個典型的分發函數,第一個參數是device是請求的目標設備,第二個參數irp 是請求的指針
NTSTATUS MyDispatch(PDEVICE_OBJECT device, PIRP irp);

具體如何處理,由分發函數內容決定。

請求

大部分請求以IRP的形式發送。IRP也是一個內核數據結構。因爲該結構要表示無數種實際請求,所以該結構非常複雜。我們沒有必要去了解所有的細節。只需要有一個初步印象,結構如下:

typedef struce DECLSPEC_ALIGN(MEMORY_ALLOCATION_ALIGNMENT) _IRP
{
	//類型和大小
	CSHORT Type;
	USHORT Size;
	//內存描述符鏈表指針。描述一個緩衝區。可以理解爲一個內核請求一般都需要一個緩衝區。(如讀硬盤需要有讀出緩衝區)
	PMDL MdlAddress;
	...
	//下面這個共用體中也有一個SystemBuffer。這是比MdlAddress稍微簡單的表示緩衝區的一種方式。IPR請求用SystemBuffer還是MdlAddress緩存取決於這次請求的IO方式。
	union {
        struct _IRP *MasterIrp;
        __volatile LONG Irpcount;
        PVOID SystemBuffer;
	}AssociatedIrp;
	//IO狀態。一般請求完成之後的返回情況放在這裏。
	IO_STATUS_BLOCK IoStatus;
	//IRP棧空間大小
	CHAR StackCount;
	//IRP當前棧空間
	CHAR CurrentLocation;
	...
	//用來取消一個未決請求函數
	__volatile PDRIVER_CANCEL CancelRoutine;
	//與SystemBuffer和MdlAddress一樣都可以表示緩衝區,但是緩衝區的特性稍有不同。
	PVOID UserBuffer;
    union{
    	...
    	//發出這個請求的線程
    	PETHREAD Thread;
    	...
        struct{
        	LIST_ENTRY LsitEntry;
            union{
            	//一個IRP棧空間元素
            	struct _IO_STACK_LOCATION *CurrentStackLocation;
            	...
            	};
        	};
        	...
        } Overlay;
        ...
    } Tail;
}IRP,*PIRP;

一個IRP往往要傳遞n個設備才能完成。在傳遞過程中,有可能會有一些“中間變換”,導致請求的參數變化。爲了保存變化的參數,給每次“中轉“都留一個”棧空間“。一個請求並非簡單的一個輸入並等待一個輸出,而是經過許多中轉才得以完成。而且在中轉的每一個步驟,輸入都可以改變,所有可變部分的輸入信息保存在一個棧似的結構中。即IRP棧空間的作用。

函數調用

WDK中出現的特殊代碼

define IN
define OUT

這樣以來,IN和OUT就被定義成了空。無論出現在代碼的任何地方,都不會對代碼產生實質性的影響。在WDK的代碼中,用來作爲函數的說明。IN表示這個參數用於輸入;OUT表示這個參數用來返回結果。如:

NTSATUS ZwQueryInformationFile(
IN HANDLE FileHandle,
OUT PIO_STATUS_BLOCK IoStatusBlock,
OUT PVOID FileInformation,
IN ULONG Length,
IN FILE_INFORMATION_CLASS FileInofrmationClass
);

IN和OUT是比較傳統的參數說明宏。在WDK紅到處可見更復雜的參數說明宏,比如:

__in_bcount(StausBufferSize) IN PVOID StatusBuffer,
...

__in_bount 不但說明參數StatusBuffer是一個輸入參數,而且也說明StatusBuffer作爲一個緩衝區,它的字節長度被另一個參數StausBufferSize所指定。再見到類似的說明宏,按字面意思理解即可。

​ 然後對於函數指定位置的預編譯指令。比如下面的例子:

#pragma alloc_text(INIT,DriverEntry)
#pragma alloc_text(PAGE,NdisProtUnload)
#pragma alloc_text(PAGE,NdisProtOpen)
#pragma alloc_text(PAGE,NdisProtClose)

​ #pragma alloc_text這個宏僅僅用來指定某個函數的可執行代碼在編譯出來後在sys文件中的位置。內核模塊編譯出來之後是一個PE格式的sys文件,這個文件的代碼段(text段)中有不同的節(section),不同的節被加載到內存中之後處理情況不同。我們主要關心三種節,INIT節的特點是在初始化完畢之後就被釋放,不佔據內存;PAGE節的特點是位於可以進行分頁交換的內存空, 這些空間在內存緊缺的時候可以被交換到硬盤上以節省內存。如果沒有用預編譯指令,則代碼默認位於PAGELK節,加載後位於不可分頁交換的內存空間中。

​ 如函數 DriverEntry 顯然只需要在初始化階段執行一次,因此這個函數一般都用#pragma alloc_text(INIT,DriverEntry) 使之位於初始化後立刻釋放的空間內。爲了節約內存,可以把很多函數放在PAGE節中。但是這種函數不可以在Dispatch級調用,因爲這種函數的調用可能誘發缺頁中斷,而缺頁中斷處理不能在Dispatch級完成。爲此,一般用一個宏PAGED_CODE() 進行測試,如果發現當前中斷級爲Dispatch級,則程序直接報異常,好讓程序員及早發現。

示例:

#pragma alloc_text(PAGE,SfAttchToMountedDevice)
...
NTSTATUS
SfAttachToMountedDevice(
	IN PDEVICE_OBJECT DeviceObject,
	IN PDEVICE_OBJECT SFilterDeviceObject
)
{
    ...
    PAGED_CODE();
    ...
}

代碼的中斷級

​ 代碼的中斷級主要有兩種:Passive 和 Dispatch,Dispatch級比Passive級高。在實際編程時,許多具有比較複雜功能的內核API都要求必須在Passive級執行。只有比較簡單的函數能在Dispatch級執行。

​ 在調用任何一個內核API之前,必須查看WDK文檔,瞭解這個內核API的中斷級要求。

​ 如何判斷我們正在編寫的代碼的中斷級。暫時可以簡單的根據蝦米那的規則來處理:

  • 規則一: 如果在調用路徑上沒有特殊的情況,則一個函數執行時的中斷級和它的調用元的中斷級相同。
  • 規則二: 如果在調用路徑上有獲取自旋鎖,則中斷級隨之升高;如果在調用路徑上有釋放自旋鎖,則中斷級隨之下降。

當前代碼的中斷級基本上取決於調用元的中斷級和調用路徑。給出內核代碼主要調用元的運行中斷級。

調用源 一般的運行中斷級
DriverEntry, DrierUnload Passive級
各種分發函數 Passive級
完成函數 Dispatch級
各種NDIS回調函數 Dispatch級
今明日後續計劃:

繼續學習驅動編程。

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