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

文件、註冊表、線程

4.1 文件操作

4.1.1 使用 OBJECT_ATTRIBUTES

​ 一般打開文件應該傳入這個文件的路徑。但是內核並不直接接受一個字符串,所以使用者必須首先填寫一個 OBJECT_ATTRIBUTES 結構。文檔中並沒有公開,但是該結構總是被 InitializeObjectAttributes 初始化。

VOID InitializeObjectAttributes(
	OUT POBJECT_ATTRIBUTES InitializedAttributes,
	IN PUNICODE_STRING ObjectName,
	IN	HANDLE RootDirectory,
	IN PSECURITY_DESCRIPTOR SecurityDescriptor);

其中 InitializedAttributes 是要初始化的 OBJECT_ATTRIBUTES 結構的指針。objectName 是對象名字字符串,也就是文件名。

​ 在 Windows 內核中,無論是打開文件、著惡策表鍵還是打卡設備,都會先調用InitializedAttributes 初始化一個 OBJECT_ATTRIBUTES

Attributes 只需填寫 OBJ_CASE_INSENSITIVE OBJ_KERNEL_HANDLE 即可。OBJ_CASE_INSENSITIVE 意味着字符串不區分大小寫。OBJ_KERNEL_HANDLE 表明打開的是一個內核句柄。內核舉報比應用層句柄使用更方便,可以不受線程和進程的限制,在任何線程中都可以讀寫。也不需要顧及當前進程是否有權限訪問該文件的問題。如果不使用內核句柄,則有時不得不填寫後面的 SecurityDescriptor 參數。

RootDirectory 用於相對開的情況,目前省略。傳入NULL即可。

SecurityDescriptor 用於設置安全描述符,設置內核句柄時不需要填寫。

4.1.2 打開和關閉文件

​ 下面的函數用於打開一個文件。

NTSTATUS ZwCreateFile(
	OUT PHANDLE FileHandle,
	IN ACCESS_MASK DesireAccess,
	IN POBJECT_ATTRIBUTES Object_Attribute,
	OUT PIO_STATUS_BLOCK IoStatusBlock,
	In PLARGE_INTEGER AllocationSize OPTIONAL,
	IN ULONG FileAttributes,
	IN ULONG ShareAccess,
	IN ULONG CreateDisposition,
	IN ULONG CreateOptions,
	IN PVOID EaBuffer OPTIONAL,
	IN ULONG EaLength);

​ 該函數參數異常複雜,一一解釋。

FileHandle: 一個句柄的指針。如果函數調用成功,那麼打開的文件句柄就保存在這個地址裏。

DesireAccess :申請的權限。 有兩個宏分別組合了常用的讀權限和寫權限,分別爲 GENERIC_READGENERIC_WRITE。還有一個宏代表全部權限,是 GENERIC_ALL。如果想同步打開文件,要加上 SYNCHRONIZE。不同的權限描述符之間可以用 | (位或) 來組合使用。

Object_Attribute : 對象描述。爲 OBJCET_ATTRIBUTES 的結構地址。裏面包含了要打開的文件名稱,在上一節有介紹。

IoStatusBlock : 該結構在內核開發中經常使用,它往往用於表示一個操作的結果。該結構如下:

typedef struce _IO_STATUS_BLOCK{
    union{
        NTSTATUS Status;
        PVOID Pointer;
    };
    ULONG_PTR Information;
}IO_STATUS_BLOCK,*PIO_STATUS_BLOCK;

​ 在實際的編程中很少會用到 Pointer,一般返回結果在 Status 中,成功則爲 STATUS_SUCCESS ; 否則就返回一個錯誤碼,進一步的信息保存在 Information 中。

AllocationSize : 這是一個指向64位整數的指針。該數定義文件初始分配的大小。該參數僅關係到創建或者重寫文件操作,可以忽略。如果忽略它那麼文件長度從0開始,並隨着寫入而增長。

FileAttributes : 這個參數控制新建立的文件屬性,一般的設置爲 0 或者 FILE_ATTRIBUTE_NORMAL 即可。

ShareAccess : 本代碼打開這個文件時,允許別的代碼同時打開這個文件的所有的權限,所以稱爲共享訪問。一共有三種共享訪問的標誌可以設置: FILE_SHARE_READFILE_SHARE_WRITEFILE_SHARE_DELETE 。這三種標誌可以用 | 來組合使用。

CreateDisposition : 這個參數說明了本次打開文件的意圖。可能的選擇如下(不能互相組合):

  • FILE_CREATE : 新建文件。如果文件已經存在,則請求失敗。
  • FILE_OPEN: 打開文件。如果文件不存在,則請求失敗。
  • FILE_OPEN_IF: 如果文件存在則打開,否則重新創建一個文件。
  • FILE_OVERWRITE: 如果文件存在則打開並覆蓋,否則重新創建一個。
  • FILE_SUPERSEDE: 如果打開的文件已經存在,生成一個新的文件其他它。如果不存在,則簡單的重新創建文件。

CreateOptions : 作者經常使用 FILE_NON_DIRECTORY_FILE|FILE_SYNCHRONOUS_IO_NONALERT。 此時文件被同步的打開。同步打開文件的意義在於,以後每次操作文件時,比如寫入文件 調用 ZwWriteFile ,在該函數返回時,文件寫操作已經完成了,而不會有返回 STATUS_PENDING (未決) 的情況。在非同步文件的情況下,返回未決是常見的。

​ 如果希望每次讀寫文件都是直接往磁盤上操作。此時 CreateOption 中應該帶標誌 FILE_NO_INTERMEDIATE_BUFFERING 。但是要注意,對磁盤的操作必須保證文件每次讀寫都必須以磁盤扇區大小對齊。(常見的是512字節),否則會返回錯誤。

直接給出該函數調用的示例:

//要返回的文件句柄
HADNLE file_handle = NULL;
//返回值
NTSTATUS status;
//首先初始化含有文件路徑的 OBJECT_ATTRIBUTES
OBJECT_ATTRIBUTES object_attributes;
UNICODE_STRING ufile_name = RTL_CONSTANT_STRING(L"\\??\/C:\\a.dat");
InitializeObjectAttributes(
	&object_attributes,
	&ufile_name,
	OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
	NULL,
	NULL);
//以 FILE_OPEN_IF 方式打開
status = ZwCreateFile(
	&file_handle,
	GENERIC_READ|GENERIC_WRITE,
	&object_attributes,
	&io_status,//這裏原文裏並沒有創建這個參數,應該要創建吧。
	NULL,
	FILE_ATTRIBUTE_NORMAL,
	FILE_SHARE_READ,
	FILE_OPEN_IF,
	FILE_NON_DIRECTORY |
    FILE_RANDOM_ACCESS |
    FILE_SYNCHRONOUS_IO_NONALERT,
    NULL,
    0);

​ 注意路徑的寫法,並不像是應用層一樣直接寫 “C:\a.dat”,而是寫成 “\??\\C:\\a.dat”。這是因爲 ZwCreateFile 使用的是對象路徑,而 “C:” 是一個符號鏈接對象,符號鏈接對象一般都在 “\??\\” 下。

​ 關閉文件則比較簡單,直接使用文件句柄和 ZwClose 函數。

ZwClose(file_handle);

4.1.3 文件讀/寫操作

​ 打開文件後,最重要的操作是對文件的讀寫。首先介紹文件的讀。

NTSTATUS ZwReadFile(
	IN HANDLE FileHandle,
	IN HANDLE Event OPTIONAL,
	IN PIO_APC_ROUTINE ApcRoutine OPTIONAL,
	IN PVOID ApcContext OPTIONAL,
	OUT PIO_STATUS_BLOCK IoStasusBlock,
	OUT PVOID Buffer,
	IN ULONG Length,
	IN PLARGE_INTEGER ByteOfferset OPTIONAL,
	IN PUNLONG Key OPTIONAL);

FileHandle: 是ZwCreateFile創建成功之後所得到的 FileHandle。如果是內核句柄,文件的讀和寫不需要在同一個進程中。

Event: 一個時間,用於異步完成讀時。使用同步讀時候,可以忽略該參數,填寫NULL。

ApcRoutine: 回調歷程,用於異步讀完成時。使用同步讀時填NULL。

IoStatusBlock: 返回結果狀態。同 ZwCreateFile 中的同名參數。

Buffer: 緩衝區,讀取到的內容保存的地方。

Length: 描述緩衝區的長度。也是試圖讀取文件的長度。

ByteOffset: 要讀取的文件的偏移量,也是要讀取的內容在文件中的位置。一般的不要設置爲NULL。文件句柄不一定支持直接讀取當前偏移量。

Key: 讀取文件時使用的一種附加信息。一般不使用,設爲NULL。

​ 返回值: 成功時候返回值爲 STATUS_SUCCESS

ZwWriteFile 的參數與 ZwReadFile 的參數完全相同。接下來,實現自己編寫一個拷貝文件函數。因爲內核裏並沒有直接提供這個函數。

NTSTATUS MyCopyFile(
	PUNICODE_STRING target_path,
	PUNICODE_STRING source_path)
{
    //源文件和目標文件的句柄
    HANDLE target = NULL, source = NULL;
    //用來拷貝的緩衝區
    PVOID buffer = NULL;
    LARGE_INTEGER offset = {0};
    IO_STATUS_BLOCK io_status = {0};
    
    do{
        //打開源文件和目標文件的句柄
        OBJECT_ATTRIBUTES source_object_attributes,target_object_attributes;
		InitializeObjectAttributes(
            &source_object_attributes,
            &source_path,
            OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
            NULL,
            NULL);
        InitializeObjectAttributes(
            &target_object_attributes,
            &target_path,
            OBJ_CASE_INSENSITIVE|OBJ_KERNEL_HANDLE,
            NULL,
            NULL);
        //獲取源文件句柄
        status = ZwCreateFile(
            &source,
            GENERIC_READ|GENERIC_WRITE,
            &source_object_attributes,
            &io_status,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN_IF,
            FILE_NON_DIRECTORY |
            FILE_RANDOM_ACCESS |
            FILE_SYNCHRONOUS_IO_NONALERT,
            NULL,
            0);
            
    	//獲取目標文件句柄
    	status = ZwCreateFile(
            &target,
            GENERIC_READ|GENERIC_WRITE,
            &target_object_attributes,
            &io_status,
            NULL,
            FILE_ATTRIBUTE_NORMAL,
            FILE_SHARE_READ,
            FILE_OPEN_IF,
            FILE_NON_DIRECTORY |
            FILE_RANDOM_ACCESS |
            FILE_SYNCHRONOUS_IO_NONALERT,
            NULL,
            0);
            
		//爲 buffer 分配4KB內存
    	PCHAR buffer = {0};
        buffer = (PCHAR)ExAllocatePoolWithTag(NonPagePool,1024*4,MEM_TAG)
            if(buffer == NULL){
                //錯誤處理
            }
        //用一個循環來讀取文件。每次從源文件中讀取4kb內容,往後往目標文件中寫入4KB內容,直到拷貝結束。
            while(1){
                length = 4*1024;
                //讀取源文件,注意status
                status = ZwReadFile(
                	source,NULL,NULL,NULL,
                    &my_io_status,buffer,length,
                    &offset,NULL);
                if(!NT_SUCCESS(status))
                {
                    //如果狀態爲 STATUS_END_OF_FILE,則說名拷貝已經結束了
                    if(status == STATUS_END_OF_FILE)
                        status = STATUS_SUCCESS;
                    break;
                }
                //獲取實際讀取到的長度
                legnth = IoStatus.Information;
                
                //現在讀取到了內容。讀出的長度爲length,那麼寫入的長度也應該是length。
                status = ZwWriteFile(
                	target,NULL,NULL,NULL,
                	&my_io_status,
                	buffer,length,&offset,
                	NULL);
                if(!NT_SUCCESS(status))
                    break;
                //offset移動,然後繼續讀,直到讀到源文件的末尾
                offset.QuadPart += length;
            }while(0);
    }
	//在退出之前,釋放資源,關閉所有的句柄
    if(target != NULL)
        ZwClose(target);
    if(source != NULL)
        ZwClose(source);
    if(buffer != NULL)
        ExFreePoo(buffer);
    return STATUS_SUCCESS;
}

4.2 註冊表操作

4.2.1 註冊表鍵的打開

​ 子鍵一般用一個路徑來表示。與應用程序編程最大的一點不同是這個路徑的寫法不一樣。一般應用程序編程中需要提供一個根子鍵的句柄,而驅動編程中則全部用路徑表示。

應用編程中對應的子鍵 驅動編程中的路徑寫法
HKEY_LOCAL_MACHINE \Registry\Machine
HKEY_USERS \Registry\User
HKEY_CLASSES_ROOT 無對應的路徑
HKEY_CURRENT_USER 沒有簡單的對應路徑,但是可以求得

​ 應用程序和驅動程序很大的一個不同在於應用程序總是由某個“當前用戶”啓動,所以可以直接的讀取當前用戶的 HKEY_CLASSES_ROOTHKEY_CURRENT_USER 。但是內核裏沒有這種透明切換用戶的功能。

​ 打開註冊表鍵使用函數 ZwOpenKey ,新建或者打開則使用 ZwCreateKey。在驅動編程中,一般使用前者。

NTSTATUS ZwOpenKey(
	OUT PHANDLE KeyHandle,
	IN ACCESS_MASK DesireAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes
);

​ 該函數與 ZwCreateFile 類似,它並不會直接接受一個字符串來表示一個子鍵,而是要求一個 OBJECT_ATTRIBUTES 的指針。這裏與前面文件操作時如何初始化 OBJECT_ATTRIBUTES 相同。

DesireAccess 參數,可以使用 KEY_READ 來作爲通用的讀權限組合。對應的還有 KEY_WRITE。如果獲取全部權限,應該用 KEY_ALL_ACCESS。這些都是組合的宏。

​ 下面一個例子讀取保存在註冊表中的 Windwos 系統目錄。Windows 目錄的位置被稱爲 SystemRoot ,這個值保存在註冊表路徑 HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion 中。

HANDLE my_key = NULL;
NTSTATUS status;
//定義要獲取的路徑
UNICODE_STRING my_key_path = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
OBJECT_ATTRIBUTE my_obj_attr = {0};

//初始化OBJECT_ATTRIBUTES
InitializeObjectAttributes(
	&my_obj_attr,
	&my_key_path,
	OBJ_CASE_INSENSITIVE,
	NULL,
	NULL
);
//打開Key
status = ZwOpenKey(&my_key,KEY_READ,&my_obj_attr);
if(!NT_SUCCESS(status))
{
    //失敗處理
    ...
}

以上打開即可得到對應註冊表的句柄。接下來的步驟就是讀取該子鍵的 SystemRoot 值。

4.2.2 註冊表鍵的讀

​ 一般使用 ZwQueryValueKey 來讀取註冊表中鍵的值。但是需要注意的是,註冊表中鍵的值可能有多種數據類型,而且長度也沒有定數。ZwQueryValueKey 函數的原型如下:

NTSTATUS ZwQueryValueKey(
    IN HANDLE KeyHandle, //之前打開的一個註冊表鍵的句柄
    IN PUNICODE_STRING ValueName, //要讀取的值的名字
    IN KEY_VALUE_INFORMATION_CLASS KeyValueInformationClass,
    OUT PVOID KeyValueInformation,
    IN ULONG Length,//用戶傳入的需要的空間長度
    OUT PULONG ResultLength //返回實際需要的長度
);

KeyValueInformationClass: 所需查詢的信息類型。有如下三種可能的信息類型。

  • KeyValueBasicInformation: 獲得基礎信息,包含值名和類型。
  • KeyValueFullInformation: 獲得完整信息,包含值名、類型和值的數據。
  • KeyValuePartialInformation: 獲取局部信息,包含類型和值數據。

其中獲取局部信息最常用。因爲名字是已知的,獲取基礎信息沒有必要。而獲取完整信息浪費內存。

KeyValueInformationClass: 當該參數被設置爲 KeyValuePartialInformation 時,HKEY_VALUE_PARTIAL_INFORMATION 結構將被返回到這個指針所指的內存中。給出該結構的原型:

typedef struct _KEY_VALUE_PARTIAL_INFORMATION{
    ULONG TitleIndex;		//忽略該成員
    ULONG Type;				//數據類型
    ULONG DataLength;		//數據長度
    UCHAR Data[1];			//可變長度的數據
}KEY_VALUE_PARTIAL_INFORMATION,*PKEY_VALUE_PARTIAL_INFORMATION;

​ 返回值: 如果實際需要的長度比 Length 要大,那麼返回 STATUS_BUFFER_OVERFLOW 或者 STATUS_BUFFER_TOO_SMALL。如果成功讀取出了全部數據,那麼返回 STATUS_SUCCESS 。其他的情況則返回一個錯誤碼。

​ 接下來補全上一節中的操作代碼。

HANDLE my_key = NULL;
NTSTATUS status;
//定義要獲取的路徑
UNICODE_STRING my_key_path = RTL_CONSTANT_STRING(L"\\Registry\\Machine\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion");
OBJECT_ATTRIBUTE my_obj_attr = {0};

//初始化OBJECT_ATTRIBUTES
InitializeObjectAttributes(
	&my_obj_attr,
	&my_key_path,
	OBJ_CASE_INSENSITIVE,
	NULL,
	NULL
);
//打開Key
status = ZwOpenKey(&my_key,KEY_READ,&my_obj_attr);
if(!NT_SUCCESS(status))
{
    //失敗處理
    ...
}
//要讀取的值的名字
UNICODE_STRING my_key_name = RTL_CONSTANT_STRING(L"SystemRoot");
//用來試探大小的 key_infor
KEY_VALUE_PARTIAL_INFORMATION key_infor;
//最後實際用到的 key_infor,內存分配在堆中
KEY_VALUE_PARTIAL_INFORMATION ac_key_infor;
ULONG ac_length;
//讀取值
status = ZwQueryValueKey(
	my_key,
    &my_key_name,
    KeyValuePartialInformation,
    &key_infor,
    sizeof(KEY_VALUE_PARTIAL_INFORMATION),
    &ac_length
);
if(!NT_SUCCESS(status) && status!= STATUS_BUFFER_OVERWRITE && STATUS_BUFFER_TOO_SMALL){
    //錯誤處理
    ...
}
//如果沒有失敗,那麼分配足夠的空間再次讀取。
ac_key_infor = (PKEY_VALUE_PARTIAL_INFORMATION)ExAllocatePoolWithTag(NonpagedPool,ac_length,MEM_TAG);
if(ac_key_infor == NULL){
    status = STATUS_INSUFFICIENT_RESOURCES;
    //...錯誤處理
}
status = ZwQueryValue(
	my_key,
    &my_key_name,
    KeyValuePartialInformation,
    ac_key_infor,
    ac_length,
    &aclength
);
//到此爲止,如果返回值爲 STATUS_SUCCESS 則表明成功的讀取了鍵值。存儲與ac_key_infor->data 中。

4.2.3 註冊表鍵的寫

​ 註冊表鍵的寫比讀要簡單,相比之下不需要去嘗試數據的大小。直接將數據寫入即可。使用函數 ZwSetValueKey:

NTSTATUS ZwSetValueKey(
	IN HANDLE KeyHandle,
	IN PUNICODE_STRING ValueName,
	IN ULONG TitleIndex OPTIONAL,
	IN ULONG Type,
	IN PVOID Data,
	IN ULONG DataSize
);

​ 其中的 TitleIndex 參數始終填入0。Data 是要寫入的數據的開始地址 Datasize是要寫入的數據大小。因爲 Data 是一個空指針,因此 Data 可以指向任意數據。即不用管 Type是什麼,都可以在 Data 中填寫相應的數據寫入。

​ 在寫入鍵值時,如果該Value已經存在,那麼會覆蓋寫,否則會重新創建一個新的Value。下面代碼寫入一個名爲 “TEST”、值爲 “My Test Value” 的字符串值。假設 my_key 是一個已經打開的子健的句柄。

UNICODE_STRING name = RTL_CONSTANT_STRING(L"Test");
PWCHAR value = {L"My Test Value"};
//在寫入數據時候數據長度加一是爲了把最後一個空結束的字符串寫入。
status = ZwSetValueKey(
	my_key,
	&name,
	0,
	REG_SZ,
	value,
	(wcslen(value)+1)*sizeof(WCHAR)
);
if(!NT_SUCCESS(status))
{
	//錯誤處理    
}

4.3 時間與定時器

4.3.1 獲取當前“滴答”數

​ 在 Win32 程序中有一個函數 GetTickCount,用來返回系統自啓動之後經歷的毫秒數。在驅動開發中有一個對應的函數 KeyQueryTickCount ,不同的是,該函數得到的是一個“滴答數”。不同的硬件環境下滴答對應的時間不同。因此,想要獲取毫秒數需要結合另一個函數的使用。給出例子

VOID KeQueryTickCount(
	OUT PLARGE_INTEGER TickCount
);
ULONG KeQueryTimeIncrement();
void MyGetTickCount(PULONG msec)
{
    LARGE_INTEGER tick_count;
    ULONG myinc = KeQueryTimeIncrement();
    KeQueryTickCount(&tick_count);
    tick_count.QuadPart *= myinc;
    tick_count.QuadPart /= 1000;
    *msec = tick_count.LowPart;
}

4.3.2 獲取當前系統時間

​ 使用 KeQuerySystemTime 得到當前格林威治時間。之後使用 ExSystemTimeToLocalTime 轉換成當地時間。這兩個函數的原型如下:

VOID KeQuerySystemTime(
	OUT PLARGE_INTEGER CurrentTime
);
VOID ExSystemTimeToLocalTime(
	IN PLARGE_INTEGER SystemTime,
	OUT PLARGE_INTEGER LocalTime
)

​ 這兩個函數使用的時間都是長長整數型數據結構,必須同ing過函數 RtlTimeToTimeFields 轉換爲 TIME_FIELDS 。這個函數原型如下:

VOID RtlTimeToTimeFields(
	IN PLARGE_INTEGER Time,
	IN PTIME_FIELDS TimeFields
);

下面舉例應用:

PWCHAR MyCurTimeStr()
{
    LARGE_INTEGER snow,now;
    TIME_FIELDS now_fields;
    static WCHAR time_str[32] = {0};
    //獲得標準起來
    KeQuerySystemTime(&snow);
    //轉化爲當地時間
    ExSystemTimeToLocalTime(&snow,&now);
    //轉化爲人們可以理解的時間要素
    RtlTimeToTimeFields(&now,&now_fields);
    //打印到字符串中
    RtelStringCchPrintfw(
    	time_str,
        32,
        L"%4d-%2d-%2d %2d-%2d-%2d",
        now_fields.Year,now_fields.Month,
        now_fields.Dat,now_fields.Hour,
        now_fields.Minute,now_fields.Second,
    )
    return time_str;
}

兩個注意點:

  • RtelStringCchPrintfwRtelStringCbhPrintfw 兩個函數的區別是第二個參數的不同和。一個以字符爲單位,一個以字節爲單位。
  • time_str 是靜態變量,於是這個函數就不具備多線程安全性。需要使用鎖。

4.3.3 使用定時器

​ 在驅動開發中可以通過一些不同的替代方法來實現 SetTimer 。比較經典的對應是 KeSetTimer,這個函數的原型如下:

BOOLEAN KeSetTimer(
	IN PKTIMER Timer,			//定時器
	IN LARGE_INTEGER DueTime,	//延後執行的時間
	IN PKDPC Dpc OPTIONAL 		//要執行的回調函數結構
);

​ 其中的定時器 Timer 和要執行的回調函數結構 Dpc 都必須先初始化。 Timer 的初始化比較簡單。

KTIMER my_timer;
KeInitializeTimer(&my_timer);

Dpc 的初始化比較麻煩,這是因爲需要提供一個回調函數。初始化 Dpc 的函數原型如下:

VOID KeInitializeDpc(
	IN PRKDPC Dpc,
    IN PKDEFERRED_ROUTINE DeferredRoutine,
    IN PVOID DeferredContext
);

PKDEFERRED_ROUTINE 這個函數指針類型所對應的函數類型爲:

VOID CustomDpc(
	IN struct _KDPC *Dpc,
	IN PVOID DeferredContext,
	IN PVOID SystemArgument1,
	IN PVOID SystemArgument2
)

​ 我們只需要關心的是 DeferredContext 。這個參數是 KeInitializeDpc 調用時傳入的參數,用來提供給 CustomDpc 被調用時,讓用戶傳入一些參數。

​ 後面的 SystemArgument1 SystemArgument2不需要管。 Dpc 是回調這個函數的 KDPC 結構。

​ 注意這是一個“延時執行”的過程,而不是一個定時執行的過程。因此如果想要反覆執行,就必須在每次 CustomDpc 函數被調用時,再次調用 KeSetTimer 來保證下次還可以執行。

CustomDpc 運行在 APC 中斷級別,因此並不是所有的事都可以做。下面給出定時器實現的示例代碼:

//內部時鐘結構
typedef struct MY_TIMER_
{
    KDPC dpc;
    KTIMER timer;
    PKDEFERRED_ROUTINE func;
    PVOID private_context;
}MY_TIMER,*PMY_TIMER;
//初始化這個結構
void MyTimerInit(PMY_TIMER my_timer,PKDEFERRED_ROUTINE func)
{
	//這裏將回調函數的上下文參數設置爲my_timer
    KeInitializeDpc(&my_timer->dpc,MyOntimer,my_timer);
    my_timer->func = MyOntimer;
    KeInitializeTimer(&timer-timer);
}

//讓這個結構中的回調函數在n秒之後開始運行
BOOLEAN MyTimerSet(PMY_TIMER my_timer,ULONG msec,PVOID context)
{
    //due 的單位是100ns 即0.0001毫秒,爲負表示它是相對於當前時間的一段時間間隔。
    LARGE_INTEGER due;
    //將ns轉化爲毫秒
    due.QuadPart = -10000*msec;
    //用戶私有上下文
    my_timer->private_context = context;
    return KeSetTimer(&my_timer->timer,due,&my_timer->dpc);
}

//停止執行
VOID MyTimerDestroy(PMY_TIMER my_timer)
{
    KeCancelTimer(&mytimer-> timer);
}
//要執行的定時函數
VOID MyOntimer(
	IN struct _KDPC *Dpc,
    IN PVOID DeferredContext,
    IN PVOID SystemArgument1,
    IN PVOID systemArgument2
)
{
    //這裏傳入的上下文是my_timer結構。與前面呼應
    PMY_TIMER my_timer = (PMY_TIMER)DeferredContext;
    //獲得用戶上下文
    PVOID my_context= my_timer->priviate_context;
    
    //do someting...
	...
	//再次調用執行該定時器。實現隔一秒實現執行一次的功能
	MyTimerSet(my_timer,1000,my_context);
}

這裏書上關於Dpc函數講的不那麼仔細,查了相關資料。詳細說明

4.4 線程與事件

4.4.1 使用系統線程

​ 在驅動中生成的線程一般爲系統線程。系統線程所在的進程名爲“System”,用到的內核API函數原型如下:

NTSTATUS PsCreateSystemThread(
	OUT PHANDLE ThreadHandle,
	IN ULONG DesireAccess,
	IN POBJECT_ATTRIBUTES ObjectAttributes OPTIONAL,
	IN HANDLE ProcessHandle OPTIONAL,
	OUT PCLIENT_ID ClientId OPTIONAL,
	IN PKSTART_ROUTINE StartRoutine,
	IN PVOID StartContext);

​ 這個函數的參數也很多。其中 ThreadHandle 用來返回句柄,放入一個句柄指針即可。 DesireAccess 總是填寫0; 接下來的三個參數都填 NULL。最後兩個參數分別爲線程啓動的函數和傳入該函數的參數。

​ 啓動函數的原型也十分簡單,只有一個參數

VOID CustomThreadProc(IN PVOID context)

線程的結束應該由縣城自己調用 PsTerminateSystemThread 來完成,得到的句柄必須要用 ZwClose 關閉。但是關閉句柄並不結束線程。下面舉例用一個線程打印一個字符串。

VOID MyThreadProc(IN PVOID context)
{
    PUNICODE_STRING str = (PUNICODE_STRING)context;
    //打印字符串
    KdPrint(("PrintInMyThread: %wZ\r\n",str));
    //結束自己
    PsTerminateSystemThread(STATUS_SUCCESS);
}

VOID MyFunction(){
   UNICODE_STRING str = RTL_CONSTANT_STRING(L"HELLO WORLD!");
   HANDLE thread = NULL;
   NTSTATUS status;
   status = PsCreateSystemThread(&thread,0,NULL,NULL,NULL,MyThreadProc,(PVOID)&str);
   if(!NT_SUCCESS(status))
   {
   		//錯誤處理
   }
   ZwClose(thread);
}

​ 這裏的一個錯誤是 str 是局部變量,一旦 MyFunction執行完了就會被銷燬,那時候線程裏的打印就會出錯。所以傳遞給線程的參數必須是全局變量或者是分配在堆裏的空間。或者是 MyFunction 中等待線程結束。

4.4.2 在線程中睡眠

​ 有許多程序需要長期連續的執行,但是又不期望它太佔據CPU使用率,所以在中間加入睡眠。在驅動中使用的內核睡眠函數原型如下:

NTSTATUS KeDelayExecutionThread(
	IN KPROCESSOR_MODE WaitMode,//總是填寫KernelMode
	IN BOOLEAN Alertable,//總是填FALSE
	IN PLARGE_INTEGER Interval//需要睡眠多久
);

給出使用例子:

#define DELAY_ONE_MICROSECOND (-10)
#define DELAY_ONE_MILLISECOND (DELAY_ONE_MICROSECOND*1000) //毫秒
VOID MySleep(LONG msec)
{
    LARGE_INTEGER my_interval;
    my_interval.QuadPart = DELAY_ONE_MILLISECOND;
    my_interval.QuadPart *= msec;
    KeDelayExecutionThread(KernelMode,0,&my_interval);
}

​ 在線程中睡眠的使用可以當作定時器。只要創建一個線程,並在for 循環中使用睡眠,就可以起到定時器的作用。並且線程執行中的中斷是 Passive 級別,睡眠之後依然是這個級別,可以做的事比之前提到的定時器更多。

4.4.3 使用同步事件

​ 內核中的是一個數據結構,這個結構的指針可以當作一個參數傳入一個等待函數中。如果這個事件不被“設置”,那麼這個等待函數不會返回,線程就會被阻塞;直到該時間被“設置”,那麼等待結束,就可以繼續下去了。

​ 該技術常常用於多個線程之間的同步。如果一個線程需要等待另一個線程完成某事後才能做某事,則可以使用事件等待,另一個線程完成後設置事件即可。

​ 這個數據結構是 KEVENT ,我們沒有必要了解其內部結構,這個結構總是用 KeInitlizeEvent 初始化。這個函數的原型如下:

VOID KeInitializeEvent(
	IN PRKEVENT Event,
	IN EVENT_TYPE Type,
	IN BOOLEAN State
);

​ 第一個參數是要初始化的事件; 第二個參數是事件類型;第三個參數是初始化狀態,一般設置爲 FALSE ,也就是未設置狀態,這樣等待者需要等待設置之後才能通過。

​ 設置事件使用函數 KeSetEvent。這個函數的原型如下:

LONG KeSetEvent(
	IN PRKEVENT Event,
	IN KPRIORITY Increment,
	IN BOOLEAN Wait
);

Event 是要設置的事件; Increment 用於提升優先權, 目前設置爲 0 即可; Wait 表示是否後面馬上緊接一個 KeWaitSingleObject 來等待這個事件,一般設置爲 TRUE

​ 示例代碼:

//定義一個事件
KEVENT event;
//將事件初始化
KeInitializeEvent(&event,SynchronizationEvent,TRUE);
...
//事件初始化之後就可以使用了。在一個函數中可以等待某個事件。如果這個事件沒有被設置,那麼就會阻塞在這裏繼續等待。
KeWaitForSingleObject(&event,Evecutive,KernelMode,0,0);
...
//這裏是另一個地方,設置了這個事件。只要一設置,前面等待的地方就會繼續執行。
KeSetEvent(&enent,0,TRUE);

KeInitializeEvent 中的參數 SynchronizationEvent 導致這個事件成爲 “自動重設” 事件。一個事件如果被設置,那麼當前所有的等待事件的地方都會通過。如果想要繼續使用這個事件,則必須重設這個事件。如果不想自動重設,則使用參數 NotificationEvent。手動重設使用函數 KeResetEvent

​ 如果這個事件初始化時是 SynchronizationEvent ,那麼只有一個線程的 KeWaitForSingleObject可以通過,通過之後被自動重設,其他的線程就只能繼續等待了。這可以起到同步作用,所以叫同步事件。

明日計劃

繼續學習驅動編程。

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