天天看點

Windows Vista for Developers——第四部分:使用者帳号控制(User Account Control,UAC)

自從Windows 2000以來,Windows開發者一直試圖為使用者創造一個安全穩妥的工作環境。Windows 2000引入了一種名為“受限通路令牌(Restricted Token)”的技術,能夠有效地限制應用程式的許可和權限。Windows XP則在安全方面更進一步,不過對于普通使用者來講,這種安全控制卻并不是那麼的深入人心……直到現在為止還是如此。不管你最初反對的理由是什麼,現在使用者帳号控制(User Account Control,UAC)就擺在你的面前,其實它并不像批評中所說的那樣一無是處。作為開發者的我們有責任掌握這項技術,進而讓我們所開發的Vista應用程式不會總是彈出那些“讨厭”的提示視窗。

什麼是安全上下文(Security Context)?

安全上下文指的是一類定義某個程序允許做什麼的許可和權限的集合。Windows中的安全上下文是通過登入會話(Logon Session)定義的,并通過通路令牌維護。顧名思義,登入會話表示某個使用者在某台計算機上的某次會話過程。開發者可以通過通路令牌與登入會話進行互動。通路令牌所有用的許可和權限可以與登入會話的不同,但始終是它的一個子集。這就是UAC工作原理中的最核心部分。

那麼UAC的工作原理是什麼呢?

在Windows Vista作業系統中,有兩種最主要的使用者帳号:标準使用者(stand user)和管理者(administrator)。你在計算機上建立的第一個使用者将成為管理者,而後續使用者按照預設設定将成為标準使用者。标準使用者用來提供給那些不信任自己能夠控制整個計算機的使用者,而管理者則為那些希望能夠完全控制計算機的使用者所準備。與先前版本的Windows不同,在Windows Vista中,你不再需要以标準使用者的身份登入到系統中以便防止某些惡意代碼/程式的惡意行為。标準使用者和管理者的登入會話擁有同樣的保護計算機安全的能力。

當一個标準使用者登入到計算機時,Vista将建立一個新的登入會話,并通過一個作業系統建立的、與剛剛建立的這個登入會話相關聯的shell程式(例如Windows Explorer )作為通路令牌頒發給使用者。

而當一個管理者登入到計算機時,Windows Vista的處理方式卻與先前版本的Windows 有所不同。雖然系統建立了一個新的登入會話,但卻為該登入會話建立了兩個不同的通路令牌,而不是先前版本中的一個。第一個通路令牌提供了管理者所有的許可和權限,而第二個就是所謂的“受限通路令牌”,有時候也叫做“過濾通路令牌(filtered token)”,該令牌提供了少得多的許可和權限。實際上,受限通路令牌所提供的通路權限和标準使用者的令牌沒什麼差別。然後系統将使用該受限通路令牌建立shell應用程式。這也就意味着即使使用者是以管理者身份登入的,其預設的運作程式許可和權限仍為标準使用者。

若是該管理者需要執行某些需要額外許可和權限的、并不在受限通路令牌提供權限之内的操作,那麼他/她可以選擇使用非限制通路令牌所提供的安全上下文來運作該應用程式。在由受限通路令牌“提升”到非限制通路令牌的過程中,Windows Vista将通過給管理者提示的方式确認該操作,以其確定計算機系統的安全。惡意代碼不可能繞過該安全提示并在使用者不知不覺中得到對計算機的完整控制。

正如我前面提到的那樣,受限通路令牌并不是Windows Vista中的新特性,但在Windows Vista中,該特性終于被無縫地內建到使用者的點滴操作中,并能夠實實在在地保護使用者在工作(或遊戲)時的安全。

受限通路令牌

雖然在通常情況下,我們不用自行建立受限通路令牌,但了解其建立的過程卻非常有用,因為它可以幫助我們更好地了解受限通路令牌能夠為我們做什麼,進而更深入地了解我們的程式将運作于的環境。作為開發者,我們可能需要建立一個比UAC提供的更為嚴格的限制環境,這時了解如何建立受限通路令牌就顯得至關重要了。

這個名副其實的CreateRestrictedToken 函數用來根據現有的通路令牌的限制建立一個新的通路令牌。該令牌可以用如下的方式限制通路權限:

通過指定禁用安全标示符(deny-only security identifier,deny-only SID)限制通路需要被保護的資源。

通過指定受限SID實作額外的通路檢查。

通過删除權限。

UAC所使用的受限通路令牌在建立時指定了禁用SID并删除了某些權限,而并沒有使用受限SID。讓我們通過一個簡單示例說明這一點。第一步就是得到目前正在使用的通路令牌,以便稍後進行複制并基于它删除某些權限:

接下來需要搞定需要禁用的SID數組,以確定這些SID不能被用來通路資源。下面的代碼使用了我編寫的WellKnownSid 類建立系統内建的管理者組的SID。WellKnownSid 類可以在本文的下載下傳代碼中找到。

然後即可調用CreateRestrictedToken 函數建立該受限通路令牌:

這樣,我們指定了SE_GROUP_USE_FOR_DENY_ONLY 标記所得到的通路令牌就将包含内建的管理者組的SID,不過這些SID是用來禁用,而不是用來允許通路的。我們還剝奪了該通路令牌的SeShutdownPrivilege 權限,保證該令牌不能重新啟動、休眠或關閉計算機。

若你覺得很有意思,那麼可以嘗試做個小實驗。将上面的代碼拷貝到某個控制台應用程式中,然後添加如下的CreateProcessAsUser 函數調用,并相應地更新代碼中Windows Explorer可執行程式的路徑:

現在殺掉計算機中的所有Explorer.exe 程序并運作上述代碼。你會注意到再也無法重新啟動、休眠或關閉計算機了。開始菜單中的這些選項也會被禁用。

最後還要介紹一個函數:IsTokenRestricted。這個函數不會告訴你該通路令牌是否是由CreateRestrictedToken 建立的,但卻會告訴你該通路令牌是否包含受限SID。是以,除非你要使用受限SID,否則這個函數并沒有什麼太大用處。

完整性級别(Integrity levels)

UAC提供了個很少有人注意到的特性,那就是強制完整性控制(Mandatory Integrity Control)。這是一個新的添加到程序和安全描述符(security descriptor)上的授權特性。我們可以為需要安全保護的資源在其安全描述符中指定一個完整性級别。系統中的每個程序也有相應的完整性級别标記,然後即可與資源的完整性級别互相驗證,并提供額外的安全保護。這不但非常簡單,也是個極為有用的特性,能夠幫助你簡單有效地将程序的可通路資源分隔開來。

設想作為開發者的你需要開發一個應用程式,該應用程式必須處理從無法信任的源(例如Internet)中擷取的資料。因為資料中可能包含有惡意代碼,是以你必須想方設法保護計算機的安全,是以為你的程式添加一個“深度防禦(defense in depth)”層就顯得非常有用。其中一個非常有效的解決方案就是使用前面一節中描述的受限通路令牌。但是這種解決方案可能會很複雜,因為你需要明确地指出哪些資源的哪些SID可以被允許、哪些SID需要被禁用,考慮到程式本身也需要一定的權限來正常運作,你不得不做出大量的授權工作。這正是引入完整性級别的意義所在。完整性級别一般用來阻止寫通路,而允許讀通路和運作。而有了讀和運作的權限,程式基本上即可完成大部分的工作,而阻止了寫權限則可以限制其對系統的危害,例如覆寫系統檔案或修改某些路徑資訊等。這也正是IE 7的實作方式。IE 7的部分功能運作于一個低完整性級别的獨立的程序中,隻允許程序修改少數幾個位置的檔案。

使用者态程序可以設定為如下四種完整性級别:

Low

Medium

High

System

按照預設,子程序将繼承父程序的完整性級别。在建立程序時我們可以更改其完整性級别,但一旦建立完畢就不能再更改。另外,我們也不能将子程序的完整性級别設定得高于父程序。這可以阻止低完整性級别的程式借機會竊取更高的完整性級别。

讓我們首先看一下如何查詢并設定某一程序的完整性級别,然後再讨論如何為需要保護的資源設定完整性級别。

程序完整性級别(Process integrity levels)

我們可以通過檢查程序的通路令牌來取得其完整性級别資訊。GetTokenInformation 函數可以傳回不同種類的資訊。例如,若想通過通路令牌圖的目前的使用者帳号,我們可以指定TokenUser 類,然後,GetTokenInformation 函數将基于該通路令牌生成一個TOKEN_USER 結構。類似地,使用TokenIntegrityLevel 類器可查詢該程序的完整性級别,随後将傳回TOKEN_MANDATORY_LABEL 結構。大多數GetTokenInformation 傳回的結構體的長度都是可變的,因為隻有GetTokenInformation 函數本身才知道到底需要多少記憶體空間,是以我們在調用時必須格外小心。因為大多數底層的安全相關函數均使用LocalAlloc 和LocalFree 來配置設定/釋放記憶體,是以我使用了一個名為LocalMemory 的類模闆和一個GetTokenInformation 函數模闆來簡化所需要的工作,該類可以在本文的下載下傳代碼中找到。這裡我們先把注意力放在手頭的主題上:

這裡我們使用了OpenProcessToken 來取得需要查詢的程序的通路令牌。然後調用了我編寫的GetTokenInformation 函數模版(當然,提供了适當的類的資訊),并用LocalMemory 類模闆指定了資訊的類型。函數傳回的TOKEN_MANDATORY_LABEL 結構包含了表示完整性級别的SID。分析這個SID即可得到我們想要的表示完整性級别的相對标示符(relative identifier,RID)。

設定子程序的完整性級别非常直覺簡單。首先複制一份父程序的通路令牌,然後使用前面執行個體程式中用來查詢完整性級别的那些資訊類和資料結構設定其完整性級别。這時即可使用SetTokenInformation 函數。最後調用CreateProcessAsUser 函數,并使用修改過的通路令牌即可建立出需要的子程序。請參考下述代碼:

這個執行個體程式将打開記事本程式。你會注意到,雖然該記事本程式可以打開大多數位置中的文本檔案,但卻無法儲存至任何位置,因為它的完整性級别為低。

還要說一句,我們可以使用LookupAccountSid 函數得到完整性級别的可顯示名稱,但該函數的傳回值對使用者卻并不是那麼友好,是以你最好另外設定一個字元串表,包含類似“低”、“中等”、“高”以及“系統”等文字。

系統為标準使用者建立的通路令牌的完整性級别為中等。系統為管理者建立的受限通路令牌的完整性級别也是中等,但未受限管理者通路令牌的完整性級别為高。

現在讓我們看看如何為指定的資源設定完整性級别。

資源完整性級别(Resource integrity levels)

資源的完整性級别存放在資源安全描述符的系統通路控制表(ystem access control list,SACL)中的一個特殊的通路控制條目(access control entry,ACE)中。更新該值的最簡單方法就是使用SetNamedSecurityInfo 函數。Windows Vista還提供了一個新的名為AddMandatoryAce 的函數,用來将一類特殊的ACE(強制ACE)添加至ACL中。記住,安全相關的縮寫詞總是會讓人一頭霧水……認真地說,若你熟悉安全描述符相關程式設計的話,那麼這段代碼看起來将相當簡單。首先使用InitializeAcl 函數準備了一個足夠容納一個單獨ACE的ACL。接下來建立用SID表示的完整性級别,并使用AddMandatoryAce 函數将其添加至ACL中。最後使用SetNamedSecurityInfo 函數更新完整性級别。注意在下面的代碼中,我們使用了一個新的LABEL_SECURITY_INFORMATION标記:

得到資源的完整性級别也非常簡單,可以看到大多數資源并沒有顯式地設定其完整性級别。系統将沒有顯式聲明完整性級别的資源看作帶有中等完整性級别。首先使用同樣的安全資訊标記LABEL_SECURITY_INFORMATION調用GetNamedSecurityInfo 函數。然後即可簡單地通過GetAce 函數得到指向存儲了完整性級别SID的ACE的指針,随後通過讀取其RID值即可判斷其完整性級别。下面是一段示例:

以管理者身份運作(Run as Administrator)

目前為止,我們已經注意分析了組成UAC的各個部分,例如受限通路令牌和完整性級别等。接下來讓我們看看“以管理者身份運作”是什麼意思,我們如何以程式設計方式實作這個功能。或許你已經注意到了,在Windows Vista中你可以右鍵單擊某個應用程式或快捷方式圖示,并在彈出的上下文菜單中選擇“以管理者身份運作”。無論是管理者還是标準使用者,Vista都提供了這個選項。以管理者身份運作的概念可以簡單地了解為作了一次“提升”或是建立一個“提升”了的程序。若想“以管理者身份運作”,那麼标準使用者需要輸入管理者的使用者名和密碼,而管理者則需要在彈出對話框中進行一次确認。無論那種情況,結果都是一樣的:系統将建立一個擁有不受限制管理者權限的新的程序,該程序擁有系統所有的許可和權限。

程序的“提升”顯得有些複雜,但幸運的是,大多數複雜性都被隐藏在更新版本的ShellExecute(Ex) 函數中了。Windows Vista中的ShellExecute 函數通過一個非公開的COM接口使用新的應用程式資訊服務(Application Information,appinfo)來執行提升操作。ShellExecute 首先調用CreateProcess ,嘗試建立一個新的程序。CreateProcess 負責包括檢查應用程式相容性設定、應用程式清單(application manifest)以及運作時加載器(runtime loader)等任務。若CreateProcess 發現應用程式需要一個“提升”而其調用程序卻沒有提升的話,則函數調用會以ERROR_ELEVATION_REQUIRED失敗告終。然後ShellExecute 調用應用程式資訊服務來處理提升操作并建立被“提升”過的程序,因為調用程序顯然沒有執行該任務所需要的足夠的權限。最後,應用程式資訊服務調用CreateProcessAsUser 以獲得必需的非限制的管理者通路令牌。

還有一種方法:若你隻想要一個經過“提升”了的程序,而不關心使用哪個應用程式資訊服務的話,那麼隻要在ShellExecute中使用這個鮮為人知的“runas”就可以了。無論應用程式清單或相容性資訊有多麼變态,這個指令均可以實作“提升”功能。實際上,runas并不是Windows Vista中的新東西。在Windows XP和Windows 2003中就已經出現了,常用在通過shell直接建立受限通路令牌。可是在Vista中,它的行為卻有了些變化,請參考如下的示例程式:

想想系統在背景默默地為你做了多少工作吧!隻要這麼一行代碼就搞定了如此複雜的功能,是不是覺得很爽呢?雖然建立一個“提升”過的程序有時候顯得很合理,但若你隻想暫時提升一下的話,或許這并不是最恰當的解決方案。讓我們接下來看看如何通過提升COM對象完成同樣的工作。

建立一個被“提升”了的COM對象

若你對COM有所造詣的話,應該知道COM支援我們在一個代理程序中建立COM伺服器。現在這項技術又有了一些發展,我們可以在一個“提升”了的代理程序中建立COM伺服器了。這項技術非常有用,借助于它的幫助,我們就可以在應用程式運作期間簡單地建立一個COM對象,而不必去建立一個全新的程序。

使用這個技術中最難的一部分就是如何正确地注冊該COM伺服器,保證将其加載到一個“提升”了的代理程序中,因為COM對象需要我們顯式地聲明其協作方式。

我們要做的第一件事就是更新COM的注冊,用來保證我們的庫(DLL)伺服器能夠運作于一個代理程序中。隻要将“DllSurrogate”添加至伺服器的AppID系統資料庫鍵中即可。在ATL中,隻要簡單地更新項目的主RGS檔案,如下所示:

DllSurrogate 的空值表示系統提供的代理程序即刻可以使用。現在COM用戶端就能夠指定CLSCTX_LOCAL_SERVER 運作上下文,在該代理程序中建立COM伺服器了:

下一步就是啟用該COM類的“提升”運作。這需要我們在該COM類的注冊腳本中添加一些東西——一個用來表示支援“提升”的提升鍵,以及一個名為“LocalizedString”的值,加上用來顯示在UAC确認對話框中的名稱。ATL中COM類的注冊腳本将類似如下所示:

不要忘記在你的字元串表中為本地化名稱添加一個條目。現在該COM客戶即可啟動該“提升”了的COM伺服器了。CoCreateInstance 不像CreateProcess那樣直接建立一個“提升”了的COM對象,我們需要使用“COM提升名稱(COM elevation moniker)”來完成。最簡單的方法就是使用CoGetObject 函數建立這個名稱(moniker )并傳回最終建立好的對象的代理:

如同ShellExecute一樣,UAC使用該窗體的句柄判斷該提示是否會得到輸入焦點,還是隻在背景默默等待。

使用應用程式清單(application manifests)

還記得我曾經提到過CreateProcess 會檢查應用程式相容性設定以及應用程式清單麼?确實如此,Windows Vista為了確定遺留的32位應用程式能夠正常運作而做了很多努力。以往的應用程式可以輕松地完全控制檔案系統以及系統資料庫,而為了讓其也能夠在UAC所提供的更加嚴格的運作環境中繼續可用,Vista作了令人難以想象的大量的模拟工作。但盡管如此,其核心理念還是盡可能地避免這些模拟。這種模拟隻對那些遺留的應用程式有意義,如果你現在開發新的應用程式的話,那麼請確定提供相應的應用程式清單,以避免這類不必要的模拟。微軟公司也在計劃在後續版本中的Windows中删除對這些模拟的支援。

應用程式清單的架構在Windows Vista下有所更新,應用程式可以在該清單中給出其需要的安全性上下文。但令人不爽的是,Visual C++ 會自動生成應用程式清單。實際上這是件好事。連接配接器始終對應用程式的各個依賴保持清醒,而應用程式清單則用來定義并行程式集之間的依賴。幸運的是,Visual C++ 也提供了将額外的應用程式清單與現有的清單合并的選項,并能夠将合并後的整體清單一起嵌入到應用程式的可執行檔案中。Visual C++ 項目的“Additional Manifest Files”設定正是為此而設。下面就是一份示例應用程式清單,其中聲明了對Common Controls 6.0的依賴,并指定了其希望得到的安全性上下文:

requestedExecutionLevel可以指定為三個值:

asInvoker:預設選項,新的程序将簡單地繼承其父程序的通路令牌。

highestAvailable:應用程式會選擇該使用者允許範圍内盡可能寬松的安全上下文。對于标準使用者來說,該選項與asInvoker一樣,而對于管理者來說,這就意味着請求非限制通路令牌。

requireAdministrator:應用程式需要管理者的非限制通路令牌。運作該程式時,标準使用者将要輸入管理者的使用者名和密碼,而管理原則要在彈出的确認對話框中進行确認。

需要記住的是,CreateProcess 将會檢查應用程式清單,如果所請求的執行級别高與其父程序的完整性級别,那麼這次調用将失敗。隻有ShellExecute 才會使用應用程式資訊服務來執行“提升”。

我真的被“提升”了麼?

如果你想要知道現在是否已經被“提升”過,那麼簡單地調用IsUserAnAdmin 函數即可。如果你還嫌不夠精确,那麼也可以使用GetTokenInformation 函數,但大多數情況下似乎都有些高射炮打蚊子——大材小用了。

結論

這就使我能講出的關于UAC的一切,希望能夠對你有所幫助。這篇文章中的内容基本都不在官方文檔中,是以改變也是在所難免的。

再說一句,本文示例程式中我大多使用了斷言(assertion)及類似的宏來檢查可能發生的異常。這麼做是為了判斷哪些地方應該使用異常處理。如果你想在應用程式中使用部分其中的代碼,那麼請確定将這些宏替換成适合你自己的處理機制,無論是異常、HRESULT還是别的什麼東西。