Windows的自主访问模型与对象管理器集成在一起,每一种对象类型都定义了一个security方法,该方法返回一个对象的安全信息。线程在访问一个对象以前必须先打开这个对象,并获得一个指向该对象的句柄。在打开对象的操作中,对象管理器调用SRM的函数,根据调用线程的安全凭证、在打开操作中请求的访问类型(比如读、写、删除等),以及该类型对象的security方法提供的对象安全信息,来决定此打开操作是否允许。如果可以打开该对象,那么,对象管理器在线程的进程句柄中创建一个句柄,记录下该对象以及它所请求的访问类型。所以,线程在成功打开对象后获得一个指向该对象的句柄。以后,当该线程访问此对象时,他传递对象的句柄,而对象管理器将该线程所请求的访问操作与该对象被打开时所获得的访问类型进行比较,如果当前的访问操作允许,则安全检查通过,否则此次对象访问失败。
需要注意的是:第一,每个线程都有一个安全环境,其中最重要的信息是访问令牌,代表该线程的用户的一次登录;每个对象都有一个自主访问控制列表(ACL),指明了允许谁以何种方式访问该对象,而拒绝哪些用户以何种方式访问它。为了访问一个对象,线程可以不使用它所属进程的安全环境,而是以其他账户身份来运行的安全环境,这称为模仿(ipmersonation)。第二,同一个进程中的其他线程也可以利用已经得到的句柄来访问该对象。同样地,对象管理器使用调用线程的安全环境和它所请求的访问操作,对照该对象被打开时获得的访问类型进行检查,以决定是否允许该操作。
下面对象管理器如何让使用SRM的安全检查的来进行介绍,当一个进程打开一个对象时,对象管理器在名字空间中查到目标对象后,但是在返回给句柄调用者以前,它调用\base\ntos\ob\obse.c文件中的ObCheckObjectAccess函数检查访问许可。ObCheckObjectAccess是对象管理器的函数,它将对象管理器与SRM的安全机制连接起来。它首先调用ObGetObjectSecurity函数以获得目标对象的SD(安全描述符),数据类型为SECURITY_DESCRIPTOR。对象的安全描述符包含了对象的所有者的SID(安全标识符)、自主访问控制列表(DACL)和系统访问控制列表(SACL),以及其他一些描述对象安全特征的属性。这里的自主访问控制列表规定来了谁可以或不可以以何种方式访问该对象;系统访问控制列表规定了哪些用户的哪些操作应该被记录到安全审计日志中。ObGetObjectSecurity函数也位于\base\ntos\ob\obse.c文件中,它利用对象头信息定位到目标对象的类型对象,然后调用类型对象的SecurityProcedure函数成员,由它填充一个自包含的安全描述符的缓冲区。
对于每一种类型的对象,系统在创建它的类型对象时,可以指定该类对象的Security方法,即SecurityProcedure函数成员;如果在创建类型对象时不指定该函数成员,那么,对象管理器使用一个默认的安全函数SeDefaultObjectMethod,具体见ObCreateObjectType函数的代码。如注册表键对象的Security方法为CmpSecurityMethod,文件对象和设备对象的security方法为IopGetSetSecurityObject,而驱动程序对象使用默认的安全函数。在获得了对象的安全描述符后,ObCheckObjectAccess函数锁住当前的安全环境,并调用SeAccessCheck函数执行安全访问许可检查。SeAccessCheck是SRM的一个内核接口函数,真正执行检查任务是在SepAccessCheck函数中,这些函数的代码都在base\ntos\se\acccessck.c文件中。
线程的安全环境是一个SECURITY_OBJECT_CONTEXT类型的对象,它包含了线程的主令牌(PrimaryToken成员)、模仿级别、进程审计ID(直接用进程对象的地址来表示)、以及一个可选的客户令牌(ClientToken)。当线程模仿一个客户时,客户令牌不为空,指向被模仿客户的令牌,在这种情况下,SRM使用客户令牌来检查访问许可。
令牌(token) 代表了SRM中用到的主体,它描述了一个用户的一次登录,由winlogon进程的认证了用户身份的以后创建。同一个用户会话中运行的进程都使用了同样的令牌,令牌的数据结构为TOKEN,它包含了令牌ID、认证ID、用户账号的SID和所属组的SID、一组与该令牌关联的特权,以及其他一些用于各种安全操作的属性。
在学习完线程安全环境和对象的安全描述符后,接着学习SepAccessCheck函数的实现:
- 检查调用者是否有SeSecurityPrivilege特权,然后从对象的安全描述符中获得DACL,如果对象没有DACL,则总是允许访问,返回成功。
- 如果请求WRITE_OWNER操作,则检查调用者是否有SeTakeOwnershipPrivilege特权。如果DACL为空,则拒绝所有访问。
- 若调用者请求MAXIMUM_ALLOWED操作或者想要检查所有的对象和子对象,则调用SepMaximumAccessCheck函数执行一个慢速的检查过程;否则,调用SepNormalAccessCheck函数执行正常的许可检查。SepNormalAccessCheck比较直截了当,对DACL中的逐项检查是否允许或者是拒绝此次访问。其代码在accessck.c文件中。
综上内容,一个线程打开一个对象时所执行的安全检查过程如下图所示:
一旦线程成功打开一个对象,以后当它在该对象上执行操作时,将使用句柄来引用该对象,而对象管路器或其他内核组件在将句柄解析成对象时仍然要检查调用者所请求的操作是否已被授权或者被拒绝。如ObReferenceObjectByHandle函数使用宏SeComputeDeniedAccesses来判断要请求的访问是否被拒绝;ObReferenceFileObjectForWrite函数使用宏SeComputeGrantedAccesses来判断所请求的文件操作是否已被授权,此外,I/O管理器也使用同样的方法来检查它所接到的访问操作。