發布日期: 4/1/2004 | 更新日期: 4/1/2004
摘要:本文概述了 Microsoft .NET 框架安全系統的基本特性,包括對于動態下載和執行模式以及遠程執行模式,限制代碼在嚴格約束的、管理員定義的安全上下文中運行的能力。(21頁打印頁)
Demien Watkins 博士,項目 42Sebastian Lange,Microsoft Corporation
2002 年 1 月
本頁內容
簡介
傳統的程序開發模式中,管理員通常將軟件安裝到本地磁盤的一個固定位置;當從這種模式轉向支持動態下載和執行、甚至支持遠程執行的環境時,安全性是要考慮的一個最重要的因素。為了支持這種模式,Microsoft .NET 框架提供了一個強健的安全系統,該系統可以限制代碼在嚴格約束的、管理員定義的安全上下文中運行。本文研究了 .NET 框架中的一些基本的安全性特性。
許多安全模型將安全性與用戶和它們的組(或角色)關聯起來。這就意味著,用戶和代表這些用戶運行的所有代碼或者被允許對重要資源執行操作,或者不被允許執行操作。在大多數操作系統中安全性都是按照這種模型構建的。.NET 框架提供了一種開發人員定義的安全模型,稱為基于角色的安全性,它也是按照這種類似的結構運行的。基于角色的安全性的主要抽象是 Principals 和 Identity。此外,.NET 框架也在代碼上提供了安全性,這種安全性稱為代碼訪問安全性(也稱為基于證據的安全性)。使用代碼訪問安全性,某個用戶可能會獲得信任以訪問某個資源,但是如果用戶執行的代碼不受信任,則訪問資源也會被拒絕。與基于特定用戶的安全性對比,基于代碼的安全性是允許安全性得以在可移動代碼 (mobile code) 上體現的一個基本工具。可移動代碼可能會由任意數量的用戶下載和執行,而所有這些用戶在開發時都是不了解的。代碼訪問安全性主要集中于一些核心的抽象,它們是:證據、策略和權限。基于角色的安全性和代碼訪問安全性的安全抽象是用 .NET 框架類庫中的類型來表示的,而且是用戶可擴展的。這里有兩個值得注意的挑戰:以一致和連貫的方式向一些編程語言公開安全模型,以及保護和公開 .NET 框架類庫中代表資源(使用這些資源可能會導致安全性破壞)的類型。
.NET 框架安全系統是在傳統操作系統安全性上層運行的。這種方式在操作系統安全性上又增加了一層更具表現力和可擴展的安全性。這兩層安全性相互補充。(操作系統安全系統也可以將一些責任委托給托管代碼的公共語言運行庫安全系統,因為該運行庫安全系統比傳統的操作系統安全性粒度更細,可配置性也更強。)
本文提供了 .NET 框架安全性方面的一個概述,具體講述了基于角色的安全性、驗證、代碼訪問安全性和堆棧審核方面的內容。并且使用一些小的編程示例揭示一些概念。本文沒有論及其他的運行庫安全工具,如加密和獨立存儲。
順便提一下,本文通常描述的是上面這些安全特性的默認行為。然而 .NET 框架安全系統具有極強的可配置性和極強的可擴展性。這就是該系統的一個主要優點,但遺憾的是,在這篇概念性描述的文章中不能詳細地對這一點進行討論了。
執行概述
運行庫既執行托管代碼,又執行非托管代碼。托管代碼是在運行庫的控制下執行的,因此可以訪問運行庫提供的服務,如內存管理、實時 (JIT) 編譯,以及本文涉及的最重要的安全性服務(如安全策略系統和驗證)。非托管代碼是經過編譯、要運行在特定硬件平臺上的代碼,它不可直接利用運行庫。然而,當語言編譯器生成托管代碼時,該編譯器的輸出是以 Microsoft 中間語言 (MSIL) 表示的。MSIL 通常被描述為一種用于抽象的、基于堆棧計算機、面向對象的偽匯編語言。之所以說 MSIL 是面向對象的,是因為它有一些支持面向對象概念的指令,如對象分配 (newobj) 和虛函數調用 (callvirt)。之所以說是抽象的計算機,是因為 MSIL 不依賴于任何特定的平臺。就是說,它對于在其上運行的硬件不會作出任何假設。它之所以是基于堆棧的,是因為從本質看,MSIL 是通過向堆棧壓入 (push) 值和從中彈出 (pop) 值以及調用方法來執行的。MSIL 通常會在執行前被實時編譯為本機代碼。(MSIL 也可以在運行該代碼前編譯為本機代碼。這有助于縮短程序集的啟動時間,但是 MSIL 代碼通常是在方法級別進行 JIT 編譯的。)
驗證
在運行庫會進行兩種形式的驗證:MSIL 驗證和程序集元數據驗證。運行庫中的所有類型都指定了它們將要實現的協定,這些信息將作為元數據與 MSIL 一起保留在托管 PE/COEFF 文件中。例如,如果一個類型指定它從另一個類或接口進行繼承,這就表明它將要實現一些方法,這就是一個協定。協定也可以與可見性聯系起來。例如,可以將類型聲明為從它們的程序集公開(導出)或其他的內容。因為類型安全只能根據它們的協定訪問類型,所以就此而言,它是代碼的一個屬性。可以驗證 MSIL 以證明它是類型安全的。驗證是 .NET 框架安全系統中的一個基本構造塊,目前只在托管代碼上執行驗證。因為非托管代碼不可由運行庫進行驗證,所以由運行庫執行的非托管代碼必須是完全受信任的。
為了理解 MSIL 驗證,關鍵在于理解如何對 MSIL 進行分類。MSIL 分為下列類型:無效的 MSIL、有效的 MSIL、類型安全的 MSIL 以及可驗證的 MSIL。
注 應該指出的是,下面的定義比標準定義提供了更多信息。有關更加準確的定義版本,請參閱其他文檔,如 ECMA standard:
? |
無效 MSIL 是 JIT 編譯器不能為它生成本機表示的 MSIL。例如,不可將包含無效操作碼的 MSIL 翻譯成本機代碼。另一個示例是跳轉指令,該指令的目標是操作數的地址,而不是操作碼。
|
? |
有效 MSIL 可以被認為是滿足 MSIL 語法的所有 MSIL,因此它可以用本機代碼表示。這種分類包括這樣的 MSIL,即,使用非類型安全形式的指針算法獲取對類型成員的訪問的 MSIL。
|
? |
類型安全 MSIL 只通過它們的向公眾公開的協定與類型交互。試圖從一個類型中訪問另一個類型的私有成員的 MSIL 就不是類型安全的。
|
? |
可驗證 MSIL 是可以通過一個驗證算法來證明是類型安全的 MSIL。驗證算法比較保守,因此一些類型安全的 MSIL 可能不會通過驗證。可驗證 MSIL 自然既是類型安全的又是有效的,當然不是無效的。
|
除了類型安全檢查之外,運行庫中的 MSIL 驗證算法還會檢查堆棧上溢/下溢的發生、異常處理工具的正確使用以及對象初始化。
對于從磁盤加載的代碼,驗證過程是 JIT 編譯器的一部分,它會在 JIT 編譯器中間歇性地進行。驗證和 JIT 編譯不是作為兩個獨立的進程來執行的。如果驗證期間在程序集中找到了一連串不可驗證的 MSIL,那么安全系統就會檢查該程序是否足夠受信任,可以跳過驗證。例如,如果一個程序集是在安全模型的默認設置下從本地硬盤上加載的,那可能就是這樣的情況。如果程序集受信任可跳過驗證,MSIL 則會翻譯成本機代碼。如果程序集的受信任程度不夠,不可跳過驗證,則會用一個存根來代替有問題的 MSIL,如果使用了該執行路徑,該存根就會引發一個異常。一個常見的問題是:“為什么不在驗證程序集之前檢查它是否需要驗證呢?”因為驗證通常是作為 JIT 編譯的一部分執行的,所以它通常比檢查是否允許程序集跳過驗證更快。(決定跳過驗證比這里描述的過程更加智能。例如,可以緩存前面一些驗證嘗試的結果,以提供快速的查找方案。)
除了 MSIL 驗證之外,還要驗證程序集元數據。事實上,類型安全依賴于這些元數據檢查,因為它假定 MSIL 驗證期間使用的元數據標記是正確的。在全局程序集緩存 (GAC) 或下載緩存中加載程序集時,會驗證程序集元數據;如果沒有將它插入到 GAC 中,則從磁盤中讀取它時也會驗證程序集元數據。(GAC 是由一些程序使用的程序集的中央存儲。下載緩存保存了從其他位置(如 Internet)下載的程序集。)元數據驗證包括檢查元數據標記和消除緩沖區溢出,前者用于檢查它們是否會正確索引到它們訪問的表中,以及到字符串表的索引是否并不指向長度大于應該保存它們的緩沖區大小的字符串。通過 MSIL 驗證和元數據驗證消除非類型安全的類型安全代碼是運行庫上安全性的第一部分。
代碼訪問安全性
從本質上講,代碼訪問安全性根據程序集證據向程序集分配權限。在決定代碼對哪些資源應該有訪問權限時,代碼訪問安全性會使用可執行代碼從中取得的位置和有關代碼標識的其他信息作為一個主要因素。有關程序集標識的信息稱為 證據。一旦將程序集加載到運行庫用于執行時,宿主環境就會向程序集附加一些證據。運行庫中的代碼訪問安全系統負責將這些證據映射到一個權限集,該權限集又將決定此代碼對一些資源(比如注冊表或文件系統)具有什么訪問權限。這種映射是以可管理的安全策略為基礎的。
對于托管代碼的大多數應用程序方案,所設計的默認代碼訪問安全性策略是安全而且是足夠的。它嚴格地限制了來自不完全受信任或不受信任環境(如 Internet 或本地 Intranet)的何種代碼在本地計算機上執行時能夠運行。因此代碼訪問安全性默認策略模型代表著通往安全的一種可行途徑。默認情況下資源是安全的;管理員需要采取顯式的操作才能使得系統安全性稍差一些。
我們為什么還需要另一種安全方案呢?與用戶標識相對,代碼訪問安全性是以代碼標識為中心的。這使得代碼可以在一個用戶上下文中,以無限數量的信任級別運行。例如,即使運行其中的操作系統用戶上下文允許完全訪問所有系統資源,來自 Internet 的代碼也只能在限定的安全邊界中運行。
現在讓我們來看一下代碼訪問安全系統的主要輸入和輸出:證據和權限。
權限
權限代表著可執行受保護操作的授權。這些操作通常包括對特定資源的訪問。通常,這些操作可以包括訪問資源,如文件、注冊表、網絡、用戶界面或執行環境等。不涉及實際資源的權限的一個示例是跳過驗證功能。
注
System.Security.Permissions.SecurityPermission 類包含一個標志,該標志確定是否允許權限實例的接收者跳過驗證。SecurityPermission 類包含了其他類似的權限標志,它們涵蓋了核心的運行庫技術,如果不正確使用這些技術(如控制為在特定應用程序域中運行的程序集提供的證據的能力),就可能公開安全漏洞。核心運行庫技術是由請求調用方來保護的,以便使得必需的 SecurityPermission 類設置合適的權限標志。
權限的基本抽象是 IPermission 接口,它要求特定的權限類型來實現一組標準的權限操作,如返回與具有相同權限類型的其他權限實例的聯合或子集。
可以將權限整理到一個權限集中,該權限集代表著對各種資源的訪問權限的一種聲明。 System.Security.PermissionSet 類代表著權限的集合。這個類上的方法包括 Intersect 和 Union。這些方法采用另一個 PermissionSet 作為參數,并提供了一個 PermissionSet,該PermissionSet要么是這兩個集合中所有權限的聯合,要么是這兩個集合中所有權限的交集。(運行庫中的權限集合是以一個簡單的、未排序的集合表示的。)有了這些工具,安全系統就可以使用權限集,而不必理解每種權限類型的語義了。這使得開發人員可以擴展權限的層次結構,而無需修改安全引摯的功能。
注 每種權限類型必須派生自要求任何權限類型來實現標準權限操作的 IPermission 接口,這些標準權限操作如聯合、交集、子集和請求方法。這些權限類型不用實現特定于它們包含的權限狀態類型的語義。例如,一個包含文件名的權限將與一個包含簡單布爾值狀態的權限相交而產生不同結果。當權限集 A 與權限集 B 相交時,如果 A 和 B 包含相同權限類型 X 的不同實例,那么權限集類 A 就會調用 X 的實例上的交集方法,而不必知道有關 X 的語義的任何內容。
根據在程序集加載時提供給安全系統的證據,安全系統將授予一個權限集,該權限集代表著訪問各種受保護資源的權限。相反,資源是由權限請求來保護的,該請求會觸發一個安全檢查,以查看是否將一個特定的權限授予了該資源的所有調用方;如果請求失敗,就會引發一個異常。(有一個稱為鏈接請求的特定安全檢查,它只檢查直接調用方。但通常會檢查調用方的整個調用堆棧。)
證據
無論何時向運行庫中加載程序集時,宿主環境都會向安全系統提供該程序集的證據。證據構成了向代碼訪問安全性策略系統輸入的內容,這些輸入決定了程序集會收到哪些權限。
.NET 框架附帶了一些類,在安全系統中,這些類是以證據的標準形式使用的:
? |
區域:與 Internet 中使用的區域具有相同的概念。
|
? |
URL
:一個標識特定資源的特定 URL 文件位置,如 http://www.microsoft.com/test。
|
? |
散列:使用像 SHA1 這樣的散列算法生成的程序集散列值。
|
? |
強名稱:程序集的強名稱簽名。強名稱代表著一種版本化、加密的加強方式,用于引用和標識特定簽名方的一個(或全部)程序集。有關詳細信息,請參閱 .NET Framework SDK。
|
? |
站點:代碼來自的站點。URL 比站點的概念更具體;例如 www.microsoft.com 就是一個站點。
|
? |
應用程序目錄:要從中加載代碼的目錄。
|
? |
發行者證書:程序集的 Authenticode 數字簽名。
|
注 從理論上講,任何托管對象都可以構成證據。上面只是一些在 .NET 框架中具有對應成員條件的類型,因此可以將它們集成到安全策略中,而不必編寫自定義安全對象。有關安全策略和代碼組的詳細信息,請參閱下面的內容。
下面的程序是證據的一個簡單示例,其中證據是在加載程序集時被傳遞到運行庫安全系統的。本例中,mscorlib 是加載的程序集,它是包含了許多運行庫類型(如 Object 和 String)的程序集。
using System;
using System.Collections;
using System.Reflection;
using System.Security.Policy;
namespace AssemblyEvidence
{
class Class1
{
static void Main(string[] args)
{
Type t = Type.GetType("System.String");
Assembly a = Assembly.GetAssembly(t);
Evidence e = a.Evidence;
IEnumerator i = e.GetEnumerator();
while(i.MoveNext())
Console.WriteLine(i.Current);
}
}
}
程序的輸出顯示了對于此程序集向安全系統傳遞了哪些證據。為簡潔起見,已對下面的輸出進行了編輯。安全系統采用這個證據,然后根據管理員設置的安全策略為該程序集生成了一個權限集。
<System.Security.Policy.Zone version="1">
<Zone>MyComputer</Zone>
</System.Security.Policy.Zone>
<System.Security.Policy.Url version="1">
<Url>
file:///C:/winnt/microsoft.net/framework/v1.0.2728/mscorlib.dll
</Url>
</System.Security.Policy.Url>
<StrongName version="1"
Key="00000000000000000400000000000000"
Name="mscorlib"
Version="1.0.2411.0"/>
<System.Security.Policy.Hash version="1">
<RawData>4D5A90000300000004000000FFFF0000B8000000000000...
0000000000000000000000000000000000000000000000000000
</RawData>
</System.Security.Policy.Hash>
安全策略
可管理的安全策略決定了宿主環境提供給程序集的證據和授予該程序集的權限集之間的映射。System.Security.SecurityManager 類實現了這種映射功能。 因此,您可以將代碼訪問安全性策略系統看作一個帶有兩個輸入變量(證據和可管理的安全策略)的函數,可以將程序集特定的權限集看作輸出值。本節重點講述可管理的安全策略系統。
有一些安全管理器可以識別的可配置策略級別,它們是:
? |
企業策略級別
|
? |
計算機策略級別
|
? |
用戶策略級別
|
? |
應用程序域策略級別
|
企業策略級別、計算機策略級別和用戶策略級別可由安全策略管理員進行配置。應用程序域策略級別可以通過宿主以編程的方式來配置。
當安全管理器需要決定安全策略授予程序集的權限集時,它是從企業策略級別開始的。將程序集證據提供給這個策略級別將會從該策略級別授予權限集。通常,安全管理器會以相同的方式繼續收集企業策略級別以下策略級別的權限集。然后這些權限集會進行相交,以生成該程序集的策略系統權限集。所有策略級別都必須首先允許一個特定的權限,然后才能使其進入為該程序集授予的權限集中。例如,如果在程序集的計算期間,企業策略級別沒有授予一個特定的權限,那么不管其他級別指定了什么權限,也不會授予權限。
注 有一些特殊的情況,某個策略級別中(如企業策略級別)可能包含了一條指令,該指令指定不計算該級別之下的任何策略級別,如計算機策略級別和用戶策略級別。在該種情況下,計算機策略級別和用戶策略級別都不會生成一個權限集,并且在授予程序集權限集的計算中不會考慮這兩個級別。
程序集的開發人員可以影響程序集運行庫進行的權限計算。盡管程序集不能簡單地取得運行所需的權限,但它可以聲明一個最低限度需要的權限集或拒絕某些權限。安全管理器會確保只有需要的一個(或多個)權限是策略級別結構授予的權限集的一部分時,程序集才會運行。相反,安全管理器還確保程序集不會收到它拒絕獲取的任何權限。程序集的開發人員可以通過使用安全自定義屬性將最低限度需要權限、拒絕權限或可選權限放入程序集中。有關詳細信息,請參閱下面 聲明性方式和命令性方式 部分或 .NET Framework SDK。
決定授予程序集一個實際權限集的過程包括三個步驟:
1.各個策略級別計算程序集的證據,然后生成特定于策略級別的授予權限集。
2.為每個策略級別計算的權限集彼此進行相交。
3.得到的權限集與程序集聲明的運行所需的權限集或拒絕權限進行比較,然后相應地修改權限的授予。
圖
1.
一個程序集的授予權限集的計算
圖 1 顯示了大概的計算過程。運行庫的宿主提供有關程序集的證據,該證據作為程序集收到權限集的計算的一個輸入。可管理的安全策略(企業策略、計算機策略和用戶策略)是決定程序集的權限集計算的第二個輸入,本文前面稱之為安全性設置。然后安全策略代碼(包含在 SecurityManager 類中)遍歷各個策略級別設置提供的程序集證據,然后生成一個權限集,它代表了該程序集可訪問受保護資源的權限集合。
每個策略級別是如何管理的呢?策略級別表示一個獨立、可配置的安全策略單元 – 每個級別都將程序集證據映射到一個權限集。每個策略級別都具有類似的結構。每個策略級別都由三個部分組成,這三個部分組合在一起用來表示策略級別的配置狀態:
? |
代碼組樹
|
? |
命名權限列表
|
? |
策略程序集列表
|
現在我們來詳細闡述所有策略級別的這些組成部分。
代碼組
每個策略級別的核心是代碼組樹。它表示策略級別的配置狀態。代碼組實質是一個條件表達式和一個權限集。如果程序集滿足該條件表達式,那么就會被授予該權限集。每個策略級別的代碼組集是按樹的形式組織的。一旦條件表達式的計算結果為真,就會授予該權限集,然后繼續在那個分支中遍歷。只要不滿足條件,就不授予權限集,也就不再進一步檢查那個分支。例如,有一個代碼組樹,它是按照下面的這種情況運行的。
圖
2.
策略級別的代碼組樹
注 這里我們只討論用于實現默認安全策略的代碼組類型的代碼組語義。也可以包括自定義編寫的、語義完全不同于這里所描述語義的代碼組。再提一下,安全系統是完全可以擴展的,因此它為引入新的策略計算語義提供了無限的可能性。
假設有一個程序集,它具有下列證據:它來自 www.monash.edu.au,因為它來自 Monash University 的 m-Commerce Center,所以它的強名稱為 mCommerce。
代碼組樹遍歷則會按照下列方式進行:
根節點有一個任何代碼都滿足的條件 "all code"。因此將 "all code" 權限授予了我們的這個程序集,這個權限集稱為 ”Nothing”,它不允許代碼具有任何權限。下一個檢查的代碼組是要求從 My Computer 加載代碼的代碼組。因為這個條件不滿足,所以未授予權限集,因此也不會檢查這個條件的任何下級節點。然后我們返回上一個成功授權的代碼組(本例中是 all code),然后繼續檢查它的下級節點。下一個代碼組是 Zone:Internet。因為我們的代碼是從 Internet 上下載的,所以滿足這個條件,從而授予了權限集(可能是 Internet 權限集),然后您就可以繼續檢查這個分支中的下一個下級代碼組了。下一個代碼組有一個 Url: 條件:指出代碼來自 www.microsoft.com。由于代碼是來自 www.monash.edu.au 的,所以不滿足這個條件。此時我們返回到 Zone:Internet 代碼組,查找它下面的其他節點。我們為 URL:www.monash.edu.au 查找節點。由于滿足了這個條件,我們得到了 MonashPSet 權限集。接下來我們為 Strong Name:m-Commerce center 查找節點。由于滿足這個條件,我們得到了 m-Commerce 權限集。因為這個級別下面沒有代碼組,所以我們返回到匹配條件并且具有下級代碼組的上一個代碼組,然后繼續。
最終,滿足的條件和從這個策略級別中授予的權限集包括:
? |
條件:All code,權限集:Nothing
|
? |
條件:Zone:Internet,權限集:Internet
|
? |
條件:URL:www.monash.edu.au,權限集:MonashPSet
|
? |
條件:Strong Name:m-Commerce,權限集:m-CommercePSet
|
在一個策略級別找到的適用于某個特定程序集的所有權限集通常會進行聯合,以生成該策略級別授予的總權限集。
檢查一個策略級別的代碼組樹是非常簡單的。附錄 A 描述了一個 Microsoft 管理控制臺單元,該單元提供了一個用于查看和修改代碼組的層次結構(以及策略級別的其他所有可配置組成部分,請參閱下面的內容)的可視界面。
命名的權限集
一個策略級別包含一個命令權限集列表。每個權限集代表著一個信任聲明,用于訪問各種受保護資源。命名的權限集是代碼組按其名稱進行引用的一些權限集。如果滿足了代碼組的條件,那么就授予被引用的命名權限集(請參閱上面的示例)。下面是一些預定義的命名權限集示例:
? |
FullTrust
:允許不受限制地訪問系統資源。
|
? |
SkipVerification
:可以跳過驗證的程序集。
|
? |
Execution
:允許代碼執行。
|
? |
Nothing
:不授予權限。不授予執行的權限可有效停止代碼的運行。
|
? |
Internet
: 適合來自 Internet 的代碼的權限集。代碼將不能收到對于文件系統或注冊表的訪問權限,但可以執行一些有限的用戶界面操作,并且可以使用稱為獨立存儲的安全文件系統。
|
為了查看策略級別的權限集,只要在附錄 A 提及的 GUI 工具中打開策略級別節點,然后打開權限集文件夾即可。
下面是一個很小的示例程序,它列出了所有策略級別上所有已知的命名權限集:
下面的程序顯示了所有策略級別上的命名權限集列表。該應用程序是一個從本地磁盤上運行的 C# 程序,因此它會從默認的策略設置中收到一個相當強大的權限集。
using System;
using System.Collections;
using System.Security;
using System.Security.Policy;
namespace SecurityResolver
{
class Sample
{
static void Main(string[] args)
{
IEnumerator i = SecurityManager.PolicyHierarchy();
while(i.MoveNext())
{
PolicyLevel p = (PolicyLevel) i.Current;
Console.WriteLine(p.Label);
IEnumerator np = p.NamedPermissionSets.GetEnumerator();
while (np.MoveNext())
{
NamedPermissionSet pset = (NamedPermissionSet)np.Current;
Console.WriteLine("\tPermission Set: \n\t\t Name: {0}
\n\t\t Description {1}",
pset.Name, pset.Description);
}
}
}
}
}
該程序的輸出。為簡潔和明確起見,已對該輸出進行了編輯。
Enterprise
Permission Set:
Name: FullTrust
Description: Allows full access to all resources
Permission Set:
Name: LocalIntranet
Description: Default rights given to applications
on your local intranet
...
Machine
Permission Set:
Name: Nothing
Description: Denies all resources, including the right to execute
...
User
...
Name: SkipVerification
Description: Grants right to bypass the verification
Permission Set:
Name: Execution
Description: Permits execution
...
策略程序集
在安全計算期間,可能需要加載其他的程序集以在策略計算過程中使用。例如,一個程序集可以包含代碼組發出權限集的用戶定義權限類部分。當然也需要計算這個包含自定義權限的程序集。如果這個自定義權限的程序集被授予包含它自己實現的自定義權限的權限集,那么就會引發循環依賴項。為了避免這種情況,每個策略級別都包含了一個它在策略計算中所需的受信任程序集列表。這個必需程序集的列表自然地被稱為了“策略程序集”列表,它包含了在這個策略級別實現安全策略所需的所有程序集的過渡閉包。包含在該列表中的所有程序集的策略計算是短路的,以避免發生循環依賴項。該列表可以使用附錄 A 提及的GUI 管理工具來修改。
這就完成了每個策略級別上的可配置組成部分的檢查:代碼組樹、命名權限集列表和策略程序集列表。現在該來看一下從安全策略狀態派生的授予權限集是如何在由每個策略級別的配置實例化時,與安全強制的基礎結構進行連接的。換言之,迄今為止,我們只討論了程序集是如何收到授予的權限集的。如果沒有一個基礎結構要求程序集具有某一級別的權限,那么安全系統將不能發揮任何作用。事實上,使安全強制成為可能的技術是安全堆棧審核。
堆棧審核
堆棧審核是安全系統的重要組成部分。堆棧審核以如下方式進行操作。每次調用方法時,都會在堆棧中放入一個新的激活記錄。這個記錄包含了要傳遞給該方法的參數(如果有參數的話)、此函數結束時要返回的地址以及任何局部變量。當程序執行時,堆棧就會隨著函數的調用而增長和收縮。在執行中的某些階段,該線程可能需要訪問系統資源,如文件系統。在允許這種對受保護資源的訪問之前,可能需要進行一次堆棧審核來驗證調用鏈中的所有方法是否具有訪問系統資源的權限。在這一階段會進行堆棧審核,并且通常會檢查每個激活記錄,以查看這些調用方是否確實具有所需的權限。與完全堆棧審核相比,CAS 系統還允許開發人員使用鏈接時間檢查來批注資源,這種鏈接時間檢查只檢查直接調用方。
修改堆棧審核
在執行期間的任何階段,在某個函數訪問特定資源之前,它都可能需要檢查其調用方的權限。在這一階段,該函數可以要求對一個特定的權限或權限集進行安全檢查。這就會觸發堆棧審核,其結果是:如果所有的調用方都具有授予的權限,那么該函數就繼續執行;如果調用方不具有所需的一個(或多個)權限,就會引發異常。下圖說明了這個過程。
圖
3.
堆棧審核示例
函數可以選擇修改堆棧審核,有一些機制可完成這種修改。首先,一個函數可能需要保證調用它的多個函數。在這種情形下,它可以斷言一個特定的權限。如果發生了查找斷言的權限的堆棧審核,那么在檢查這個函數的激活記錄尋找該權限時,如果該函數具有它斷言的權限,則該檢查將成功,堆棧審核也會終止。斷言本身是一個受保護操作,因為它將向斷言訪問受保護資源權限的函數的所有調用方開放對該受保護資源的訪問權限。因此,在運行庫中,安全系統會檢查包含自我斷言的函數的程序集是否具有它試圖斷言的權限。
修改堆棧審核的另一種辦法是支持函數拒絕權限。當一個函數知道它不應該訪問某個資源并拒絕權限時,就可能發生這種情形。PermitOnly 提供了類似 deny 的功能,因為它會導致堆棧審核失敗。但 deny 指定的是會導致堆棧審核失敗的權限集,而 PermitOnly 指定的則是繼續堆棧審核所需的權限集。
注 當使用 Deny 堆棧修飾符時,您應該非常謹慎。如果早期的堆棧幀是一個斷言,則會忽略 Deny 修飾符。另外,拒絕基于路徑的權限是相當困難的,這是因為經常有各種不同的路徑字符串實際是指向相同位置的。拒絕一個特定路徑表達式仍會開放其他的路徑。
還有最后一個需要知道的要點。在任何時刻,一個堆棧幀只能有一個 Deny,一個 PermitOnly 和一個 Assert 處于有效狀態。例如,如果開發人員需要斷言許多權限,他們就應該創建一個 PermissionSet 來表示該集合,并只進行一個單獨的斷言。有一些方法可用來刪除一個堆棧審核修飾符的當前 PermissionSet 設置,以便注冊其他的權限集。這樣方法的一個示例是 System.Security.codeAccessPermission.RevertPermitOnly。
下面的示例說明了前面介紹的各種堆棧修改技術:
using System;
using System.Security;
using System.Security.Permissions;
namespace PermissionDemand
{
class EntryPoint
{
static void Main(string[] args)
{
String f = @"c:\System Volume Information";
FileIOPermission p =
new FileIOPermission(
FileIOPermissionAccess.Write, f);
p.Demand();
p.Deny();
p.Demand();
CheckDeny(p);
p.Assert();
CheckDeny(p);
}
static void CheckDeny(FileIOPermission p)
{
try
{
p.Demand();
}
catch(SecurityException)
{
Console.WriteLine("Demand failed");
}
}
}
}
前面的程序產生了下面的輸出,這些輸出起先看起來很不直觀:
Demand failed
Demand failed
上面的代碼示例中,盡管第一個 Demand 訪問的是一個受限制系統目錄,但它還是成功了。記住,運行庫安全系統是在基礎操作系統設置上面的。因此,可以讓運行庫安全策略對某些目錄授予這樣的訪問權限,即托管代碼實際試圖訪問這些目錄時,它將會因為這樣做引發操作系統訪問沖突。Deny 后面的下一個 Demand 也成功了。當執行 Demand 時,沒有檢查請求函數的激活記錄,而只檢查它的調用方。因此,盡管函數已經拒絕訪問,但也不會被 Demand 檢測到。調用 CheckDeny 和后面的 Demand 確實失敗了。現在檢查前面方法中的 Deny,因為它位于調用方的堆棧幀中。接下來我們返回到 Main 并進行一個 Assert。這里已經斷言的一個權限在這個堆棧幀中也被拒絕了。我們進入 CheckDeny ?±,Demand 再次引發一個異常,但這是為什么呢?從本質上講,Deny 重寫了 Assert;這是因為 Deny 權限集總是在 Assert 權限集之前進行檢查。
概括起來,使托管資源可以引發托管安全堆棧審核的功能是保護資源的運行庫安全系統方法。授予的權限集是程序集從每個策略級別上運行的授權計算收到的,該權限集會與資源請求的權限進行對照檢查。如果后者形成了前者的一個子集,那就可以訪問受保護資源。除非已像上面描述的那樣對堆棧進行了修改,否則就會對調用鏈中托管資源的所有調用方執行這種子集檢查。因此安全堆棧審核綜合了運行庫安全系統的這兩個方面:1} 據和權限之間的可配置映射;2) 通過強制所有的調用方擁有一定級別的權限來保護資源。
實際上有兩種不同的方式可用于以編程的方式表達堆棧審核請求和堆棧修改操作 — 聲明性安全和命令性安全。
聲明性方式和命令性方式
.NET 框架允許開發人員以兩種方式表達安全約束。以 聲明性方式 表達安全約束意味著使用自定義屬性語法。這些批注保留在類型的元數據中,在編譯時會有效地溶入到程序集中。下面是聲明性方式安全的一個示例:
[PrincipalPermissionAttribute(SecurityAction.Demand,
Name=@"culex\damien")]
以命令性方式表達安全要求意味著在運行時創建 Permission 對象的實例并調用它們的方法。下面是本文前面示例中聲明性方式安全的一個示例:
FileIOPermission p = new FileIOPermission(
FileIOPermissionAccess.Write, f);
p.Demand();
為什么選擇一種方式而不選擇另一種方式的原因有幾個。首先,可以使用聲明性方式來表達所有的安全操作;但對于命令性方式,則不行。不過,聲明性方式要求在編譯時表達所有安全約束,而且只允許在完全方法、類或程序集范圍內存在批注。命令性方式更加靈活,因為它允許在運行時表達約束,如對于方法中可能執行路徑的子集。將命令性安全要求保留在元數據中的一個副作用是,有一些工具可提取這些元數據信息并根據這些信息提供功能。例如,某個工具可以顯示程序集上聲明性安全屬性集的列表。而對于命令性安全,這種情況是不可能發生的。開發人員需要了解并熟悉這兩種方式。
忠告
因為按照默認策略,從本地硬盤上執行的代碼將會明顯比從其他任何位置執行的代碼取得更多的信任,所以下載代碼、將代碼儲存到磁盤然后執行它與從遠程位置執行代碼具有完全不同的語義。對于以前的系統,這種情況不是很明顯,例如瀏覽器選擇的是下載代碼而不是遠程執行它。這里假定在代碼下載后,執行前還會對它進行檢查,比如病毒掃描。使用了代碼訪問安全性,情況就完全不同了。如果使用默認的安全策略,則將代碼作為遠程代碼執行會明顯增加安全性。然而這時可能會增加系統或用戶的負擔,因為他們需要知道托管代碼和非托管代碼的區別。最后是有關運行庫安全系統的另一方面。對于舊式安全系統的用戶來說,他們應該更熟悉這個系統,因為它是以用戶標識為基礎的:基于角色的安全性。
基于角色的安全性
迄今為止介紹的代碼訪問安全系統,基本上是以代碼標識為中心的,而不是以用戶或角色為中心的。然而仍需要根據用戶標識來表達安全設置。因此運行庫安全系統也附帶了基于角色的安全性特性。
基于角色的安全性利用了用戶和角色的概念,這與目前許多操作系統中的安全實現相類似。基于角色的安全性中的兩個核心抽象是 Identity 和 Principal。 Identity 是代碼執行時所代表的用戶。請謹記,它們可能是應用程序或開發人員定義的邏輯用戶,而不必是操作系統可以看到的用戶。 Principal 代表了用戶和用戶所屬角色的抽象。代表用戶標識的類實現 Identity 接口。在 .Net 框架中,提供這個接口的默認實現的一般類是 GenericIdentity。代表 Principal 的類實現 IPrincipal 接口。在 .Net 框架中,提供這個接口的默認實現的一般類是 GenericPrincipal。
在運行庫,每個線程都有且只有一個當前的 Principal 對象與之相關聯。當然代碼可以根據安全要求來訪問和更改這個對象。每個 Principal 都有且只有一個 Identity 對象。從邏輯上講,對象的運行庫結構與下面的內容相類似:
圖
4.
基于角色安全性結構
下面的程序說明了開發人員可以如何使用這些一般類。本例中,開發人員正在提供安全模型。例如,名稱 “Damien” 和角色 “Lecturer“、“Examiner“ 與操作系統可以支持的任何用戶和角色都無關。
using System;
using System.Threading;
using System.Security;
using System.Security.Principal;
namespace RoleBasedSecurity
{
class Sample
{
static void Main(string[] args)
{
String [] roles = {"Lecturer", "Examiner"};
GenericIdentity i = new GenericIdentity("Damien");
GenericPrincipal g = new GenericPrincipal(i,
roles);
Thread.CurrentPrincipal = g;
if(Thread.CurrentPrincipal.Identity.Name ==
"Damien")
Console.WriteLine("Hello Damien");
if(Thread.CurrentPrincipal.IsInRole("Examiner"))
Console.WriteLine("Hello Examiner");
if(Thread.CurrentPrincipal.IsInRole("Employee"))
Console.WriteLine("Hello Employee");
}
}
}
程序產生了如下的輸出:
Hello Damien
Hello Examiner
如果開發人員希望的話,也可以使用 Microsoft (R) Windows (R) 安全模型。在這種情形中,用戶和角色會與宿主計算機中的用戶和角色緊密相連,因此可能需要在宿主系統上創建這些帳戶。下面的示例使用的是本地計算機上的用戶帳戶。本例中也使用了一些 syntactic sugar(語法糖塊),.NET 框架中的 PrincipalPermissionAttribute 類有效地封裝了一些方法的調用(如 IsInRole),以使得開發人員可以使用簡化的語法。
namespace RoleBased
{
class Sample
{
[PrincipalPermissionAttribute(SecurityAction.Demand,
Name=@"culex\damien")]
public static void UserDemandDamien()
{
Console.WriteLine("Hello Damien!");
}
[PrincipalPermissionAttribute(SecurityAction.Demand,
Name=@"culex\dean")]
public static void UserDemandDean()
{
Console.WriteLine("Hello Dean!");
}
static void Main(string[] args)
{
AppDomain.CurrentDomain.SetPrincipalPolicy(
PrincipalPolicy.WindowsPrincipal);
try
{
UserDemandDamien();
UserDemandDean();
}
catch(Exception)
{
Console.WriteLine("Exception thrown");
}
}
}
}
PrincipalPermissionAtribute 保證了每次調用 UserDemandDamien 和 UserDemandDean 方法時都會進行運行庫檢查。當然,程序可能由 Dean、Damien 也可能由其他人執行,因此對這兩個方法調用的安全檢查即使未全部失敗,也至少有一個會失敗。Main 的第一行將用戶策略設置成了 Windows(執行本例的操作系統)的用戶策略。當用戶 ”culex\damien” 執行程序時將產生如下輸出:
Hello Damien!
Exception thrown
小結
安全性是 .NET 框架的一個基本和內置的因素。本文旨在對安全系統進行一下概述。可以從本文總結出來的一些主要概念有:
? |
安全系統是可擴展的,因為在 .NET 框架中,很多概念都是以類型表示的,因此開發人員可根據他們自己的需要進行擴展和修改。
|
? |
安全系統提供了不同類型的安全模型,具體地說,有基于角色的安全模型和基于證據的安全模型。這些不同的模型滿足了不同的需要,并且是相互補充的。
|
? |
代碼訪問安全性是以代碼的標識為中心的,因此即使代碼運行的操作系統用戶上下文授予了它對計算機的管理權限,也只允許在不完全受信任的安全上下文中執行該代碼。
|
本文沒有論及像加密這些方面的安全系統問題,也沒對任何方面進行非常詳細的論述。請參閱白皮書,特別是有關這些主題的內容,以了解本文沒有討論的一些細節。
致謝
在編寫本文的過程中,Brian Pratt、Loren Kohnfelder 和 Matt Lyons 提供了大力的幫助和支持,謹在此表示感謝。
附錄 A:MsCorCfg.msc
有一個 Microsoft 管理控制臺單元使我們能夠可視化地操縱代碼訪問安全性策略。下圖顯示了這個管理單元的界面,其中突出顯示了本文論及的一些概念。
圖
5. Microsoft
管理控制臺單元界面
您可以在 Control Panel 中單擊 Administrative Tools,然后單擊 Microsoft .NET Framework Configuration 快捷鍵,來訪問該工具。
http://wayfarer.cnblogs.com/articles/48022.html