Windows Via C/C++ note 4

说到安全防护,以前听到很多的声音说 Windows 的安全防护做的没有 Linux 的好。其实我也不太懂Linux或者Windows做了些啥,不过就是无脑跟风这种声音。这一篇将会介绍一下 Windows 在安全防护上究竟做了一些什么。


Windows 安全相关(上)

在这一章节将会出现大量的定义来说明清楚一些功能,所以可能会出现【定义之间相互嵌套】【某些同义词被混用】的情况。我会尽可能的避免,不过可能还是有这种事情,所以对于全局搜索关键字来找上下文来说可能会有点不方便。。。并且文章内容很多,可能会分成两部分

基本介绍

安全对象 Securable Objects

标准定义为:安全对象就是能够拥有安全描述符的对象。基本上就是 Windows 中定义的大部分内核对象。具体来讲,就是

  • 所有的命名对象
  • 部分无名对象,例如进程和线程。

每一种不同的安全对象都定了属于自己的对安全描述符的控制函数。例如对于文件对象来捉,相关的安全描述符操控API就有:

1
GetNamedSecurityInfo, SetNamedSecurityInfo, GetSecurityInfo, SetSecurityInfo

这四种。
具体的定义可以查看这里:
https://docs.microsoft.com/en-us/windows/win32/secauthz/securable-objects

安全标识符 SID security identifier

基本上,所有的安全对象,安全对象从属的组(Group),以及安全对象被定义的规则 ACE 等等,用来确定一个安全操作实体的唯一标识符,就是SID。SID的结构如下:

1
2
3
4
5
6
7
8
9
10
typedef struct _SID {
BYTE Revision;
BYTE SubAuthorityCount;
SID_IDENTIFIER_AUTHORITY IdentifierAuthority;
#if ...
DWORD *SubAuthority[];
#else
DWORD SubAuthority[ANYSIZE_ARRAY];
#endif
} SID, *PSID;

其实就是一串数字定义,SDK中的的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
////////////////////////////////////////////////////////////////////////
// //
// Security Id (SID) //
// //
////////////////////////////////////////////////////////////////////////
//
//
// Pictorially the structure of an SID is as follows:
//
// 1 1 1 1 1 1
// 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
// +---------------------------------------------------------------+
// | SubAuthorityCount |Reserved1 (SBZ)| Revision |
// +---------------------------------------------------------------+
// | IdentifierAuthority[0] |
// +---------------------------------------------------------------+
// | IdentifierAuthority[1] |
// +---------------------------------------------------------------+
// | IdentifierAuthority[2] |
// +---------------------------------------------------------------+
// | |
// +- - - - - - - - SubAuthority[] - - - - - - - - -+
// | |
// +---------------------------------------------------------------+
//
//

简单来说,一般由以下这几部分组成:

1
2
3
4
+----------------|------------------------------|-----------------------------------------------------|
|SID结构体版本号 |48bit的身份授权(authority)值|32bit的子认证(subauthority)机构或者相对标识符(RID)|
+----------------|------------------------------|-----------------------------------------------------|
S-R-X-Y1-Y2-Yn-1-Yn
  • S: 表示这段字符串是一段SID
  • R:表示当前版本号
  • X:表示身份授权机构的值
  • Y:子授权机构的相关值
    • Y1 - Yn-1:这些一般是域身份标识符
    • Yn:用来标识当前身份在当前域中的身份

一个SID的格式一般为:

1
S-1-5-21-3132430673-2316732214-352203560-1001

这个SID是我机器上用户的SID。
身份授权机构的值(上述的5)定义了提出SID的从属对象,例如5来表示当前的SID是由Windows local system/Domain这一块提出,则这一段SID将会与Windows local system/Domain相关。
子授权值表明了与身份授权机构相关的身份的委托人标识符。如果用刚刚的例子继续表示的话,21就是子授权机构。

RID

RID(上述的Yn)表述域内用户的身份。一般普通用户从1000开始,每出现一个普通用户(User)就+1。如果一个SID的RID=1003,意味着已经有4个User在这个电脑上了。其中这些RID有一些是默认值,例如:

  • 500:admin
  • 501:guest

除了上述提到的RID有固定的定义,其实还有许多SID是微软预定义的,例如:
除此之外,还有一些微软预定义的SID,例如:

  • S-1-5-32-544:内置管理员组

这里的32表示的域其实是表示的内置(builtin)的意思。所以每台计算机上都会有一个当前内建组。
除此之外还有:

  • S-1-0-0: Nobody (SID不知道的时候使用)
  • S-1-1-0: Everyone (除了anonymous之外的所有人)
  • S-1-2-0: Local (凡是登陆了当前中断机的人)
  • S-1-3-0:Creator Owner ID (通常在ACE(Acess Control Entry)中使用,表明会被创建者的SID替换)
  • S-1-3-1:Create Group ID(同上,只是会被创建者的个人组替换)
  • S-1-5-18:Local System account(就是System)
  • S-1-5-19:Local Service account(这个是Local service,不同于system)
  • S-1-5-20:Network Service account(这个是Network service,不同于system)

进程Winlogin 会为每一个互动登陆回话创建一个独立的登陆SID。一个典型的logon SID的作用就是对于每一个登陆用户在登陆期间的回话中的权限控制ACE(Aceess Control Entry)。例如API

1
LogonUser

可以用来创建一个新的登陆会话。API将会返回一个access token。这个access token可以被service解析成login SID。这个SID将可以在各种ACE中被使用来控制权限。这种Logon SID的形式一般为:

1
S-1-5-5-X-Y

SID使用的场合

正如前文所说,确定一个安全操作实体的唯一标识符,具体来说包括

  • 安全描述符(Security Descriptor):确认对象(objet,例如内核对象)的从属者(owner)和主要组(primary group)
  • 访问控制实体(Access Control Entries):确认这个SID绑定的用户拥有的权限(allow, denied, audited)
  • 访问令牌(Access Tokens):确认当前用户以及当前用户(User)从属的组(Group)

GUID与SID

AD(Active Directory)会将一个用户SID存储在用户或者组对象的ObjectSID这个属性中。如果这个对象/组是新创建的话,还会给这个对象赋予一个GUID值。这个guid则是存放在ObjectGUID这个属性中。

SID只能保证在域domain中的唯一性。如果一个用户从一个域迁移到另一个域的时候,必须要获得一个新的SID。此时上一个SID会存储在属性SIDHistory(一个list)中。

当认证通过的时候,domain里的认证机构将会查询和当前用户相关的所有SID,包括

  • 当前的SID(ObjectSID)
  • 用户曾经用过的SID(SIDHistory)
  • 用户从属组的SID

这些值都会被计算,然后将所有计算出能够获得的权限放在token中。

假设A用户被设置对B资源的权限为deny,如果用户换了域之后,此时的SIDObject未被设置Deny,那么A用户可以接触资源B吗?

这个是不可以的,SIDHistory中会记录当前deny的结果。

Capability SIDs

Capability SID 用于UWP进程,也就是从Win8开始出现的AppContainer进程中的权限管理。这个SID用于管理一些比较上层的资源,通常包括文档,摄像头等。

这类SID的开头标志通常为:

1
S-1-15-3

实例:创建一个SID对象

Windows提供了一套相关的API,放在了acl这个头文件里面。不过需要在代码的前面加上:

1
#pragma comment(lib,"Advapi32")

来表示自己引用了这个lib库。

创建一个SID的代码如下:

1
2
3
4
5
6
7
8
9
10
SID_IDENTIFIER_AUTHORITY SIA_NT = SECURITY_NT_AUTHORITY;
PSID pAdminSID = nullptr;
BOOL bRet = AllocateAndInitializeSid(
&SIA_NT, 2, SECURITY_BUILTIN_DOMAIN_RID,
DOMAIN_ALIAS_RID_ADMINS, 0, 0, 0, 0, 0, 0, &pAdminSID);
if (bRet)
{
std::cout << " Create admin sid success" <<std::endl;
FreeSid(AdministratorsGroup);
}

这是一个手动创建SID的过程。首先我们来看这个SID的整体定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifndef SID_IDENTIFIER_AUTHORITY_DEFINED
#define SID_IDENTIFIER_AUTHORITY_DEFINED
typedef struct _SID_IDENTIFIER_AUTHORITY {
BYTE Value[6];
} SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY;
#endif
#ifndef SID_DEFINED
#define SID_DEFINED
typedef struct _SID {
BYTE Revision;
BYTE SubAuthorityCount;
SID_IDENTIFIER_AUTHORITY IdentifierAuthority;
#ifdef MIDL_PASS
[size_is(SubAuthorityCount)] DWORD SubAuthority[*];
#else // MIDL_PASS
DWORD SubAuthority[ANYSIZE_ARRAY];
#endif // MIDL_PASS
} SID, *PISID;
#endif

定义了SIA_NT,这个值对应的宏SECURITY_NT_AUTHORITY其实是一个SID_IDENTIFIER_AUTHORITY对象。在这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
////////////////////////////////////////////////////////////////////////
// //
// Security Id (SID) //
// //
////////////////////////////////////////////////////////////////////////
//
//
// Pictorially the structure of an SID is as follows:
//
// 1 1 1 1 1 1
// 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
// +---------------------------------------------------------------+
// | SubAuthorityCount |Reserved1 (SBZ)| Revision |
// +---------------------------------------------------------------+
// | IdentifierAuthority[0] |
// +---------------------------------------------------------------+
// | IdentifierAuthority[1] |
// +---------------------------------------------------------------+
// | IdentifierAuthority[2] |
// +---------------------------------------------------------------+
// | |
// +- - - - - - - - SubAuthority[] - - - - - - - - -+
// | |
// +---------------------------------------------------------------+
//
//

就是IdentifierAuthority这个成员变量,其对应的数值{0,0,0,0,5},根据约定,这个值就表示当前SID的授权机构是NT。基本可以和本地安全对象以及admin相关的SID都是由这个机构授权的。

第二个参数是SubAuthority Count,我们给了2,表示当前存在两个子授权机构。根据代码,分别是SECURITY_BUILTIN_DOMAIN_RID(0x00000020L)DOMAIN_ALIAS_RID_ADMINS(0x00000220L)。前者表明,当前子授权机构为当前的内置域(built-in domain),此时当前的SID所在的组可以认为是为S-1-5-0x20,然后后者DOMAIN_ALIAS_RID_ADMINS则是一个已知别名(well-known aliases),表示是域内的一个管理员(Admin)。将这两个颁发机构组合一下,就能够知道当前的SID为:

1
S-1-5-0x20-0x220

也就是表示当前域内管理员这个含义,其实也就是常见的管理员SID。用相关工具可以查看:

1
2
3
4
5
6
7
8
PsGetsid.exe S-1-5-32-544
PsGetSid v1.45 - Translates SIDs to names and vice versa
Copyright (C) 1999-2016 Mark Russinovich
Sysinternals - www.sysinternals.com
Account for DESKTOP-7UDMUOF\S-1-5-32-544:
Alias: BUILTIN\Administrators

ACE Access Control Entry 访问控制实体

访问控制实体(ACE)是ACL(Access Control List 访问控制链)中的一个元素,每个ACL可能包含多个ACE。ACE 用来控制/监控每一个特定受托者(Trustee)对对象的各类权限(读写,执行等)

Windows 中存在6中不同的ACE,其中有三种是所有的Windows对象共用的

Type Description
Access-denied ACE 在DACL中用来拒绝一个受托者的访问
Access-allowed ACE 在DACL中用来允许一个受托者的访问
System-audit ACE 当托管尝试执行相关权限的时候,在SACL中生成一个审计日志

ACE 访问权限和访问掩码 (Access Rights and Access Masks)

访问权限(Access Rights)是一个由 bit 位组成的flag,用来表示线程可以对安全对象进行的操作。
访问掩码(Access Masks)是一个32-bit的整数,其中的每一个bit都表示了一个对象支持的一种访问权限。所有的Windows 安全对象都会使用一种叫做access mask format(访问掩码格式)的方式存储访问方式。这其中包含如下的访问权限:

  • 通用访问权限(Generic access rights)
  • 标准访问权限(Standard access rights)
  • SACL
  • 目录服务访问权限(Directory services access rights)

当线程尝试打开一个对象句柄的时候,就会请求响应的安全掩码来请求对应的访问权限。不同的对象有不同的设置访问掩码的API

Access Mask Format 访问掩码格式

上文提到的访问掩码格式在内存中的保存形式通常如下:

  • 低16bit通常用来表示一个对象特定的权限

  • 之后的8bit用来表示标准访问权限,对应的权限分别是

  • 16bit DELETE

  • 17bit READ_CONTROL

  • 18bit WRITE_DAC

  • 19bit WRITE_OWNER

  • 20bit SYNCHRONIZE

  • 24bit ACCESS_SYSTEM_SECURITY用来表示对对象的SACL的访问权限。

  • 25bit 为 Maximum allowd(MAXIMUM_ALLOWED),可以用来获得一个对象的所有访问权限(但是ACE对象不能以这个权限打开)

  • 最高位的4bit用来表示通用访问权限

Generic Access Rights 通用访问权限

这个权限表示的是每一个安全对象都会拥有的一些权限(但是细节上指代的并不是相同的内容)。每一个Windows下的安全对象都会将这些权限映射到一些合适的权限上。例如Windows下的文件对象会将GENERIC_READ映射到READ_CONTROL这个权限上,而SYNCHRONIZE这个权限则会映射到FILE_READ_DATA,FILE_READ_EA,FILE_READ_ATTRIBUTES 这三个权限上。不同的对象都会有不同的映射方式。
当尝试打开一个对象句柄的时候,就能够指定我们需要的权限。
通用访问权限总共有如下几种:

Constant Generic meaning
GENERIC_ALL All possible access rights
GENERIC_EXECUTE Execute access
GENERIC_READ Read access
GENERIC_WRITE Write access

Standard Access Rights 标准访问权限

每种安全对象都有一组访问权限,这些访问权限针对于当前对象的特定操作。除了这种特定对象的访问权限,还有上述提到的通用访问权限,用于指代每一个安全对象中都有的通用权限控制。
Winnt.h中定义的标准访问权限有如下几种:

Constant Meaning
DELETE 删除对象的权限
READ_CONTROL 读取对象安全描述符的权限,但是不包括SACL
SYNCHRONIZE 同步对象的权限。这个权限保证线程能够等待这个对象,直到这个对象进入信号状态(signaled state)(也就是能够wait mutex一类的)有一些对象不支持这个访问权限
WRITE_DAC 修改对象安全描述符中的DACL的权限
WRITE_OWNER 修改对象安全描述符中的从属者(onwer)

Winnt.h中还定义了一些上述权限的组合(这个用的比较多)

Constant Meaning
STANDARD_RIGHTS_ALL Combines DELETE, READ_CONTROL, WRITE_DAC, WRITE_OWNER, and SYNCHRONIZE access.
STANDARD_RIGHTS_EXECUTE Currently defined to equal READ_CONTROL.
STANDARD_RIGHTS_READ Currently defined to equal READ_CONTROL.
STANDARD_RIGHTS_REQUIRED Combines DELETE, READ_CONTROL, WRITE_DAC, and WRITE_OWNER access.
STANDARD_RIGHTS_WRITE Currently defined to equal READ_CONTROL.

Directory Services Access Rights 目录服务访问权限

每一个AD(Active Directory)对象都有一个安全描述符。一系列指定为目录服务的的受托者能够被设置在安全描述符内。具体可以查看这里

SACL访问权限

SACL的访问权限收到之前提到的ACCESS_SYSTEM_SECURITYbit位的控制。只有在access token(访问token)活动了SE_SECURITY_NAME的特权的时候,才能够修改SACL。MSDN上给出的访问对象SACL的途径为:

  • 调用AdjustTokenPrivileges获得SE_SECURITY_NAME特权。
  • 打开对象句柄的时候,请求ACCESS_SYSTEM_SECURITY权限。
  • 通过调用GetSecurityInfo/SetSecurityInfo尝试获取SACL。
  • 操作结束后,调用AdjustTokenPrivileges来去除SE_SECURITY_NAME特权。

ACE的继承关系

通常的ACE都是具有继承关系的(具体来说,就是设置了INHERITED_ACE这个标志位)。ACE的继承关系为:

  • 一个ACL中的ACE继承顺序受到其插入到ACL中的顺序影响。
  • 设置了INHERITED_ACE权限的ACE可以被继承。
  • 对于AppContainer进程,需要设置了特殊的标志位才能够被继承。

Object-specific ACEs 特定对象ACEs

这个对象用于支持directory service(DS)。也就是如果一个对象中的属性按照类似于目录的形式保存的时候,可以特定对象中对于一些属性的访问权限。
一个DS对象的DAC对于App L可以包含一个具有层级关系的ACEs,具有如下特性

  • 保护对象本身的ACEs
  • 保护对象中,指定属性集合的特定对象ACEs
  • 保护对象中,指定属性的特定对象ACEs

使用阶级关系的ACE,就能对更加底层的属性进行设置。如果一个作用在属性集上的特定对象ACE允许一个受托者ADS_RIGHT_DS_READ_PROP的权限,那么这个受托者将会被隐式的允许读取这个属性集下的所有属性。相似的,如果一个作用在对象的ACE允许一个受托者ADS_RIGHT_DS_READ_PROP的权限,那么这个受托者将能够直接读取对象中的所有属性。
特定对象ACE包含了一些GUID,用于特定对象的一些基本属性。(通常用于对象继承)
实例
下图展示了的DS对象的树及其属性集和属性。

加入想要对DS对象的属性进行如下的控制:

  • 允许GroupA的人访问对象的所有属性
  • 允许每个人访问除了属性D以外的所有属性

那么属性可以以如下的方式展示:

Trustee Object GUID ACE type Access rights
Group A None Access-allowed ACE ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP
Everyone Property Set 1 Access-allowed object ACE ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP
Everyone Property C Access-allowed object ACE ADS_RIGHT_DS_READ_PROP | ADS_RIGHT_DS_WRITE_PROP

由于DACL默认未设置的对象就会被deny,所以这边需要写清楚allow的权限。首先GroupA中不知名Object GUID,表明这个ACE将会作用在Object的所有属性上,所以GroupA的人将会获得对这个对象所有属性的读写权限。而EvenryOne第一次设置的Object GUID为属性集1,此时根据上图可知,Everyone拥有对属性AB的读写权限。最后一个ACE设置了Object GUID为属性C,此时表明Everyone将对属性C有访问权限。所以此时Everyone将无法对属性D进行访问。

Trustee 受托者

说白了就是ACE的从属者。可以是应用ACE的一个用户的账户,一个登陆的session或者一个组。每一个在ACL中的ACE都会有一个SID。
这里的账户除了指用户登陆之外,还可以指代Windows Services(例如网络)在本地登陆时候的权限
组用户是不能用于登陆的(因为必须要指定某个人,不能以某个组的身份登陆),不过在ACE用于限制权限的时候非常方便,可以快速限制多个用户的访问权限。受托者使用了一个叫做TRUESTEE的结构体来定义当前身份。:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct _TRUSTEE_A {
struct _TRUSTEE_A *pMultipleTrustee;
MULTIPLE_TRUSTEE_OPERATION MultipleTrusteeOperation;
TRUSTEE_FORM TrusteeForm;
TRUSTEE_TYPE TrusteeType;
union {
LPSTR ptstrName;
SID *pSid;
OBJECTS_AND_SID *pObjectsAndSid;
OBJECTS_AND_NAME_A *pObjectsAndName;
};
LPCH ptstrName;
} TRUSTEE_A, *PTRUSTEE_A, TRUSTEEA, *PTRUSTEEA;

如上,我们可以发现ptstrNamepSid是一个union关系,这意味着一个用户可以使用命名字符串或者SID的形式来区分一个受托者。对于使用名字的相关操作ACE的函数,这些函数都将会分配SID所需要的buffer,并且能够找到SID与名字的对应关系。

相关函数:

1
2
BuildTrusteeWithSid //sid初始化
BuildTrusteeWithName //name(str)初始化

Windows还允许使用指定ACE来初始化TRUSTEE结构体:

1
2
BuildTrusteeWithObjectsAndSid
BuildTrusteeWithObjectsAndName

这几个API涉及结构体中的这两个变量:

1
2
OBJECTS_AND_SID *pObjectsAndSid;
OBJECTS_AND_NAME_A *pObjectsAndName;

这两个变量指明了受托者名字/SID附加了特定对象ACE(object-specific ACE)的附加信息。这将会让例如SetEntriesInAcl或者GetExplicitEntriesFromAcl这类函数来将特定对象ACE存到TRUSTEE结构体中

ACL Access Control List 访问控制链

是一系列ACE组成的链表(注意,有顺序的概念)。每一个ACL中的ACE确定一个受托者(Trustee)以及其相关的权限,包括aceess/deny/audit。每一个安全对象的安全描述符中定义了两种不同的ACL:DACL和SACL。
在头文件中的ACL定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
////////////////////////////////////////////////////////////////////////
// //
// ACL and ACE //
// //
////////////////////////////////////////////////////////////////////////
//
// Define an ACL and the ACE format. The structure of an ACL header
// followed by one or more ACEs. Pictorally the structure of an ACL header
// 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
// +-------------------------------+---------------+---------------+
// | AclSize | Sbz1 | AclRevision |
// +-------------------------------+---------------+---------------+
// | Sbz2 | AceCount |
// +-------------------------------+-------------------------------+
//
// The current AclRevision is defined to be ACL_REVISION.
//
// AclSize is the size, in bytes, allocated for the ACL. This includes
// the ACL header, ACES, and remaining free space in the buffer.
//
// AceCount is the number of ACES in the ACL.
//

DACL discretionary access control list 任意访问控制列表

DACL定义受托者对一个安全对象的access/deny权限。当一个进程尝试去访问一个安全对象的时候,系统就会检查当前对象中的DACL中的ACEs以确认是否有权限去访问。

工作流程

具体来说,系统会对比每一个ACE中的受托者与线程访问token中的受托者的权限。一个访问token包含安全标识符(SID),这些标识符将会用于表示指定的用户和组。token还包含用于标识当前session的session token。在权限确认阶段,系统将会忽略当前的没有被启用的组SID。

通常情况下,线程使用请求访问的线程的主要访问token(也就是通常意义上的用户普通token,也就是进程里面的那个token)。然而如果线程模拟了另一个用户(impersonate another user),此时系统将会使用模拟用户的token。

检查流程

  • 如果当前对象没有DACL,那么系统会默认分配任意权限给所有人(grant full access to everyone)。
  • 如果对象的DACL中没有ACEs明确的提到了对受访者的权限控制,那么受访者将不会被授予任何权限
  • 系统检查ACEs的时候,会依次检查DACL中的所有ACE,直至确定了一个或者多个ACE中对所有请求的权限都被赋予了allow权限,或者找到了其中请求的某个权限被deny。如果查找到最后,仍然有权限没有被显示的allow的话,那么当前的权限依旧被被认为是deny

如果看到上述的描述会注意到,可能会存在一个ACEs的顺序问题。现在举一个例子:

实例:DACL 的检查过程

一个访问过程发生的时候,实际上是线程与安全对象之间进行权限确认的过程。对于线程A,系统将会读取ACE1,此时会发现线程A的token是来自Andrew的,而Andrew在ACE1中被明确定义了不允许有RWX的权限,那么会直接返回拒绝访问。此时不会继续检查ACE2和ACE3
对于线程B,ACE1将不会生效(因为此时的ACE1不包含相关信息)那么系统将会继续检查ACE2,然后发现此时GroupA的人拥有读权限,那么此时Thread B 对Object的读权限就被许可了。之后发现ACE3中提到,任意对象都有写和执行的权限,所以最后Thread B将对对象拥有读写和执行三种不同的权限。

因此,当设置ACE的时候需要注意。一旦检查满足条件,那么ACE的check逻辑将会停止。如果上述例子将ACE3写在了最前面,那么Thread A也将获得读写执行的权限。

Security Descriptors 安全描述符

安全描述符,也就是描述一个安全对象中的安全相关信息的一个结构。一个安全描述符中由如下结构体组成:

1
2
3
4
5
6
7
8
9
typedef struct _SECURITY_DESCRIPTOR {
BYTE Revision;
BYTE Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control;
PSID Owner;
PSID Group;
PACL Sacl;
PACL Dacl;
} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;

一个安全描述符通常包括:

  • 用于表示当前安全描述符的从属者(owner)和主从属组(primary group)的SIDs
  • 一个描述对不同用户/组的allowed与denied权限的DACL
  • 一个描述对不同用户/组的各种操作的权限与审计等级的SACL
  • 一组用于描述当前安全描述符的意义,或者它单独的成员(?)

应用不应该直接操作这个安全描述符,而应该尝试使用微软提供的一些API来对这个属性进行操作。微软提供了很多API能够创建和修改一个对象的安全描述符,接下来就会介绍创建的方法。

Security Descriptor String Format SDDL

安全描述符定义语言定义了一个字符串格式,可以用来快速的生成安全描述符。如下的两组API就能够让SDDL和安全描述符之间进行快速转换:

1
2
ConvertSecurityDescriptorToStringSecurityDescriptor
ConvertStringSecurityDescriptorToSecurityDescriptor

一个完整的SDDL由以下四部分组成:

1
2
3
4
5
owner (O:), primary group (G:), DACL (D:), and SACL (S:).
O:owner_sid 指定对象的SID
G:group_sid 指定对象组的SID
D:dacl_flags(string_ace1)(string_ace2)... (string_acen)
S:sacl_flags(string_ace1)(string_ace2)... (string_acen)

owner_sid: 指定对象的SID Strings

SID Strings 除了之前已知的SID写法之外,还可以使用一些预先定义的短字符。例如:

SDDL SID string Constant in Sddl.h Account alias and corresponding RID
“AN” SDDL_ANONYMOUS Anonymous logon. The corresponding RID is SECURITY_ANONYMOUS_LOGON_RID.
“AO” SDDL_ACCOUNT_OPERATORS Account operators. The corresponding RID is DOMAIN_ALIAS_RID_ACCOUNT_OPS.
“DA” SDDL_DOMAIN_ADMINISTRATORS Domain administrators. The corresponding RID is DOMAIN_GROUP_RID_ADMINS.
“DC” SDDL_DOMAIN_COMPUTERS Domain computers. The corresponding RID is DOMAIN_GROUP_RID_COMPUTERS.

这里只罗列了一些,其余可以看这里:https://docs.microsoft.com/en-us/windows/win32/secauthz/sid-strings

group_sid: 指定对象组的SID Strings

dacl_flags:用于DACL上的安全描述符的控制位。控制位中可以包含下列字符串:

Control Constant in Sddl.h Meaning
“P” SDDL_PROTECTED The SE_DACL_PROTECTED flag is set.
“AR” SDDL_AUTO_INHERIT_REQ The SE_DACL_AUTO_INHERIT_REQ flag is set.
“AI” SDDL_AUTO_INHERITED The SE_DACL_AUTO_INHERITED flag is set.
“NO_ACCESS_CONTROL” SSDL_NULL_ACL The ACL is null.

sacl_flags:同DACL

string_ace:用于定义ACE的字符串。每一个完整的ACE需要用()括起来。接下来就来详细的介绍一下这段内容:

ACEs String

每一个ACEs对象都有一个ACE_HEADER的语法头结构

1
2
3
4
5
typedef struct _ACE_HEADER {
BYTE AceType;
BYTE AceFlags;
WORD AceSize;
} ACE_HEADER;

一个ACE定义字符串的常见格式为:

1
ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;(resource_attribute)

ace_type
表明了ACE_HEADER中的AceType成员。也就是allow/deny等相关定义。常见的有

ACE type string Constant in Sddl.h AceType value
“A” SDDL_ACCESS_ALLOWED ACCESS_ALLOWED_ACE_TYPE
“D” SDDL_ACCESS_DENIED ACCESS_DENIED_ACE_TYPE
“OA” SDDL_OBJECT_ACCESS_ALLOWED ACCESS_ALLOWED_OBJECT_ACE_TYPE
“OD” SDDL_OBJECT_ACCESS_DENIED ACCESS_DENIED_OBJECT_ACE_TYPE
“AU” SDDL_AUDIT SYSTEM_AUDIT_ACE_TYPE
“AL” SDDL_ALARM SYSTEM_ALARM_ACE_TYPE

其余查看小节末尾的文档

ace_flags
用来表明ACE_HEADER中的AceFlags成员。一般是和继承相关的属性:

ACE flags string Constant in Sddl.h AceType value
“CI” SDDL_CONTAINER_INHERIT CONTAINER_INHERIT_ACE
“OI” SDDL_OBJECT_INHERIT OBJECT_INHERIT_ACE
“NP” SDDL_NO_PROPAGATE NO_PROPAGATE_INHERIT_ACE
“IO” SDDL_INHERIT_ONLY INHERIT_ONLY_ACE
“ID” SDDL_INHERITED INHERITED_ACE
“SA” SDDL_AUDIT_SUCCESS SUCCESSFUL_ACCESS_ACE_FLAG
“FA” SDDL_AUDIT_FAILURE FAILED_ACCESS_ACE_FLAG

rights
表明了ACE控制的控制权限,也就是当前定义的控制权。当前值可以定义为十六进制的字符串数字,或者是如下的字符串:

Generic access rights

Access rights string Constant in Sddl.h AceType value
“GA” SDDL_GENERIC_ALL GENERIC_ALL
“GR” SDDL_GENERIC_READ GENERIC_READ
“GW” SDDL_GENERIC_WRITE GENERIC_WRITE
“GX” SDDL_GENERIC_EXECUTE GENERIC_EXECUTE

Standard access rights

Access rights string Constant in Sddl.h AceType value
“RC” SDDL_READ_CONTROL READ_CONTROL
“SD” SDDL_STANDARD_DELETE DELETE
“WD” SDDL_WRITE_DAC WRITE_DAC
“WO” SDDL_WRITE_OWNER WRITE_OWNER

省略了不少,直接查看小节末尾文档即可。

object_guid
表明了一个**特定对象ACE(object-specific ACE)**结构体中的ObjectType对象的GUID。(在使用ACCESS_ALLOWED_OBJECT_ACE等操作的时候可以生效),可以使用API

1
UuidToString

对GUID进行转换
这个也有一些常见的对象GUID

Rights and GUID Permission
CR;ab721a53-1e2f-11d0-9819-00aa0040529b Change password
CR;00299570-246d-11d0-a768-00aa006e0529 Reset password

inherit_object_guid
同上,不过用于特定对象ACE的InheritedObjectType这个属性

account_sid
表明受托者SID的字符串

resource_attribute
可选:针对资源ACE。用于表明数据类型。

在resource_attribute中, '#'这个符号和’0’同一个意思
这个特性直到Windows 8之后才生效

Resource attribute ace data type string Constant in Sddl.h Data type
“TI” SDDL_INT Signed integer
“TU” SDDL_UINT Unsigned integer
“TS” SDDL_WSTRING Wide string
“TD” SDDL_SID SID
“TX” SDDL_BLOB Octet string
“TB” SDDL_BOOLEAN Boolean

实例:ACE

1
(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-1-0)

首先这个不是一个特定对象ACE,所以和object相关的位置并没有填入相关的guid。然后我们查表可知:

  • ‘A’:这个是一个ACCESS_ALLOWED_ACE_TYPE类型的ACE
  • ‘RPWPCCDCLCSW’:说明这个对象是一个和目录服务(directory service (DS))相关的ACE。SDDL_READ_PROPERTY|SDDL_WRITE_PROPERTY|SDDL_CREATE_CHILD|SDDL_DELETE_CHILD|SDDL_LIST_CHILDREN|SDDL_SELF_WRITE。属性中说明具有对对象属性的读写权限,创建、罗列和删除子属性的权限,以及验证的写权限
  • ‘RCWDWO’:这一段是说明对对象的安全描述符的控制权限。SDDL_READ_CONTROL|SDDL_WRITE_DAC|SDDL_WRITE_OWNER表示对当前对象的安全描述符有读权限(对SACL没有),然后对对象安全描述符中的DACL有写权限,同事能够更改这个对象的安全描述符的拥有者。
  • ‘GA’:这个表明当前的权限是正对哪种操作而言。SDDL_GENERIC_ALL表示是所有的权限
  • S-1-1-0:Everyone

这个实际的权限是:

1
2
3
4
5
6
7
8
9
AceType: 0x00 (ACCESS_ALLOWED_ACE_TYPE)
AceFlags: 0x00
Access Mask: 0x100e003f
READ_CONTROL
WRITE_DAC
WRITE_OWNER
GENERIC_ALL
Other access rights(0x0000003f)
Ace Sid : (S-1-1-0)

实例:手动创建ACE:

这段代码其实来自于chromium,是创建ACE的其中一种办法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bool AddSidToDacl(const Sid& sid,
ACL* old_dacl,
ACCESS_MODE access_mode,
ACCESS_MASK access,
ACL** new_dacl) {
EXPLICIT_ACCESS new_access = {0};
new_access.grfAccessMode = access_mode;
new_access.grfAccessPermissions = access;
new_access.grfInheritance = NO_INHERITANCE;//这里表示当前ACE不可被继承
new_access.Trustee.pMultipleTrustee = nullptr;//固定值。。。
new_access.Trustee.MultipleTrusteeOperation = NO_MULTIPLE_TRUSTEE;//固定值。。。
new_access.Trustee.TrusteeForm = TRUSTEE_IS_SID;
new_access.Trustee.ptstrName = reinterpret_cast<LPWSTR>(sid.GetPSID());
if (ERROR_SUCCESS != ::SetEntriesInAcl(1, &new_access, old_dacl, new_dacl))
return false;
return true;
}

首先代码声明了一个EXPLICIT_ACCESS,表明此时需要显示的声明一个ACE,然后分别指定了其access_mode(表明当前的权限种类,可以是denied/revoke/grant等等),AceesssPermissions(访问权限) 以及 grfInheritance,此时相当于制定了当前权限控制的所有权限。之后通过指定Trustee的一些基本属性(主要是SID),确认当前ACE的受托者是谁(也就是确认了作用对象),然后讲当前的ACE塞到指定的acl中。

除此之外,还能够通过类似的方式创建ACE。常见的API有:

1
2
AddAccessAllowedAce
AddAccessDeniedAce

这两个API能够直接添加指定的ACE。

实例:SDDL

1
"O:AOG:DAD:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0)"

O:AO,也就是登陆账号的那个人
G:AD,域管理员权限
D:(A;;RPWPCCDCLCSWRCWDWOGA;;;S-1-0-0) 上述实例中的ACE。
所以整段连起来看结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Revision: 0x00000001
Control: 0x0004
SE_DACL_PRESENT
Owner: (S-1-5-32-548)
PrimaryGroup: (S-1-5-21-397955417-626881126-188441444-512)
DACL
Revision: 0x02
Size: 0x001c
AceCount: 0x0001
Ace[00]
AceType: 0x00 (ACCESS_ALLOWED_ACE_TYPE)
AceSize: 0x0014
InheritFlags: 0x00
Access Mask: 0x100e003f
READ_CONTROL
WRITE_DAC
WRITE_OWNER
GENERIC_ALL
Others(0x0000003f)
Ace Sid : (S-1-0-0)
SACL
Not present

实例:创建一个DACL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
#define _WIN32_WINNT 0x0500
#include <windows.h>
#include <sddl.h>
#include <stdio.h>
#pragma comment(lib, "advapi32.lib")
BOOL CreateMyDACL(SECURITY_ATTRIBUTES *);
void main()
{
SECURITY_ATTRIBUTES sa;
sa.nLength = sizeof(SECURITY_ATTRIBUTES);
sa.bInheritHandle = FALSE;
// Call function to set the DACL. The DACL
// is set in the SECURITY_ATTRIBUTES
// lpSecurityDescriptor member.
if (!CreateMyDACL(&sa))
{
// Error encountered; generate message and exit.
printf("Failed CreateMyDACL\n");
exit(1);
}
// Use the updated SECURITY_ATTRIBUTES to specify
// security attributes for securable objects.
// This example uses security attributes during
// creation of a new directory.
if (0 == CreateDirectory(TEXT("C:\\MyFolder"), &sa))
{
// Error encountered; generate message and exit.
printf("Failed CreateDirectory\n");
exit(1);
}
// Free the memory allocated for the SECURITY_DESCRIPTOR.
if (NULL != LocalFree(sa.lpSecurityDescriptor))
{
// Error encountered; generate message and exit.
printf("Failed LocalFree\n");
exit(1);
}
}
// CreateMyDACL.
// Create a security descriptor that contains the DACL
// you want.
// This function uses SDDL to make Deny and Allow ACEs.
//
// Parameter:
// SECURITY_ATTRIBUTES * pSA
// Pointer to a SECURITY_ATTRIBUTES structure. It is your
// responsibility to properly initialize the
// structure and to free the structure's
// lpSecurityDescriptor member when you have
// finished using it. To free the structure's
// lpSecurityDescriptor member, call the
// LocalFree function.
//
// Return value:
// FALSE if the address to the structure is NULL.
// Otherwise, this function returns the value from the
// ConvertStringSecurityDescriptorToSecurityDescriptor
// function.
BOOL CreateMyDACL(SECURITY_ATTRIBUTES * pSA)
{
// Define the SDDL for the DACL. This example sets
// the following access:
// Built-in guests are denied all access.
// Anonymous logon is denied all access.
// Authenticated users are allowed
// read/write/execute access.
// Administrators are allowed full control.
// Modify these values as needed to generate the proper
// DACL for your application.
TCHAR * szSD = TEXT("D:") // Discretionary ACL
TEXT("(D;OICI;GA;;;BG)") // Deny access to
// built-in guests
TEXT("(D;OICI;GA;;;AN)") // Deny access to
// anonymous logon
TEXT("(A;OICI;GRGWGX;;;AU)") // Allow
// read/write/execute
// to authenticated
// users
TEXT("(A;OICI;GA;;;BA)"); // Allow full control
// to administrators
if (NULL == pSA)
return FALSE;
return ConvertStringSecurityDescriptorToSecurityDescriptor(
szSD,
SDDL_REVISION_1,
&(pSA->lpSecurityDescriptor),
NULL);
}

运行结束之后,可以通过属性查看目录的权限:

  • 当前用户有读写执行权限
  • 管理员有读写执行权限
  • Anonymous和guests没有所有权限

SACL system access control list 系统权限控制链

本质上用于审计的一个东西。管理员可以记录尝试访问安全对象。每个ACE将会指定由某个受托者尝试访问的类型,这些尝试将会在系统的安全事件日志中生成记录。当访问尝试失败,成功或两者都有的时候,SACL中的ACE可以生成审核记录。
从VISTA之后,SACL还可以用来指定强制访问等级与策略 mandatory access level and policy,也就是用来设置对象的完整性等级(Integrity Levels)。默认的策略是No-Write-Up较低完整性级别的进程允许读访问较高完整性级别的对象,但是不允许写访问。
从Win8之后,引入了AppContainer,实现了更细粒度的控制

Integrity Levels(完整性等级,IL)

Windows使用IL来保护了每一个用户的每一个进程中对每一个对象的操作的权限操作。这个功能叫做强制完整性控制(Mandatory Integrity Control MIC)。MIC通过SRM,保证拥有低完整性的对象不能访问到高完整性的对象。
IL可以通过APIGetTokenInformation来获取。这个IL同样受到SID的控制。尽管IL可以是任何值,不过系统中有6个默认值

  • S-1-15-2-1: APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES(可用’AC’表示)。这种表示的是普通的UWP进程
  • S-1-15-2-2: APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES。这种是表示所有的AppContainer
  • S-1-16-0x0: Untrusted(0) 由Anonymous Group创建,此时blocks大部分的write access
  • S-1-16-0x1000: Low(1) AppContainer也是这个等级,会block大部分对系统对象的write access
  • S-1-16-0x2000:Medium(2)通常程序的IL等级,一般是UAC enable的时候
  • S-1-16-0x3000:High(3)UAC启动的时候启动的程序,或者admin启动的程序
  • S-1-16-0x4000:System(4)系统级别的程序
  • S-1-16-0x5000:Protected(5)暂时未使用

每个进程的的IL都存在其token中,并且会以类似创建自进程的方式传播。

每个对象其实也像进程一样,存放了一个类似IL的东西在其安全描述符中,一般被称为(mandatary label ML)
。ML与IL同样被存放在ACE中。ML中有如下的限制权限

Policy Present on Default Description
No-Write-Up Implicit on a objects 限制来自低权限的进程对对象的写权限
No-Read-Up Only on process objects 限制低权限的进程会泄露敏感信息
No-Write-Up Only on binraries implementing COM classes 限制来自低权限的进程尝试在高权限进程中执行COM对象

实例:创建SACL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#define OBJECT_TYPE_CREATE 0x0001
NTSTATUS WINAPI SetKernelObjectIntegrityLevel(
_In_ HANDLE Object,
_In_ TOKEN_INTEGRITY_LEVELS_LIST IL
) {
SID_IDENTIFIER_AUTHORITY SIA_IL = SECURITY_MANDATORY_LABEL_AUTHORITY;
const size_t AclLength = 88;
NTSTATUS status = 0;
PSID pSid = nullptr;
PACL pAcl = nullptr;
SECURITY_DESCRIPTOR sd;
HANDLE hNewHandle = nullptr;
status = PFNNtDuplicateObject(
GetCurrentProcess(),
Object,
GetCurrentProcess(),
&hNewHandle,
DIRECTORY_ALL_ACCESS,
0, 0
);
CHECKKIL
// initialize Interity sid
status = PFNRtlAllocateAndInitializeSid(
&SIA_IL, 1, IL,
0, 0, 0, 0, 0, 0, 0, &pSid);
CHECKKIL
pAcl = reinterpret_cast<PACL>(HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, AclLength));
if (pAcl == nullptr) {
std::cout << "Create Heap error" << std::endl;
goto FuncEnd;
}
status = PFNRtlCreateSecurityDescriptor(
&sd, SECURITY_DESCRIPTOR_REVISION
);
CHECKKIL
status = PFNRtlCreateAcl(
pAcl, AclLength, ACL_REVISION
);
CHECKKIL
// Add integrity level to ACL
status = PFNRtlAddMandatoryAce(
pAcl, ACL_REVISION, 0, pSid,
SYSTEM_MANDATORY_LABEL_ACE_TYPE, OBJECT_INHERIT_ACE);
CHECKKIL
// set SACL
status = PFNRtlSetSaclSecurityDescriptor(&sd, true, pAcl, false);
CHECKKIL
// set kernel security object
status = PFNNtSetSecurityObject(
hNewHandle, LABEL_SECURITY_INFORMATION, &sd);
CHECKKIL
FuncEnd:
HeapFree(GetProcessHeap(), NULL, pAcl);
PFNRtlFreeSid(pSid);
PFNNtClose(hNewHandle);
return status;
}

开头的时候调用的这个DuplicateObject其实是一个trick,主要是为了能够让当前的object从伪句柄转换成真实句柄,从而让之后的操作能够生效
首先创建了一个SID,这一次使用的是SECURITY_MANDATORY_LABEL_AUTHORITY,也就是IL,代表此时的SID由IL授权。
然后我们创建一个空的安全描述符和ACL,之后调用APIRtlAddMandatoryAce向这个ACL中填充SID,并且指定ACE的种类为SYSTEM_MANDATORY_LABEL_ACE_TYPE,也就是ACE种类为SACL中的ACE,最后指定这个object类型为OBJECT_INHERIT_ACE,也就是说当前的ACE能够被非AppContainer的object继承。
完成之后,将当前的ACL设置到到安全描述符,再将安全描述符赋给当前对象,从而完成IL的赋值。

安全描述符是如何作用在一个新的目录对象(Directory Object)上的

当在活动目录域(Active Directory Domain)中创建一个对象的时候,是可以显示的知名一个对象的安全描述符的,此时对象的nTSecurityDescriptor属性将被设置为安全描述符。
活动目录域(Active Directory Domain)使用如下的规则来创建DACL:

  • 如果显示的指明了一个安全描述符,那么系统将会将从父对象中可以吧继承的ACE合并到指定的DACL中,除非SE_DACL_PROTECTED这个属性在安全描述符中被设置了。
  • 如果没有指明安全描述符的时候,那么系统将会从对象的classSchema 中查找默认DACL,然后让父对象中可以继承的ACE合并到DACL中
  • 如果对象的classShema中不包含DACL,那么此时的DACL将会和创建这个对象的线程的token(primary/impersonate token)一致
  • 如果上述条件都不满足,那么之后会生成不包含DACL的对象,此时该对象就可以被所有对象访问。

小节末参考网址

https://docs.microsoft.com/en-us/windows/win32/secauthz/ace-strings

(未完待续)

相关工具

PsGetsid(64)

可以用来获取SID对应关系的神器,非常好用。

使用Powershell查看SID

1
WMIC useraccount get name,sid

查看当前电脑上的sid

参考文章

http://drops.xmd5.com/static/drops/tips-11803.html
https://docs.microsoft.com/en-us/windows/win32/secauthz/security-descriptor-string-format