SECURITY_DESCRIPTOR

发布时间:2010-5-21 18:15
分类名称:Private


你注意到了吗

  我们在调用一些函数时,都会用到LPSECURITY_ATTRIBUTES参数,如CreateProcess、CreateProcessAsUser、CreateMutex、CreateEvent、CreateThread、CreateFileMapping、CreateFile、CreateDirectory、CreatePipe。这些函数都是创建安全对象的。
  什么是安全对象(Securable Objects)呢? 根据微软MSDN定义,安全对象就是具有security descriptor的对象。所有命名对象(有时也叫具名对象,即named object)都是安全对象,一些非命名对象也可以具有security descriptor。
  我们在编写普通应用程序的时候一般不会注意到这个参数,一般都设置为NULL,但是在编写跨平台、多进程、多线程程序的时候就必须注意这个参数,一不留心就会造成程序无法正常运转。

词语解释

1. SECURITY_ATTRIBUTES

SECURITY_ATTRIBUTES是一个结构:

typedef struct _SECURITY_ATTRIBUTES { DWORD nLength; LPVOID lpSecurityDescriptor; BOOL bInheritHandle; } SECURITY_ATTRIBUTES, *PSECURITY_ATTRIBUTES; 

  其中nLength代表整个结构长度,bInheritHandle代表安全属性所依附的安全对象的句柄(handle)在新创建进程时是否可以继承。一个进程创建的内核对象可以传递给子进程,但是创建内核对象其LPSECURITY_ATTRIBUTES参数中的bInheritHandle必须为TRUE,而且在CreateProcessAsUser中的bInheritHandles参数也必须为TRUE,这样才能够将内核对象的句柄共享。父进程在创建完子进程后创建的内核对象不再被继承。 
  我们最感兴趣的是lpSecurityDescriptor,它指向一个可以控制安全对象是否可以跨进程共享的security descriptor。

2. Security Descriptors

  Security Descriptors包含SECURITY_DESCRIPTOR结构及其附加安全信息。SECURITY_DESCRIPTOR结构的内部结构是可变的,应用程序可以使用这个结构来设置和获取安全对象的安全状态,但是应用不能直接去修改这个结构,必须使用一些API函数去操作。security descriptor中包含的安全对象的安全信息有:

  有兴趣的朋友可以参看英文文章《The Windows Access Control Model》或其中文翻译版《Windows 访问控制模型》及《Windows 访问控制模型》,这两个链接实际上指向的是同一个人的blog的同一篇文章,还有另外一个人的翻译《Windows访问控制模型》,个人感觉没有前一个人翻译的好,当然了,还是英文原文写的好。另外还有一篇文章讲解ACL的《Access Control List and Process(如何设置DACL)》,大家可以看看。

3. Security Identifiers

  SID是一个可变的授权对象(trustee)的全局唯一值,可信任实体可以是用户帐号、组帐号或者是Logon Session。用户帐号包括注册帐号或者用户登录本地计算机的程序,如服务。组帐号不能用户登录计算机,但是可以用于允许或拒绝一个或多个用户帐号的访问权限。Logon Session见《CSP中关于PIN缓存的相关问题》中的“Logon Session && Logon ID”一节。
  每一个帐户都有一个由authority生成的唯一的SID,这个authority不好翻译,可以叫“颁发机构”,如果是域环境,那么Windows Domain Controller就是一个这样的机构。生成的SID存储在安全数据库(security database)中。
  当一个用户登录时,系统产生一个安全令牌(access token),其中包含该Logon Session的安全信息,系统从安全数据库中将用户的SID取出,并放到用户的访问令牌中。每一个以该用户身份执行的进程都有一份这个Token的拷贝,这个Token可以标识用户、用户所在的组及用户的权限。操作系统在随后的和Windows安全相关的操作中使用安全令牌中的SID来标识用户,使用这个访问令牌来控制对安全对象的访问及控制是否能够进行一些和系统相关的操作。一旦一个SID被唯一标识一个用户或组,它将不能同时用来标识另外一个用户或组。用户登录时系统创建的安全令牌叫做primary token,经过使用API函数对属性进行修改的token称之为impersonation token。
  SID主要用在以下元素中:

4. SID的表示

  SID结构为:

typedef struct _SID_IDENTIFIER_AUTHORITY { BYTE Value[6]; } SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY; typedef struct _SID { BYTE Revision; BYTE SubAuthorityCount; SID_IDENTIFIER_AUTHORITY IdentifierAuthority; DWORD SubAuthority[ANYSIZE_ARRAY]; 

从中可以看出,SID的组成部分包括:

标识颁发机构及子标识颁发机构连接在一起保证了没有任何两个标识不同实体的SID会相同,甚至在两个不同颁发机构颁发出相同的RID的情况下也不会相同,因为他们的颁发机构值不同。

  SID机构存储的值为二进制值,但是我们可以使用ConvertSidToStringSid和ConvertStringSidToSid函数在字符串和二进制值之间转换。字符串表示的SID比较形象的描述了各个组成部分,格式为:S-R-I-S-S…

  字符'S'表示SID为一个数字序列串联(series),R表示版本号,I表示标识颁发机构值,S表示一个或多个子颁发机构值。
  我们举一个例子,S-1–5-32-544包含以下部分:

5. 公开的(Well-Known)SIDs

  Well-known SIDs代表了一些通用的帐号和组,例如:

  有一些在所有操作系统上通用的Well-known SIDs,称为universal well-known SID,只要是使用Windows NT方式的安全模型的安全系统(不仅仅是指操作系统)都有意义。当然还有一些只在Windows系统有意义的well-known SID。
  我们来看一些标识颁发机构的值的定义(可以看出是6个字节的SID_IDENTIFIER_AUTHORITY):

#define SECURITY_NULL_SID_AUTHORITY {0,0,0,0,0,0} //value = 0,即S-1–0 #define SECURITY_WORLD_SID_AUTHORITY {0,0,0,0,0,1} //value = 1,即S-1–1 #define SECURITY_LOCAL_SID_AUTHORITY {0,0,0,0,0,2} //value = 2,即S-1–2 #define SECURITY_CREATOR_SID_AUTHORITY {0,0,0,0,0,3} //value = 3,即S-1–3 #define SECURITY_NON_UNIQUE_AUTHORITY {0,0,0,0,0,4} //value = 4,即S-1–4 #define SECURITY_NT_AUTHORITY {0,0,0,0,0,5} //value = 5,即S-1–5 #define SECURITY_RESOURCE_MANAGER_AUTHORITY {0,0,0,0,0,9} //value = 9,即S-1–9 

  其中SECURITY_NT_AUTHORITY为Windows操作系统产生SID的颁发机构,颁发的并不是universal well-known SID。
  下面列出的是用在universal well-known SID中的RID,可以和上面的颁发机构SID合并成universal well-known SID:

#define SECURITY_NULL_RID (0x00000000L) //S-1–0颁发 #define SECURITY_WORLD_RID (0x00000000L) //S-1–1颁发 #define SECURITY_LOCAL_RID (0x00000000L) //S-1–2颁发 #define SECURITY_CREATOR_OWNER_RID (0x00000000L) //S-1–3颁发 #define SECURITY_CREATOR_GROUP_RID (0x00000001L) //S-1–3颁发 

  组合成的universal well-known SID为:

Universalwell-known SID ValueIdentifies
Null SID(S-1–0–0)A group with no members. This is often used when a SID value is not known.
World(S-1–1–0)A group that includes all users.
Local(S-1–2–0)Users who log on to terminals locally (physically) connected to the system.
Creator Owner ID(S-1–3–0)A security identifier to be replaced by the security identifier of the user who created a new object. This SID is used in inheritable ACEs.
Creator Group ID(S-1–3–1)Identifies a security identifier to be replaced by the primary-group SID of the user who created a new object. Use this SID in inheritable ACEs.

  其他Windows自己颁发的RID请参看《Windows 操作系统中的常见安全标识符》及《Well-known SIDs》。

SDDL

  SDDL即为Security Descriptor Definition Language,定义Security Descriptor的字符串格式,这个格式可以被ConvertSecurityDescriptorToStringSecurityDescriptor及ConvertStringSecurityDescriptorToSecurityDescriptor来使用。

SID字符串

  在字符串形式的Security Descriptor中,SID字符串既可以使用S-R-I-S-S…的字符串形式,也可使用在sddl.h头文件中定义的两个字符的缩写别名,例如

#define SDDL_DOMAIN_ADMINISTRATORS TEXT("DA") // Domain admins #define SDDL_DOMAIN_GUESTS TEXT("DG") // Domain guests #define SDDL_DOMAIN_USERS TEXT("DU") // Domain users #define SDDL_CREATOR_OWNER TEXT("CO") // Creator owner #define SDDL_CREATOR_GROUP TEXT("CG") // Creator group 

  需要注意的是,ConvertSidToStringSid及ConvertStringSidToSid中使用的是S-R-I-S-S…的字符串形式。其他别名请参看sddl.h。

ACE字符串

  访问控制项字符串用来表示Security Descriptor中的DACL和SACL,每个ACE都是使用“()”括起来的,ACE字符串中的每个域都是用分号来分隔,格式如下:
ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid
  具体解释可以参看《ACE Strings》。

Security Descriptor的字符串形式

  有了以上的基础,我们可以使用字符串形式来表示一个Security Descriptor了。格式如下:

O:owner_sid G:group_sid D:dacl_flags(string_ace1)(string_ace2)... (string_acen) S:sacl_flags(string_ace1)(string_ace2)... (string_acen) 

  给定一个字符串形式SD后我们可以仿照《The Windows Access Control Model》那样来进行分隔并分析。具体可以参看《Security Descriptor String Format》。
  这里唯一需要说明的是我们怎么知道一SACL和DACL的版本号。ACL即为ACE的列表,在《ACE Strings》中说ACE的类型有:

ACE type stringConstant in sddl.hAceType value
“A”SDDL_ACCESS_ALLOWEDACCESS_ALLOWED_ACE_TYPE
“D”SDDL_ACCESS_DENIEDACCESS_DENIED_ACE_TYPE
“OA”SDDL_OBJECT_ACCESS_ALLOWEDACCESS_ALLOWED_OBJECT_ACE_TYPE
“OD”SDDL_OBJECT_ACCESS_DENIEDACCESS_DENIED_OBJECT_ACE_TYPE
“AU”SDDL_AUDITSYSTEM_AUDIT_ACE_TYPE
“AL”SDDL_ALARMSYSTEM_ALARM_ACE_TYPE
“OU”SDDL_OBJECT_AUDITSYSTEM_AUDIT_OBJECT_ACE_TYPE
“OL”SDDL_OBJECT_ALARMSYSTEM_ALARM_OBJECT_ACE_TYPE

  那么只要在ACL中出现ACCESS_ALLOWED_OBJECT_ACE_TYPE、ACCESS_DENIED_OBJECT_ACE_TYPE、SYSTEM_AUDIT_OBJECT_ACE_TYPE及SYSTEM_ALARM_OBJECT_ACE_TYPE这种类型的ACE,那么ACL版本号必须为ACL_REVISION_DS(值为4),否则为ACL_REVISION(值为2)。

SecurityAttributes版本1

  到此,我们了解了SECURITY_DESCRIPTOR的相关概念,尤其是在《Windows 访问控制模型》第4节上有一段话写得很好:

在大多数情况下,Windows 已经为你确立了一个良好的自由访问控制列表。如果你遇到一个需要安全描述符或者 SECURITY_ATTRIBUTES 结构的 API,可以简单地为此参数传递 NULL。将 SECURITY_ATTRIBUTES 或者 SECURITY_DESCRIPTOR 传递为 NULL 会通知系统使用缺省的安全描述符。And that is probably what you were after.
如果需要更高级的安全性,则要你要确保创建了一个拥有填充过的 DACL 的安全描述符。如果你初始化了一个安全描述符,却忘记了为之构建一个关联的 DACL,你会得到一个 NULL DACL。Windows 将此 NULL DACL 对待为其具有以下 ACE:
“所有人:完全控制”(”Everyone: Full control”)
所以,Windows 会允许使用任何操作访问该对象。这是微软打破自己的最优方法的一个地方。当遇到了非预期的东西时,Windows 应该安全地失败,而在这种情况下它却不是。利用 NULL DACL,包括恶意软件在内的任何人都可以对对象做任何事情,包括设置一个具有威胁性的 rootkit 式 DACL 在内。出于这一原因,所以,不要创建具有 NULL DACL 的安全描述符。
设置一个 NULL DACL 并不等同于使用一个 NULL 安全描述符。将 NULL 作为安全描述符传递时,Windows 将使用一个缺省的安全描述符来替换它,而此安全描述符是安全的,允许对对象进行恰当的访问。设置一个 NULL DACL 意味着传递一个合法的安全描述符,只不过它的 Dacl 成员为 NULL。
出于同样的理由,你也许不希望设置一个没有包含任何访问控制项的 DACL。对于这样的 DACL,Windows 在遍历访问控制项时,第一次尝试就会失败,因此,它会拒绝任何操作。其结果就是一个完全不可访问的对象,这样的对象不会比一个不存在的对象好到哪儿去。如果你还想要访问一个对象,那你就必须有一个填充过的 DACL。

  以上所述在Vista上做了改动:

  NULL DACL – full control to everyone, including the guest
  empty DACLS – full control to owner; no access to everyone else

  NULL DACL – full access to anyone, including the guest
  empty DACLS – no access to anyone, including the owner

  考虑我们的中间件DLL,既可能被服务调用,有可能被普通应用程序调用。如果先被服务或管理员权限的程序调用(1、不要认为普通权限的CERTD永远优先启动,在支持PC/SC的ePass中间件中,如果启用智能卡登录功能,那么Winlogon进程就会优先调用CSP,此时还没有登录,CERTD肯定不会起来;2、也不要以为CERTD永远运行在普通用户权限下,考虑一下第一次安装后,安装程序是管理员权限,如果不做特殊处理,由安装程序启动的CERTD也是管理员权限),那么普通权限应用程序也应该能够访问。我们中间件中为了缓存、通知、同步、互斥,创建了N多内核对象,包括共享内存、Mutex、Event,如果不给这些内核对象设置一个安全描述符,那么就会使用缺省的安全描述符,造成管理员进程创建的内核对象无法被普通用户进程所使用。创建的安全描述符应该指定所有人都可以访问,虽然说有引用文章中所述的安全问题,但是我们为了让中间件正常运转,必须设置为所有人完全控制。
  我们设计一个类,实现如下:

class CSecurityAttributes { public: CSecurityAttributes(); virtual ~CSecurityAttributes();   operator SECURITY_ATTRIBUTES*(); private: CSecurityAttributes(const CSecurityAttributes& rhs); CSecurityAttributes& operator=(const CSecurityAttributes& rhs); protected: PSECURITY_DESCRIPTOR m_pSD; SECURITY_ATTRIBUTES m_sa; }; CSecurityAttributes::CSecurityAttributes() { m_pSD = NULL; ZeroMemory(&m_sa, sizeof(SECURITY_ATTRIBUTES)); if (win9x系统) return;   m_pSD = new SECURITY_DESCRIPTOR; if(!InitializeSecurityDescriptor(m_pSD, SECURITY_DESCRIPTOR_REVISION)) { return; } // add a NULL disc. ACL to the security descriptor. if (!SetSecurityDescriptorDacl(m_pSD, TRUE, (PACL)NULL, FALSE)) { return; }   m_sa.nLength = sizeof(m_sa); m_sa.lpSecurityDescriptor = m_pSD; m_sa.bInheritHandle = TRUE; }   CSecurityAttributes::~CSecurityAttributes() { if(NULL != m_pSD) { delete m_pSD; m_pSD = NULL; } }   CSecurityAttributes::operator SECURITY_ATTRIBUTES*() { if(m_sa.nLength > 0) { return &m_sa; } else return (SECURITY_ATTRIBUTES*)NULL; }

  调用时只需要进行如下调用即可,不用再像构造函数那样实现一遍:

 SecurityAttributes sa; HANDLE hMutex = CreateMutex(sa, FALSE, namestring);

  一切看起来非常正常,在各个Windows操作系统上运转良好。

CheckPoint来了:版本2

  在进行CheckPoint认证时候测试出来我们共享内存处理有问题,以及PIN码处理有问题,进行了代码修改,同时写了两篇文章《CSP中关于PIN缓存的相关问题》及《Shuttle中间件在Vista上共享内存的处理》,当时修改了这个封装类,将NULL DACL换成了Everyone DACL,也能够正常工作,现在已经无法记起为什么要修改这个封装类了。今天尝试了一下使用版本1的NULL DACL,也是可以正常工作的(当然前提是在修改了共享内存处理方式和PIN码处理方式)。Everyone DACL实现为:

SecurityAttributes::SecurityAttributes() { m_pSD = NULL; ZeroMemory(&m_sa, sizeof(SECURITY_ATTRIBUTES));   if(g_osver.IsWin9x()) { return; }   m_pSD = new SECURITY_DESCRIPTOR; if(!InitializeSecurityDescriptor(m_pSD, SECURITY_DESCRIPTOR_REVISION)) { return; } ////////////////////////////////////////////////////////////////////////// // Retrieve the SID of the Everyone group.   SID_IDENTIFIER_AUTHORITY WorldAuth = SECURITY_WORLD_SID_AUTHORITY; if (AllocateAndInitializeSid( &WorldAuth, // Top-level SID authority 1, // Number of subauthorities SECURITY_WORLD_RID, // Subauthority value 0, 0, 0, 0, 0, 0, 0, &m_pEveryoneSid // SID returned as OUT parameter ) == FALSE) { return; } // Calculate the amount of memory that must be allocated for the DACL. DWORD cbDacl = sizeof(ACL) + sizeof(ACCESS_ALLOWED_ACE)- sizeof(DWORD); cbDacl += GetLengthSid(m_pEveryoneSid);   // Create and initialize an ACL.   m_pDacl = (PACL) new BYTE[cbDacl]; if (m_pDacl == NULL) { return; } memset(m_pDacl, 0, cbDacl); if (InitializeAcl( m_pDacl, cbDacl, ACL_REVISION // Required constant ) == FALSE) { return; }   if (AddAccessAllowedAce( m_pDacl, // Pointer to the ACL ACL_REVISION, // Required constant GENERIC_ALL, // Access mask m_pEveryoneSid // Pointer to the trustee's SID ) == FALSE) { return; }   if (!SetSecurityDescriptorDacl(m_pSD, TRUE, (PACL)m_pDacl, FALSE)) { return; } m_sa.nLength = sizeof(m_sa); m_sa.lpSecurityDescriptor = m_pSD; m_sa.bInheritHandle = TRUE; ////////////////////////////////////////////////////////////////////////// } SecurityAttributes::~SecurityAttributes() { if(NULL != m_pEveryoneSid) { FreeSid(m_pEveryoneSid); m_pEveryoneSid = NULL; } if (NULL != m_pDacl) { delete[] m_pDacl; m_pDacl = NULL; } if(NULL != m_pSD) { delete m_pSD; m_pSD = NULL; } }

  当然这是从《Creating a Security Descriptor for a New Object in C++》上参考过来的,同时《VISTA下怎样使普通权限应用访问指定的注册表键》也使用了这种中方式。
  那么NULL DACL与Everyone DACL有什么区别呢?从《DACL, NULL or not NULL》中我们可以看到,Everyone DACL强制指定everyone(SECURITY_WORLD_SID_AUTHORITY)都可以访问的权限,而NULL DACL则是真正的连everyone以外随便什么都能访问的无设防对象的权限。从MSDN好多函数的说明中我们发现了以下的话语:

Windows NT/2000/: An anonymous token includes the Everyone group SID. 
Windows XP: Anonymous tokens do not include the Everyone Group SID unless the system default has been overridden by setting the HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Lsa\EveryoneIncludesAnonymous registry value to DWORD=1.

  说明NULL DACL是比Everyone DACL更不设防的权限,anonymous Token也可以访问,也同时说明了anonymous Token在XP之后就不包含在Everyone Goup中了。

保护模式也来了:版本3 

  我们使用Vista客户端在内网测试CA上申请证书时,都要将这个链接https://192.168.0.69/certsrv加入可信任站点,并且是SSL访问方式,否则CertEnroll COM组件就无法在Vista的IE7上使用,也就是说启用了保护模式就无法申请证书。这对于CSP说很正常,因为微软就是这样规定的,而且通过CSP申请证书必须使用CertEnroll COM组件。
  但是当我们碰到北京CA这样的客户时,情况就变了:他们通过一个ActiveX控件来调用厂家提供的DLL对Key进行操作来申请证书,要求Vista IE7必须运行在保护模式下。我们提供的“厂家DLL”为bjcakey_ft.dll,直接调用了我们的PKCS#11 DLL,此时在保护模式下无法访问我们CERTD进程所常见的Mutex、Event及共享内存,具体事宜见bug:0020867
  从网上找到了一篇帖子,见《A Developer's Survival Guide to IE Protected Mode》及其中文翻译版《面对IE保护模式的开发者生存之道》,其中写了在IE7保护模式下怎么使IE插件和自己的应用进行通讯,怎么使内核对象在IE7保护模式下能够和其他进行互访。其中有一段代码

// The LABEL_SECURITY_INFORMATION SDDL SACL to be set for low integrity LPCWSTR LOW_INTEGRITY_SDDL_SACL_W = L"S:(ML;;NW;;;LW)";   bool SetObjectToLowIntegrity( HANDLE hObject, SE_OBJECT_TYPE type = SE_KERNEL_OBJECT) { bool bRet = false; DWORD dwErr = ERROR_SUCCESS; PSECURITY_DESCRIPTOR pSD = NULL; PACL pSacl = NULL; BOOL fSaclPresent = FALSE; BOOL fSaclDefaulted = FALSE;   if ( ConvertStringSecurityDescriptorToSecurityDescriptorW ( LOW_INTEGRITY_SDDL_SACL_W, SDDL_REVISION_1, &pSD, NULL ) ) { if ( GetSecurityDescriptorSacl ( pSD, &fSaclPresent, &pSacl, &fSaclDefaulted ) ) { dwErr = SetSecurityInfo ( hObject, type, LABEL_SECURITY_INFORMATION, NULL, NULL, NULL, pSacl );   bRet = (ERROR_SUCCESS == dwErr); }   LocalFree ( pSD ); }   return bRet; }

  这段代码是设置对象的完整性级别(integrity level),因为IE7是以低于普通用户级别的最低完整性级别运行的,因此被Windows Vista的完整性检查机制所限制。将SecurityAttributes类改成如下实现:

class SecurityAttributes { public: SecurityAttributes(); virtual ~SecurityAttributes(); operator SECURITY_ATTRIBUTES*(); private: SecurityAttributes(const SecurityAttributes& rhs); SecurityAttributes& operator=(const SecurityAttributes& rhs); protected: PSECURITY_DESCRIPTOR m_pSD; SECURITY_ATTRIBUTES m_sa; HMODULE m_hAdv32Dll; }; typedef BOOL (WINAPI* ConvertStrSDToSDFunc)(LPCTSTR, DWORD, PSECURITY_DESCRIPTOR*, PULONG); SecurityAttributes::SecurityAttributes() { m_pSD = NULL; ZeroMemory(&m_sa, sizeof(SECURITY_ATTRIBUTES)); ) { //MSGLOG_INF("WIN32", ("SecurityAttributes not necessary on Windows 9X")); return; } m_hAdv32Dll = LoadLibrary("advapi32.dll"); if (NULL == m_hAdv32Dll) return ; ConvertStrSDToSDFunc func = (ConvertStrSDToSDFunc)GetProcAddress(m_hAdv32Dll, "ConvertStringSecurityDescriptorToSecurityDescriptorA"); if (NULL == func) return; LPCSTR LOW_INTEGRITY_SDDL_SACL = "S:(ML;;NW;;;LW)"; if (!func (LOW_INTEGRITY_SDDL_SACL, SDDL_REVISION_1, &m_pSD, NULL ) ) return; m_sa.nLength = sizeof(m_sa); m_sa.lpSecurityDescriptor = m_pSD; m_sa.bInheritHandle = TRUE; } SecurityAttributes::~SecurityAttributes() {  { LocalFree(m_pSD); m_pSD = NULL; } if (NULL != m_hAdv32Dll) FreeLibrary(m_hAdv32Dll); } 

  其中ConvertStringSecurityDescriptorToSecurityDescriptor函数使用动态获取的方式而不是直接去调用是因为Win98上没有这个函数。如果直接编译链接,那么在Win98上程序开始运行时就会报错,而不是运行到这行时报错。其中使用了”S:(ML;;NW;;;LW)“这个SD字符串。

Vista的完整性机制 

引入完整性机制

  Vista完整性机制(integrity mechanism)的目的是为了限制运行在同一个用户帐号下的、低可信性的程序的程序的访问权限。它通过赋予应用程序进程和对象一个完整性级别(integrity level)来扩展操作系统的安全架构。Windows系统安全架构基于内核级的系统安全监控器(Security Reference Monitor),系统安全监控器通过比较进程的安全访问令牌(security access token)中的用户、组的SID和要访问的对象的安全描述符ACL中允许的访问权限中的用户、组的SID来进行安全控制。而完整性机制在安全访问令牌中增加了一个完整性级别(integrity level),在对象安全描述符的SACL中增加了一个强制标签访问条目(mandatory label access control entry)来达到目的。
  Vista完整性机制独立于其它安全策略选项,一直有效。完整性级别检查是不可选的,不能被禁止,即使从安全组策略中将UAC机制禁止。

完整性级别

  Windows使用SID定义完整性级别,完整性级别的颁发机构标识符为16(SECURITY_MANDATORY_LABEL_AUTHORITY),即S-1-16-XXXX,其中XXXX为相对于16的完整性级别的值:

描述符号
0×0000Untrusted levelSECURITY_MANDATORY_UNTRUSTED_RID
0×1000(4096)Low integrity levelSECURITY_MANDATORY_LOW_RID
0×2000(8192)Medium integrity levelSECURITY_MANDATORY_MEDIUM_RID
0×3000(12288)High integrity levelSECURITY_MANDATORY_HIGH_RID
0×4000(16384)System integrity levelSECURITY_MANDATORY_SYSTEM_RID

  这样SID即为:

SID名字
S-1-16-4096Mandatory Label\Low Mandatory Level
S-1-16-8192Mandatory Label\Medium Mandatory Level
S-1-16-12288Mandatory Label\High Mandatory Level
S-1-16-16384Mandatory Label\System Mandatory Level

  Windows Vista在安全访问令牌使用SID来表示主体(访问对象的实体,一般地,主体的概念等同于进程)的安全级别,在对象的安全描述符的SACL中增加一个强制性标签访问条目来表示对向的安全级别。

完整性策略

  Vista的完整性策略分为Access token mandatory policies及Mandatory label policies,分别对应访问令牌及安全对象。Access token mandatory policies有以下几种:

策略描述
TOKEN_MANDATORY_NO_WRITE_UP在所有的访问令牌中都包含的一个缺省策略,该策略限制主体对任何具有高完整性级别的对象的写访问
TOKEN_MANDATORY_NEW_PROCESS_MIN控制子进程的完整性级别的赋予方式。通常情况下,子进程生成时从父进程继承安全访问令牌从而继承完整性级别,但是当这个策略被设定时,子进程的完整性级别应该是父进程的完整性级别及子进程可执行文件对象的完整性级别中的最小者。这个策略也是缺省策略。

Mandatory label policies有以下几种:

策略描述
SYSTEM_MANDATORY_POLICY_NO_WRITE_UP缺省策略,该策略类似于访问令牌的NO_WRITE_UP策略,该策略限制低完整性级别的主体对高完整性级别的对象的写访问
SYSTEM_MANDATORY_POLICY_NO_READ_UP限制低完整性级别的主体对高完整性级别对象的读访问
SYSTEM_MANDATORY_POLICY_NO_EXECUTE_UP限制低完整性级别的主体对高完整性级别对象的可执行访问

访问令牌的完整性级别

  安全访问令牌是内核所使用的一个内部数据结构,它包含对应于特权(privileges)、组(group membership)及其他安全相关的不同信息。当一个用户交互式登录到Windows或者一个网络认证(network authentication)发生时,安全令牌被初始化,用户的SID、组SID、特权(privileges)及其他一些信息被添加到这个安全令牌中,同时Vista将一个完整性级别赋给这个访问令牌。
  内核给每一个进程和线程分配安全访问令牌,而进程的主令牌(primary access token)包含了这个进程的完整性级别。在Windows完整性机制中,进程的完整性级别通常被称为主体完整性级别。如果一个应用的访问令牌包含了一个medium-integrity SID(S-1-16-8192),那么这个进程就运行在中等完整性级别。在同一个用户帐号下运行的所有应用进程被赋予相同的主安全访问令牌,这也就是这些应用如何被赋予相同的完整性级别的原因。
  不同内在用户帐号和组账号所赋予的完整性级别如下:

访问令牌中的SID被赋予的完整性级别
LocalSystemSystem
LocalServiceSystem
NetworkServiceSystem
AdministratorsHigh
Backup OperatorsHigh
Network Configuration OperatorsHigh
Cryptographic OperatorsHigh
Authenticated UsersMedium
Everyone (World)Low
AnonymousUntrusted

  完整性级别为运行在不同访问级别的应用程序定义不同的可信级别。Vista上绝大部分应用程序都是以标准用户身份运行的中等完整性级别,这些应用与同样为中等完整性级别的其他应用和数据进行交互没有任何限制。需要管理员权限的有些任务或者应用运行在High完整性级别,系统服务运行在System完整性级别。少部分程序被设计为带有更少的权限运行,比如运行在保护模式下的IE,这种程序运行在低完整性级别。
  当创建子进程使时,应用可以给一个复制的访问令牌赋予一个低完整性级别。如果一个可执行程序的文件拥有一个低完整性级别的强制标签(mandatory label),那么Windows将使用低完整性级别运行这个程序。

获取访问令牌的完整性级别

  完整性级别使用TOKEN_GROUPS域存在访问令牌中。TOKEN_GROUPS结构是一组标识用户帐号的组属性和组SID的列表,而完整性级别的SID就在这个列表中,这个SID属性肯定包含SE_GROUP_INTEGRITY及SE_GROUP_INTEGRITY_ENABLED。
  你可以通过GetTokenInformation函数来获取完整性级别,TOKEN_INFORMATION_CLASS参数应该为TokenIntegrityLevel,返回值就是TOKEN_MANDATORY_LABEL类型的结构。
  那么通常情况下判断一个进程的完整性级别可以通过:OpenProcessToken、GetTokenInfomation(TokenIntegrityLevel)、比较SID和系统与定义的完整性级别SID这样的步骤来确定。

void ShowProcessIntegrityLevel() { HANDLE hToken; HANDLE hProcess;   DWORD dwLengthNeeded; DWORD dwError = ERROR_SUCCESS;   PTOKEN_MANDATORY_LABEL pTIL = NULL; LPWSTR pStringSid; DWORD dwIntegrityLevel;   hProcess = GetCurrentProcess(); if (OpenProcessToken(hProcess, TOKEN_QUERY, &hToken)) { // Get the Integrity level. if (!GetTokenInformation(hToken, TokenIntegrityLevel, NULL, 0, &dwLengthNeeded)) { dwError = GetLastError(); if (dwError == ERROR_INSUFFICIENT_BUFFER) { pTIL = (PTOKEN_MANDATORY_LABEL)LocalAlloc(0, dwLengthNeeded); if (pTIL != NULL) { if (GetTokenInformation(hToken, TokenIntegrityLevel, pTIL, dwLengthNeeded, &dwLengthNeeded)) { dwIntegrityLevel = *GetSidSubAuthority(pTIL->Label.Sid, (DWORD)(UCHAR)(*GetSidSubAuthorityCount(pTIL->Label.Sid)-1));   if (dwIntegrityLevel == SECURITY_MANDATORY_LOW_RID) { // Low Integrity wprintf(L"Low Process"); } else if (dwIntegrityLevel >= SECURITY_MANDATORY_MEDIUM_RID && dwIntegrityLevel < SECURITY_MANDATORY_HIGH_RID) { // Medium Integrity wprintf(L"Medium Process"); } else if (dwIntegrityLevel >= SECURITY_MANDATORY_HIGH_RID) { // High Integrity wprintf(L"High Integrity Process"); } else if (dwIntegrityLevel >= SECURITY_MANDATORY_SYSTEM_RID) { // System Integrity wprintf(L"System Integrity Process"); } } LocalFree(pTIL); } } } CloseHandle(hToken); } }

  如果你只是想查看并不想编代码,那么通过最新版的Process Explorer在进程列表的最右边就可以看到完整性级别。

设置访问令牌的完整性级别

  应用程序一般情况下不必去改变进程的完整性级别,特别是安全访问令牌中的任何值。但是在一些特殊情况下要修改。比如如果一个服务允许运行在不同的完整性级别下的本地客户端去执行一个较低完整性级别的任务。扮演(Impersonation)是一个服务的线程可以运行在较低完整性级别的方法。当这个线程扮演了一个本地客户端,扮演线程就拥有了客户端的安全上下文环境(security context),如果这个客户端和服务运行在同一台计算机,那么这个上下文环境中就包含了客户端的完整性级别。另外一种方法就是通过设置当前线程的访问令牌TokenIntegrityLevel来使应用程序能够在一个较低完整性级别创建一系列文件和注册表。
  由于在一个进程地址空间的运行在不同完整性级别各个线程并没有安全界限,所以应用的设计者应该考虑在一个较高完整性级别的进程空间内运行一个较低完整性级别的线程的潜在安全威胁。大多数情况下我们可以通过创建一个以较低完整性级别运行的进程来执行较低完整性级别的任务。
  下面的示例演示了怎么启动一个低完整性级别的进程:

void CreateLowProcess() {   BOOL fRet; HANDLE hToken = NULL; HANDLE hNewToken = NULL; PSID pIntegritySid = NULL; TOKEN_MANDATORY_LABEL TIL = {0}; PROCESS_INFORMATION ProcInfo = {0}; STARTUPINFO StartupInfo = {0};   // Notepad is used as an example WCHAR wszProcessName[MAX_PATH] = L"C:\\Windows\\System32\\Notepad.exe";   // Low integrity SID WCHAR wszIntegritySid[20] = L"S-1-16-1024"; PSID pIntegritySid = NULL;   fRet = OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE | TOKEN_ADJUST_DEFAULT | TOKEN_QUERY | TOKEN_ASSIGN_PRIMARY, &hToken);   if (!fRet) { goto CleanExit; }   fRet = DuplicateTokenEx(hToken, 0, NULL, SecurityImpersonation, TokenPrimary, &hNewToken);   if (!fRet) { goto CleanExit; }   fRet = ConvertStringSidToSid(wszIntegritySid, &pIntegritySid);   if (!fRet) { goto CleanExit; }   TIL.Label.Attributes = SE_GROUP_INTEGRITY; TIL.Label.Sid = pIntegritySid;   // // Set the process integrity level //   fRet = SetTokenInformation(hNewToken, TokenIntegrityLevel, &TIL, sizeof(TOKEN_MANDATORY_LABEL) + GetLengthSid(pIntegritySid));   if (!fRet) { goto CleanExit; }   // // Create the new process at Low integrity //   fRet = CreateProcessAsUser(hNewToken, NULL, wszProcessName, NULL, NULL, FALSE, 0, NULL, NULL, &StartupInfo, &ProcInfo);   CleanExit:   if (ProcInfo.hProcess != NULL) { CloseHandle(ProcInfo.hProcess); }   if (ProcInfo.hThread != NULL) { CloseHandle(ProcInfo.hThread); }   LocalFree(pIntegritySid);   if (hNewToken != NULL) { CloseHandle(hNewToken); }   if (hToken != NULL) { CloseHandle(hToken); }   return fRet; }

  下面的示例演示了怎么去设置一个对象的完整性级别:

#include <sddl.h> #include <AccCtrl.h> #include <Aclapi.h>   void SetLowLabelToFile() { //The LABEL_SECURITY_INFORMATION SDDL SACL to be set for low integrity   #define LOW_INTEGRITY_SDDL_SACL_W L"S:(ML;;NW;;;LW)"   DWORD dwErr = ERROR_SUCCESS; PSECURITY_DESCRIPTOR pSD = NULL;   PACL pSacl = NULL; // not allocated BOOL fSaclPresent = FALSE; BOOL fSaclDefaulted = FALSE; LPCWSTR pwszFileName = L"Sample.txt";   if (ConvertStringSecurityDescriptorToSecurityDescriptorW( LOW_INTEGRITY_SDDL_SACL_W, SDDL_REVISION_1, &pSD, NULL)) { if (GetSecurityDescriptorSacl(pSD, &fSaclPresent, &pSacl, &fSaclDefaulted)) { // Note that psidOwner, psidGroup, and pDacl are  // all NULL and set the new LABEL_SECURITY_INFORMATION dwErr = SetNamedSecurityInfoW((LPWSTR) pwszFileName, SE_FILE_OBJECT, LABEL_SECURITY_INFORMATION, NULL, NULL, NULL, pSacl); } LocalFree(pSD); } }

强制性标签ACE

  在上面的代码示例中有一个字符串S:(ML;;NW;;;LW),这个就是Windows完整性机制新定义的一个ACE类型——system mandatory label ACE。这个ACE用来表示一个对象的安全描述符中的强制标签。强制标签包含了一个对象的完整性级别及其策略。这个ACE仅在安全描述符的SACL中出现,不能用在DACL中。
  在安全描述符中,DACL包含了一个对象的用户或组的访问许可。任何拥有WRITE_DAC许可的用户或组都可以更新这个对象的DACL。完整性机制的设计目标之一就是一旦一个强制标签被设置到一个对象,那么安全系统要在这个对象的安全描述符中维护这个强制标签。SACL本来就在安全系统的控制之下,通过在SACL中放置一个强制性标签,成功地强行将一个强制性标签放到了安全描述符中,且对于应用程序ACL的管理没有或几乎没有任何影响。这个强制性标签从逻辑上来说和SACL中系统审核条目是隔离的,他们在SACL中的顺序没有要求。
  强制性标签ACE和其他ACE非常相似,也包含一个ACE header,一个access mask和一个SID,这个SID就是上面所说的完整性级别的SID。ACE header中的AceType一定是SYSTEM_MANDATORY_LABEL_ACE_TYPE。
  一个对象只能有一个有效的强制性标签,如果一个安全描述符的SACL中包含了多个强制性标签,那么第一个为有效。

读、改变mandatory label ACE

  Vista使用GetSecurityInfo从一个对象中获取强制性标签信息。GetSecurityInfo可以获取owner、group、DACL或SACL信息,SECURITY_INFORMATION参数决定安全描述符中的哪部分信息被获取。Vista新定义了一个LABEL_SECURITY_INFORMATION,被用来从SACL中获取强制性标签ACE。注意,如果通过参数SACL_SECURITY_INFORMATION调用GetSecurityInfo,那么返回的仅仅是system audit ACE,没有强制标签ACE。
  同理,可以使用SetSecurityInfo(LABEL_SECURITY_INFORMATION)来改变一个对象的强制性标签信息。对象的强制性标签信息是在对象创建时被设置的,一般情况下这个创建时设置的完整性级别是正确的,在以后的运行中不必要去修改强制性标签信息。但是当一个应用被设计支持多个运行在不同完整性级别的客户端进程时可能需要改变这个强制性标签。
  典型的改变就是降低一个对象的完整性级别来允许一个运行在低完整性级别的进程对这个对象有写或修改权限。例如一个运行在中等完整性级别的应用需要和运行在低完整性级别的进行进行同步,那么这个中等完整性级别的应用需要创建一个Mutex并且

对象强制性标签的设置

  设置一个对象的完整性级别可以通过以下三种方式之一:

  其实最容易理解的方式是假设每个对象在创建时被赋予了和创建它的进程或线程相同的完整性级别的强制性标签。
  强制性标签可以被应用于具有基于安全描述符的访问控制的所有的安全对象。有好多这样的对象,比如进程、线程对象,具名对象,象文件和注册表之类的持续性对象。当对象被创建时,Windows并不是对所有对象类型都设置显式的强制性标签,只有进程、线程、Access Token、Job等被设置,其他对象要不具有一个隐式缺省的要不就具有一个继承的强制标签。
  我们回想一下一个交互式登录的过程:当access token初始化时,这个对象就被赋予了完整性级别。登录后,一个代表用户登录的进程userinit.exe就开始运行了,这个进程启动Shell进程——explorer.exe。Userinit.exe是被系统服务Winlogon启动,并且将交互式登录的用户的access token传给下去。
  CreateProcessAsUser创建一个Process对象及初始线程。当进程对象产生时,进程的安全描述符中就会被设置上该进程的主访问令牌中的完整性级别。当Userinit.exe调用CreateProcess启动一个Shell,explorer.exe的进程对象被初始化,这个进程对象包含安全描述符及主访问令牌。explorer进程的完整性级别被设置为父进程的userinit.exe的完整性级别,即为中等级别。explorer进程的主访问令牌从父进程userinit继承,具有中等完整性等级。当explorer进程创建一个新的线程时,这个线程对象被赋予一个安全描述符,安全子系统给这个线程对象赋予一个和创建者进程相同的完整性级别。这样这个线程就是中等完整性级别,和explorer进程的主访问令牌的完整性级别相同。
  由于对于进程、线程、令牌和job等对象总是分配强制性标签,完整性机制同一个用户帐户身份下的低完整性级别的进程访问这些对象或更改它们的内容和行为,例如注入DLL或者扮演较高级别的访问令牌。

缺省完整性级别

  并不是所有的对象类型在其安全描述符中都分配了一个强制标签ACE。如果在安全描述符中存在强制完整性标签ACE,那么成为显式强制标签,如果不存在,那么在强制性策略检查的时候安全子系统会使用这个对象的隐式缺省强制标签。对于所有的安全对象,缺省强制标签具有中等完整性级别。对于所有对象,如果在安全描述符中没有定义强制性标签,那么将被赋予隐式缺省强制标签。
  对象缺省为中等完整性级别,在加上缺省强制机制——NO_WRITE_UP,限制了所有比中等完整性级别低的进程对对象的修改/写访问,也就限制了所有处于低完整性级别的的进程对用户或系统文件或系统注册表的修改,即使这些对象在DACL中是允许写访问的。

  从Vista的完整性机制可以看出,IE7在保护模式下是运行在低完整性级别的,而其他进程是运行在中等完整性级别的,如果不设置“S:(ML;;NW;;;LW)”这个字符串,那么在IE7保护模式及其他进程中创建出来的对象将是中等安全级别,这样IE7就无法访问这些在我们中间件中大量使用的Event、Mutex、FileMapping对象。
  以上翻译至MSDN,更详细的内容请参看原文《Windows Vista Integrity Mechanism Technical Reference》。

管理员权限的又有问题了:版本4

  在加入了完整性强制标签后,IE7和CERTD通讯之间进行共享和通知、同步没有问题了。但是在打包安装后,由于CERTD是以管理员身份启动的,从版本1我们可以看出必须有DACL存在,否则会造成使用管理员权限先打开一个访问中间件的进程,那么其他进程就无法访问这些内河对象了。而现实测试却是如此。
  那么“S:(ML;;NW;;;LW)”中没有D:的存在,是否使用函数ConvertStringSecurityDescriptorToSecurityDescriptor函数就将其转换成一个NULL DACL呢?答案是否定的,在Security Descriptor String Format中有一句“The security descriptor string format does not support NULL ACLs”,说明使用这种方式无法表示NULL DACL。
  那么使用“S:(ML;;NW;;;LW)D:”呢?这个明显是一个Empty DACL,将禁止任何访问。我们必须使用显示的指定NULL DACL。我们将构造函数改为:

class SecurityAttributes { public: SecurityAttributes(); virtual ~SecurityAttributes();   operator SECURITY_ATTRIBUTES*();   private: SecurityAttributes(const SecurityAttributes& rhs); SecurityAttributes& operator=(const SecurityAttributes& rhs);   protected: char m_pSD[SECURITY_DESCRIPTOR_MIN_LENGTH]; SECURITY_ATTRIBUTES m_sa; HMODULE m_hAdv32Dll; };   typedef BOOL (WINAPI* ConvertStrSDToSDFunc)(LPCTSTR, DWORD, PSECURITY_DESCRIPTOR*, PULONG); SecurityAttributes::SecurityAttributes() { ZeroMemory(&m_sa, sizeof(SECURITY_ATTRIBUTES));   if(g_osver.IsWin9x()) { return; }     m_hAdv32Dll = LoadLibrary("advapi32.dll"); if (NULL == m_hAdv32Dll) return ;   ConvertStrSDToSDFunc func = (ConvertStrSDToSDFunc)GetProcAddress(m_hAdv32Dll, "ConvertStringSecurityDescriptorToSecurityDescriptorA"); if (NULL == func) return;   PSECURITY_DESCRIPTOR pSD; LPCSTR LOW_INTEGRITY_SDDL_SACL = "S:(ML;;NW;;;LW)";   if (!func (LOW_INTEGRITY_SDDL_SACL, SDDL_REVISION_1, &pSD, NULL ) ) return;   if(!InitializeSecurityDescriptor(m_pSD, SECURITY_DESCRIPTOR_REVISION)) { return; } SetSecurityDescriptorDacl(m_pSD, TRUE, 0, FALSE);   PACL pSacl = NULL; // not allocated BOOL fSaclPresent = FALSE; BOOL fSaclDefaulted = FALSE; GetSecurityDescriptorSacl( pSD, &fSaclPresent, &pSacl, &fSaclDefaulted); SetSecurityDescriptorSacl(m_pSD, TRUE, pSacl, FALSE);   LocalFree(pSacl);   m_sa.nLength = sizeof(m_sa); m_sa.lpSecurityDescriptor = m_pSD; m_sa.bInheritHandle = FALSE; }

强制标签只适用于Vista及之上:版本5 

  在使用版本4后一切好像都很正常,在各个平台下测试都没有出现问题。由于Minidriver也用到了Mutex和共享内存,同样碰到了IE7保护模式的问题1),所以将这段代码加到了Minidriver上。使用Minidriver在XP下进行测试的时候发现,使用这段代码在XP下无法正常使用Minidriver,根据返回错误信息,发现是ConvertStringSecurityDescriptorToSecurityDescriptor失败,原因就是S:(ML;;NW;;;LW)只在Vista及之上操作系统上能够使用。而3003中间件由于失败之后直接return,没有报告任何错误信息,没有返回到上层,没有发现这个错误,且在常规情况下不会造成中间件异常。在XP上将CERTD使用管理员权限启动,再正常启动管理工具则无法访问,因为return后创建的内核对象就没有设置安全描述符,直接使用进程继承下来的安全描述符,这样管理员权限创建的内核对象无法被标准用户权限的进程使用。将代码调整为:

SecurityAttributes::SecurityAttributes() { ZeroMemory(&m_sa, sizeof(SECURITY_ATTRIBUTES)); m_hAdv32Dll = NULL;   if(g_osver.IsWin9x()) { return; }     if(!InitializeSecurityDescriptor(m_pSD, SECURITY_DESCRIPTOR_REVISION)) { return; } SetSecurityDescriptorDacl(m_pSD, TRUE, 0, FALSE);   m_sa.nLength = sizeof(m_sa); m_sa.lpSecurityDescriptor = m_pSD; m_sa.bInheritHandle = FALSE;   m_hAdv32Dll = LoadLibrary("advapi32.dll"); if (NULL == m_hAdv32Dll) return ;   ConvertStrSDToSDFunc func = (ConvertStrSDToSDFunc)GetProcAddress(m_hAdv32Dll, "ConvertStringSecurityDescriptorToSecurityDescriptorA"); if (NULL == func) return;   PSECURITY_DESCRIPTOR pSD; LPCSTR LOW_INTEGRITY_SDDL_SACL = "S:(ML;;NW;;;LW)";   if (!func (LOW_INTEGRITY_SDDL_SACL, SDDL_REVISION_1, &pSD, NULL ) ) return;   PACL pSacl = NULL; // not allocated BOOL fSaclPresent = FALSE; BOOL fSaclDefaulted = FALSE; GetSecurityDescriptorSacl( pSD, &fSaclPresent, &pSacl, &fSaclDefaulted);   SetSecurityDescriptorSacl(m_pSD, TRUE, pSacl, FALSE); LocalFree(pSacl); }

一切好像就正常了。

1) 此处碰到了一个非常奇怪的问题,过去我们一直强调在Vista下申请证书的时候要将CA加入可信任站点并关闭保护模式,但是测试Minidriver的时候却发现即使不加入可信任站点开启保护模式也是可以申请的,因此Minidriver就碰到了保护模式的问题