浅析Windows的访问权限检查机制

网友投稿 909 2023-03-16

本站部分文章、图片属于网络上可搜索到的公开信息,均用于学习和交流用途,不能代表睿象云的观点、立场或意见。我们接受网民的监督,如发现任何违法内容或侵犯了您的权益,请第一时间联系小编邮箱jiasou666@gmail.com 处理。

浅析Windows的访问权限检查机制

0x00 简介

在操作系统中,当我们提到安全的时候,意味着有一些资源需要被保护,在Windows操作系统中,这些被保护的资源大多以对象(Object)的形式存在,对象是对资源的一种抽象。每个对象都可以拥有自己的安全描述符(Security Descriptor),用来描述它能够被谁、以何种方式而访问。这些对象是客体,那么访问这些对象的主体是什么呢?这些主体就是操作系统中的各个进程,更准确地说是这些进程中的每个线程。每个进程都有一个基本令牌 (Primary Token),可以被进程中的每个线程所共享,而有些线程比较特殊,它们正在模拟(Impersonating)某个客户端的身份,因此拥有一个模拟令牌(Impersonation Token),对于这些线程来说,有效令牌就是模拟令牌,而其他线程的有效令牌则是其所属进程的基本令牌。当主体尝试去访问客体时,操作系统会执行访问权限检查(Access Check),具体来说,是用主体的有效令牌与客体的安全描述符进行比对,从而确定该次访问是否合法。为了提高效率,访问权限检查(Access Check)提供了一种缓存机制,即只有当一个对象被一个进程创建或者打开时,才会进行访问权限检查,访问权限检查的结果会被缓存到进程的句柄表(Handle Table)中,该进程对该对象的后续操作只需要查询句柄表中内容即可确定访问的合法性,而不必每次都执行开销更大的访问权限检查(Access Check)。因为对象和进程都有各自的继承层次,所以对象的安全描述符和进程的令牌从逻辑上讲也是可以继承的,比如文件系统中的文件可以继承其所在目录的安全描述符,子进程可以继承父进程的令牌,这种继承机制使让对象和进程拥有了缺省的安全属性,因此,在一个系统中的安全描述符和令牌的实例数目非常有限,大多数情况下是从父辈们继承过来的缺省值。

0x01 访问权限检查

访问权限检查包含了多个维度上的检查:

DACL检查

DACL(自主访问控制表,Discretionary Access Control List)检查是最基本的访问权限检查。

在安全描述符中存在着一个DACL表(见安全描述符图解),里面描述了拥有何种身份的主体申请的何种访问权限会被允许或者拒绝。DACL表中的每个表项拥有如下的结构:

We'll define the structure of the predefined ACE types. Pictorally

the structure of the predefined ACE's is as follows:

3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1

1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0

    +---------------+-------+-------+---------------+---------------+    |    AceFlags   | Resd  |Inherit|    AceSize    |     AceType   |    +---------------+-------+-------+---------------+---------------+    |                              Mask                             |    +---------------------------------------------------------------+    |                                                               |    +                                                               +    |                                                               |    +                              Sid                              +    |                                                               |    +                                                               +    |                                                               |    +---------------------------------------------------------------+Mask is the access mask associated with the ACE.  This is either theaccess allowed, access denied, audit, or alarm mask.Sid is the Sid associated with the ACE.其中,对于DACL来说,AceType可以有#define ACCESS_ALLOWED_ACE_TYPE (0x0)#define ACCESS_DENIED_ACE_TYPE (0x1)

+---------------+-------+-------+---------------+---------------+ | AceFlags | Resd |Inherit| AceSize | AceType | +---------------+-------+-------+---------------+---------------+ | Mask | +---------------------------------------------------------------+ | | + + | | + Sid + | | + + | | +---------------------------------------------------------------+Mask is the access mask associated with the ACE. This is either theaccess allowed, access denied, audit, or alarm mask.Sid is the Sid associated with the ACE.其中,对于DACL来说,AceType可以有#define ACCESS_ALLOWED_ACE_TYPE (0x0)#define ACCESS_DENIED_ACE_TYPE (0x1)

两种可能;Mask是一个代表相关权限集合的位图;主体的身份使用Sid(Security Identifier)来表示,这是一个变长的数据结构:

static NTSTATUS  RtlSidHashInitialize  (      __in PSID_AND_ATTRIBUTES Groups,      __in size_t GroupsCount,      __inout PSID_AND_ATTRIBUTES_HASH HashBuffer  )  {      if (NULL == HashBuffer)          return 0xC000000D;         memset(HashBuffer, 0, 0x110);        if (0 == GroupsCount || NULL == Groups)          return 0;         HashBuffer->SidCount = GroupsCount;      HashBuffer->SidAttr = Groups;         if (GroupsCount > 0x40)          GroupsCount = 0x40;        if (0 == GroupsCount)          return 0;         size_t bit_pos = 1;         for (size_t i = 0; i < GroupsCount; i++)      {          PISID sid = reinterpret_cast((Groups + i)->Sid);              size_t sub_authority_count = sid->SubAuthorityCount;             DWORD sub_authority = sid->SubAuthority[sub_authority_count - 1];             *(size_t*)(&HashBuffer->Hash[(sub_authority & 0x0000000F)]) |= bit_pos;          *(size_t*)(&HashBuffer->Hash[((sub_authority & 0x000000F0) >> 4) + 0x10]) |= bit_pos;             bit_pos <<= 1;      }         return 0;  } 

static NTSTATUS RtlSidHashInitialize ( __in PSID_AND_ATTRIBUTES Groups, __in size_t GroupsCount, __inout PSID_AND_ATTRIBUTES_HASH HashBuffer ) { if (NULL == HashBuffer) return 0xC000000D; memset(HashBuffer, 0, 0x110); if (0 == GroupsCount || NULL == Groups) return 0; HashBuffer->SidCount = GroupsCount; HashBuffer->SidAttr = Groups; if (GroupsCount > 0x40) GroupsCount = 0x40; if (0 == GroupsCount) return 0; size_t bit_pos = 1; for (size_t i = 0; i < GroupsCount; i++) { PISID sid = reinterpret_cast((Groups + i)->Sid); size_t sub_authority_count = sid->SubAuthorityCount; DWORD sub_authority = sid->SubAuthority[sub_authority_count - 1]; *(size_t*)(&HashBuffer->Hash[(sub_authority & 0x0000000F)]) |= bit_pos; *(size_t*)(&HashBuffer->Hash[((sub_authority & 0x000000F0) >> 4) + 0x10]) |= bit_pos; bit_pos <<= 1; } return 0; }

该算法有两处明显的局限,只计算每个Sid的最后一个Sub Authority的最低位字节,以及最多只有前64个Sid参与计算。

UAC与关联令牌

UAC是Vista版本引入的比较重要的安全机制,很多用户抱怨它带来的不便性,然而它就像Linux操作系统中的sudo一样,是保护系统安全的一道重要屏障。当用户登录Windows时,操作系统会为用户生成一对初始令牌,分别是代表着用户所拥有的全部权限的完整版本令牌(即管理员权限令牌),以及被限制管理员权限后的普通令牌,二者互为关联令牌;此后,代表用户的进程所使用的令牌都是由普通令牌继承而来,用来进行常规的、非敏感的操作;当用户需要进行一些需要管理员权限的操作时,比如安装软件、修改重要的系统设置时,都会通过弹出提权对话框的形式提示用户面临的风险,征求用户的同意,一旦用户同意,将会切换到当前普通令牌关联的管理员权限令牌,来进行敏感操作。通过这种与用户交互的方式,避免一些恶意程序在后台稍稍执行敏感操作。

0x03 对象

对象的结构

安全描述符

安全描述符保存着作为访问权限检查客体的对象的主要信息。它包含着4个关键成员,根据这4个成员是嵌入在结构体的尾部,还是引用自外部缓存位置的差异,可以分为两种不同的结构形式:

kd> dt _SECURITY_DESCRIPTORntdll!_SECURITY_DESCRIPTOR   +0x000 Revision         : UChar   +0x001 Sbz1             : UChar   +0x002 Control          : Uint2B   +0x008 Owner            : Ptr64 Void   +0x010 Group            : Ptr64 Void   +0x018 Sacl             : Ptr64 _ACL   +0x020 Dacl             : Ptr64 _ACLkd> dt _SECURITY_DESCRIPTOR_RELATIVEnt!_SECURITY_DESCRIPTOR_RELATIVE   +0x000 Revision         : UChar   +0x001 Sbz1             : UChar   +0x002 Control          : Uint2B   +0x004 Owner            : Uint4B   +0x008 Group            : Uint4B   +0x00c Sacl             : Uint4B   +0x010 Dacl             : Uint4B

kd> dt _SECURITY_DESCRIPTORntdll!_SECURITY_DESCRIPTOR +0x000 Revision : UChar +0x001 Sbz1 : UChar +0x002 Control : Uint2B +0x008 Owner : Ptr64 Void +0x010 Group : Ptr64 Void +0x018 Sacl : Ptr64 _ACL +0x020 Dacl : Ptr64 _ACLkd> dt _SECURITY_DESCRIPTOR_RELATIVEnt!_SECURITY_DESCRIPTOR_RELATIVE +0x000 Revision : UChar +0x001 Sbz1 : UChar +0x002 Control : Uint2B +0x004 Owner : Uint4B +0x008 Group : Uint4B +0x00c Sacl : Uint4B +0x010 Dacl : Uint4B

Owner和Group代表了该安全描述符的属主Sid,Sacl是系统访问控制表(System Access Control List)的简称,里面可以包含当前对象的审计(Audit)、完整性级别(Integrity Level)以及可信赖级别(Trust Level)等信息,与前面提到的Dacl共用同一结构体。

可选头部

对象除了拥有共同的固定头部外,还可以有可选的头部,保存着名称等可选信息。通过固定头部的InfoMask成员查表可以得到可选头部的位置,如图所示:

对象目录

只有部分对象拥有名称信息,它们被称为命名对象。命名对象的主要作用是方便对象在不同的进程中共享,它们被按类别编纂成对象目录,因此可以通过在对象目录中的路径信息找到该对象。对象目录的实现如下图所示:

对象类型

对象类型是对象中非常重要的信息,Windows将对象的类型信息从同一类对象中抽象出来,保存成一个单独的类型对象。系统中全部的类型对象被集中放置在一个表中,对象通过维护一个指向该表的索引(TypeIndex)来表明当前对象的类型。这个索引值直接保存在对象的头部,而对象体与对象头部直接相邻,如果对象体被损坏,有可能导致头部的索引值被改变,使所谓的类型混淆(Type Confusion)利用成为可能。为了缓解这一问题,Windows 10对该索引值做了特殊保护,如下图所示:

这种保护方式简单而强大,新的索引值由3部分经过异或操作得到:类型对象在类型对象表中的真实索引值,对象头部地址的第二个字节(即第8到第15位),保存在ObHeaderCookie全局变量中的因每次系统启动而异的Cookie字节。其中,ObHeaderCookie的引入,使同一类型的对象在不同机器上,甚至是同一机器上两次启动之间的索引值不同,然而这样并不足以缓解类型混淆利用,我们还可以利用信息泄露(Info Leak)来绕过(Bypass)该保护,因此还引入了对象头部地址,使得在同一时刻、同一系统中的两个相同类型对象的不同实例间的索引值也不相同,从而有效地缓解类型混淆利用。

0x04 完整性级别检查

完整性级别(IL)检查是沙盒的最简单实现方式,通过完整性级别在对象和进程层次之间的继承关系,就如同在操作系统中建立起了“世袭制度”。通过严格控制完整性级别的继承规则,以及设置严格的完整性级别检查制度,可以保证“出身低微”的主体无法访问到“出身高贵”的客体资源。

完整性拥有以下几个级别:

SeUntrustedMandatorySid

SeLowMandatorySid

SeMediumMandatorySid

SeHighMandatorySid

SeSystemMandatorySid

主体的缺省完整性级别是SeUntrustedMandatorySid,而客体的缺省完整性级别是SeMediumMandatorySid,这种差异也进一步地强化了这种“世袭制度”。

0x05 保护进程

保护进程(Protected Process)是Windows操作系统为了保护某些关键进程,防止其被普通进程调试、注入、甚至是读取内存信息而建立起来的一种安全机制。

保护进程与其他普通进程的区别在于,保护进程的Protection成员不为0。

kd> dt nt!_EPROCESS Protection

+0x6b2 Protection : _PS_PROTECTION

kd> dt nt!_PS_PROTECTION

+0x000 Level            : UChar

+0x000 Type             : Pos 0, 3 Bits

+0x000 Audit            : Pos 3, 1 Bit

+0x000 Signer           : Pos 4, 4 Bits

保护进程的Type成员可以代表两种保护类型:Protected Process(PP),Protected Process Lite(PPL),两者的区别在于PP保护进程拥有更高的权限。保护进程的Signer成员代表该进程的签名发布者的类别。对于Signer为PsProtectedSignerWindows(5)和PsProtectedSignerTcb(6)的保护进程,其Type和Signer信息会被抽取出来,组装成一个Sid,代表着该进程的可信赖级别,保存到基本令牌中的TrustLevelSid成员中。当一个令牌中的TrustLevelSid被使用时,需要保证与当前进程的Protection信息保持同步,这主要是为了应对令牌在不同进程间传递时(比如子进程继承父进程的令牌)导致的TrustLevelSid成员过时的情形。

当调试或者创建一个进程时,会调用内核函数PspCheckForInvalidAccessByProtection进行权限检查,该函数根据当前进程以及目标进程的Protection来判定当前操作是否需要遵守保护进程规定的权限限制,具体判定规则如下:

如果操作来自于内核态代码,不需要遵守限制;

如果目标进程的Protection成员为空,表示目标进程不是保护进程,不需要遵守限制;

如果当前进程是PP类型保护进程,该类型保护进程拥有最高权限,不需要遵守限制;

如果当前进程与目标进程都是PPL类型保护进程,需要根据RtlProtectedAccess表来判断当前进程的Signer是否优先于(dominate)目标进程的Signer,如果是,不需要遵守限制;

其他情况,需要遵守限制。

RtlProtectedAccess表如下所示:

RTL_PROTECTED_ACCESS RtlProtectedAccess[] =

{

//   Domination,       Process,         Thread,

//         Mask,  Restrictions,   Restrictions,

{         0,             0,             0}, //PsProtectedSignerNone               Subject To Restriction Type

{         2,    0x000fc7fe,    0x000fe3fd}, //PsProtectedSignerAuthenticode       0y00000010

{         4,    0x000fc7fe,    0x000fe3fd}, //PsProtectedSignerCodeGen            0y00000100

{         8,    0x000fc7ff,    0x000fe3ff}, //PsProtectedSignerAntimalware        0y00001000

{      0x10,    0x000fc7ff,    0x000fe3ff}, //PsProtectedSignerLsa                0y00010000

{      0x3e,    0x000fc7fe,    0x000fe3fd}, //PsProtectedSignerWindows            0y00111110

{      0x7e,    0x000fc7ff,    0x000fe3ff}, //PsProtectedSignerTcb                0y01111110

};

其中,Process Restrictions以及Thread Restrictions分别代表着进程和线程的权限限制,详解如下图:

static NTSTATUS  NtDebugActiveProcess(      __in HANDLE ProcessHandle,      __in HANDLE DebugObjectHandle      )  {      PEPROCESS target_process = nullptr;      NTSTATUS result = ObReferenceObjectByHandleWithTag( ProcessHandle, &target_process);      if (! NT_SUCCESS(result))          return result;         PEPROCESS host_process = PsGetCurrentProcess();         if (host_process == target_process)          return 0xC0000022;         if (PsInitialSystemProcess == target_process)          return 0xC0000022;         if (PspCheckForInvalidAccessByProtection(PreviousMode, host_process->Protection, target_process->Protection))          return 0xC0000712;         // ......  }        static NTSTATUS  NtCreateUserProcess(      __out PHANDLE ProcessHandle,      __out PHANDLE ThreadHandle,      __in  ACCESS_MASK ProcessDesiredAccess ,      __in  ACCESS_MASK ThreadDesiredAccess ,      __in  POBJECT_ATTRIBUTES ProcessObjectAttributes OPTIONAL ,      __in  POBJECT_ATTRIBUTES ThreadObjectAttributes OPTIONAL ,      __in  ULONG CreateProcessFlags,      __in  ULONG CreateThreadFlags,      __in  PRTL_USER_PROCESS_PARAMETERS ProcessParameters ,      __in  PVOID Parameter9,      __in  PNT_PROC_THREAD_ATTRIBUTE_LIST AttributeList      )  {      ACCESS_MASK allowed_process_access = ProcessDesiredAccess ;      ACCESS_MASK allowed_thread_access = ThreadDesiredAccess ;      PS_PROTECTION protection = ProcessContext.member_at_0x20;      if (PspCheckForInvalidAccessByProtection(PreviousMode, host_process->Protection, target_process->Protection))      {          // 1 << 0x19 = 0x80000,  WRITE_OWNER          if ( MAXIMUM_ALLOWED == ProcessDesiredAccess )              allowed_process_access = (((~RtlProtectedAccess[protection.Signer].DeniedProcessAccess) & 0x1FFFFF) | ProcessDesiredAccess) & (~(1 << 0x19));             if ( MAXIMUM_ALLOWED == ThreadDesiredAccess )              allowed_thread_access = (((~RtlProtectedAccess[protection.Signer].DeniedThreadAccess) & 0x1FFFFF) | ThreadDesiredAccess) & (~(1 << 0x19));      }      //PspInsertProcess(..., allowed_process_access, ...);      //PspInsertThread(..., allowed_thread_access, ...);  } 

static NTSTATUS NtDebugActiveProcess( __in HANDLE ProcessHandle, __in HANDLE DebugObjectHandle ) { PEPROCESS target_process = nullptr; NTSTATUS result = ObReferenceObjectByHandleWithTag( ProcessHandle, &target_process); if (! NT_SUCCESS(result)) return result; PEPROCESS host_process = PsGetCurrentProcess(); if (host_process == target_process) return 0xC0000022; if (PsInitialSystemProcess == target_process) return 0xC0000022; if (PspCheckForInvalidAccessByProtection(PreviousMode, host_process->Protection, target_process->Protection)) return 0xC0000712; // ...... } static NTSTATUS NtCreateUserProcess( __out PHANDLE ProcessHandle, __out PHANDLE ThreadHandle, __in ACCESS_MASK ProcessDesiredAccess , __in ACCESS_MASK ThreadDesiredAccess , __in POBJECT_ATTRIBUTES ProcessObjectAttributes OPTIONAL , __in POBJECT_ATTRIBUTES ThreadObjectAttributes OPTIONAL , __in ULONG CreateProcessFlags, __in ULONG CreateThreadFlags, __in PRTL_USER_PROCESS_PARAMETERS ProcessParameters , __in PVOID Parameter9, __in PNT_PROC_THREAD_ATTRIBUTE_LIST AttributeList ) { ACCESS_MASK allowed_process_access = ProcessDesiredAccess ; ACCESS_MASK allowed_thread_access = ThreadDesiredAccess ; PS_PROTECTION protection = ProcessContext.member_at_0x20; if (PspCheckForInvalidAccessByProtection(PreviousMode, host_process->Protection, target_process->Protection)) { // 1 << 0x19 = 0x80000, WRITE_OWNER if ( MAXIMUM_ALLOWED == ProcessDesiredAccess ) allowed_process_access = (((~RtlProtectedAccess[protection.Signer].DeniedProcessAccess) & 0x1FFFFF) | ProcessDesiredAccess) & (~(1 << 0x19)); if ( MAXIMUM_ALLOWED == ThreadDesiredAccess ) allowed_thread_access = (((~RtlProtectedAccess[protection.Signer].DeniedThreadAccess) & 0x1FFFFF) | ThreadDesiredAccess) & (~(1 << 0x19)); } //PspInsertProcess(..., allowed_process_access, ...); //PspInsertThread(..., allowed_thread_access, ...); }

0x06 结语

Windows操作系统中的权限检查机制以及安全系统一直都在不断地进化着,探索其实现的细节以及推测其背后的设计理念是一件非常有趣有益的事情,本文仅仅是作者在探索中的小结,难免有疏漏甚至错误,更加详细的内容可以参考下列材料,或者直接分析和调试Windows内核,以了解最新最真实的Windows。

James Forshaw: The Windows Sandbox Paradox

James Forshaw: A Link to the Past: Abusing Symbolic Links on Windows

Alex Ionescu: The Evolution of Protected Processes Part 1: Pass-the-Hash Mitigations in Windows 8.1

Alex Ionescu: The Evolution of Protected Processes Part 2: Exploit/Jailbreak Mitigations, Unkillable Processes and Protected Services

Alex Ionescu: Protected Processes Part 3 : Windows PKI Internals (Signing Levels, Scenarios, Root Keys, EKUs & Runtime Signers)

Daniel & Azure: Did You Get Your Token?

上一篇:Linux防火墙iptables学习笔记(二)参数指令
下一篇:Centos修改swap虚拟内存大小方法
相关文章

 发表评论

暂时没有评论,来抢沙发吧~