保护对象(Protect Object)
对象保护和访问日志记录是自主访问控制和审计功能的本质所在。在Windows平台上能够被保护的对象包括文件、设备、邮件槽、管道(命名的和匿名的)、作业、进程、线程、事件、带键的事件、事件对、互斥体、信号量、共享内存区、I/O完成端口、LPC端口、可等待的定时器、访问令牌、卷(Volume)、窗体站、桌面、网络共享体、服务、注册表键、打印机和活动目录对象等--理论上包括所有由执行体对象管理器管理的对象。实际情况是不暴露给用户模式的对象(例如驱动程序对象)通常是不受保护的。内核模式代码是受信任的,并且通常使用不执行访问检查的那些接口访问对象管理器。那些被导出到用户模式中(因而需要安全验证)的系统资源,在内核模式中都是以对象形式来实现的,所以Windows对象管理器在实施对象安全性方面扮演了关键的角色。
我们可以通过Sysinternals中的WinObj工具来查看对象保护。下面的图中显示了在用户的绘画中,一个内存区对象的安全描述符。虽然与对象保护相关联的最常见的资源是文件,但Windows也对执行体对象采用了与文件系统中文件相同的安全模式和机制。从访问控制的角度来看,执行体对象和文件的差别只是在于每种对象支持的访问方法不同。
图7-6中显示的实际上是对象的自主访问控制列表,或者叫做DACL。我们在后面的一节中详细描述DACL。
为了控制谁可以操纵一个对象,安全系统首先必须确定每个用户的身份。之所以需要保证用户的身份,是因为Windows要求在访问任何系统资源以前,必须存在已认证的登录会话。当一个进程请求一个对象句柄时,对象管理器和安全系统利用调用者的安全标识和该对象的安全描述符来确定是否将一个句柄赋予此用户,从而允许该进程访问此对象。
一个线程可以假设它所使用的安全环境不同于其进程的安全环境。这一机制称为模仿(impersonation)。当一个线程处于模仿模式时,系统的安全验证机制将使用该线程的安全环境而不是其进程的安全环境。当一个线程不在模仿模式的时候,安全验证机制退回到使用该线程所属的进程的安全环境。一下这一点是非常重要的:一个进程中的所有线程共享了同一个句柄表,所以,当一个线程打开某个对象的时候,即使该线程当前处于模仿模式,该进程的所有线程也可以访问此对象。
有的时候,只验证用户的身份还不足以让系统授权对某个资源的访问,尽管该资源应该能被该账户访问。逻辑上你可以想象,在Alice的账户下运行的一个服务和Alice在浏览Internet是下载的一个未知程序之间是有明显区别的。通过在Windows完整性机制中实现多个完整性级别,Windows实现了这种同一用户内的隔离。Windows完整性机制被用于用户控制(UAC)的权限提升(elevation)、User Interface Privilege Isolation (UIPI)和AppContainers。
访问检查(Access checks)
Windows安全模型要求当一个线程打开一个对象的时候,它必须指定它期望在该对象上执行哪些类型的动作。对象管理器根于一个线程期望的访问要求,调用安全引用监视器(SRM,Security Reference Monitor)来执行访问检查,如果它允许此访问,则向该线程的进程赋予一个句柄,因而该线程(或者该进程中的其他线程)可以通过词句柄在对象上执行进一步的操作。对象管理器在进程的句柄表中记录下每个句柄被授予的访问许可。
一种可触发对象管理器执行安全访问验证过程的事件是:当一个进程使用一个名称来打开一个已有对象的时候。当一个对象被通过名称来打开是,对象管理器在对象管理器名字空间中执行一个查找此指定对象的操作。如果该对象不在某个从属名字空间(比如配置管理器的注册表名字空间,或者一个文件系统驱动程序的文件系统名字空间)中,那么,一旦对象管理器找到了该对象,及调用内部函数ObpCreateHandle。
另一个引发对象管理器执行安全访问验证过程的事件是:一个进程通过一个已有的句柄来一用一个对象。这样的引用往往是间接发生的,比如,一个进程通过Windows API来操纵一个对象,它把对象句柄传递进去。例如,一个线程在打开一个文件时,可以请求获得读取文件的许可。如果按照该线程的安全环境和该文件的安全设置的指示,该线程有以这种方式访问该对象的许可,那么,对象管理器在该线程所属进程的句柄表中,创建一个代表该文件的句柄。对象管理器将此进程所得到的访问类型与该句柄储存在一起。
Windows安全功能也允许Windows应用程序定义它们自己的私有对象,并且请求SRM的服务(通过AuthZ用户模式API),以便在这些对象上也强制使用Windows的安全模型。对象管理器和其他执行体组件通常使用许多内核模式函数来保护它们的对象,而这些内核模式函数也被导出成Windows用户模式API。例如,与SeAccessCheck等价的用户模式函数是AuthZ API AccessCheck。因此,Windows应用程序可以充分利用此安全模型的灵活性,从而与Windows中已有的认证和管理界面透明的集成在一起。
SRM安全模型的本质是一个包含3项输入的等式,这三项输入是:一个线程的安全身份、该线程想要获得的对象的访问权,以及该对象的安全设置。输入“是”或“否”,表明了此安全模型是否授予该线程它所期望的访问权。
安全标识符(SID,Security Indenifier)
Windows并不是使用名称来标识系统中执行各种动作的实体,相反,Windows使用了安全标识符。用户有SID,本地用户组、域中的用户组、本地计算机、域、域成员和服务都用SID。SID是一个可变长度的数值,包含3部分:一个SID结构版本号、一个48位标识符机构(authority)值,以及可变数量的32位子机构值或相对标识符(RID,relative identifier)值。机构值标识了发放此SID的代理机构,此代理机构通常是一个Windows本地系统或者一个域。子机构值标识了相对于此发放机构的托管者,而RID只不过是Windows在一个公共SID基础上创建唯一SID的一种方法而已。因为SID有足够的的长度,而且Windows负责为每个SID生成真正随机的值,所以,在世界上任何地方的机器上或者域中,Windows几乎不可能两次发放相同的SID。
当以文本方式来显示每一个SID时,SID以S为前缀,每个部分用连字号来分隔,例如
S-1-5-21-1463437245-1224812800-863842198-1128
在这个SID中,版本号为1,标识符机构值是5(Windows安全权威机构),余下部分是4个子机构值加上一个RID(1128)。此SID是一个域SID,但是,该域中的计算机的SID也具有相同的版本号、标识符机构值,以及相同数量的子机构值。
当你安装Windows的时候,Windows Setup程序给计算机发放一个机器SID。Windwos为该计算机上的本地账户分配SID。Windows 为本地账户分配SID。每个本地账户SID是以此源计算机的SID为基础的,只是尾部有一个RID。用户账户和组的RID是从1000开始的,对弈每个新的用户或组,RID向上递增1。类似的,用于创建Windows新域的工具,即Dcpromo.exe(Domain Controller Promote,域控制器升级工具),重用了将要升级为域控制器的计算机的SID来作为域的SID,而当该计算机降级为非域控的计算机时,为它重新创建一个新的SID。Windows为域中新账户发放SID的方法是,基于域SID,再追加一个RID(也是从1000开始,对于每个新用户或组,RID递增1)。例如,RID为1028,则表明了这是该域中发放的第29个SID。
Windows在计算机或域SID的基础上,加上一个预定义的RID,从而构成一些知名的SID。并且将这些SID发放给许多预定义的账户和组。例如,administrator账户的RID是500,guest账户的RID是501.例如,一台计算机的本地管理员账户以该计算机的SID为基础,再追加上500作为RID:
S-1-5-21-13124455-12541255-61235125-500
Windows也定义了许多内置的本地SID和域SID来代表一些知名的组。例如,表示任何一个账户或者所有账户(不包括匿名用户)的组Everyone,其SID是S-1-1-0。另一个代表一个组的SID例子是网络组,它代表了所有从网络登录到一台机器上的用户。网络组的SID是S-1-5-2。表7.2显示了一些基本的知名SID、他们的数值以及用途。与用户的SID不同,这些SID是预定义的常量,他们在所有的Windows系统和域上都有着相同的值。因此,对于一个文件来说如果在创建它的系统上,他能被Everyone组的成员访问,那么假如它所在的硬盘移到另一个系统或域上,它能被Everyone组的成员访问,那么假如将它所在的硬盘移动到另一个系统或域上,它就能被那个系统或域上的Everyone组的成员访问。当然,在那些系统上,用户必须被认证为该系统上的一个账户,才能成为Everyone组中的成员。
最后,Winlogon为每个交互式的登陆会话创建一个唯一的登录SID。登录SID的一个典型用途是,用于仅在一个客户的登陆会话中才允许访问的访问控制下(ACE)中。例如,一个Windows服务可以使用LogonUser函数来开始一个新的登陆会话。而且,它可以从LogonUser函数返回的访问令牌中提取出对应的登录SID。然后,该服务可以将此SID用在一个“允许该客户的登陆会话访问交互式窗口和桌面”的ACE中。登陆会话的SID是S-1-5-5-0,其RID是随机生成的。
实验:使用PsGetSid和进程管理器来查看SID
通过运行Sysinternals的PsGetSid实用工具,你可以很容易的看到你使用的任何账户的SID。
进程管理器(Process Explorer)也能够显示关于你的系统上账户和组的SID信息,此信息显示在它的Security标签页上。该标签页会显示如下信息:
完整性级别
像前面讲过的那样,完整性级别能够修改自主访问行为,以便区分以同一用户身份运行,并被同一用户拥有的不同进程和对象,从而提供在同一用户账户内隔离代码和数据的能力。强制完整性控制(MIC,Mandatory Integrity Control)机制通过把调用者关联到一个完整性级别,让SRM能够得到调用者自身属性的更详细信息。它给要访问的对象定义了一个完整性级别,从而指出了要访问该对象的调用者必须拥有的信任信息。
这些完整性级别是通过一个SID来制定。虽然完整性级别可以使任意值,但是,系统用了6个主要级别,以便分隔特权级别,如下表所示:
实验:查看进程的完整性级别
你可以使用Sysinternals的进程管理器,快捷的显示你系统上进程的完整性级别。
令牌(Token)
SRM使用一个称为令牌的对象来标识一个进程或线程的安全环境。安全环境中包含的信息描述了与该进程或线程相关联的账户、组和特权。令牌也包含了诸如会话ID、完整性级别和UAC虚拟化状态之类的信息。
在登录过程中,LSASS创建了一个初始的令牌来代表这一次用户登录。然后,他会确定这一次登录的用户是否属于一个权力较大的组,或者拥有一个权力较大的特权。这一步检查的组如下所示:
Built-In Administrators
Certificate Administrators
Domain Administrators
Enterprise Administrators
Policy Administrators
Schema Administrators
Domain Controllers
Enterprise Read-Only Domain Controllers
Read-Only Domain Controllers
Account Operators
Backup Operators
Cryptographic Operators
Network Configuration Operators
Print Operators
System Operators
RAS Servers
Power Users
Pre-Windows 2000 Compatible Access
上面列出的组里面,大多数只用于已加入域的系统,并且不会直接让用户拥有本地的管理员权限。相反,它们允许用户可以修改域范围的设置。检查的特权如下:
SeBackupPrivilege
SeCreateTokenPrivilege
SeDebugPrivilege
SeImpersonatePrivilege
SeLabelPrivilege
SeLoadDriverPrivilege
SeRestorePrivilege
SeTakeOwnershipPrivilege
SeTcbPrivilege
如果存在着这些组或特权中的一个或多个,那么LSASS将为用户创建一个受限的令牌(也称为“已过滤的管理员令牌”),并且两者创建一个登陆会话。标准的用户令牌被附载到Winlogon启动的一个或多个初始进程上(默认是Userinit.exe)。
因为子进程在默认情况下继承其创建者令牌的一份拷贝,所以,在该用户的会话中的所有进程都运行在同样的令牌下面。令牌的大小是不固定的,因为不同的用户账户有不同的权限集合,他们关联的组账户集合也不相同。然而,所有的令牌包含了同样类型的信息。令牌中最重要的内容如图
Windows中的安全机制使用了两部分信息来决定哪些对象可以被访问,以及哪些安全操作可以被执行。第一部分由令牌的用户账户SID和组SID域组成。安全引用监视器(SRM)使用这些SID来决定一个进程或线程是否可以获得所请求的、对于一个被保护对象的访问许可。
令牌中的组SID说明了用户的账户是哪些组的成员。例如,当服务器应用程序在执行用户请求的一些动作是,它可以禁止某些特定的组,以限制一个令牌的凭证。像这样禁止一个组,其效果几乎等同于这个组没有出现在令牌中。组SID也能包含一个特殊的SID,用来表示进程或线程的完整性级别。SRM使用令牌中的另外一个域来描述强制完整性策略,从而执行强制完整性检查。
模仿(Impersonation)
在Windows的安全模型中,模仿是一项强大的、广为使用的安全特性。Windowws也在它的客户/服务器程序设计模型中使用模仿特性。例如,服务器应用程序可以提供对诸如文件、打印机或数据库这样的资源的访问。客户若想要访问一个资源,则可以向服务器发送一个请求。当服务器接收到此请求是,它必须确保该客户有权在目标资源上执行它所期望的操作。例如,如果一台远程机器上的用户试图删除一个NTFS共享体中的文件,那么,服务器必须决定是否允许该用户删除此文件。想要确定一个用户是否有此许可,很显然的办法是,服务器查询该用户的账户和组SID,并且扫描该文件上的安全属性。这一做法对于程序来说是非常琐碎的,也很容易出错,如果有了新的安全特性,也不能透明的支持。因此Windows提供了模仿服务来简化服务器的工作。
模仿机制是的一个服务器可以告诉SRM,他现在临时接受了一个发出资源请求的客户的安全档案(Security Profile)。然后,服务器可以代表该客户访问这些资源,SRM仍然执行访问验证过程,但他使用被模仿的客户的安全环境来做这一件事。通常,服务器可以访问的资源要比客户多得多,所以服务器在模仿过程中要丢失某些安全凭证(Security Credential)。然而,反过来是有可能的:服务器在模仿过程中可能会获得一些新的安全凭证。
服务器只是在处理此模仿请求的线程的内部才模仿一个客户。线程控制的数据结构中包含了一个针对模仿令牌的可选项。然而,代表一个线程实际安全凭证的主令牌,在线程的控制结构中总是可以访问的。
为了防止滥用模仿机制,Windows不允许服务器在没有得到客户同意的情况下执行模仿。客户进程在连接到服务器的时候可以指定一个安全服务质量(SQOS,Security quality of service),以此来限制服务器进程可以执行的模仿级别。每个级别都可以在客户的安全环境中执行不同类型的操作:
SecurityAnonymous:最为限制的模仿级别--服务器不能模仿或者识别出客户
SecurityIdentification:允许服务器得到客户的身份(SID)和特权,但是服务器不能模仿客户
SecurityImpersonation:允许服务器在本地系统上识别和模仿该客户
SecurityDelegation:最为随意的模仿级别,它允许服务器在本地系统或远程系统上模仿该客户。
如果客户没有设置模仿级别,则Windows默认选择SecurityImpersonation级别。
虚拟服务账户
为了增强Windows服务的安全隔离和访问控制,又要尽可能的不增加管理负担,Windows提供一种特殊类型的账户,称为虚拟服务账户(Virtual Service Account,或简称为虚拟账户)。如果没有这一机制,则Windows服务要么必须运行在为Windows内置服务定义的某个账户(如本地服务账户,网络服务账户)下,要么运行在一个常规的域账户下。类似于本地服务的账户被很多已有的服务共享,所以只能提供有限粒度的特权和访问控制;进一步来说,它们无法被跨域管理。域账户则为了安全起见,要求定期修改密码,而在修改密码期间,服务的可用性可能会受到影响。为了实现最好的隔离,每个服务都应该在自己的账户下运行,但是只使用普通账户的话,将使管理的工作量成倍增加。
使用虚拟服务账户后,每个服务可以运行在它自己的账户下,并拥有自己的安全ID。该账户的名称总是“NT SERVICE\”后面跟着服务的内部名称。虚拟服务账户可以出现在访问控制列表(ACL,Access Control List)中,而且可以通过组策略把它与某些特权联系起来。
Windows会自动的设置虚拟服务账户的密码,并定期更新它。和本地系统(Local System)账户和其他服务账户一样,它也是有密码的,但是对于系统管理员来说,改密码是未知的。
实验:使用虚拟服务账户
C:\Windows\system32>sc create srvany obj= "NT SERVICE\srvany" binPath="c:\temp\srvany.exe"
安全描述符和访问控制
令牌标识了一个用户的凭证,他只是对象安全等式的一部分。对象安全等式的另一部分是与一个对象关联在一起的安全信息,这些信息规定了谁可以在这个对象上执行哪些操作。这些信息的数据结构被称为安全描述符(Security Descriptor)。一个安全描述符由以下的属性构成:
Revision number 版本号:创建此描述符的SRM安全模型的版本
Flags标志:一些可选的修饰符,定义了该描述符的行为或特征
Owner SID所有者SID:所有者的安全ID
Group SID组SID:该对象的主组的SID
Discretionary access control list (DACL):自主访问控制列表:规定了谁可以用什么方式访问该对象
System access control list (SACL) 系统访问控制列表:规定了哪些用户的哪些操作应该被记录到安全审计日志中,以及对象的显示完整性级别。
访问控制列表(ACL)是有一个头和零个或多个访问控制项(ACE)结构组成的。一共有两种类型的ACL:DACL和SACL。在DACL中,每个ACE包含一个SID和一个访问掩码,访问掩码的经典用途是指定授予或拒绝SID拥有者的访问权限(读、写、删除等)。这些单独的ACE所授予的访问权限累计起来,就构成了一个ACL所授予的访问权限的集合。如果在一个安全描述符中没有出现DACL(null DACL),则任何人对于该对象都有完全的访问权限。如果DACL为空(既0个ACE),则没有用户对该对象有访问权限。
SACL包含两种类型的ACE:系统审计ACE和系统审计-对象ACE。这些ACE指定了:在该对象上由指定用户或组执行的哪些操作应该被审计下来。审计信息被存储在系统审计日志中。成功的操作和不成功的企图都会被审计下来。
ACL分配
为了确定给一个新的对象分配什么样的DACL,安全系统使用一下四条分配规则中的第一条可以适用的规则。
1.如果调用者在创建一个对象的时候现实的提供了一个安全描述符,则安全系统将此安全描述符应用于该对象。如果该对象有一个名称,并且驻留在一个容器对象中(例如,在对象管理器命名空间目录\BaseNamedObjects中一个命名的事件对象),系统将任何可继承的ACE(可能是从该对象的容器对象中传递过来的ACE)合并到该DACL中,除非该安全描述符设置了SE_DACL_PROTECTED标志(该标志阻止继承性)。
2.如果调用者没有提供一个安全描述符,并且该对象有一个名称,那么,安全系统检查此新对象所在的容器对象中的安全描述符。该对象目录的有些ACE可能被标记为可继承的,这意味着这些ACE应该被应用到该对象目录中新创建的对象上。如果出现这样可继承的ACE,则安全系统将他们编成一个ACL,并附在此新对象上(分别有单独的标志指明了专门让容器对象来继承的ACE,或者由非容器对象来继承的ACE)。
3.如果调用者没有指定安全描述符,而且该对象也不继承任何ACE,那么安全系统从调用者的访问令牌中获得默认的DACL,它们在创建对象的时候将这些DACL分配到对象上。在Windows中,有几个子系统具有硬编码的DACL,它们在创建对象的时候将这些DACL分配到对象上(例如,Windows服务、LSA和SAM对象)。
4. 如果调用者没有指定安全描述符,没有集成的ACE,也没有默认的DACL,那么,系统创建的对象没有DACL,这使得任何人都可以完全访问该对象。
系统为一个新对象分配SACL时使用的规则,与分配DACL所使用的规则类似,但有一些例外。第一个例外是,继承得到的系统审计ACE并不传递到那些在安全描述符中被设置了SE_SACL_PROTECTED标志的对象上。第二个例外是,如果没有指定的安全审计SACL,也没有继承的ACE,则该对象上不应该用任何SACL。这一行为不同于应用默认DACL的做法,因为访问令牌中没有默认的SACL。
如果一个新的安全描述符中包含了可继承的ACE,则当它被应用到一个容器上时,系统自动的将这些可继承的ACE传播到其子对象的安全描述符中。这些可继承的ACE与已有子对象的安全描述符的合并顺序是,任何被显式应用于该ACL的ACE都位于该对象继承得到的ACE的前面。在传播这些可以继承的ACE时,系统使用了下面的规则:
1.如果一个不包含DACL的子对象继承了一个ACE,则结果是,该子对象有一个DACL,并且该DACL只包含这一继承的ACE;
2. 如果子对象有一个空的DACL,它继承一个ACE,那么,其结果是,该子对象有一个DACL,并且该DACL只包含这一个继承的ACE;
3.这一条仅针对活动目录中的对象,如果从一个父对象中删除了一个可继承的ACE,则自动继承性是的子对象继承的这一ACE的任何拷贝也被删除;
4.这一条仅针对活动目录中的对象,如果自动继承性导致一个子对象中的所有ACE都被删除,则该子对象有一个空的DACL而不是没有DACL。
对于Windows的安全模型来说,一个ACL中的ACE的顺序是非常重要的。
注:继承性通常并不是由对象存储体(比如文件系统、注册表,或者活动目录)直接支持的。支持继承性的Windows API(包括SetEntriesInAcl)是通过调用“安全继承性支持DLL”中适当的函数来做到的,这些函数知道如何遍历这些对象存储体。
确定访问权
有以下两种方法用于确定对一个对象的访问权:
1. 强制完整性检查,它确定调用者的完整性级别是否足够高,以至于能够访问某资源;这一检查的根据是该资源自己的完整性级别,以及它的强制策略
2.自主访问检查,它确定某个特定的用户账户对某个对象的访问权
当一个进程试图打开一个对象时,在内核的SeAccessCheck函数中,完整性检查是在标准的Windows DACL检查之前发生的,因为它执行起来更快,从而在某些情况下能快速的排除整个自主访问检查执行的必要性。由于进程在它的访问令牌里有了默认的完整性策略,如果它的完整性级别等于或高于该对象的完整性级别,并且DACL授予了它所请求的访问权,那么它就可以打开该对象来进行写入访问。例如,一个低完整性级别的进程不能以写入方式打开一个中完整性级别进程的对象,即使DACL授予了该进程写入访问的权限也不行。
通过默认的完整性策略,进程只要得到对象DACL的授权,就能打开任意对象来进行读访问,但以下类型的对象除外:进程、线程和令牌。这意味着运行于低完整性级别的进程可以读取它运行所使用的用户账户能访问的任意文件。
前面说过,进程和线程对象时特例,这是因为它们的完整性策略还包含了No-Read-Up。这意味着,当一个进程想要打开另一个进程或线程时,它的完整性级别必须等于或高于它想打开的对象的完整性级别,同时DACL也想把它想要的访问权授予它,这样才能成功。在DACL允许所请求的访问的前提下,
用户界面特权隔离
Windows的消息子系统也把完整性级别纳入考虑范畴,从而实现了用户界面特权隔离(User Interface Privilege Isolation, UIPI)。它的做法是,阻止进程向其他完整性级别比它高的进程和窗口发送窗口消息,除非是一下这些提供信息用的消息:
WM_NULL
WM_MOVE
WM_SIZE
WM_GETTEXT
WM_GETTEXTLENGTH
WM_GETHOTKEY
WM_GETICON
WM_RENDERFORMAT
WM_DRAWCLIPBOARD
WM_CHANGECBCHAIN
WM_THEMECHANGED
采用了完整性级别后,标准的用户进程就无法故意的把消息发往已提升权限进程的窗口,或进行“粉碎”攻击(shatter attack,例如向目标进程发送格式异常的消息,来引发进程内部的缓冲区溢出,从而导致已提升权限进程所在特权级上的代码执行)。UIPI也防止窗口钩子函数影响到完整性级别更高的进程的窗口,从而,举例来说,一个标准的用户进程无法将用户输入的键击事件记录到管理类应用程序中。类似的,它也防止完整性级别较低的进程利用日志钩子(journal hook)函数来监视完整性级别更高的进程的行为。