1. 程式人生 > >Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)

Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)

Windows核心編程之核心總結

學習目標

第三章內核對象的概念較為抽象,理解起來著實不易,我不斷上網找資料和看視頻,才基本理解了內核對象的概念和特性,其實整本書給我的感覺就是完整代碼太少了,沒有多少實踐的代碼對內容的實現,而且書本給的源碼例子,有太多我們不知道的知識,並且這些知識對本章主要內容來說是多余的,所以我們理解起來也非常困難。為了更好的學習這章,我補充了一些輔助性內容。這一章的學習目標:
1.Windows會話和安全機制
2.什麽是內核對象?
3.使用計數和安全描述符
4.內核對象句柄表
5.創建內核對象
6.關閉內核對象
7.跨進程邊界共享內核對象-使用對象句柄繼承
8.跨進程邊界共享內核對象-為對象命名
9.防止運行一個應用程序的多個實例

10.終端服務命名空間和專有命名空間
11.結合專有命名空間實現防止運行一個應用程序的多個實例
12.跨進程邊界共享內核對象-復制對象句柄

Windows會話和安全機制

Vista系統開始,Windows就建立了session(會話)的概念。Windows系統啟動後就建立session0(會話0),將公用服務載入session0(會話0)中,例如:通常將一些與硬件緊密相關的模塊(如:中斷處理程序等)、各種常用設備的驅動程序(聲卡驅動、打印機驅動、顯卡驅動)以及運行頻率較高的模塊(如:時鐘管理、進程調度和許多模塊所公用的一些基本操作),這些都放在內存,稱為操作系統內核。系統啟動後,第一個用戶登陸了該系統,就建立起了session1(會話1),那麽當你運行所有的應用程序,魔獸啊,飛車啊,絕地求生啊,這些應用程序都是在session1(會話1)下運行。會話下運行多個程序會涉及多任務,例如:多個進程並發執行,那麽就通過進程管理隔離實現多任務。當有另一個用戶遠程登陸,那麽建立起session2(會話2),session2(會話2)也有獨有的應用程序---進程。如果依次有用戶登錄該系統,那麽就順序依次建立起新的會話,也依次擁有獨有的應用程序---進程。采用這個設計後,系統核心組件就可以更好地與用戶不慎啟動的惡意軟件隔離。不同用戶的進程通過會話進行隔離,這就是多用戶的過程,多用戶是依靠會話進行隔離而實現用戶之間相互獨立,互不影響。這裏引出了會話Session的概念後,就要考慮安全機制問題。假如張三登陸了這個系統,建立起了會話1,然後系統就會給張三一個會話令牌,這個令牌包含了它的用戶信息,還有該用戶訪問的權限、屬於什麽組等信息。在會話1下, 張三運行了一個程序,系統會給這個程序分配一個令牌,這個程序令牌就是繼承於會話建立時獲得的會話令牌,然後這個程序想要打開一個文件(內核對象),這個文件(內核對象)就會有一個安全描述符(SD),系統會根據程序令牌和文件的安全描述符相互匹配,安全描述符含有創建的用戶、哪些用戶或組允許訪問此對象,哪些用戶或組拒絕訪問此對象。匹配之後發現這個用戶屬於安全描述符的拒絕訪問名單內,那麽這個程序就無法打開該文件,否則能夠打開文件。

什麽是內核對象?

在系統和我們寫的應用程序中,內核對象用於管理進程、線程和文件等諸多種類的大量資源。作為Windows開發人員,我們經常都要創建、打開和處理內核對象。當我們在會話中啟動一個應用程序,那麽當應用程序載入內存,就會生成一個進程(這是一個主調進程)。每個進程對應都有一個虛擬地址空間,然後由內存管理程序對虛擬地址空間和物理地址空間的轉換。進程的虛擬內存空間分為內核層和應用層,每個內核對象,其實就是一塊內存塊,這個內存塊位於操作系統的內核地址空間(內核層),而用戶的應用程序運行在應用層,註意:這裏都是說明在虛擬地址空間,然後會映射到真正的物理地址空間中。而內核對象是由操作系統內核分配的,並只能由操作系統內核訪問。因此,應用程序不能直接操作內核對象,需要用Windows系統給定的函數來操作。每一個內核對象都有特定的創建函數和操作函數。所以,當一個主調進程裏調用了創建一個內核對象函數,那麽這個內核對象(內存塊)就會在進程的虛擬內存空間的內核層裏,實際映射到物理內存的操作系統內核區域。內核對象這個內存塊是一個數據結構,其成員維護著與對象相關的信息。

使用計數和安全描述符

我們上節說到,內核對象這個內存塊實際是一個數據結構,內核對象的結構分為兩個部分:公用部分(安全描述符(security descriptor,SD),使用計數)和特有部分。其中特有部分,例如:進程內核對象有一個進程ID、一個基本的優先級和一個退出代碼。使用計數是每一個內核對象都有的一個數據成員,當有一個內核對象被創建時,使用計數被設為1,當另一個進程獲得對現有內核對象的訪問後,使用計數就會遞增,進程終止運行後,操作系統內核將自動遞減此進程仍然打開的所有內核對象的使用計數,如果一旦內核對象的使用計數為0,操作系統內核就會銷毀該內核對象。安全描述符描述了誰擁有內核對象,哪些組和用戶被允許訪問或使用此對象,哪些組和用戶被拒絕訪問或使用此對象。用於創建內核對象的所有函數幾乎都有一個指向SECURITY_ATTRIBUTES結構的指針作為參數。下面給出這個結構的簽名:

        typedef struct _SECURITY_ATTRIBUTES {
            DWORD  nLength;//結構的大小
            LPVOID lpSecurityDescriptor;//安全描述符
             BOOL   bInheritHandle;//表示所創建的內核對象是否可被繼承,一般是具有父子關系的進程才可以繼承
            } SECURITY_ATTRIBUTES;

如果想對我們創建的內核對象加以訪問限制,就必須創建一個安全描述符。在Windows核心編程有這一內核對象的概念,而Windows程序設計又有著GDI對象(例如畫筆,窗口,畫刷,位圖。)的概念,我們能知道的只有內核對象在內核層,而用戶對象(例如:GDI對象)在應用層。那麽我們要怎麽區分一個對象是內核對象還是非內核對象?剛剛我們學習了安全描述符,每一個內核對象的創建函數基本都有一個SECURITY_ATTRIBUTES屬性作為參數,所以很明顯了。我們可以看創建對象的函數,如果創建對象的函數有安全描述符參數,那麽這個函數創建的對象就是內核對象。

內核對象句柄表

一個進程在初始化時,系統為進程分配了一個內核對象句柄表。下圖顯示了一個進程的句柄表。可以看出內核對象句柄表是一個由數據結構組成的數組,每個結構都包含索引、指向一個內核對象內存塊的指針、一個訪問掩碼和一些標誌(例如:是否可被繼承的標誌,在創建內核對象就被指定了)。
技術分享圖片

創建內核對象

一個進程首次初始化的時候,其內核對象句柄表為空。然後,當進程中的線程調用創建內核對象的函數時,比如CreateFileMapping,操作系統內核就為該對象分配一個內存塊,並對它初始化。這時,操作系統內核對進程的句柄表進行掃描,找出一個空項。操作系統內核找到索引1位置上的結構並對它進行初始化。該指針成員將被設置為內核對象的數據結構的內存地址,訪問屏蔽設置為全部訪問權,同時,各個標誌也作了設置。我列舉以下部分創建內核對象的函數簽名:

HANDLE CreateThread(
   PSECURITY_ATTRIBUTES psa,
   size_t dwStackSize,
   LPTHREAD_START_ROUTINE pfnStartAddress,
   PVOID pvParam,
   DWORD dwCreationFlags,
   PDWORD pdwThreadId);

HANDLE CreateFile(
   PCTSTR pszFileName,
   DWORD dwDesiredAccess,
   DWORD dwShareMode,
   PSECURITY_ATTRIBUTES psa,
   DWORD dwCreationDisposition,
   DWORD dwFlagsAndAttributes,
   HANDLE hTemplateFile);

HANDLE CreateFileMapping(
   HANDLE hFile,
   PSECURITY_ATTRIBUTES psa,
   DWORD flProtect,
   DWORD dwMaximumSizeHigh,
   DWORD dwMaximumSizeLow,
   PCTSTR pszName);

HANDLE CreateSemaphore(
   PSECURITY_ATTRIBUTES psa,
   LONG lInitialCount,
   LONG lMaximumCount,
   PCTSTR pszName);

我們可以看到這些創建內核對象的函數簽名,參數都有一個SECURITY_ATTRIBUTES結構參數,然後返回一個內核對象句柄,這個句柄值其實就是作為內核對象句柄表的索引來使用的,所以這些句柄是與當前這個進程相關的,無法供其他進程使用,如果我們真的在其他進程中使用它,那麽實際引用的只是那個進程的句柄表中位於同一個索引的內核對象----只是索引值相同而已。那麽要得到實際的句柄表的索引值,內核對象句柄值應該除以4才得到索引值。

關閉內核對象

無論怎樣創建內核對象,都要向系統指明將通過調用CloseHandle函數來結束對該對象的操作,下面給出該函數的簽名:

HRESULT CloseHandle( 
   HANDLE hHandle  
);

調用這個函數,函數內部會先檢查主調進程的句柄表,看下主調進程對這個內核對象句柄是否有權訪問。如果內核對象句柄是有效的,系統將獲得內核對象的數據結構的地址,並將結構中的使用計數成員遞減。如果使用計數變成0,內核對象將被銷毀,並且清除對應內核對象句柄表中對應的記錄項;如果使用計數遞減後不為0,說明其他進程還在使用該內核對象,那麽只清除對應內核對象句柄表中對應的記錄項,不銷毀內核對象。講了這麽多,有什麽方法可以看見進程有多少個內核對象嗎?當然有,微軟提供了一個小工具:Process Explorer,下面圖顯示了我自己的應用程序,裏面創建了一個名為"ydm"的互斥量內核對象,我將會關閉這個內核對象。下方的mutant類型這一行,就是我在內部創建的互斥量內核對象。
技術分享圖片

跨進程邊界共享內核對象-使用對象句柄繼承

在每個進程中都有一個內核句柄表,這也就說明同一個內核對象其在不同的進程中其內核對象句柄值可能是不一樣的。但是內核對象的作用很大程度上就在於能夠在進程間共同訪問,即跨進程邊界共享內核對象。那我們怎麽實現不同進程間共享同一個內核對象呢?Windows核心編程這本書給我們提供了三個方法實現這個功能。這一小節先講使用對象句柄繼承來實現跨進程邊界共享內核對象。
只有在進程之間有一個父子關系時,才可以使用對象句柄繼承。為了使子進程繼承父進程的內核對象句柄表,必須執行以下幾個步驟:
1.當父進程創建一個內核對象時,父進程必須向系統指出它希望這個對象的句柄是可繼承的。註意,這裏說的繼承是指繼承內核對象句柄,而非內核對象。為了創建一個可以繼承的內核對象句柄,父進程必須分配並初始化一個SECURITY_ATTRIBUTES結構,並將這個結構的地址傳遞給具體的Create*創建內核對象函數。下面舉個鮮明的例子:

SECURITY_ATTRIBUTES sa;//安全屬性結構
sa.nLength=sizeof(sa);//結構大小
sa.lpSecurityDescriptor=NULL;//安全描述符
sa.bInheritHandle=TRUE;//指定內核對象是否可被繼承

HANDLE hMutex=CreateMutex(&sa,FALSE,NULL);//創建一個互斥量內核對象

我們都知道內核對象句柄表的每個記錄項含有索引、指向內核對象內存塊的地址、訪問掩碼和標誌,其中標誌就是指是否可以繼承。如果在創建內核對象的時候將NULL作為PSECURITY_ATTRIBUTES參數傳入,則返回的句柄將是不可繼承的,這個標誌也會被設為0,如果bInheritHandle成員設為TRUE,則這個標誌被設為1.
2.父進程生成子進程,通過在主調進程內調用CreateProcess函數完成。下面給出CreateProcess的函數簽名:

BOOL WINAPI CreateProcess(
  _In_opt_    LPCTSTR               lpApplicationName,
  _Inout_opt_ LPTSTR                lpCommandLine,
  _In_opt_    LPSECURITY_ATTRIBUTES lpProcessAttributes,
  _In_opt_    LPSECURITY_ATTRIBUTES lpThreadAttributes,
  _In_        BOOL                  bInheritHandles,
  _In_        DWORD                 dwCreationFlags,
  _In_opt_    LPVOID                lpEnvironment,
  _In_opt_    LPCTSTR               lpCurrentDirectory,
  _In_        LPSTARTUPINFO         lpStartupInfo,
  _Out_       LPPROCESS_INFORMATION lpProcessInformation
);

註意參數bInheritHandles,如果設為TRUE,子進程就會繼承父進程的“可繼承的內核對象句柄”的值,註意:如果是父進程的“不可繼承的內核對象句柄”,那麽子進程就不會繼承到。我說過每個進程都有一個內核對象句柄表,子進程也不例外。系統在創建子進程後會分配一個新的、空白的內核對象句柄表。總的執行流程如下:系統會先遍歷父進程的內核對象句柄表,對它的每一個記錄項進行檢查,凡是包含一個有效的“可繼承的內核對象句柄”的項,都會被完整地復制到子進程的內核對象句柄表,在子進程的內核對象句柄表中,復制項的位置與它在父進程句柄表中的位置完全一樣,這一特性意味著:在父進程和子進程中,對每一個內核對象進行標識的內核對象句柄值是完全一樣的。除了復制內核句柄表的記錄項,系統還會遞增內核對象的使用計數,因為兩個進程現在都在使用這個內核對象。記住一個要點:內核對象句柄的繼承只會在生成子進程的時候發生,假如父進程後來又創建了新的內核對象,並同樣將它們的句柄設為可繼承的句柄,那麽正在運行的子進程是不會繼承這些新句柄的。前面都是先創建一個父進程的可繼承的內核對象,父進程調用CreateProcess函數創建第一個子進程,然後系統自動將父進程可繼承的內核對象句柄復制到子進程的內核對象句柄表中,然後創建第二個子進程,過程還是依然如此。但是我希望在創建第二個子進程時繼承不到父進程的這一內核對象。簡單來說,就是我們想控制哪些子進程能繼承內核對象句柄,可以調用SetHandleInformation函數來改變已經創建好了的內核對象句柄的繼承標誌。那麽只要在調用CreateProcess函數生成第二個子進程前調用SetHandleInformation函數關閉內核對象的繼承標誌,就可以實現我們目的啦。這個函數簽名如下:

BOOL SetHandleInformation(
   HANDLE hObject,//標識了一個有效的內核對象句柄,為什麽有效?因為還是需要主調進程有訪問權限。
   DWORD dwMask,//告訴函數我們想更改哪個或者哪些標誌
   DWORD dwFlags);//指出把標誌設為什麽

下面給出參數2,dwMask的兩種取值:

HANDLE_FLAG_INHERIT 0x00000001  
If this flag is set, a child process created with the bInheritHandles parameter of CreateProcess set to TRUE will inherit the object handle. 
HANDLE_FLAG_PROTECT_FROM_CLOSE  0x00000002  
If this flag is set, calling the CloseHandle function will not close the object handle. 

1.如果要打開一個內核對象句柄的繼承標誌,可以這樣寫:

SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,HANDLE_FLAG_INHERIT);

2.要關閉這個標誌,可以這樣寫:

SetHandleInformation(hObj,HANDLE_FLAG_INHERIT,0);

3.HANDLE_FLAG_PROTECT_FROM_CLOSE標誌是告訴系統不允許關閉內核對象句柄:

SetHandleInformation(hObj,HANDLE_FLAG_PROTECT_FROM_CLOSE,HANDLE_FLAG_PROTECT_FROM_CLOSE);//如果在這個函數之後調用CloseHandle關閉這個句柄就會報錯

4.如果需要告訴系統允許關閉內核對象句柄,我們可以這樣寫:

SetHandleInformation(hObj,HANDLE_FLAG_PROTECT_FROM_CLOSE,0);//這時候在這個函數調用之後調用CloseHandle函數關閉內核對象句柄不會報錯,成功關閉

5.我們可以通過GetHandleInformation函數獲取指定內核對象句柄的當前標誌。如果要檢查一個內核對象句柄是否可以被繼承,我們可以這樣寫:

DWORD dwFlags;
GetHandleInformation(hObj,&dwFlags);
BOOL fHandleIsInheritable=(0!=(dwFlags & HANDLE_FLAG_INHERIT));

後面內容晚點補充...

Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)