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

學習目標

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

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屬性作爲參數,所以很明顯了。我們可以看創建對象的函數,如果創建對象的函數有安全描述符參數,那麼這個函數創建的對象就是內核對象。

內核對象句柄表

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

創建內核對象

一個進程首次初始化的時候,其內核對象句柄表爲空。然後,當進程中的線程調用創建內核對象的函數時,比如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核心編程之核心總結(第三章 內核對象)(2018.6.2)

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

在每個進程中都有一個內核句柄表,這也就說明同一個內核對象其在不同的進程中其內核對象句柄值可能是不一樣的。但是內核對象的作用很大程度上就在於能夠在進程間共同訪問,即跨進程邊界共享內核對象。那我們怎麼實現不同進程間共享同一個內核對象呢?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提供的創建內核對象的函數中有一個名叫pszName的參數,這個參數就是用來設置所創內核對象的名字,如果向此參數傳入NULL,相當於向系統表明我們要創建一個未命名的內核對象,簡稱爲匿名內核對象。如果創建的是一個匿名內核對象,因爲沒有名字的緣故,所以要實現進程間的內核對象共享,可以使用上一節講的繼承技術或者最後一節講的複製內核對象句柄。不管進程是子進程還是其他進程,如果要根據內核對象名稱來實現進程間內核對象的共享,我們必須爲此內核對象指定一個名稱。那到底怎麼利用內核對象的名稱來實現進程間的內核對象共享呢,這就是我們這一節要講的內容了。如果在創建內核對象函數中指定名稱,則應該給名字參數傳入一個“以0爲終止符的名稱字符串”的地址。需要注意的是,微軟沒有提供一個專門的機制來保證爲內核對象指定的名稱是唯一的,這什麼意思?意思是假如我在會話1中運行兩個來自不同公司的應用程序,那麼在這兩個應用程序中都需要創建一個命名內核對象,如果一不小心剛好內核對象名字一樣呢,那就不是唯一了,懂了吧。,在同一個會話中,即使內核對象類型不同,在這個會話創建的所有內核對象都共享着同一個命名空間(這個命名空間就是本地命名空間-Local,在後面內容會將本地命名空間和全局命名空間的區別和用法,你只要知道本地命名空間的範圍是在會話內,而全局命名空間的範圍是在所有會話之間)。例如以下代碼:

HANDLE hMutex=CreateMutex(NULL,FALSE,TEXT("JeffObj"));//創建一個名稱叫JeffObj的互斥量內核對象
HANDLE hSem=CreateSemaphore(NULL,1,1,TEXT("JeffObj"));//創建一個名稱叫JeffObj的信號量內核對象
DWORD dwErrorCode=GetLastError();//獲取錯誤代碼

執行上述代碼,我們可以調試出dwErrorCode的值爲6(ERROR_INVALID_HANDLE),hMutex變量有值,但hSem變量的值爲NULL,調試結果如下:
Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)
現在我們知道如何命名內核對象了,接着就來看看如何以這種方式共享內核對象,包括父進程和子進程間的共享(這裏不是指繼承技術實現的共享),還有進程與其他無關聯進程間的共享哦。我們進行這樣的假設,先生成一個進程A,而進程B可以是進程A的子進程或者是其他應用程序生成的進程,並且進程A啓動並調用了以下函數:

HANDLE hMutexProcessA=CreateMutex(NULL,FALSE,TEXT("JeffMutex"));

這個函數調用創建了一個新的互斥量內核對象,並將其命名爲"JeffMutex"。這時,進程B開始執行時,它執行了以下代碼:

HANDLE hMutexProcessB=CreateMutex(NULL,FALSE,TEXT("JeffMutex"));

當進程B調用上面的函數時,系統首先會查看本地命名空間是否存在一個名爲“JeffMutex”的內核對象,如果不存在該名稱的內核對象,那麼進程B它會自動創建名爲“JeffMutex”的內核對象。但若確實存在這樣的一個對象(進程A已經先在本地命名空間創建這一名字的內核對象了),所以內核對象接着檢查內核對象的類型,如果內核對象的類型也完全一樣,那麼系統就接着執行一次安全檢查(如果類型不同了,直接返回NULL了,就不必要安全檢查咯),驗證調用者是否擁有對該內核對象的完全訪問權限。如果有這一權限,那麼系統就會在進程B的內核句柄表中查找一個空白記錄項,並將其初始化爲指向現有的內核對象,並且函數返回對應的進程B的內核對象句柄值(極有可能與進程A對應的內核對象句柄值不同),還有會產生一個叫ERROR_ALREADY_EXISTS的錯誤代碼,一般利用這個錯誤代碼來判斷是否有兩個實例在運行。但是如果在檢查內核對象類型時就不匹配了或者調用者被拒絕訪問,CreateMutext函數就會失敗(返回NULL)。進程B調用CreateMutex成功之後,不會實際地創建一個互斥量內核對象,而是引用內核中的一個現有的互斥量內核對象,系統會爲進程B分配一個新的句柄值。由於在進程B的句柄表中,用一個新的紀錄項來引用了這個內核對象,所以這個互斥量內核對象的使用計數會被遞增。還有一點要注意:進程B調用CreateMutex時,它會向函數傳遞安全屬性參數和第二個參數,如果已經存在一個指定名稱的內核對象,這些參數將會被省略,說明進程A的內核對象句柄可繼承特性也會被初始化到進程B的內核對象句柄。好了,講了這麼多,我們已經實現了用名稱來共享內核對象,但我們會發現一個問題就是,調用Create函數創建內核對象,就算內核對象不存在也會自動創建它,有沒有一種函數在內核對象不存在的時候不會自動創建它,而是以調用失敗來告終。Windows提供了另一個函數Open,下面舉幾個Open*函數的簽名:

HANDLE WINAPI OpenMutex(
  _In_ DWORD   dwDesiredAccess,
  _In_ BOOL    bInheritHandle,
  _In_ LPCTSTR lpName
);
HANDLE WINAPI OpenEvent(
  _In_ DWORD   dwDesiredAccess,
  _In_ BOOL    bInheritHandle,
  _In_ LPCTSTR lpName
);
HANDLE WINAPI OpenSemaphore(
  _In_ DWORD   dwDesiredAccess,
  _In_ BOOL    bInheritHandle,
  _In_ LPCTSTR lpName
);

這些函數的最後一個參數pszName指出內核對象的名稱,不能爲這個參數傳入NULL,必須傳入一個以0爲終止符的字符串作爲地址。這些函數將在內核對象命名空間(本地命名空間)搜索,以查找一個匹配的對象,如果沒有找到函數返回NULL,並且產生錯誤代碼2(ERROR_FILE_NOT_FOUND)。如果找到了這樣名稱的內核對象,但類型不同,函數將返回NULL,並且產生錯誤代碼6(ERROR_INVALID_HANDLE)。如果名稱相同,類型也相同,系統會檢查權限(利用dwDesiredAccess參數指定)是否允許。如果允許,則更新主調進程的句柄表,並使該內核對象的使用計數遞增,如果參數2(bInheritHandle)同時指定爲TRUE,那麼返回的句柄就是可繼承的。總結來說,調用Create和Open的區別就是如果內核對象不存在,Create函數會創建它,而Open函數則不會,如果內核對象不存在,它只是默默地調用失敗。
前面我們說過,微軟沒有提供一個專門的機制來保證我們創建的內核對象名是獨一無二的。換句話來說,如果用戶試圖運行來自兩個不同公司的應用程序,而且這兩個程序都試圖創建一個名爲“MyObject”的對象,那麼就會出現問題,因爲假如先運行第一個應用程序(假設爲程序A),先創建了一個名爲“MyObject”的內核對象,那麼當運行第二個應用程序(假設爲程序B),這時程序內調用的Create*函數,就會在本地命名空間搜索名爲“MyObject”的內核對象,肯定找的到,因爲程序A已經創建好了,所以這時程序B就會以爲已經有一個實例了,自然會出問題。爲了保證名稱的唯一性,我的建議是創建一個GUID(Globally Unique Identifier,全局唯一標識符)。

GUID(全局唯一標識符)

全局唯一標識符(GUID,Globally Unique Identifier)是一種由算法生成的二進制長度爲128位的數字標識符。GUID的總數達到32^128個,所以隨機生成兩個相同GUID的可能性極小,但並不爲0。GUID的格式爲:"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",其中每個 x 是 0-9 或 a-f 範圍內的一個4位十六進制數,所以每個x代表4個二進制位,所以4*32=128個二進制位,即GUID的總數達到32^128個。例如:6F9619FF-8B86-D011-B42D-00C04FC964FF 即爲有效的 GUID 值。現在來教教怎麼使用GUID生成器,我使用的VS版本是2013,我們點擊菜單中的工具,然後找到創建GUID,選擇第六個格式,點擊右側新建GUID就可以生成GUID啦,然後上面還有個複製,太人性化了,連複製都準備了,只要粘貼到我們想用的地方就可以了,例如下方圖片那樣:
Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)

防止運行一個應用程序的多個實例

接下來寫的是測試代碼,是實現防止運行一個應用程序的多個實例的例子。

#include<Windows.h>
#include<tchar.h>
#include<iostream>
using namespace std;
int _tmain()
{
    HANDLE hMutex = CreateMutex(NULL, FALSE, TEXT("JeffObj"));//創建一個名稱叫JeffObj的互斥量內核對象
    if (GetLastError() == ERROR_ALREADY_EXISTS)
    {
        CloseHandle(hMutex);
        return 0;
    }
    cout << "這是第一個實例" << endl;
    _gettchar();
    CloseHandle(hMutex);
    return 0;
}

你會發現,運行一次應用程序沒問題,照樣輸出"這是第一個實例"字符串,但是當運行第二次應用程序時,就會一閃而過,這是因爲第二個應用程序它捕捉到了錯誤代碼ERROR_ALREADY_EXISTS,所以直接關閉互斥量內核對象,然後返回0了。運行結果如下圖所示:
Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)

終端服務命名空間

什麼是終端服務?終端服務的工作原理是客戶機和服務器通過TCP/IP協議和標準的局域網構架聯繫。通過客戶端終端,客戶機的鼠標、鍵盤的輸入傳遞到終端服務器上,再把服務器上的顯示傳遞迴客戶端。客戶端不需要具有計算能力,至多隻需提供一定的緩存能力。衆多的客戶端可以同時登錄到服務器上,彷彿同時在服務器上工作一樣,它們之間作爲不同的會話連接是互相獨立的。
在正在運行終端服務的計算機中,有多個用於內核對象的命名空間,其中一個就是全局命名空間,所有會話裏的所有進程都能訪問的內核對象都放在全局命名空間,說明所有會話都將共享全局命名空間裏的內核對象。此外,每個會話都有一個自己的命名空間,這就是本地命名空間。Windows核心編程書中的一句原話是:“對於兩個或多個會話正在運行同一個應用程序的情況,這樣的安排可以避免會話之間彼此干擾----一個會話不會訪問另一個會話的內核對象,即使對象名相同。”,大概意思就是說假如我在會話1的某應用程序創建了一個名爲“A”的內核對象(不寫前綴,默認是在本地命名空間,至於前綴後面會說怎麼創建全局命名空間和本地命名空間),而在會話2的某應用程序也創建了一個名爲“A”的內核對象(默認是在本地命名空間),那麼由於本地命名空間的範圍在會話內,所以會話1和會話2分別創建的名爲“A”的內核對象是沒有關係的,都可以創建成功。後面我會對全局命名空間和本地命名空間(默認命名空間)的用法舉個例子你就懂了!

  1. 要想獲取我們的進程在哪個終端服務會話中運行,必須使用GetCurrentProcessId或者GetCurrentProcess、GetProcessId和ProcessIdToSessionId函數:

    #include<windows.h>
    #include<iostream>
    #include<tchar.h>
    using namespace std;
    int _tmain()
    {
    //DWORD processid = GetCurrentProcessId()
    //GetCurrentProcess()獲取當前進程的句柄,然後GetProcessId函數是返回指定進程的ID,參數是進程的句柄。
    DWORD processid=GetProcessId(GetCurrentProcess());//也可以間接使用兩個函數來實現獲取進程ID
    DWORD sessionid = 0;
    if (ProcessIdToSessionId(processid, &sessionid))
    {
        printf("%s,%d\n", "this is the first instance! and session id is", sessionid);
    }
    system("pause");
    return 0;
    }

    2.如何創建全局命名空間和本地命名空間?
    (1)一般名字的內核對象,默認內核對象放在對應會話的本地命名空間內。不過我們也可以強制把一個命名內核對象放入全局命名空間內,具體做法是在其名稱前加上“Global\”前綴,例如:

    CreateEvent(NULL,FALSE,FALSE,L”Global\\ydm”);

    (2)如果要在本會話下創建一個命名對象,也就是在會話的命名空間中,創建內核對象,那麼必須在內核對象前加上”Local\”前綴!注意這個關鍵字是大小寫敏感的。
    例如:

    CreateMutex(NULL,FALSE,L”Local\\ydm”);

    (3)注意:“Session\”前綴被系統保留,不要使用這個前綴來創建命名內核對象。
    3.對全局命名空間和本地命名空間(默認命名空間)的用法舉例:
    我在Win10環境寫了幾行代碼,做了個簡單的測試。
    首先在administrator帳戶(一個會話)中,我創建了一個控制檯程序,.cpp文件的main函數中有以下代碼:

    #include<windows.h>  
    #include<cstdio>  
    #include<iostream>  
    int main(void)  
    {  
    //在全局命名空間中創建內核對象,用不同帳戶運行該程序,不能創建同名的內核對象。  
    HANDLE hMutex = CreateMutex(NULL,FALSE,TEXT("Global\\Handle")) ;     
    if(NULL == hMutex || GetLastError() == ERROR_ALREADY_EXISTS)     
    {  
        printf("已經存在互斥量對象\n") ;  
        return 0 ;  
    }  
    printf("成功創建互斥量對象\n") ;  
    Sleep(INFINITE) ;  
    return 0 ;  
    }  

    上面代碼是創建一個放在全局命名空間中的互斥量對象,並調用Sleep(INFINITE)掛起程序,讓個內核對象一直保持着。接着我在administrator帳戶(一個會話)中,運行這個程序,這個程序自然會輸出"成功創建互斥量對象“,然後不關閉,接着按下Ctrl+Alt+Del鍵切換到FBY帳戶(另一個會話)並運行這個程序,發現互斥量創建失敗,CreateMutex返回NULL,ErrorCode爲ERROR_ALREADY_EXISTS。
    然後,我將上述代碼改爲:

    HANDLE hMutex = CreateMutex(NULL,FALSE,TEXT("Local\\Handle")) ; 

    隨後執行同上的操作,發現在FBY帳戶中,這個程序能夠成功創建這個同名的內核對象。
    接着,我又將上述代碼改爲:

    HANDLE hMutex = CreateMutex(NULL,FALSE,TEXT("Handle")) ;  

    隨後執行同上的操作,發現在FBY帳戶中,這個程序能夠成功創建這個同名的內核對象。
    總結: 在全局命名空間中創建內核對象,用不同帳戶運行該程序,不能創建同名的內核對象。 在局部命名空間中創建內核對象,用不同帳戶運行該程序,可以創建同名的內核對象,而不添加任何前綴,默認是本地命名空間。

    專有命名空間

    前面我們說到,假如用戶試圖運行來自不同公司的兩個應用程序,兩個程序都試圖創建一個內核對象,我們只有確保內核對象的名字不同纔不會出問題,而前面使用的是GUID標識符方法。現在我們自己創建一個屬於自己的專有命名空間,如果想確保我們的應用程序創建的內核對象名稱永遠不會和其他應用程序的名稱衝突,或者想確保它們免遭劫持,可以定義一個自定義的前綴,並把它作爲自己的專有命名空間使用。具體步驟如下:
    1.創建一個邊界描述符BoundaryDescriptor;

    HANDLE WINAPI CreateBoundaryDescriptor(
    __in  LPCTSTR Name,
    __in  ULONG Flags
    );

    函數功能:創建一個邊界描述符:
    Name:邊界描述符的名字,這個名字由你自己來取;
    Flags:保留,=NULL。
    返回值:如果函數成功,那麼返回值就是一個邊界描述符的句柄。如果函數失敗,返回值NULL。
    備註:一個新的邊界描述符至少要有一個安全描述符SID。可以使用AddSIDToBoundaryDescriptor函數,爲邊界描述符增加SID。
    2.獲取一個SID;

    BOOL WINAPI CreateWellKnownSid(
    __in       WELL_KNOWN_SID_TYPE WellKnownSidType,
    __in_opt   PSID DomainSid,
    __out_opt  PSID pSid,
    __inout    DWORD *cbSid
    );

    函數功能:創建一個有預定義別名的SID
    WellKnownSidType:WELL_KNOWN_SID_TYPE列舉的成員,用這個成員標識要獲取的SID。
    DomainSid:一個指向SID的指針,這個SID代表了創建SID所使用的域控制安全描述符,如果是本地計算機,那麼使用NULL。
    pSid:一個指向內存塊的指針,CreateWellKnownSid函數會將創建好的新SID保存到這個內存塊中。
    cbSid:一個DWORD類型的指針。代表了生成的pSid的大小。
    返回值:如果函數成功,返回非零,否則返回零。
    3.將這個SID放到邊界標識符中,只有具有這個SID的程序,才能打開這個邊界標識符;

    BOOL WINAPI AddSIDToBoundaryDescriptor(
    __inout  HANDLE *BoundaryDescriptor,
    __in     PSID RequiredSid
    );

    函數功能:將一個安全描述符SID添加到指定的邊界描述符中
    BoundaryDescriptor:一個指向邊界描述符的句柄。CreateBoundaryDescriptor函數返回這個句柄。
    RequiredSid:一個指向安全描述符Sid的指針。
    返回值:如果函數成功,返回值是非零。如果函數失敗,返回零。
    備註:AddSIDToBoundaryDescriptor函數必須爲每個SID調用一次,將其增加到邊界描述符中。你可以將這個函數理解爲,爲邊界描述符創建邊界!!!
    4.創建一個安全描述符(允許訪問安全描述符);

    ConvertStringSecurityDescriptorToSecurityDescriptor()//實際用法不過多解釋

    5.用這個邊界描述符和安全描述符,來創建一個私有命名空間,同時給這個私有命名空間一個別名,例如:”ydm”;
    (1)CreatePrivateNamespace

    HANDLE WINAPI CreatePrivateNamespace(
    __in_opt  LPSECURITY_ATTRIBUTES lpPrivateNamespaceAttributes,
    __in      LPVOID lpBoundaryDescriptor,
    __in      LPCTSTR lpAliasPrefix
    );

    函數功能:創建一個私有命名空間
    lpPrivateNamespaceAttributes:一個指向SECURITY_ATTRIBUTES結構的指針,這個結構定義了命名空間的安全屬性。
    lpBoundaryDescriptor:一個描述符,定義了命名空間如何被隔離。調用者必須在這個邊界內。函數CreateBoundaryDescriptor函數創建一個邊界描述符。
    lpAliasPrefix:命名空間的前綴。爲了在這個命名空間中創建命名對象,定義對象名稱的前綴,形式爲”pre fix\”對象名稱。系統支持不同邊界中,可以有相同的私有命名空間。
    返回值:如果函數成功,返回一個指向新命名空間的句柄,否則返回NULL。
    備註:其他的應用程序,可以使用OpenPrivateNamespac函數,來打開一個已經存在的命名空間。創建命名控件的程序,使用ClosePrivateNamespace函數來關閉指向命名控件的句柄。當創建這個命名空間的進程退出時,被創建的命名空間也會被關閉,那麼此後在使用OpenPrivateNamespace函數,就會失敗,但這並不影響對該命名控件中的對象的操作。
    (2)OpenPrivateNamespace

    HANDLE WINAPI OpenPrivateNamespace(
    __in  LPVOID lpBoundaryDescriptor,
    __in  LPCTSTR lpAliasPrefix
    );

    函數功能:打開一個私有命名空間。
    lpBoundaryDescriptor:定義私有命名空間如何被隔離的描述符。調用這個函數的程序必須在這個邊界之內。函數CreateBoundaryDescriptor創建一個邊界描述符。
    lpAliasPrefix:命名空間前綴。爲了在這個私有命名空間中創建一個命名對象,那麼這個對象名稱的前綴必須是lpAliasPrefix\。系統支持多個私有命名空間具有相同的別名,只要邊界描述符不同就可以了。
    返回值:函數返回一個已經存在的私有命名空間的句柄。
    6.要在這個私有命名空間中,創建一個內核對象,例如一個Mutex,那麼如下就可以了:

    CreatetMutex(NULL,FALSE,L”ydm\\object_name”);

    結合專有命名空間實現防止運行一個應用程序的多個實例

    #include<Windows.h>
    #include<tchar.h>
    #include"resource.h"
    #include<Sddl.h>
    #include<Strsafe.h>
    HANDLE h_Bundary;//邊界描述符句柄
    HANDLE h_Namespace;//私有命名空間句柄
    HANDLE hMutex;//內核對象句柄
    BOOL opened=FALSE;//使用ClosePrivateNamespace函數,銷燬私有命名空間的時候,需要知道
    //私有命名空間是否是本程序創建的,如果是,那麼這個ClosePrivateNamespace函數的第二個參數
    //爲1,否則爲0
    INT_PTR CALLBACK Dlg_pro(
    __in  HWND hwndDlg,
    __in  UINT uMsg,
    __in  WPARAM wParam,
    __in  LPARAM lParam
    );
    BOOL CheckInstance(HWND);
    int APIENTRY _tWinMain(HINSTANCE hInstance,HINSTANCE,LPTSTR lpCmdLine,int nCmdShow)
    {
    DialogBox(hInstance,MAKEINTRESOURCE(IDD_DIALOG1),NULL,Dlg_pro);
    if(hMutex)
        CloseHandle(hMutex);//殺死內核對象
    if(h_Namespace)//銷燬私有命名空間。
    {
        if(opened)
            ClosePrivateNamespace(h_Namespace,0);
            //如果是打開私有命名空間,ClosePrivateNamespace的第二個參數是0
        else
            ClosePrivateNamespace(h_Namespace,1);
            //如果是創建的私有命名空間,ClosePrivateNamespace的第二個參數是1
    }
    if(h_Bundary)
        DeleteBoundaryDescriptor(h_Bundary);//銷燬邊界描述符!!
    return 0;
    }
    INT_PTR CALLBACK Dlg_pro(
    __in  HWND hwndDlg,
    __in  UINT uMsg,
    __in  WPARAM wParam,
    __in  LPARAM lParam
    )
    {
    if(uMsg==WM_INITDIALOG)//初始化對話框消息
    {
        SetDlgItemText(hwndDlg,IDC_STATIC1,L"");//將靜態文本框中的內容清空。
        CheckInstance(hwndDlg);//真正的檢查函數
    }
    if(uMsg==WM_COMMAND)//退出消息
    {
        if(HIWORD(wParam)==BN_CLICKED&&LOWORD(wParam)==IDCANCEL)
            EndDialog(hwndDlg,0);
    }
    return  0;
    }
    BOOL CheckInstance(HWND hwnd)
    {
    h_Bundary=CreateBoundaryDescriptor(L"ydm",0);//創建一個邊界描述符!
    BYTE LocalAdmiSID[SECURITY_MAX_SID_SIZE];
    DWORD cbSID=sizeof(LocalAdmiSID);
    CreateWellKnownSid(WinBuiltinAdministratorsSid,NULL,&LocalAdmiSID,&cbSID);
    //創建一個管理員SID
    AddSIDToBoundaryDescriptor(&h_Bundary,&LocalAdmiSID);
    //將管理員SID加入到邊界描述符中,此後,只有管理員才能夠打開這個邊界描述符對應的私有命名空間!
    SECURITY_ATTRIBUTES sa;
    sa.nLength=sizeof(SECURITY_ATTRIBUTES);
    sa.bInheritHandle=FALSE;
    ConvertStringSecurityDescriptorToSecurityDescriptor(L"D:(A;;GA;;;BA)",SDDL_REVISION_1,
        &sa.lpSecurityDescriptor,NULL);//創建了一個安全描述符:
    /*
    D:  這個代表這個安全描述符中有DACL-discretionary Access Control list訪問控制列表
    A   表示這個安全描述符是一個允許安全描述符,如果是D-deny,那麼表示拒絕安全描述符allow
    GA: 表示訪問的權限是所有權限;
    BA: 代表了管理員SID
    這個"D:(A;;GA;;;BA)"字符串是按照:Security Descriptor Definition Language語言,來編寫的。
    */
    h_Namespace=CreatePrivateNamespace(&sa,h_Bundary,L"ygg");
    //創建私有命名空間!!!注意,最後一個參數只是這個命名空間的別名!!!
    if(GetLastError()==ERROR_ALREADY_EXISTS)//如果私有命名空間已經創建,那麼我們打開這個私有命名空間!
    {
        h_Namespace=OpenPrivateNamespace(h_Bundary,L"ygg");
        //打開私有命名空間
        opened=TRUE;//設置打開標誌,用於ClosePrivateNamespace函數最後一個參數的設置!
    }
    
    LocalFree(sa.lpSecurityDescriptor);
    //ConvertStringSecurityDescriptorToSecurityDescriptor函數,會自動給被創建的安全描述符分配內存
    //當這個安全描述符不再使用時,使用LocalFree函數釋放他。
    TCHAR szMutex[64];
    StringCchPrintf(szMutex,_countof(szMutex),L"%s\\%s",L"ygg",L"mutex");
    //形成一個L"ygg\\mutex"字符串!
    hMutex=CreateMutex(NULL,FALSE,szMutex);//在ygg命名空間中,創建一個互斥內核對象!
    if(GetLastError()==ERROR_ALREADY_EXISTS)
    {
        SetDlgItemText(hwnd,IDC_STATIC1,L"已經存在一個程序");
    }
    else
    {
        SetDlgItemText(hwnd,IDC_STATIC1,L"這是第一個程序實例");
    }
    return TRUE;
    }

    跨進程邊界共享內核對象-複製對象句柄

    現在講第三種跨進程邊界共享內核對象-複製對象句柄。這種方法可以用在各種情況,例如:父子進程(但通常使用繼承方法更好)、毫無關聯的兩個進程、當前進程跟當前進程來複制對象句柄。我們先來講講前面所使用的函數GetCurrentProcess(),我們通過GetCurrentProcess函數,獲得的句柄都是僞句柄!僞句柄的值爲-1,其實是一個固定的值!如果你在使用句柄的地方,直接寫上-1,那麼就代表本進程句柄,但不建議這麼做,也就是說,GetCurrentProcess函數,在目前,總是返回-1。
    可以通過GetProcessId函數獲取指定進程內核對象句柄值的進程ID。那怎麼才能獲取真實的進程句柄呢?有兩種方法獲得真實的進程句柄。一種是使用OpenProcess函數,另外一種是使用DuplicateHandle函數!下面貼上GetCurrentProcess、GetProcessId、OpenProcess、DuplicateHandle的函數簽名:
    (1)GetCurrentProcess函數

    HANDLE WINAPI GetCurrentProcess(void);
    獲得的句柄都是僞句柄

    (2)GetProcessId函數

    DWORD WINAPI GetProcessId(
    __in  HANDLE Process
    );
    通過進程句柄,獲得進程Id;
    Process:進程句柄。

    (3)OpenProcess函數

    HANDLE WINAPI OpenProcess(
    __in  DWORD dwDesiredAccess,
    __in  BOOL bInheritHandle,
    __in  DWORD dwProcessId
    );
    獲得進程內核句柄;
    dwDesiredAccess:希望獲得的進程內核對象的句柄,具有什麼樣的權限。
    bInheritHandle:這個句柄是否可以被繼承。
    dwProcessID:要獲得進程內核對象句柄的進程ID。
    如果成功,返回句柄,否則返回NULL。

    (4)DuplicateHandle函數

    BOOL WINAPI DuplicateHandle(
    __in   HANDLE hSourceProcessHandle,
    __in   HANDLE hSourceHandle,
    __in   HANDLE hTargetProcessHandle,
    __out  LPHANDLE lpTargetHandle,
    __in   DWORD dwDesiredAccess,
    __in   BOOL bInheritHandle,
    __in   DWORD dwOptions
    );
    hSourceProcessHandle:源進程的進程句柄;
    hSourceHandle:源內核對象句柄;
    hTargetProcessHandle:目標進程的進程句柄;
    lpTargetHandle:指向接收目標內核對象句柄內存的指針;
    dwDesiredAccess:訪問權限;
    bInheritHandle:是否可被繼承;
    dwOptions:
    Value                                                      Meaning
    DUPLICATE_CLOSE_SOURCE    複製完畢後,將源句柄關閉,無論發生什麼錯誤,源句柄都將被關閉。
    0x00000001                                     
    DUPLICATE_SAME_ACCESS      忽視dwDesiredAccess參數。複製的句柄具有和源句柄同樣的權限。
    0x00000002                                     

    對上述函數有了一定了解(瞭解參數是啥,返回是啥,函數幹啥的就行,不用死記)後就分析一段代碼,放代碼:

    #include<Windows.h>
    #include<tchar.h>
    int _tmain()
    {
    HANDLE hProcess=GetCurrentProcess();
    //GetCurrentProcess函數,無論在什麼情況下,他都返回-1!!!!
    //我們把GetCurrentProcess函數返回的句柄,叫僞句柄,當前請情況下,這個僞句柄一定是-1、
    _tprintf(L"hProcess=%d\n",hProcess);
    DWORD sessionid=0;
    DWORD processid=0;
    processid=GetProcessId((HANDLE)-1);//獲取當前進程進程ID
    //GetCurrentProcess()獲得當前進程句柄!
    HANDLE hProcess1=OpenProcess(PROCESS_ALL_ACCESS,FALSE,GetProcessId((HANDLE)-1));
    //OpenProcess函數的參數1是所獲得的進程內核對象的句柄的權限,參數2是可否繼承,參數3是要獲取指定的內核對象句柄的進程ID,        //參數3通過GetProcessId函數獲取進程ID
    HANDLE hProcess2,hProcess3;
    DuplicateHandle((HANDLE)-1,(HANDLE)-1,(HANDLE)-1,&hProcess2,NULL,FALSE,0);
    DuplicateHandle((HANDLE)-1,(HANDLE)-1,(HANDLE)-1,&hProcess3,NULL,FALSE,0);
    _tprintf(L"hprocess1=%0x\nhprocess2=%0x\nhProcess3=%0x\n",hProcess1,hProcess2,hProcess3);
    //關閉所獲取的進程內核對象句柄,包括hProcess1獲取的真實的進程句柄。
    CloseHandle(hProcess1);
    CloseHandle(hProcess2);
    CloseHandle(hProcess3);
    _gettchar();
    return 0;
    }

    運行結果如下:
    Windows核心編程之核心總結(第三章 內核對象)(2018.6.2)
    我們對兩個DuplicateHandle函數功能分析:我們都知道當一個應用程序運行時,就生成了一個進程,那當然有進程內核對象在進程內核對象句柄表中,上面的函數參數1代表我要在當前進程的進程內核對象句柄表中複製一份進程內核對象到當前進程的進程內核對象句柄表的其他位置,並返回這個新的句柄值到hProcess2和hProcess3,但其實hProcess1、hProcess2、hProcess3雖然句柄值不同,但都是指向同一個內核對象地址。爲什麼會這樣?這是因爲Windows使用計數機制問題所導致這樣做的。假如兩個內核對象句柄的值相同,並且都是指向同一個內核對象地址,那麼當調用CloseHandle函數釋放內核對象句柄時,參數指定了這個內核對象句柄,那這個內核對象數據結構的使用計數也只減1,但如果再次調用CloseHandle函數,參數依然時這個內核對象句柄(因爲句柄值相同),必然會報錯,因爲已經釋放了一次了。而如果兩個內核對象句柄值不同,就不會出現這樣的問題,所以Windows採取了這樣的方式來複制內核對象。

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