《Windows內核原理與實現筆記》(二)註冊表和配置管理器,事件追蹤,安全性管理

註冊表和配置管理器

Windows系統很多組件都是可以配置的,內核組建通常支持一些參數,甚至有些完全依賴於系統配置信息。例如I/O管理器和即插即用管理器在初始化階段根據系統設置來例句和加載設備驅動程序。Windows操作系統提供了一個稱爲“註冊表”的中心存儲設施來作爲系統的配置和管理中心。應用程序和捏合通過訪問註冊表來讀寫設置Windows同時提供API供訪問註冊表,API接到註冊表訪問請求,轉發給系統服務。在內核中,執行體包含一個稱爲“配置管理器(configuration manager)”組件,是註冊表的真正實現。註冊表由一組稱爲儲巢(hive)的文件構成,每個儲巢內部包含一個樹形層次結構,每個儲巢可以想象成一個文件系統。

windows註冊表是樹狀結構,每個節點是一個鍵值。註冊表值可以多種類型,絕大多說註冊表值類型爲REG_DWORD(32位整數),REG_BINARY(二進制數據)和REG_SZ(字符串)。還有REG_LINK(符號鏈接,執行另一個鍵或值)。

除了HKEY_PERFORMANCE_<XXX>以外,在其他的5個根鍵中,真正存放系統設置信息的子樹是HKLM和HKU。HKLM存放有關係統全局的信息,包括5個子鍵,分別爲HARDWARE(硬件設置)、SAM(本地賬戶和組的信息)、SECURITY(系統全局範圍的安全策略和用戶權限設置)、SOFTWARE(系統中的全局配置信息,在系統引導時不需要)和SYSTEM(系統中的全局配置信息,在系統引導時需要,包括設備驅動程序和系統服務等)。HKU爲系統中每個加載過的用戶輪廓包含一個子鍵,也包含一個名爲.DEFAULT的子鍵,這是系統的默認輪廓,當登錄進程winlogon.exe爲第一次登錄到系統中的用戶創建輪廓時將以此爲基礎。

關於註冊表存儲結構,註冊表是由一組儲巢構成的,每個儲巢包含了一個由鍵和值構成的層次結構。上圖列出了Windows Server 2003系統中各個儲巢的註冊表路徑和文件路徑。一個系統的儲巢列表存放在HKLM\SYSTEM\CurrentControlSet\Control\hivelist鍵下,如下圖所示。當系統初始化時,HKLM\SYSTEM總是先被加載進來,然後配置管理器找到hivelist鍵,繼而加載其他儲巢,並創建註冊表根鍵,將這些儲巢鏈接起來,從而建立起完整的註冊表結構。

 儲巢的內部結構類似於一個文件系統,而儲巢相當於是一個磁盤分區。儲巢的基本分配單元稱爲塊(block),類似於文件系統定義的簇(cluster)。當儲巢爲了存儲新的數據而需要擴展時,它總是按照塊的粒度來增長。在Windows中,註冊表的塊的大小爲4KB(4096B)。儲巢的第一個塊稱爲基本塊,它包含了儲巢文件標識、最新序列號、最後一次寫操作的時間戳、儲巢格式的版本號、校驗和,以及儲巢的內部文件名。儲巢中的註冊表數據是按照巢室(cell)來組織的。巢室可大可小,具體取決於它的類型和數據,每個巢室可以存放一個鍵、值、安全描述符、子鍵列表或者值列表,對應的巢室分別稱爲鍵巢室、值巢室、安全描述符巢室、子鍵列表巢室和值列表巢室。巢室在儲巢文件中的偏移稱爲該巢室的索引(cell index),其他巢室可以利用此巢室索引來引用它,從而建立起巢室之間的關係。

配置管理器使用了一種類似於Intel x86處理器的頁表映射的做法來解決巢室地址轉譯,一個32位的巢室索引被分成四個組成部分:存儲類型、巢室目錄索引、巢室表索引和塊內偏移。存儲類型有兩種可能:穩定的(stable,最高位用0表示)和易失的(volatile,最高位用1表示)。每個儲巢在內存中有兩個巢室目錄,分別對應於穩定的和易失的配置數據;每個巢室目錄有1024項,每一項指向一個巢室表;每個巢室表包含512個表項,每一項指向一個塊。由於配置管理器用巢箱來管理內存分配,而巢箱總是以塊爲邊界(4KB),所以,巢室索引的最後12位指定了一個巢室在塊內的偏移。基於這樣的巢室索引結構,配置管理器將只爲每個儲巢映射那些需要用到的巢箱,而不是所有的巢箱。巢室目錄和巢室表仍然佔用換頁內存池的空間,但通常情況下,相比於整個儲巢文件,它們要小得多。配置管理器通過這種巢室映射的做法,可有效地降低註冊表數據的內存使用量。

Windows內核中配置管理器的實現

配置管理器是執行體中的組件,它的實現依賴於內存管理器和緩存管理器(以及文件系統),這意味着它必須要在這些組件初始化以後才能正常工作;然而,在系統初始化的早期(比如I/O子系統的初始化),Windows已經需要使用註冊表中的配置信息了,但此時配置管理器尚未被初始化。Windows的做法是,在內核初始化以前,內核加載器(ntldr)已經將整個HKLM\SYSTEM儲巢作爲一個只讀文件加載到了內存中,因而配置管理器在完全初始化以前只需直接把巢室索引加上該儲巢的內存映像地址,就可以得到巢室的內存地址。這一做法有一個限制,即,在配置管理器完全初始化以前,系統只能訪問HKLM\SYSTEM中的設置,換句話說,Windows必須把初始化早期用到的各種設置存放在HKLM\SYSTEM中。

配置管理器和註冊表的初始化過程

配置管理器建立起完全的註冊表視圖分三個階段來完成:第一,在內核初始化階段,建立起HKLM\SYSTEM和HKLM\HARDWARE儲巢;第二,由會話管理器(smss.exe進程)建立起HKLM\SAM、HKLM\SECURITY、HKLM\SOFTWARE和HKU\.DEFAULT儲巢;第三,當加載用戶輪廓時建立起HKU\<用戶的SID>儲巢,這是由登錄進程(winlogon.exe)來完成的。這裏第一階段可以看做配置管理器的初始化,以及註冊表的臨時初始化;第二階段可以看做註冊表中系統部分的初始化;第三階段可以看做註冊表中用戶部分的初始化。

首先來看第一階段的初始化,它發生在一個關鍵點上:在內核初始化過程中,在對象管理器和緩存管理器初始化以後,但在I/O子系統初始化以前。內核在這個點以前,不能訪問註冊表中的任何信息;而在這個點以後,可以訪問HKLM\SYSTEM和HKLM\HARDWARE中的設置。執行這一初始化過程的函數爲CmInitSystem1,它是在內核初始化過程中由Phase1InitializationDiscard函數調用的。

CmInitSystem1函數(參見base\ntos\config\cmsysini.c文件)負責完成以下事項:

初始化配置管理器的全局變量,包括各種鏈表和同步對象。

創建註冊表鍵的類型對象CmpKeyObjectType,CmInitSystem1通過調用CmpCreateObjectTypes函數來完成。

創建主儲巢CmpMasterHive,這是一個易失儲巢,代表了註冊表的根。創建儲巢的函數爲CmpInitializeHive。

用CmpCreateRegistryRoot函數建立起註冊表的根:在主儲巢中創建節點“\REGISTRY”,並創建一個鍵對象指向該節點,然後將該對象插入到對象名字空間的根下面。

調用NtCreateKey函數創建“\REGISTRY\MACHINE”和“\REGISTRY\USER”節點。

調用CmpInitializeSystemHive函數創建系統儲巢。在CmpInitializeSystemHive函數中,它根據ntldr傳遞進來的已加載的原始SYSTEM儲巢映像,來初始化內存中的SYSTEM儲巢。CmpInitializeSystemHive函數調用CmpInitializeHive來初始化SYSTEM儲巢,並調用CmpLinkHiveToMaster將它鏈接到主儲巢中。

調用CmpCreateControlSet函數,根據加載信息創建符號鏈接“\Registry\Machine\System\CurrentControlSet”。

調用CmpInitializeHive,創建HARDWARE儲巢,這是一個易失儲巢。然後調用CmpLinkHiveToMaster將它鏈接到主儲巢中。

接下來,利用加載塊參數,將有關當前這次引導的信息寫到註冊表中:

  • 調用CmpInitializeHardwareConfiguration,創建“\Registry\Machine\Hardware”節點,並且把硬件信息設置到註冊表中。
  • 調用CmpInitializeMachineDependentConfiguration函數,把與機器相關的配置數據設置到註冊表HARDWARE儲巢中。
  • 調用CmpSetSystemValues,將這次系統啓動的信息寫到註冊表中。
  • 調用CmpSetNetworkValue,將這次啓動的網絡信息寫到註冊表中。

因此,CmInitSystem1函數將註冊表結構初步建立起來,它構造了主儲巢、HKLM\SYSTEM和HKLM\HARDWARE三個儲巢,並且也建立起與這次啓動有關的符號鏈接和配置信息,爲系統的進一步初始化提供了基本的配置信息。

再來看註冊表的進一步初始化。數組CmpMachineHiveList包含6個儲巢,對應於表2.6中的前6個儲巢。這些儲巢(包括HKLM\SYSTEM和HKLM\HARDWARE)是由會話管理器進程(smss.exe)通過NtInitializeRegistry系統服務加載和初始化的。在一次正常啓動過程中,它調用CmpCmdInit函數執行註冊表的進一步初始化。在正常啓動情形下,CmpCmdInit函數調用CmpInitializeHiveList來初始化儲巢列表中的指定儲巢,以及建立相應的符號鏈接。

由於CmpInitializeHiveList是在會話管理器進程環境中執行的,而加載和初始化儲巢的動作必須在System進程中完成,因此,CmpInitializeHiveList會爲儲巢列表中的每一個儲巢創建一個系統線程,由該系統線程來初始化該儲巢。系統線程的主例程爲CmpLoadHiveThread,參數爲每個儲巢在CmpMachineHiveList數組中的索引。

在CmpLoadHiveThread函數中,對於尚未加載的儲巢,包括HKLM\SAM、HKLM\SECURITY、HKLM\SOFTWARE和HKU\.DEFAULT,它會調用CmpInitHiveFromFile來完成儲巢的加載和初始化;而對於已經被初始化的非易失儲巢,即HKLM\SYSTEM,則調用CmpOpenHiveFiles打開系統儲巢文件,因爲在此之前系統儲巢文件實際上一直沒有被通過文件系統打開過。經過這一步以後,系統儲巢被完全初始化。

隨着系統的進一步引導,當需要特定於用戶的配置信息時,註冊表的HKU子樹下的用戶儲巢也必須建立起來。這些儲巢是按需加載和初始化的,由登錄進程(winlogon.exe)在建立起用戶運行環境時完成,譬如當用戶登錄到系統中,或者系統以特定的用戶身份來啓動一個進程或服務時。Winlogon通過NtLoadKey系統服務將一個儲巢文件鏈接到註冊表中,而NtLoadKey又進一步調用CmLoadKey來完成實際的加載和鏈接操作。

以上討論了配置管理器的初始化以及Windows註冊表的建立過程。儲巢是配置管理器的核心概念,也是註冊表存儲結構中的文件實體。WRK包含了配置管理器的完整代碼,儲巢的數據類型爲CMHIVE,其內嵌的HHIVE成員是它的數據管理結構。

巢內部的數據管理類似於一個文件系統,它的數據存儲單元按照巢箱來分配,而巢箱以塊(4KB大小)爲邊界;儲巢內部的邏輯數據結構爲巢室,巢室有不同的類型,其大小亦不盡相同。在配置管理器的實現中,巢室的數據結構爲HCELL,巢箱的數據結構爲HBIN。空閒的巢箱形成一個空閒鏈表。實際上,HHIVE數據結構包含兩個Storage成員,分別對應於穩定的儲巢和易失的儲巢;在Storage成員中,有空閒巢箱鏈表,以及一套用於轉譯巢室索引的巢室目錄和巢室表。

註冊表的層次結構形成了一個名字空間,配置管理器定義了一個以“Key”命名的對象類型,從而將該名字空間與對象管理器的全局名字空間整合起來。配置管理器在初始化階段調用CmpCreateObjectTypes函數,創建了類型對象全局變量CmpKeyObjectType。配置管理器充分利用了對象管理器提供的對象管理框架,讓註冊表中的每個鍵自動成爲對象管理器中的一個對象。對於每個打開的註冊表鍵,配置管理器分配一個鍵控制塊(key control block),其數據結構爲CM_KEY_CONTROL_BLOCK,它包含了該控制塊所引用的鍵節點所在的儲巢和巢室索引。配置管理器將所有的鍵控制塊放在一張散列表(全局變量CmpCacheTable)中,因而可以快速地根據名稱來搜索已有的鍵控制塊。散列表CmpCacheTable實際上是一個包含2048個元素的數組,散列表的鍵ID是由鍵控制塊所引用的鍵對象的名稱通過計算而獲得。每個鍵控制塊然後被放到散列表的相應桶中,放到同一個散列桶中的所有鍵控制塊形成一個鏈表。

當內核或應用程序訪問一個註冊表鍵時對象管理器和配置管理器的名稱解析過程。這涉及兩個常用的操作:系統服務NtOpenKey和NtQueryValueKey,或者ZwOpenKey和ZwQueryValueKey。根據內核函數的命名約定,我們知道,Nt<Xxx>函數供用戶模式應用程序使用,而Zw<Xxx>函數供內核代碼直接調用。NtOpenKey和NtQueryValueKey函數,其原型如下:

 這兩個函數的代碼位於base\ntos\config\ntapi.c文件中。NtOpenKey系統服務接收到的對象名稱位於ObjectAttributes.ObjectName中,它檢查KeyHandle和對象名稱參數是否可以正確地訪問,然後將打開註冊表鍵對象的操作全盤交給對象管理器的ObOpenObjectByName函數來完成。從這裏也可以看出,註冊表的接口與實現,都跟對象管理器的框架融合在一起。

ObOpenObjectByName函數通過ObpLookupObjectName函數來完成對象打開操作,它層層遞進解析一個名稱串,若碰到目錄對象,則在目錄中查詢剩餘的名稱串;若碰到支持Parse方法的對象,則交給Parse方法來解析剩餘的名稱串。在NtOpenKey的情形中,它的ObjectAttributes參數可能已經指定了一個搜索根目錄,即RootDirectory;也可能直接從全局名字空間的根下開始查找,此時調用者應該指定註冊表鍵的全路徑名。註冊表鍵的全路徑名以“\Registry”作爲開始,例如,HKLM\SYSTEM\CurrentControlSet\services的全路徑名爲“\Registry\Machine\System\CurrentControlSet\services”。

由於配置管理器已經在全局名字空間的根下創建了一個名爲“REGISTRY”的鍵對象,所以,當ObpLookupObjectName函數解析一個註冊表鍵的全路徑名稱時,它首先在根目錄下找到“REGISTRY”鍵對象,然後調用鍵對象類型的Parse方法來解析剩餘的名稱字符串。鍵對象類型的Parse方法CmpParseKey函數。CmpParseKey函數的實現並不難理解,它首先調用CmpBuildHashStackAndLookupCache函數,在散列表中查找已經打開的鍵對象,若能直接找到,則無須進一步名稱解析;否則,需要順序解析剩餘的名稱串,對於路徑上的每一個子鍵,逐個爲它們創建鍵控制塊(通過調用CmpCreateKeyControlBlock函數)。最後,CmpParseKey調用CmpDoOpen函數打開此註冊表鍵,並根據需要創建一個鍵控制塊。

ObOpenObjectByName函數接收到一個指向鍵對象的句柄,鍵對象的數據結構爲CM_KEY_BODY,其內部指向一個鍵控制塊。如果兩個應用程序打開同一個註冊表鍵的話,它們都會接收到一個鍵對象,但這兩個鍵對象指向一個公共的鍵控制塊。鍵控制塊有一個引用計數用於跟蹤一個鍵被多少個客戶引用。當引用計數爲零時,表明該鍵控制塊已不再被使用了,於是配置管理器將它從散列表中移除,並且回收該鍵控制塊。

NtQueryValueKey函數相對要簡單得多,因爲它的參數KeyHandle已經指示了要查詢哪個鍵,所以,它只需調用ObReferenceObjectByHandle函數即可獲得目標鍵的鍵對象。然後它調用CmQueryValueKey函數從目標鍵中讀取指定的值的信息。

最後值得一提的是,配置管理器提供了註冊表鍵的變化通知機制。應用程序通過調用NtNotifyChangeKey或NtNotifyChangeMultipleKeys系統服務,可以監視一個或多個註冊表鍵的創建、刪除和修改動作。實現註冊表鍵變化通知機制的關鍵在於,每個鍵對象都有一個類型爲CM_NOTIFY_BLOCK的通知塊成員,它描述了一個鍵對象的哪些事件以何種方式被通知到註冊方。由於配置管理器提供了這種變化通知能力,因而對於想要監視註冊表行爲的應用程序,它們無須頻繁地檢查註冊表來判斷感興趣的鍵是否已被修改。這對於一些安全保護或者註冊表行爲分析等程序有顯著的意義。

事件追蹤(ETW)

Windows提供了統一的跟蹤和記錄事件的機制,稱爲ETW(EventTracing for Windows)。用戶模式應用程序和內核模式驅動程序都可以使用ETW來記錄事件。ETW是直接由內核支持的事件記錄機制,在它的框架結構中,共有三種組件:

· 控制器(controller)。負責啓動、停止或配置事件記錄會話。

· 提供者(provider)。負責向ETW註冊自己的事件類,並接受控制器的命令,以便啓動或者停止它們所負責的事件類的記錄過程。

· 消費者(consumer)。負責有針對性地讀取它們想要的事件數據,選擇一個或多個記錄會話。它們既可以實時地接收ETW緩衝區中的數據,也可以接收日誌文件中的事件數據。

Windows內置了一個內核日誌記錄器(kernel logger)作爲ETW提供者,專門用於記錄內核和核心驅動程序的事件。此內核日誌記錄器是由WMI(Windows Management Instrumentation,Windows管理規範)設備驅動程序實現的,也是內核模塊ntoskrnl.exe的一部分。WMI驅動程序的名稱爲“WMIxWDM”,它是在I/O管理器初始化過程中調用WMI組件的初始化函數(WMIInitialize)而創建的。它除了實現內核日誌記錄器的功能,也管理用戶模式ETW事件類型的註冊工作。由於它的實現形式是驅動程序,因此,其他的內核例程或設備驅動程序可以通過I/O接口與它通信。

內核日誌記錄器是一個事件提供者,它有一個預定義的GUID,即內核變量SystemTraceControlGuid。內核日誌記錄器支持多種事件類,它採用標誌位(flag)來指示是否記錄某一類型的事件。

#define PERF_MASK_INDEX         (0xe0000000)
#define PERF_MASK_GROUP         (~PERF_MASK_INDEX)

#define PERF_NUM_MASKS       8
typedef ULONG PERFINFO_MASK;

//
// This structure holds a group mask for all the PERF_NUM_MASKS sets
// (see PERF_MASK_INDEX above).
//

typedef struct _PERFINFO_GROUPMASK {
    ULONG Masks[PERF_NUM_MASKS];
} PERFINFO_GROUPMASK, *PPERFINFO_GROUPMASK;

 當控制器程序指示內核日誌記錄器記錄某些類型的內核事件時,它們需要構造一個PERFINFO_GROUPMASK對象,將需要記錄的標誌位置上。PERFINFO_GROUPMASK的Masks數組包含8個ULONG成員,每個ULONG的最高3位(即29~31位)是組的索引(0~7),低29位(即0~28位)爲組內的標誌位。內核日誌記錄器的標誌位安排是由Windows系統定義的,並且允許擴展。Windows Server 2003僅僅使用了一部分標誌位,參見WRK的public\internal\base\inc\ntwmi.h文件中的PERF_<XXX>宏定義。

在Windows中,系統的全局組掩碼是由全局變量PerfGlobalGroupMask定義的。以環境切換事件爲例,當PERF_CONTEXT_SWITCH標誌位被置上時,線程切換時就會記錄一個CSwitch事件。這發生在SwapContext函數中,緩衝區管理是WMI驅動程序的重要職責之一。WMI驅動程序爲環境切換事件定義了一個緩衝區數組(WmipContextSwapProcessorBuffers變量),讓每個處理器使用它自己的緩衝區,從而避免緩衝區衝突。一旦屬於某個處理器的當前緩衝區滿了(不足以再存放一個CSwitch事件),則交由WMI刷新該緩衝區,下次記錄新的CSwitch事件時重新申請一個新的緩衝區作爲當前緩衝區。WMI爲每個記錄會話管理緩衝區,它在創建記錄會話時根據控制器的指示,創建合理數量的緩衝區。

Microsoft提供了一個性能工具xperf,它既是一個控制器,也是一個消費者。Xperf利用I/O接口(NtDeviceIoControlFile函數)與WMI驅動程序進行通信。Windows SDK提供了事件追蹤API(位於advapi32.dll模塊中),因而用戶模式應用程序可以很方便地操縱和控制WMI驅動程序。Xperf直接在ntdll.dll的ETW接口上工作,並沒有使用advapi32.dll中的事件追蹤API。

安全性管理

Windows有嚴格的安全模型,它既實現了以對象爲基礎的自主訪問控制(discretionary access control),又實現了系統級的強制訪問控制(mandatory accesscontrol)。在自主訪問控制模型中,對象的所有者授權或拒絕哪些人可以訪問該對象。而且,Windows還考慮到了對象所有者丟失的情形,即,當由於某些原因對象的所有者不再有效時,特權用戶可以將對象的所有權接管過來,從而可以訪問該對象,或授權其他的用戶訪問它。

Windows操作系統中涉及安全性管理的核心組件:winlogon、SRM和lsass,winlogon和lsass是兩個用戶模式進程,而SRM是Windows執行體中的組件。

SRM(安全引用監視器),負責執行對象的安全訪問檢查、管理用戶特權、生成安全審計消息,並且定義了訪問令牌數據結構來表示一個安全環境。

Winlogon,負責響應SAS(安全注意序列),以及管理交互式登錄會話。當用戶登錄到系統中時,winlogon創建一個初始進程,並進一步由它創建外殼(shell)進程。

Lsass(本地安全權威子系統),負責本地系統的安全策略,同時,它也認證用戶的身份,以及將安全審計消息發送到系統的事件日誌中。

SAM(安全賬戶管理器)數據庫,包含了本地用戶和用戶組,以及它們的口令和其他屬性。它位於註冊表的HKLM\SAM下面。由於HKLM\SAM鍵只允許本地系統賬戶訪問,所以,除非用戶在Local System賬戶下運行regedit.exe工具,否則無法訪問HKLM\SAM子樹。

LSA策略數據庫,包含了有關當前系統的一些信息,譬如誰允許訪問系統以及如何訪問(交互式登錄、網絡登錄或者以服務方式登錄);分配給誰哪些特權;安全審計如何進行等。如同SAM數據庫一樣,LSA策略數據庫也存儲在註冊表中,位於HKLM\SECURITY下面。同樣地,除了Local System賬戶以外的其他賬戶均無法訪問HKLM\SECURITY子樹。

簡而言之,在Windows的安全模型中,winlogon負責系統登錄,包括對用戶身份的認證;lsass負責管理系統本地安全策略,並且將這些策略通知到內核中的SRM。在內核中,SRM負責實現基於對象的訪問控制以及系統全局安全策略的實施。SRM的代碼,位於base\ntos\se目錄下面。在此模型中,lsass和SRM對於系統的安全性至關重要,一旦這兩個組件被惡意代碼修改或侵入,則系統的安全防線將不復存在。Windows對這兩個組件有特殊的保護,普通應用程序無法與它們打交道,它們相互之間通過LPC進行通信。它們的LPC連接在系統初始化時建立,而且,一旦其雙向LPC連接建立起來,它們的LPC端口便不再接受任何其他的連接請求,因而其他程序與它們無法建立LPC連接。

Windows的自主訪問模型與對象管理器集成在一起,每一種對象類型都定義了一個Security方法,該方法返回一個對象的安全信息。線程在訪問一個對象以前必須先打開這個對象,並獲得一個指向該對象的句柄。在打開對象的操作中,對象管理器調用SRM的函數,根據調用線程的安全憑證、在打開操作中請求的訪問類型(比如讀、寫、刪除等),以及該類型對象的Security方法提供的對象安全信息,來決定此打開操作是否允許。如果可以打開該對象,那麼,對象管理器在線程的進程句柄表中創建一個句柄,記錄下該對象以及它所請求的訪問類型。所以,線程在成功打開對象後獲得一個指向該對象的句柄。以後,當該線程訪問此對象時,它傳遞該對象的句柄,而對象管理器將該線程所請求的訪問操作與該對象被打開時所獲得的訪問類型進行比較,如果當前的訪問操作允許,則安全檢查通過,否則此次對象訪問失敗。

在這個模型中有兩點值得一提。第一,每個線程都有一個安全環境,其中最重要的信息是一個訪問令牌,代表了該線程的用戶的一次登錄;每個對象都有一個自主訪問控制列表(ACL),指明瞭允許誰以何種方式訪問該對象,而拒絕哪些用戶以何種方式訪問它。爲了訪問一個對象,線程可以不使用它所屬進程的安全環境,而是以其他賬戶身份來運行的安全環境,這稱爲模仿(impersonation)。第二,同一個進程中的其他線程也可以利用已經得到的句柄來訪問該對象。同樣地,對象管理器使用調用線程的安全環境和它所請求的訪問操作,對照該對象被打開時獲得的訪問類型進行檢查,以決定是否允許該操作。

SRM的安全檢查功能

當一個進程打開一個對象時,對象管理器在名字空間中查找到目標對象以後,但是在返回句柄給調用者以前,它調用ObCheckObjectAccess函數檢查訪問許可。ObCheckObjectAccess是對象管理器的函數,它將對象管理器與SRM的安全監視機制連接起來。該函數的代碼位於base\ntos\ob\obse.c文件的387~564行。它首先調用ObGetObjectSecurity函數以獲得目標對象的SD(安全描述符),數據類型爲SECURITY_DESCRIPTOR。對象的安全描述符包含了對象的所有者的SID(安全標識符)、自主訪問控制列表(DACL)和系統訪問控制列表(SACL),以及其他一些描述對象安全特徵的屬性。這裏的自主訪問控制列表規定了誰可以或不可以以何種方式訪問該對象;系統訪問控制列表規定了哪些用戶的哪些操作應該被記錄到安全審計日誌中。

對於每一種類型的對象,系統在創建它的類型對象時,可以指定該類對象的Security方法,即SecurityProcedure函數成員;如果在創建類型對象時不指定該函數成員,那麼,對象管理器使用一個默認的安全函數SeDefaultObjectMethod。接着,在獲得了對象的安全描述符以後,ObCheckObjectAccess函數鎖住當前的安全環境,並調用SeAccessCheck函數執行安全訪問許可檢查。

線程的安全環境是一個SECURITY_SUBJECT_CONTEXT類型的對象,它包含了線程的主令牌(PrimaryToken成員)、模仿級別、進程審計ID(直接用進程對象的地址來表示),以及一個可選的客戶令牌(ClientToken)。當線程模仿一個客戶時,客戶令牌不爲空,指向被模仿客戶的令牌,在這種情況下,SRM使用客戶令牌來檢查訪問許可。令牌(token)代表了SRM中用到的主體,它描述了一個用戶的一次登錄,由winlogon進程在認證了用戶身份以後創建。同一個用戶會話中運行的進程都使用同樣的令牌。令牌的數據結構爲TOKEN,它包含了令牌ID、認證ID、用戶賬戶的SID和所屬組的SID、一組與該令牌關聯的特權,以及其他一些用於各種安全操作的屬性。

代碼如下

BOOLEAN
ObCheckObjectAccess (
    __in PVOID Object,
    __inout PACCESS_STATE AccessState,
    __in BOOLEAN TypeMutexLocked,
    __in KPROCESSOR_MODE AccessMode,
    __out PNTSTATUS AccessStatus
    )

/*++

Routine Description:

    This routine performs access validation on the passed object.  The
    remaining desired access mask is extracted from the AccessState
    parameter and passes to the appropriate security routine to perform the
    access check.

    If the access attempt is successful, SeAccessCheck returns a mask
    containing the granted accesses.  The bits in this mask are turned
    on in the PreviouslyGrantedAccess field of the AccessState, and
    are turned off in the RemainingDesiredAccess field.

Arguments:

    Object - The object being examined.

    AccessState - The ACCESS_STATE structure containing accumulated
        information about the current attempt to gain access to the object.

    TypeMutexLocked - Indicates whether the type mutex for this object's
        type is locked.  The type mutex is used to protect the object's
        security descriptor from being modified while it is being accessed.

    AccessMode - The previous processor mode.

    AccessStatus - Pointer to a variable to return the status code of the
        access attempt.  In the case of failure this status code must be
        propagated back to the user.


Return Value:

    BOOLEAN - TRUE if access is allowed and FALSE otherwise

--*/

{
    ACCESS_MASK GrantedAccess = 0;
    BOOLEAN AccessAllowed;
    BOOLEAN MemoryAllocated;
    NTSTATUS Status;
    PSECURITY_DESCRIPTOR SecurityDescriptor = NULL;
    POBJECT_HEADER ObjectHeader;
    POBJECT_TYPE ObjectType;
    PPRIVILEGE_SET Privileges = NULL;

    PAGED_CODE();

    UNREFERENCED_PARAMETER (TypeMutexLocked);

    //
    //  Map the object body to an object header and the
    //  corresponding object type
    //

    ObjectHeader = OBJECT_TO_OBJECT_HEADER( Object );
    ObjectType = ObjectHeader->Type;

    //
    //  Obtain the object's security descriptor
    //獲得目標對象的SD(安全描述符)

    Status = ObGetObjectSecurity( Object,
                                  &SecurityDescriptor,
                                  &MemoryAllocated );

    //
    //  If we failed in getting the security descriptor then
    //  put the object type lock back where it was and return
    //  the error back to our caller
    //

    if (!NT_SUCCESS( Status )) {

        *AccessStatus = Status;

        return( FALSE );

    } else {

        //
        //  Otherwise we've been successful at getting the
        //  object's security descriptor, but now make sure
        //  it is not null.

        if (SecurityDescriptor == NULL) {

            *AccessStatus = Status;

            return(TRUE);
        }
    }

    //
    //  We have a non-null security descriptor so now
    //  lock the caller's tokens until after auditing has been
    //  performed.
    //鎖住當前的安全環境

    SeLockSubjectContext( &AccessState->SubjectSecurityContext );

    //
    //  Do the access check, and if we have some privileges then
    //  put those in the access state too.
    //執行安全訪問許可檢查,真正執行檢查任務是在SepAccessCheck函數中。它們的代碼位於base\ntos\se\accessck.c文件中。

    AccessAllowed = SeAccessCheck( SecurityDescriptor,
                                   &AccessState->SubjectSecurityContext,
                                   TRUE,                        // Tokens are locked
                                   AccessState->RemainingDesiredAccess,
                                   AccessState->PreviouslyGrantedAccess,
                                   &Privileges,
                                   &ObjectType->TypeInfo.GenericMapping,
                                   AccessMode,
                                   &GrantedAccess,
                                   AccessStatus );

    if (Privileges != NULL) {

        Status = SeAppendPrivileges( AccessState,
                                     Privileges );

        SeFreePrivileges( Privileges );
    }

    //
    //  If we were granted access then set that fact into
    //  what we've been granted and remove it from what remains
    //  to be granted.
    //

    if (AccessAllowed) {

        AccessState->PreviouslyGrantedAccess |= GrantedAccess;
        AccessState->RemainingDesiredAccess &= ~(GrantedAccess | MAXIMUM_ALLOWED);
    }

    //
    //  Audit the attempt to open the object, audit
    //  the creation of its handle later.
    //

    if ( SecurityDescriptor != NULL ) {

        SeOpenObjectAuditAlarm( &ObjectType->Name,
                                Object,
                                NULL,                    // AbsoluteObjectName
                                SecurityDescriptor,
                                AccessState,
                                FALSE,                   // ObjectCreated (FALSE, only open here)
                                AccessAllowed,
                                AccessMode,
                                &AccessState->GenerateOnClose );
    }

    SeUnlockSubjectContext( &AccessState->SubjectSecurityContext );

    //
    //  Free the security descriptor before returning to
    //  our caller
    //

    ObReleaseObjectSecurity( SecurityDescriptor,
                             MemoryAllocated );

    return( AccessAllowed );
}

綜上所述,當一個線程打開一個對象時所執行的安全檢查過程如圖

一旦線程成功地打開了一個對象,以後當它在該對象上執行操作時,將使用句柄來引用該對象,而對象管理器或其他內核組件在將句柄解析成對象時仍然要檢查調用者所請求的操作是否已被授權或拒絕。例如,ObReferenceObjectByHandle函數使用宏SeCompute-DeniedAccesses來判斷要請求的訪問是否被拒絕;ObReferenceFileObjectForWrite函數使用宏SeComputeGrantedAccesses來判斷所請求的文件操作是否已被授權。此外,I/O管理器也使用同樣的方法來檢查它所接到的訪問操作。

至此,我們看到了SRM中自主訪問控制的實現過程。SRM的強制訪問控制是通過特權檢查來實施的,SePrivilegeCheck函數是SRM中用於特權檢查的內核接口函數。

在Windows中,特權是由LUID對象來標識的,LUID代表一個本地唯一標識符(Locally UniqueIdentifier),由兩個LONG成員構成,因此在32位系統中LUID是一個64位的整數。每一個特權都附帶一些屬性,兩者結合起來構成了LUID_AND_ATTRIBUTES數據結構,而屬性是一個無符號長整數類型。

在令牌TOKEN數據結構的定義中,有一個Privileges成員,代表該令牌賬戶的所有特權,因此,SepPrivilegeCheck函數的實現是,用一個二重循環,對調用線程所請求檢查的每一個特權,遍歷Token參數中的Privileges數組成員,看是否能匹配上。最後檢查總共匹配了多少特權,是否所請求的特權均已匹配上,或至少匹配了一個。

Windows內核中定義了一組特權,即類型爲LUID的Se<Xxx>全局變量,其定義位於base\ntos\se\seglobal.c文件中,對於每一種特權,當相關聯的操作(即需要此特權才能進行的操作)在內核中被適當的組件激發時,應通過SePrivilegeCheck或它的包裝函數SeSinglePrivilegeCheck,來檢查調用線程是否具有相應的特權。在Windows內核中,與安全相關的函數以“Se”作爲前綴,有一些安全函數還存在對應的系統服務。這些系統服務函數的名稱以“Nt”作爲前綴,後面部分與“Se”函數相同。

 

 

發佈了168 篇原創文章 · 獲贊 17 · 訪問量 4萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章