有朋友告诉我前面有关Windows访问控制(Access Control)的博文太长,描述也偏生硬,希望我再把Windows的访问控制机制描述得简单明了一些。非常感谢这位朋友的提醒,我现在再用几篇较短的篇幅把我所理解的Windows访问控制机制重新梳理一下,希望能够说得足够明白。
访问控制说白了就是一个进程(或线程)试图访问一个可保护对象(Securable Object)或执行系统操作时系统对其所进行的一个安全检查。在Windows系统中,可保护对象就是可被赋予安全描述符的对象,即可通过安全描述符对其实施访问控制的对象。所有的可命名的Windows对象都是可保护的,某些不需命名的对象如进程和线程对象也是可保护的,也可以拥有安全描述符。可保护对象几乎涵盖我们所能遇到的所有Windows对象,比如管道(Pipes)、注册表键(Registry Key)、文件(Files)以及我们正在讨论的令牌,都是可保护的。我在文末列出一个表格,里面列出了常见的可保护对象以及适用的操作其安全信息的函数。
整个访问控制机制涉及到两个基本的组成成分,一个是访问令牌(Access Token),一个是安全描述符(Security Descriptor)。访问令牌是包含登录账户信息的数据结构,安全描述符标识可保护对象的所有者以及访问控制信息,包括一个自由访问控制列表(DACL,Discretionary Access Control List),定义允许和拒绝访问该对象的用户和组;一个系统访问控制列表(SACL,System access control list),规定系统如何审计对该对象的访问动作。访问控制列表是一个由若干访问控制条目(ACE, Access Control Entries)组成的列表,ACE规定对某个受信任实体(Trustee)所允许、拒绝或审计的访问权限。
在图1中,线程拿着访问令牌去访问可保护对象(比如一个文件),系统会根据可保护对象所拥有的安全描述符对此次访问进行安全检查,如果通过了,就允许该线程访问,通不过就拒绝其访问。访问令牌就像一个身份证,安全描述符就像一份准入客户的名单。
有了上面的基本逻辑,我们首先要了解访问令牌和安全描述符是怎么来的。
访问令牌是怎么来的
当客户登录到系统时,系统会对其身份进行验证,比如通过验证用户的密码、指纹等来判断是否允许该客户登录,这就是身份认证过程(Authentication,这也是Windows安全的一个重要组成部分,而且知识面更广,暂不在讨论之列)。如果登录成功,系统就会为该用户创建一个访问令牌,其中描述了该用户的安全环境,主要包含以下信息:
|
每一个在该用户环境运行的进程都会有一个该令牌的副本(图1中的①),被称作主令牌(Primary Token)。当进程试图访问受保护对象时,系统就用该主令牌结合被访问对象的安全描述符进行安全验证。
安全描述符是怎么来的
当一个可保护对象被创建的时候,系统会自动赋予它一个安全描述符。这个安全描述符可能是由创建者在调用创建函数的时候明确指定的。如果创建者没有明确指定,系统就会提供给这个可保护对象一个默认的安全描述符,其中将当前用户的SID作为对象的所有者SID,并取当前用户访问令牌中的默认DACL作为安全描述符的DACL。
访问令牌的一点扩展知识
1.模拟机制
在Windows系统中存在一种模拟(Impersonation)机制(其实我更愿意把它译作“扮演”),一个线程可以用另外一个用户的身份运行,即运行在另外一个它所扮演的用户的安全环境,我们熟知的RunAs命令就是这种机制的应用。扮演其他用户的线程有两个访问令牌,除了主令牌外,还多了一个模拟令牌(Impersonation Token),线程访问可保护对象或执行系统操作时出示的是模拟令牌,这时候图1就变成下面的样子了。
顺便提一下,模拟机制并不仅是使用RunAs命令那样简单,它主要用于服务端/客户端形式的访问控制,服务端掌握着资源,服务端通过创建扮演客户端的线程来代替客户端访问资源,以此利用系统的访问控制机制来决断客户端是否有权访问资源。
2.SID的属性
在令牌中包含的SID是带有属性信息的。令牌中每个用户和组的SID都有一组属性用来控制这些SID在安全检查过程中的用法。这组属性包含以下三个:
SE_GROUP_ENABLED | 带有该属性的SID可参与安全检查过程。在系统i进行安全检查时,会在被访问对象的访问控制列表中选取可用于该SID的ACE用于安全检查操作。 |
SE_GROUP_USE_FOR_DENY_ONLY | 又称作deny-only属性。具有该属性的SID在安全检查中只能做拒绝处理,系统只会挑选被访问对象访问控制列表中对该SID采取拒绝操作的ACE参与安全检查,即使存在允许某权限的ACE也会被忽略。 比如某对象的所有者允许某用户组group1的成员对其进行读取,但拒绝写入。当该组成员的令牌中组SID被设置deny-only属性后,本来允许的读操作也被禁止了。 一旦该属性被设置,已有的SE_GROUP_ENABLED属性就会被取消,而且也不能再设置了。 |
SE_GROUP_MANDATORY | 如果某组的SID具有该属性,就不能取消其SE_GROUP_ENABLED属性。 |
3.受限令牌(Restricted Token)
CreateRestrictedToken()函数可以在已有令牌的基础上创建一个功能受限的令牌,用该函数生成的令牌就是受限令牌。受限令牌可以在主令牌的基础上创建,也可以在模拟令牌的基础上创建。受限令牌可能在以下几方面被限制:
- 可能被减少特权(privileges)
- 向令牌中的组SID加入deny-only属性,使令牌中的这些SID不能用于访问可保护对象
- 向令牌加入受限SID列表(见前面令牌结构)
令牌中的受限SID列表在安全检查(图1的③)过程中起着重要作用,当一个受限的线程试图访问一个可保护对象时,系统会执行两种检查,一种是用令牌中启用的SID(即有SE_GROUP_ENABLED属性的SID),另一种用受限SID列表中的SID。只有在两种检查均未阻止访问的情况下才能获得访问可保护对象的许可。
受限令牌机制的一个重要应用场景
当一个标准用户账户试图创建以另一个用户身份运行的子进程时受限令牌机制是非常有用的。要创建以另一个用户身份运行的子进程要调用CreateProcessAsUser()函数,但要使用该函数需要其调用者拥有SE_ASSIGNPRIMARYTOKEN_NAME特权,而该特权通常是系统代码或以LocalSystem账户运行的服务才有的。这时候受限令牌机制对于普通权限的程序来说就派上用场了,因为如果用CreateRestrictedToken()从主令牌派生一个受限令牌,将它用于CreateProcessAsUser()函数,此时就不再需要SE_ASSIGNPRIMARYTOKEN_NAME特权了,也就是说普通权限程序可以利用受限令牌机制创建模拟其他用户身份的进程。
需要留意的一点是使用受限令牌运行的程序会不会通过向非受限进程发消息来获取高一些的特权,这是一个可能的安全漏洞。如果将受限应用程序运行在不同的桌面环境可避免这一点,只是需要在不同桌面间来回切换。
用于令牌操作的API函数
以下是用于令牌操作的函数。强烈建议用系统提供的函数操作令牌或安全描述符等系统结构,不要编程直接操作,因为一旦系统更新导致数据结构细节发生改变,这些操作代码就会出错了。
函数 | 简介 |
AdjustTokenGroups | 更改令牌中的组信息。该函数可用于设置或清除令牌中特定组SID的SE_GROUP_ENABLED属性,但如果SID带有SE_GROUP_USE_FOR_DENY_ONLY属性就不能再设置SE_GROUP_ENABLED属性了。 不可用于屏蔽具有SE_GROUP_MANDATORY属性的组SID,也不能用于屏蔽用户SID。 |
AdjustTokenPrivileges | 启用或关闭令牌中的特权。它不会创造新的特权,也不能作废现有的特权。 |
CheckTokenMembership | 检查指定的SID在令牌中是否有效 |
CreateRestrictedToken | 创建现有令牌的受限版本。受限的令牌可能被禁用了部分SID、被删除了部分特权或被加入了受限SID列表。 如果要为某SID设置SE_GROUP_USE_FOR_DENY_ONLY属性,可在调用CreateRestrictedToken()时将该SID放入deny-only SID列表。 该函数可将SE_GROUP_USE_FOR_DENY_ONLY属性应用到任何SID,既可以是用户SID,也可以是组SID,哪怕是带有SE_GROUP_MANDATORY属性。但它不能删除SID的该SE_GROUP_USE_FOR_DENY_ONLY属性。 |
DuplicateToken | 从现有令牌创建一个新的模拟令牌 |
DuplicateTokenEx | 从现有令牌创建一个新的主令牌或模拟令牌 |
GetTokenInformation | 获取令牌信息,包括SID的属性信息。该函数返回一个SID_AND_ATTRIBUTTES数组,其中包含各组SID及其属性。 |
IsTokenRestricted | 检查令牌中是否包含受限SID列表 |
OpenProcessToken | 获取进程的主令牌句柄 |
OpenThreadToken | 获取线程的模拟令牌句柄 |
SetThreadToken | 赋予或删除线程的模拟令牌 |
SetTokenInformation | 设置令牌信息,包括令牌的所有者,主组,默认DACL等。 |
小结
安全检查过程(图1中的③)是整个访问控制机制的核心内容,要在介绍完所有组成部分后再总结。但现在已经有一部分被揭示了,就是令牌中SID的属性在安全检查中的作用。
访问令牌本身也是可保护对象,它也有自己的安全描述符,在介绍完安全描述符后再返回来讨论访问令牌的安全保护话题。
在下篇将介绍安全描述符的细节。
附:
对象类型 | 安全描述符操作函数 |
NTFS文件系统中的文件和目录 | GetNamedSecurityInfo, SetNamedSecurityInfo, GetSecurityInfo, SetSecurityInfo |
命名管道(Named pipes),匿名管道(Anonymous pipes) | GetSecurityInfo, SetSecurityInfo |
进程(Processes)、线程(Threads) | GetSecurityInfo, SetSecurityInfo |
文件映射对象(File-mapping objects) | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
访问令牌(Access Tokens) | SetKernelObjectSecurity, GetKernelObjectSecurity |
窗口管理对象 (窗口站、桌面) | GetSecurityInfo, SetSecurityInfo |
注册表键(Registry Keys) | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
Windows 服务(Windows Services) | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
本地或远程打印机 | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
网络共享 (Network shares) | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
进程间同步对象(事件、互斥对象、信号灯、可等待定时器)(Events,Mutexes,Semophores,Waitable Timers) | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
任务对象(Task Objects) | GetNamedSecurityInfo, SetNamedSecurityInfo,GetSecurityInfo, SetSecurityInfo |
目录服务对象(Directory Service Objects) | 由活动目录处理 |