透过ACL和.net Framework实施对Windows对象访问的管理

通过ACL和.net Framework实施对Windows对象访问的管理

通过ACL和.net Framework实施对Windows对象访问的管理
2010年12月08日
  作者:Mark Pustilnik http://msdn.microsoft.com/en-us/magazine/cc163885. aspx
  相关技术:安全、C#、.NET Framework     [摘要]本文介绍了.NET Framework 2.0中用于保护文件、目录、注册表项和其他对象的类,包括如何允许和禁止规则工作、安全设置的继承和传播,最后介绍了审核规则和对象所有权。
  注:本文基于.NET Framework 2.0(原来的代号为"Whidbey")的预发布版本。文中包含的所有信息均有可能变更。
  目前,形形色色的用户在本地以及通过网络使用计算机系统是已经是一种太常见的情况了。一些由用户产生的任何数据都必须保持隔离和私有,在很长一段时间内,多用户操作系统的设计人员已经意识到需要对文件、目录、同步对象、网络共享以及类似对象的访问进行控制。例如,在Windows中,登录到特定计算机的每个用户通常都分配有他自己单独的"我的文档"文件夹,并且被禁止读写其他用户的文件(除非进行了明确的规定)。可能要求学生用户将作业提交到某个目录中,同时禁止他们改动其内容或者查看其他学生的作业。出版公司可能具有只允许桌面出版部门的雇员访问彩色打印机的策略。对象安全并不限于单个对象,它有时适用于整个层次结构。例如,按照公司策略,我的开发计算机上的Windows源代码只能由Windows组织中的开发人员访问。要列出所有有关对象安全的应用几乎是不可能的。
  Windows系列操作系统已经在一段时期内提供了对保护对象的支持。该功能最开始出现在Windows NT中,并且在Windows的后续版本(Windows 2000、Windows XP和Windows Server 2003)中略有发展。如今,您可以保护NTFS文件和目录、打印机、网络共享、Active Directory??对象和内核对象……最常见的情况是,应用程序开发人员构建的新的安全方案与现有对象的安全性基于相同的技术集。例如,定义文件和Microsoft Exchange Server邮箱的安全性的访问控制列表(Access Control Lists,ACL)是非常类似的。老道的Windows开发人员非常熟悉对安全对象的Windows API支持,并且人们已经撰写了很多有关访问控制项(Access Control Entrie,ACE)、ACL、安全描述符和安全描述符定义语言(Security Descriptor Definition Language,SDDL)的文章。但是,尽管对象安全性具有很大的优势,但操纵安全设置仍然是一项相当困难的开发任务--即使Windows SDK逐渐引入了新的辅助函数,也是如此。所幸,.NET Framework 2.0提供了一种简单的、统一的且可扩展的方式来操纵文件的安全设置、注册表项、目录服务容器和其他对象。本文介绍了为Framework设计的新功能,并且通过代码示例对其进行了说明。     直到现在为止,Microsoft仍未在.NET Framework中提供对操纵安全设置的显式支持。使用.NET Framework 1.x,只能通过一系列麻烦的平台调用(P/Invoke)来授予用户访问权限。更糟糕的是,这不仅要求开发人员熟悉很多与Windows ACL和安全描述符有关的复杂细节,而且所得到的代码可能更易出现各种各样的编程错误,并且其中一些错误具有严重的安全问题。在这一领域内,您可以在任何应用程序中找到一些最严重的安全漏洞。在最能代表此类缺陷的示例中,很多读者可能会回忆起:在对象上设置NULL任意访问控制列表(Discretionary Access ControlList,DACL)会授予每个人完全访问权限,而空的DACL则不会授予任何人任何访问权限。.NET Framework 2.0使开发人员能够使用托管代码来操纵对象的安全设置,并且只需几个简单的步骤,它通过引入安全对象和规则的概念做到这一点。我将在本文中考察新加入的System.Security.AccessControl命名空间。我的大多数示例都将使用文件系统来进行说明。其他对象的保护方式与文件基本相同,但文件系统是最佳的说明对象,因为它同时包含叶子对象(文件)和容器对象(目录)。其他类型的资源管理器通常只具有其中一个类型的对象,例如,注册表是完全分层的,而互斥锁则不具有层次结构。 // Start by opening a file
  using(FileStream file = new FileStream(
  @"M:\temp\sample.txt", FileMode.Open, FileAccess.ReadWrite))
  {
  // Retrieve the file's security settings
  FileSecurity security = file.GetAccessControl();
  // Create a new rule granting Alice read access
  FileSystemAccessRule rule = new FileSystemAccessRule(
  new NTAccount(@"FABRIKAM\Alice"), FileSystemRights.Read,
  AccessControlType.Allow);
  // Add the rule to the existing rules
  security.AddAccessRule(rule);
  // Persist the changes
  file.SetAccessControl(security);
  } 图1 添加文件访问控制
  让我们首先考察如何授予对单个文件的访问权限。在新的方案中,通过在文件安全对象中添加和移除文件访问控制规则来控制对文件的访问(参见图1)。正如您可以看到的那样,更改文件的权限是用多个步骤完成的。首先,打开文件;然后,通过对GetAccessControl方法的调用来检索该文件的安全对象(类型为FileSecurity);除了包含其他内容以外,该对象还包含一组有序的访问规则,它们共同确定了各种用户和组对该文件所具有的权利。在该示例中,将一个新的访问规则添加到FileSecurity对象中,以便向Fabrikam域中名为Alice的用户授予对文件的访问权。在更改生效之前,必须将其持久保存在存储器中。最后这个步骤是通过调用SetAccessControl方法完成的。
  上一个示例说明了如何向现有对象的用户分配访问权。另一个同等重要的方案是在创建对象时,将该对象与安全设置相关联。将访问规则与全新的对象相关联,有一个重要的安全原因:可确保安全的对象总是用一些默认的安全语义创建的。默认情况下,分层式资源管理器(例如,文件系统或注册表)中的对象从其父对象中继承它们的安全设置,文件从它们的父目录中继承它们的安全设置,注册表项从它们的父项中继承它们的安全设置……相比之下,非分层式对象(例如,互斥锁或信号量)具有使用系统默认设置分配给它们的默认权利。默认权利取决于所创建对象的类型,而且可能不是您所希望的那样。例如,您很少会有意创建每个人都具有完全访问权限的对象,但这却可能恰好是默认安全设置所指定的权限。不能简单地用默认安全设置创建对象并且在以后修改这些设置,产生此问题的原因是,在已经创建对象之后对其加以保护会打开一个机会窗口(在创建和修改之间),在此期间该对象可能被劫持。劫持可能导致创建者失去对刚刚所创建对象的控制,这会造成灾难性的后果。图2说明了如何保护全新的对象: // Create a FileSecurity object
  FileSecurity security = new FileSecurity();
  // Create a new rule granting Alice read access
  FileSystemAccessRule rule = new FileSystemAccessRule(
  new NTAccount(@"FABRIKAM\Alice"), FileSystemRights.Read,
  AccessControlType.Allow );
  // Add the rule
  security.AddAccessRule(rule);
  // Create the file passing in the security settings
  FileStream file = new FileStream(
  @"M:\temp\sample.txt", FileMode.CreateNew, FileAccess.ReadWrite,
  FileShare.None, 4096, FileOptions.None, security); 图2 保护一个新建的对象
  现在,这些步骤看起来应当很熟悉了。将执行的是相同的基本操作,但顺序不同,并且无需持久保存更改(因为对象是全新的)。在创建文件之前,将创建一个FileSecurity对象,并且用所需的访问规则填充它。随后,FileSecurity实例被传递给文件的构造函数,该文件从一开始就被正确地加以保护。     有两种类型的访问规则:"允许(allow)"和"拒绝(deny)",您总是可以通过检查规则的AccessControlType属性来确定相应规则的类型。按照约定,拒绝规则总是优先于允许规则。因而,如果您向某个对象中添加下列两个规则:"授予每个人读、写访问权限"和"拒绝Bob写访问权限",则Bob将被拒绝进行写访问,尽管他是具有该权利的组(everyone)的成员。但他仍将被允许进行读取访问。图3说明了如何列出对象的访问规则: // Start by opening a file
  using(FileStream file = new FileStream(
  @"M:\temp\sample.txt", FileMode.Open, FileAccess.ReadWrite))
  {
  // Retrieve the file's security settings
  FileSecurity security = file.GetAccessControl();
  // List each rule in order
  foreach(FileSystemAccessRule rule in
  security.GetAccessRules(true, true, typeof(NTAccount)))
  {
  Console.WriteLine("Rule {0} {1} access to {2}",
  rule.AccessControlType == AccessControlType.Allow ?
  "grants" : "denies",
  rule.FileSystemRights,
  rule.IdentityReference.ToString());
  }
  } 图3 列出一个对象的访问规则
  图3中的代码将生成如下输出: Rule denies Write, Synchronize access to FABRIKAM\Bob
  Rule grants FullControl access to BUILTIN\Administrators
  Rule grants FullControl access to NT AUTHORITY\SYSTEM
  Rule grants FullControl access to FABRIKAM\Alice
  Rule grants ReadAndExecute, Synchronize access to BUILTIN\Everyone     为方便起见,访问规则被公开为集合。该集合是只读的,因此对它的规则进行的所有修改都必须通过FileSecurity对象的专用方法(例如,AddAccessRule、SetAccessRule和RemoveAccessRule)执行。集合内部的规则对象也是不可改变的。要了解为什么拒绝规则优先于允许规则,必须知道访问检查算法是如何工作的。当执行访问权限检查时,将按照规则在访问控制列表内部出现的顺序对它们进行评估。在图3显示的示例中,当检查Bob的访问权限时,会首先评估拒绝Bob读取访问的规则,然后再评估授予BUILTIN\Everyone读取和执行访问权限的规则。一旦做出允许或拒绝的决策,评估就将停止。这就是拒绝规则"生效"的原因。如果它们被放置在允许规则之后,则它们不会总是执行它们的预期功能。
  和可以添加新的访问规则一样,还可以移除现有的访问规则。但是请记住,在从用户那里撤回某项权限和完全拒绝该权限之间存在差异。例如,假设Alice是"全职雇员"组的成员,并且被设置为:"全职雇员可以读取文件"和"Alice具有读写访问权限"。根据这一方案,撤消Alice的读取权利会产生下列规则:"全职雇员可以读取文件"和"Alice具有写入访问权限"。这恐怕不是您所期望的结果,因为撤消Alice的读取访问权限没有产生任何效果:Alice仍然可以作为全职雇员获得读取访问权限。如果您的目标是确保不会将访问权限授予Alice,则达到该目标的唯一方式是添加一个拒绝规则。此外,如果该对象根本不包含任何访问规则,则每个人都将被拒绝对该对象的所有访问权限。     我们迄今为止已经看到的示例都非常简单,部分原因在于它们只考虑了不具有子对象的简单的叶子对象。一旦您从叶子对象(例如,文件、信号量和互斥锁)转向容器对象(例如,目录、注册表项和Active Directory容器),事情就变得稍微复杂一些了。额外的复杂性源自以下事实:容器的访问规则可能被配置为不仅应用于对象本身,而且还应用于它的子对象、子容器或这两者。这就进入了继承和传播设置的领域。
  在我们深入深入该讨论之前,需要介绍一些背景知识。每个访问规则不是显式的就是继承的(IsInherited属性用来确定究竟是哪一种),显式规则是那些已经通过在对象上执行的显式操作添加到该对象的规则。相反,继承规则来自于父容器。在使用对象时,您只能操纵它的显式规则。在向容器中添加新的显式规则时,可以指定两组标志:继承标志和传播标志。继承标志有两个:容器继承(Container Inherit,CI)和对象继承(Object Inherit,OI)。指定容器继承的规则将应用于当前容器对象的子对象。对象继承规则应用于叶子子对象。当传播标志被设置为None时,这些关系是可传递的:它们将跨越当前容器下层次结构的整个子树,并且应用于该容器的子对象、孙子对象等等。 // Retrieve current security settings for the target directory
  DirectorySecurity security = Directory.GetAccessControl( @"M:\temp" );
  // Create new rules
  FileSystemAccessRule ruleAlice = new FileSystemAccessRule(
  new NTAccount(@"FABRIKAM\Alice"), FileSystemRights.Read,
  InheritanceFlags.ContainerInherit, PropagationFlags.None,
  AccessControlType.Allow );
  FileSystemAccessRule ruleBob = new FileSystemAccessRule(
  new NTAccount(@"FABRIKAM\Bob"), FileSystemRights.Write,
  InheritanceFlags.ObjectInherit, PropagationFlags.None,
  AccessControlType.Allow );
  FileSystemAccessRule ruleCarol = new FileSystemAccessRule(
  new NTAccount(@"FABRIKAM\Carol"),
  FileSystemRights.Read | FileSystemRights.Write,
  InheritanceFlags.ContainerInherit | InheritanceFlags.ObjectInherit,
  PropagationFlags.None, AccessControlType.Allow );
  // Add the rules to the existing security settings
  security.AddAccessRule( ruleAlice );
  security.AddAccessRule( ruleBob );
  security.AddAccessRule( ruleCarol );
  // Persist the changes
  Directory.SetAccessControl( @"M:\temp", security ); 图4 授予访问权限
  图4中显示的代码示例将授予Alice对父目录的所有子目录的读取权限。Bob将获得对父目录内部所有文件的写访问权限。最后,Carol将获得对子目录和文件的读取和写入访问权限。在该示例中授予的所有访问权限都将应用于目录对象本身以及指定的子对象。
  既然我们已经了解了继承标志,那么现在我们可以将注意力转移到传播标志了。同样,这类标志分为两种:仅继承(Inherit Only,IO)和非传播继承(No-propagate Inherit,NP)。仅继承标志只是意味着该规则仅应用于子对象,而并不应用于对象本身。因而,可以只将权限授予父目录内部的文件和目录,而不影响父对象本身的安全语义。非传播继承标志稍微棘手一些,幸而它不是很常见。它的存在表明该规则是不可传递的,它将影响子对象,但不会影响孙子对象。值得注意的是,如果不指定一个继承标志,而只是指定传播标志,是没有意义的。如果您尝试以这种方式指定标志,则的确是一个错误。
  
  图5 继承和传播是如何影响容器的
  图5中的图表说明了继承和传播标志的各种组合如何影响容器(Container,C)、子容器(Child Containers,CC)、子对象(Child Objects,CO)、孙子容器(Grandchild Containers,GC)和孙子对象(Grandchild Objects,GO)。对于每组继承和传播标志,图5都用黄色显示了受影响的对象,并且用蓝色显示了不受影响的对象。正如您看到的那样,您可以使用的选项是非常丰富的。 CI OI IO NP 权限(FileSystemRights枚举值) 解释 
  √ √ √ DeleteSubdirectoriesAndFiles 调用者可以删除父目录中的任何文件或目录,但不能删除目录本身 
  √ √ CreateFiles 调用者可以在当前目录或其任何子目录中创建文件 
  √ √ CreateFiles 调用者可以在当前目录的任何子目录中创建文件,但不能在该目录中创建文件 
  √ ReadData 调用者可以从当前目录及其任何子目录中读取所有文件 
  √ √ √ ReadData 调用者可以从当前目录中读取所有文件,但不能从其子目录中读取文件 
  图6 一般场景中的继承和传播
  图6说明了继承和传播标志的各种组合如何帮助满足常见方案的需求。显式和继承规则的布置方式是很有趣的,当您列出容器的访问规则时,它们将构成多个节(参见图7)。
  
  图7 容器访问规则
  正如前面提到的那样,规则的顺序是很重要的,因为它确定了优先顺序并最终影响到对象的访问方式。尽管您无法更改默认顺序,但弄明白一组规则将被授予哪个类型的访问权限是很重要的。最为重要的是,所有继承规则总是跟在显式规则后面。这样,显式规则总是优先于继承规则,接下来,父规则优先于祖父规则,等等。     如果您希望让您的对象避开由其父对象给予它的安全语义,那么会发生什么情况呢?实际上,确实存在使您可以声明"我的父对象的安全设置将不再适用于我"的机制。此时,您甚至可以指定是否希望在该情况下使继承规则保持原样,但是在父对象的设置发生更改时拒绝"侦听",另外,您可以彻底清除所有继承规则。这是通过访问控制保护实现的,如图8所示: // Start by opening a file
  using(FileStream file = new FileStream(
  @"M:\temp\sample.txt", FileMode.Open, FileAccess.ReadWrite))
  {
  // Retrieve the file's security settings
  FileSecurity security = file.GetAccessControl();
  // Protect the rules from inheriting the parent's security settings
  security.SetAccessRuleProtection(
  true, // changes from parent won't propagate
  false ); // do not keep current inheritance settings
  // Persist the changes
  file.SetAccessControl(security);
  } 图8 访问控制保护
  请注意,尽管您可以使用该技术避免从父对象那里收到继承设置,但没有办法收到您的父对象不打算给予您的继承设置,传播只会发生在其ACL没有受到保护的对象身上。您可以做的唯一一件事情就是在父对象改变它的主意之前获得继承设置的快照,这是因为一旦您的访问规则受到保护,它们就将保持这个样子,并且父对象无法重写它们。     "所有者(owner)"的概念对于对象安全性是很特殊的。所有者被赋予了特殊的权力;即使与对象相关联的规则禁止用户访问该对象,但如果该用户是所有者,则他或她仍然可以重写现有规则并重新获得对该对象的控制。完成该操作的过程与访问规则的常规操作过程没有什么不同。安全对象还允许您更改所有者,但是操作系统将禁止其他人执行该操作。通常,为了更改所有者,您必须具有对象的TakeOwnership权限或者具有特殊的"取得所有权(Take Ownership)"特权。以下代码说明了如何更改所有者(假设您具有这样做的权利): // Retrieve the file's security settings
  FileSecurity security = file.GetAccessControl();
  security.SetOwner(new NTAccount(@"FABRIKAM\Dave"));
  // Persist the changes
  file.SetAccessControl(security);     您还可以查看对象的当前所有者是谁,或者请求将所有者作为安全标识符或Windows NT帐户对象返回: // Request the object's owner as a SID
  SecurityIdentifier sid = 
  (SecurityIdentifier)security.GetOwner(typeof(Secur ityIdentifier));
  // Will display something like "S-1-5-21-8-9-10-10000"
  Console.WriteLine(sid.ToString());
  // Request the object's owner as a Windows NT Account:
  NTAccount nta = (NTAccount)security.GetOwner(typeof(NTAccount));
  // Will display something like "FABRIKAM\Alice"
  Console.WriteLine(nta.ToString());     迄今为止,我只是讨论了访问控制规则。用Windows行话说就是它们构成了对象的DACL。DACL可以由对象的所有者任意更改,由此产生了术语"*酌处权(discretionary)"。它还可以由所有者已经给予其更改DACL权限的任何人更改。对象的安全描述符包含另一个规则列表,称为系统访问控制列表(System Access Control List, SACL),该列表将控制系统对对象执行哪个类型的审核。审核是一种具有安全敏感性的操作。在Windows中,审核只能由本地安全机构(Local Security Authority,LSA)生成,因为LSA是唯一允许向安全事件日志(这里存储了审核)中写入项的组件。安全审核是一项非常严谨的业务,审核可以用来在计算机法庭中根据事实分析谁做了什么事情以及谁试图在系统中做什么事情。很多组织都长年保留它们的审核日志。不用说,规定对哪些项目进行审核的设置通常都受到严格的管理控制。如果您执行该节中的代码并且遇到UnauthorizedAccessException消息,则可能是因为您运行时所在的帐户不包含"安全特权(Security Privilege)"。为了使您能够修改甚至分析SACL,必须由本地计算机策略向您的帐户分配这一强大的特权。尽管有这些可怕的警告,但在您具有必要的特权之后,读取和操作对象的审核设置在所有方面都类似于修改访问控制设置。如果您仔细观察本文开头的代码示例,则当您查看图9时,您应该体验到一种奇特的感觉。 // Start by opening a file
  using(FileStream file = new FileStream(
  @"M:\temp\sample.txt",FileMode.Open, FileAccess.ReadWrite))
  {
  // Retrieve the file's security settings
  FileSecurity security = file.GetAccessControl(
  AccessControlSections.Audit);
  // Create a new rule to generate audits any time that a full-time
  // employee is denied write access to the file
  FileSystemAuditRule rule = new FileSystemAuditRule(
  new NTAccount( @"FABRIKAM\Full_Time_Employees"),
  FileSystemRights.Write, AuditFlags.Failure);
  // Add the rule to the existing rules
  security.AddAuditRule(rule);
  // Persist the changes
  file.SetAccessControl(security);
  } 图9 操纵审核规则
  审核设置被表示为审核规则。您可以指定您想要审核的安全主体(用户或组)的名称、您感兴趣的访问权限的类型(例如,读取、写入等等)以及您是希望在授予、拒绝访问权限还是在执行这两种操作时生成审核。例如,在图9中显示的示例中,每当全职雇员被拒绝对某个文件或给定父目录下的目录进行写入访问时,系统都将生成审核。继承标志、传播标志和保护设置对审核规则的作用方式与它们对访问控制规则的作用方式完全相同。     Windows安全系统将每个对象的安全设置与该对象一起保存在它的安全描述符中。当您希望显示对象或进行更改时,可以访问安全设置,但通常它们与要保护的对象(例如,文件或互斥锁)在一起。
  有时需要获取安全设置的快照,这可能出于多种原因。您可能希望获取对象的安全设置并将它们应用于另一个对象,或者,您可能希望将它们持久保存在XML文件内部。对于这些种类的应用程序,创建了一种称为安全描述符定义语言(Security Descriptor Definition Language,SDDL)的特殊文本。它是一种冗长的字符串表示,很少有人知道如何阅读和理解它。即使是在Microsoft,也很少有工程师能够理解如下所示的内容: D:P(A;;GA;;;SY)(A;;GRGWGX;;;BA)(A;;GR;;;WD)(A;;GR; ;;RC)     在阅读了前面的内容之后,任何使用Windows的、经验丰富的开发人员都可能会就.NET Framework对安全描述符的支持在什么地方而感到疑惑(考虑到安全描述符已经是Windows安全性的积木技术)。的确,Microsoft中那些致力于为.NET Framework 2.0实现对象安全的工程小组深感安全描述符对于一般水平甚至高于一般水平的开发人员而言太难于理解了,因此,该小组阐述了安全对象和规则的概念。然而,安全描述符并未完全作废。该工程小组意识到高级开发人员可能需要访问安全描述符以及它们所包含的ACL和ACE。实际上,诸如FileSecurity和FileSystemAccessRule之类的对象在内部由实现安全描述符、ACL和ACE的整个系列的公共类所支持。该类层次结构过于复杂,无法在此详加论述,但是有几个事情值得重点强调一下。
  ACE由一系列从基类"GenericAce"派生的类表示。ACE是不可改变的对象,提供了诸如访问掩码、安全标识符之类的属性和各种标志(ACE是否是继承的以及传播标志等等)。ACL还构成了一个类树,根为GenericAcl。ACL实质上是ACE的ICollection,但是它提供了附加功能,例如,与二进制形式之间进行转换(这对于与Windows API进行互操作很有用)。GenericAcl的值得注意的子类包括高级且强大的RawAcl(通过它可以对所有ACE类型进行任意排序)和CommonAcl(它反映了包含经过良好定义的ACE组的排序正确的ACL)。您可以在文件系统中的普通文件上看到CommonAcl。最后,安全描述符(以GenericSecurityDescriptor为根)聚合了所有者和组SID、DACL、SACL以及一组安全描述符控制标志。除了其他功能以外,安全描述符类还提供了用来在它们与SDDL以及二进制形式之间进行转换的方法。GenericSecurityDescriptor的RawSecurityDescriptor子类是一个通用且强大的工具,开发人员可以使用它来创建任意的安全描述符,而CommonSecurityDescriptor则反映通常遇到的安全描述符(带有按"规范顺序"排序的ACL)。总而言之,该类库同时适合于初级和高级开发人员,因为它没有对开发人员可能希望对安全描述符执行的操作施加任何实际限制,同时又保持了简单且吸引人的基于规则的接口,以便执行最常见的操作。     本文说明了可以用来在即将问世的.NET Framework 2.0版本中保护对象的方式。在使用Win32 API实现访问控制方面具有经验的开发人员将得以从危险且困难的低级数据结构(它们对这些功能的"纯粹的"基于Windows的编程造成了困扰)操作中解放出来。使用新的类库保护对象的简便性应当能够带来更高的应用程序安全性。低级别功能在简化的外部后面得到保留,以便高级开发人员将能够继续使用他们熟悉的功能,并且创建具有任意复杂性的安全描述符。