第16章:安全性
引言
Windows NT 从设计之初就将安全性纳入考量,这与不支持安全性的 Windows 95/98 系列操作系统形成鲜明对比。系统遵循美国国防部定义的 C2 级安全标准——这是通用操作系统能够达到的最高安全级别。
C2 级安全标准的要求包括:登录必须经过身份验证、已终止进程的内存不得泄露给其他进程、以及存在能够对文件系统对象设置权限的文件系统。
WinLogon
WinLogon(Winlogon.exe)作为登录进程,负责交互式登录并响应安全注意序列(Secure Attention Sequence,SAS),默认为 Ctrl+Alt+Del。它会切换到 Winlogon 桌面,在该桌面上显示我们熟悉的选项(锁定、切换用户、注销等)。
WinLogon 通过 LogonUI.exe 获取用户凭据并发送至 Lsass.exe 进行身份验证。成功验证后,Lsass 创建登录会话和访问令牌,该令牌代表用户的安全上下文。随后创建启动进程(默认为 userinit.exe),再由 userinit.exe 创建 Explorer.exe。访问令牌会为每个新创建的进程进行复制。相关注册表项位于 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon。
LogonUI
Windows 支持多种登录方式——用户名/密码、Windows Hello(面部识别和指纹识别)等。LogonUI.exe 由 WinLogon 启动,显示所选身份验证方法的用户界面。在 Vista 之前,这一功能由 WinLogon 直接负责,问题在于如果用户界面组件崩溃,Winlogon 也会随之崩溃。从 Vista 开始,若 UI 崩溃,LogonUI 被终止而 WinLogon 得以存活,用户可选择其他验证方式。LogonUI 的界面由凭据提供程序(Credential Provider,一个 COM DLL)提供。
本地安全授权子系统服务(LSASS)
本地安全认证服务(Local Security Authority Subsystem Service,Lsass.exe)是身份验证管理的基石。其最基本职责是对用户进行身份验证——检查本地注册表(本地登录)或与域控制器通信(域登录)。验证成功后,将结果返回 WinLogon,并为登录用户创建并填充访问令牌。
LsaIso
LsaIso.exe 存在于启用了基于虚拟化的安全性(Virtualization-Based Security,VBS)且开启凭据防护(Credential Guard)的 Windows 10+ 系统中。LsaIso 被称为信任小程序(trustlet),是一个运行在虚拟信任级别(Virtual Trust Level,VTL)1 的用户模式进程。作为 IUM(Isolated User Mode)进程,其访问受 Hyper-V 管理程序保护,即使内核也无法访问。其目的是为 Lsass 保管机密信息,以缓解"传递哈希"(Pass-the-Hash)等攻击。
安全引用监视器(Security Reference Monitor)
安全引用监视器(Security Reference Monitor,SRM)是执行体的一部分,负责在某些操作发生时进行访问检查。例如,尝试打开现有内核对象的句柄时,需由 SRM 执行访问检查。
事件日志记录器(Event Logger)
事件日志记录器服务是标准 Windows 服务之一。它并非专门用于安全性,但安全事件会通过该服务记录。常用查看工具是内置的事件查看器(Event Viewer)应用程序。
安全标识符(SIDs)
主体(Principal)描述在安全上下文中可以被引用的实体——可对其授予或拒绝权限。主体可代表用户、组、计算机等,由安全标识符(Security Identifier,SID)唯一标识。
SID 为大小可变的结构,包含修订版本(始终为 1)、一个 6 字节的颁发机构标识符(Identifier Authority),以及一个由 4 字节子颁发机构(Relative Identifier,RID)组成的数组。当前子颁发机构最大数量为 15。
字符串格式为:S-R-A-SA-SA-…-SA
来自 winnt.h 的定义:
#define SID_MAX_SUB_AUTHORITIES (15)
#define SECURITY_MAX_SID_SIZE \
(sizeof(SID)-sizeof(DWORD)+(SID_MAX_SUB_AUTHORITIES*sizeof(DWORD))) // 68字节二进制与字符串形式之间的转换函数(在 sddl.h 中声明):
BOOL ConvertSidToStringSid(
_In_ PSID Sid,
_Outptr_ LPTSTR* StringSid);
BOOL ConvertStringSidToSid(
_In_ LPCTSTR StringSid,
_Outptr_ PSID* Sid);两个函数的返回结果须由调用者使用 LocalFree 释放。
组和别名
组(Group)是主体的集合,属于该组的每个主体在其安全上下文中都会有该组的 SID。别名(Alias),也称为本地组(Local Group),是另一种容器主体,可包含组和其他主体(但不能包含其他别名)。本地管理员组即为别名。别名始终是本地计算机特有的。
SID 在统计意义上唯一。知名 SID(Well-known SIDs)在每台计算机上代表相同主体,如 S-1-1-0(Everyone 组)和 S-1-5-32-544(本地管理员别名)。winnt.h 定义了 WELL_KNOWN_SID_TYPE 枚举。
创建知名 SID 的函数:
BOOL CreateWellKnownSid(
_In_ WELL_KNOWN_SID_TYPE WellKnownSidType,
_In_opt_ PSID DomainSid,
_Out_ PSID pSid,
_Inout_ DWORD* cbSid);结合 CreateWellKnownSid 和 ConvertSidToStringSid 列出所有无需域 SID 参数的知名 SID:
BYTE buffer[SECURITY_MAX_SID_SIZE];
PWSTR name;
for (int i = 0; i < 120; i++) {
DWORD size = sizeof(buffer);
if (!::CreateWellKnownSid((WELL_KNOWN_SID_TYPE)i, nullptr , (PSID)buffer, &size))
continue ;
::ConvertSidToStringSid((PSID)buffer, &name);
printf("Well known sid %3d: %ws\n", i, name);
::LocalFree(name);
}输出示例:
Well known sid 0: S-1-0-0
Well known sid 1: S-1-1-0
Well known sid 22: S-1-5-18
Well known sid 26: S-1-5-32-544
Well known sid 27: S-1-5-32-545
Well known sid 65: S-1-16-0
Well known sid 66: S-1-16-4096
Well known sid 67: S-1-16-8192检查 SID 是否与特定知名 SID 匹配:
BOOL IsWellKnownSid(
_In_ PSID pSid,
_In_ WELL_KNOWN_SID_TYPE WellKnownSidType);获取知名 SID 的名称:
BOOL LookupAccountSid(
_In_opt_ LPCTSTR lpSystemName,
_In_ PSID Sid,
_Out_ LPWSTR Name,
_Inout_ LPDWORD cchName,
_Out_ LPWSTR ReferencedDomainName,
_Inout_ LPDWORD cchReferencedDomainName,
_Out_ PSID_NAME_USE peUse);SID_NAME_USE 枚举包括:SidTypeUser、SidTypeGroup、SidTypeDomain、SidTypeAlias、SidTypeWellKnownGroup、SidTypeDeletedAccount、SidTypeInvalid、SidTypeUnknown、SidTypeComputer、SidTypeLabel、SidTypeLogonSession。
在迭代中添加 LookupAccountSid:
WCHAR accountName[64] = { 0 }, domainName[64] = { 0 };
SID_NAME_USE use;
for (int i = 0; i < 120; i++) {
DWORD size = sizeof(buffer);
if (!::CreateWellKnownSid((WELL_KNOWN_SID_TYPE)i, nullptr , (PSID)buffer, &size))
continue;
::ConvertSidToStringSid((PSID)buffer, &name);
DWORD accountNameSize = _countof(accountName);
DWORD domainNameSize = _countof(domainName);
::LookupAccountSid(nullptr , (PSID)buffer, accountName, &accountNameSize,
domainName, &domainNameSize, &use);
printf("Well known sid %3d: %-20ws %ws\\%ws (%s)\n", i,
name, domainName, accountName, SidNameUseToString(use));
::LocalFree(name);
}LookupAccountName 是 LookupAccountSid 的对应函数:
BOOL LookupAccountName(
_In_opt_ LPCTSTR lpSystemName,
_In_ LPCTSTR lpAccountName,
_Out_ PSID Sid,
_Inout_ LPDWORD cbSid,
_Out_ LPTSTR ReferencedDomainName,
_Inout_ LPDWORD cchReferencedDomainName,
_Out_ PSID_NAME_USE peUse);其他 SID 相关 API 包括:IsValidSid、CopySid、EqualSid、GetLengthSid、AllocatedAndInitializeSid、InitializeSid、FreeSid、GetSidIdentifierAuthority、GetSidSubAuthorityCount 和 GetSidSubAuthority。
令牌(Tokens)
用户成功登录后(无论是否交互式),系统在后台创建一个登录会话对象(Logon Session)。该对象隐藏在访问令牌(Access Token,简称令牌)之后,令牌是一个维护多种信息且指向登录会话的对象。
每个进程关联一个主令牌(Primary Token),所有线程默认使用该主令牌。线程可通过使用不同的模拟令牌(Impersonation Token)进行模拟,模拟完成后恢复使用进程主令牌。
Windows 中三个内置用户——本地服务(Local Service)、网络服务(Network Service)和本地系统(SYSTEM),始终对应三个登录会话。
获取进程令牌:
BOOL OpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ DWORD DesiredAccess,
_Outptr_ PHANDLE TokenHandle);常见的 DesiredAccess 值包括:TOKEN_QUERY、TOKEN_ADJUST_PRIVILEGES、TOKEN_ADJUST_DEFAULT、TOKEN_DUPLICATE 和 TOKEN_IMPERSONATE。
获取线程令牌:
BOOL OpenThreadToken(
_In_ HANDLE ThreadHandle,
_In_ DWORD DesiredAccess,
_In_ BOOL OpenAsSelf,
_Outptr_ PHANDLE TokenHandle);OpenAsSelf 指示访问检查应根据哪个令牌进行——TRUE 时针对进程令牌,否则针对当前线程令牌。
查询令牌信息:
BOOL GetTokenInformation(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_Out_ LPVOID TokenInformation,
_In_ DWORD TokenInformationLength,
_Out_ PDWORD ReturnLength);获取与令牌关联的用户 SID:
BYTE buffer[1 << 12]; // 4KB
if (::GetTokenInformation(hToken, TokenUser, buffer, sizeof(buffer), &len)) {
auto data = (TOKEN_USER*)buffer;
printf("User SID: %ws\n", SidToString(data->User.Sid).c_str());
}辅助函数:
std::wstring SidToString(const PSID sid) {
PWSTR ssid;
std::wstring result;
if (::ConvertSidToStringSid(sid, &ssid)) {
result = ssid;
::LocalFree(ssid);
}
return result;
}使用 TokenStatistics 信息类:
TOKEN_STATISTICS stats;
if (::GetTokenInformation(hToken, TokenStatistics, &stats, sizeof(stats), &len)) {
printf("Token ID: 0x%08llX\n", LuidToNum(stats.TokenId));
printf("Logon Session ID: 0x%08llX\n", LuidToNum(stats.AuthenticationId));
printf("Token Type: %s\n", stats.TokenType == TokenPrimary ?
"Primary" : "Impersonation");
if (stats.TokenType == TokenImpersonation)
printf("Impersonation level: %s\n",
ImpersonationLevelToString(stats.ImpersonationLevel));
printf("Dynamic charged (bytes): %lu\n", stats.DynamicCharged);
printf("Dynamic available (bytes): %lu\n", stats.DynamicAvailable);
printf("Group count: %lu\n", stats.GroupCount);
printf("Privilege count: %lu\n", stats.PrivilegeCount);
printf("Modified ID: %08llX\n\n", LuidToNum(stats.ModifiedId));
}辅助函数:
ULONGLONG LuidToNum(const LUID& luid) {
return *(const ULONGLONG*)&luid;
}
const char* ImpersonationLevelToString(SECURITY_IMPERSONATION_LEVEL level) {
switch (level) {
case SecurityAnonymous: return "Anonymous";
case SecurityIdentification: return "Identification";
case SecurityImpersonation: return "Impersonation";
case SecurityDelegation: return "Delegation";
}
return "Unknown";
}TOKEN_STATISTICS 结构:
typedef struct _TOKEN_STATISTICS {
LUID TokenId;
LUID AuthenticationId;
LARGE_INTEGER ExpirationTime;
TOKEN_TYPE TokenType;
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
DWORD DynamicCharged;
DWORD DynamicAvailable;
DWORD GroupCount;
DWORD PrivilegeCount;
LUID ModifiedId;
} TOKEN_STATISTICS, *PTOKEN_STATISTICS;LUID(Locally Unique Identifier)类型:
typedef struct _LUID {
DWORD LowPart;
LONG HighPart;
} LUID, *PLUID;TokenId 是令牌实例的唯一标识符;AuthenticationId 是登录会话 ID。三个内置登录会话的 ID 有已知值:SYSTEM 为 999(0x3e7),Local Service 为 997(0x3e5),Network Service 为 996(0x3e4)。
模拟级别(Impersonation Level):
typedef enum _SECURITY_IMPERSONATION_LEVEL {
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
} SECURITY_IMPERSONATION_LEVEL;设置令牌信息:
BOOL SetTokenInformation(
_In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_In_ LPVOID TokenInformation,
_In_ DWORD TokenInformationLength);SetTokenInformation 的有效信息类及其要求:
| TOKEN_INFORMATION_CLASS | 所需访问掩码 | 所需特权 |
|---|---|---|
| TokenOwner | TOKEN_ADJUST_DEFAULT | 无 |
| TokenPrimaryGroup | TOKEN_ADJUST_DEFAULT | 无 |
| TokenDefaultDacl | TOKEN_ADJUST_DEFAULT | 无 |
| TokenSessionId | TOKEN_ADJUST_SESSIONID | SeTcbPrivilege |
| TokenVirtualizationAllowed | 无 | SeCreateTokenPrivilege |
| TokenVirtualizationEnabled | TOKEN_ADJUST_DEFAULT | 无 |
| TokenOrigin | 无 | SeTcbPrivilege |
| TokenMandatoryPolicy | 无 | SeCreateTokenPrivilege |
更改 UAC 虚拟化状态(省略错误处理):
HANDLE hToken;
::OpenProcessToken(hProcess, TOKEN_ADJUST_DEFAULT, &hToken);
ULONG enable = 1; // enable
::SetTokenInformation(hToken, TokenVirtualizationEnabled, &enable, sizeof(enable));二次登录服务(The Secondary Logon Service)
二次登录服务(seclogon)是一项内置服务,允许以与调用者不同的用户身份启动进程。它是与 runas.exe 配合使用的服务。通过 CreateProcessWithLogonW 调用:
BOOL CreateProcessWithLogonW(
_In_ LPCWSTR lpUsername,
_In_opt_ LPCWSTR lpDomain,
_In_ LPCWSTR lpPassword,
_In_ DWORD dwLogonFlags,
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation);CreateProcessWithLogonW 是 LogonUser 和 CreateProcessAsUser 的组合。LogonUser 函数:
BOOL LogonUser(
_In_ LPCTSTR lpszUsername,
_In_opt_ LPCTSTR lpszDomain,
_In_opt_ LPCTSTR lpszPassword,
_In_ DWORD dwLogonType,
_In_ DWORD dwLogonProvider,
_Outptr_ PHANDLE phToken);常见的 dwLogonType 值:
LOGON32_LOGON_INTERACTIVE:交互式用户,缓存信息用于离线操作LOGON32_LOGON_BATCH:无人值守执行,不缓存凭据LOGON32_LOGON_NETWORK:返回模拟令牌而非主令牌
dwLogonProvider 可选:LOGON32_PROVIDER_WINNT50(Kerberos)、LOGON32_PROVIDER_WINNT40(NTLM)、LOGON32_PROVIDER_DEFAULT(NTLM)。
CreateProcessAsUser 函数:
BOOL CreateProcessAsUser(
_In_opt_ HANDLE hToken,
_In_opt_ LPCTSTR lpApplicationName,
_Inout_opt_ LPTSTR lpCommandLine,
_In_opt_ LPSECURITY_ATTRIBUTES lpProcessAttributes,
_In_opt_ LPSECURITY_ATTRIBUTES lpThreadAttributes,
_In_ BOOL bInheritHandles,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCTSTR lpCurrentDirectory,
_In_ LPSTARTUPINFO lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation);调用 CreateProcessAsUser 需要 SeAssignPrimaryTokenPrivilege 权限,通常授予服务账户而非标准用户或本地管理员。
dwLogonFlags 参数:LOGON_WITH_PROFILE(加载用户配置文件)、0(不加载)、LOGON_NETCREDENTIALS_ONLY(以调用者身份执行但使用指定用户进行网络访问)。
另一个函数 CreateProcessWithTokenW:
BOOL CreateProcessWithTokenW(
_In_ HANDLE hToken,
_In_ DWORD dwLogonFlags,
_In_opt_ LPCWSTR lpApplicationName,
_Inout_opt_ LPWSTR lpCommandLine,
_In_ DWORD dwCreationFlags,
_In_opt_ LPVOID lpEnvironment,
_In_opt_ LPCWSTR lpCurrentDirectory,
_In_ LPSTARTUPINFOW lpStartupInfo,
_Out_ LPPROCESS_INFORMATION lpProcessInformation);需要 SeImpersonatePrivilege 权限。
模拟(Impersonation)
模拟允许线程使用不同于所属进程的安全上下文执行操作。DuplicateTokenEx 函数:
BOOL DuplicateTokenEx(
_In_ HANDLE hExistingToken,
_In_ DWORD dwDesiredAccess,
_In_opt_ LPSECURITY_ATTRIBUTES lpTokenAttributes,
_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
_In_ TOKEN_TYPE TokenType,
_Outptr_ PHANDLE phNewToken);hExistingToken 需具有 TOKEN_DUPLICATE 访问掩码。
将令牌附加到线程:
BOOL SetThreadToken(
_In_opt_ PHANDLE Thread,
_In_opt_ HANDLE Token);恢复原始令牌:
BOOL RevertToSelf();复制进程令牌进行模拟的完整示例(省略错误处理):
HANDLE hProcToken;
::OpenProcessToken(GetCurrentProcess(), TOKEN_DUPLICATE, &hProcToken);
HANDLE hImpToken;
::DuplicateTokenEx(hProcToken, MAXIMUM_ALLOWED, nullptr ,
SecurityIdentification, TokenImpersonation, &hImpToken);
::CloseHandle(hProcToken);
// 启用新令牌上的 UAC 虚拟化
ULONG virt = 1;
::SetTokenInformation(hImpToken, TokenVirtualizationEnabled,
&virt, sizeof(virt));
// 模拟
::SetThreadToken(nullptr, hImpToken);
// 执行操作...
::RevertToSelf();
::CloseHandle(hImpToken);一步完成模拟:
BOOL ImpersonateSelf(_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel);使用现有令牌模拟:
BOOL ImpersonateLoggedOnUser(_In_ HANDLE hToken);在 LogonUser 后使用 ImpersonateLoggedOnUser 的示例:
HANDLE hToken;
::LogonUser(L"alice", L".", L"alicesecretpassword",
LOGON32_LOGON_BATCH, LOGON32_PROVIDER_DEFAULT, &hToken);
::ImpersonateLoggedOnUser(hToken);
// 以 alice 的身份执行操作...
::RevertToSelf();
::CloseHandle(hToken);客户端/服务器中的模拟
模拟级别在令牌发送到另一台计算机上的服务器时指示其权限:
- SecurityAnonymous:服务器不知客户端身份,无法模拟
- SecurityIdentification:可查询客户端属性,但无法模拟(除非在同一台计算机)
- SecurityImpersonation:仅能在自己的计算机上模拟
- SecurityDelegation:允许调用另一台计算机上的服务器并传播令牌
不同通信机制的模拟和还原 API:
| 通信机制 | 模拟 | 还原 |
|---|---|---|
| 命名管道(Named Pipe) | ImpersonateNamedPipeClient | RevertToSelf |
| RPC | RpcImpersonateClient | RpcRevertToSelf |
| COM | CoImpersonateClient | CoRevertToSelf |
特权(Privileges)
特权是执行某些系统级操作的权利(或被拒绝的权利),它与特定对象无关。示例包括:加载设备驱动程序、调试其他用户的进程、获取对象的所有权等。
用户权限(User Rights)与特权(Privileges)的区别在于:用户权限适用于账户(关于登录),特权在用户登录后适用并存储在访问令牌中。
一旦令牌被创建或复制,就不能再添加新特权。管理员需向账户数据库本身添加特权,但这不影响现有令牌。
大多数特权默认禁用。唯一默认启用的特权是 SeChangeNotifyPrivilege(绕过遍历检查,Bypass Traverse Checking),它允许用户访问某个目录中的文件,即使该用户无法访问该文件的某些父目录。
启用、禁用或删除特权:
BOOL AdjustTokenPrivileges(
_In_ HANDLE TokenHandle,
_In_ BOOL DisableAllPrivileges,
_In_opt_ PTOKEN_PRIVILEGES NewState,
_In_ DWORD BufferLength,
_Out_opt_ PTOKEN_PRIVILEGES PreviousState,
_Out_opt_ PDWORD ReturnLength);TokenHandle 需具有 TOKEN_ADJUST_PRIVILEGES 访问掩码。TOKEN_PRIVILEGES 结构:
typedef struct _LUID_AND_ATTRIBUTES {
LUID Luid;
DWORD Attributes;
} LUID_AND_ATTRIBUTES;
typedef struct _TOKEN_PRIVILEGES {
DWORD PrivilegeCount;
LUID_AND_ATTRIBUTES Privileges[ANYSIZE_ARRAY];
} TOKEN_PRIVILEGES, *PTOKEN_PRIVILEGES;获取特权的 LUID:
BOOL LookupPrivilegeValue(
_In_opt_ LPCTSTR lpSystemName,
_In_ LPCTSTR lpName,
_Out_ PLUID lpLuid);通用启用/禁用特权的函数:
bool EnablePrivilege(PCWSTR privName, bool enable) {
HANDLE hToken;
if (!::OpenProcessToken(::GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
return false;
bool result = false;
TOKEN_PRIVILEGES tp;
tp.PrivilegeCount = 1;
tp.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0;
if (::LookupPrivilegeValue(nullptr, privName, &tp.Privileges[0].Luid)) {
if (::AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), nullptr, nullptr))
result = ::GetLastError() == ERROR_SUCCESS;
}
::CloseHandle(hToken);
return result;
}超级特权
一组极为强大的特权,拥有其中任何一项都几乎可以完全控制系统:
获取所有权(SeTakeOwnershipPrivilege):允许持有者将自己设置为任何内核对象(文件、互斥体、进程等)的所有者。作为所有者,可给自己授予对该对象的完全访问权限。通常授予管理员。
备份(SE_BACKUP_NAME):允许读取任何对象,无视其安全描述符。管理员和备份操作员组默认拥有此特权。使用 CreateFile 时需指定 FILE_BACKUP_SEMANTICS。
还原(SE_RESTORE_NAME):与备份相反,提供对任何内核对象的写入访问权限。
调试(SE_DEBUG_NAME):允许调试和操作任何进程的内存,包括调用 CreateRemoteThread。受保护的进程(Protected Process)和 PPL(Protected Process Light)除外。
可信计算基础(SE_TCB_NAME):被描述为"作为操作系统的一部分运行",是最强大的特权之一。默认不授予任何用户或组。拥有此特权允许模拟任何其他用户,拥有与内核相同的访问权限。
创建令牌(SE_CREATE_TOKEN_NAME):允许创建令牌,填充任何特权或组。默认不授予任何用户。Lsass 进程拥有此特权。
访问掩码(Access Masks)
访问掩码(Access Mask)是一个 32 位值,其位有逻辑分组:低 16 位为特定权限(如 PROCESS_TERMINATE),随后是标准权限(如 SYNCHRONIZE、DELETE、WRITE_DAC)。第 24 位为 ACCESS_SYSTEM_SECURITY,第 25 位为 MAXIMUM_ALLOWED,第 28-31 位为通用权限(GENERIC_READ、GENERIC_WRITE、GENERIC_EXECUTE、GENERIC_ALL)。
通用权限需要映射为具体访问权限,通过以下结构完成:
typedef struct _GENERIC_MAPPING {
ACCESS MASK GenericRead;
ACCESS MASK GenericWrite;
ACCESS MASK GenericExecute;
ACCESS MASK GenericAll;
} GENERIC_MAPPING;安全描述符(Security Descriptors)
安全描述符(Security Descriptor)是长度可变的结构,包含:
- 所有者 SID(Owner SID)
- 主要组 SID(Primary Group SID,用于 POSIX)
- 自由访问控制列表(Discretionary Access Control List,DACL)——指定哪些主体可对对象执行哪些操作的 ACE 列表
- 系统访问控制列表(System Access Control List,SACL)——指示哪些操作应写入安全日志审核条目的 ACE 列表
对象的所有者始终拥有 WRITE_DAC(以及 READ_CONTROL)标准访问权限,确保所有者可更改对象的 DACL。
获取内核对象安全描述符:
BOOL GetKernelObjectSecurity(
_In_ HANDLE Handle,
_In_ SECURITY_INFORMATION RequestedInformation,
_Out_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
_In_ DWORD nLength,
_Out_ LPDWORD lpnLengthNeeded);句柄需有 READ_CONTROL 访问掩码。使用 SACL_SECURITY_INFORMATION 需 SeSecurityPrivilege 权限。
提取安全描述符数据的函数:
DWORD GetSecurityDescriptorLength(_In_ PSECURITY_DESCRIPTOR pSecurityDescriptor);
BOOL GetSecurityDescriptorControl(
_In_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
_Out_ PSECURITY_DESCRIPTOR_CONTROL pControl,
_Out_ LPDWORD lpdwRevision);
BOOL GetSecurityDescriptorOwner(
_In_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
_Outptr_ PSID* pOwner,
_Out_ LPBOOL lpbOwnerDefaulted);
BOOL GetSecurityDescriptorGroup(
_In_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
_Outptr_ PSID* pGroup,
_Out_ LPBOOL lpbGroupDefaulted);
BOOL GetSecurityDescriptorDacl(
_In_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
_Out_ LPBOOL lpbDaclPresent,
_Outptr_ PACL* pDacl,
_Out_ LPBOOL lpbDaclDefaulted);
BOOL GetSecurityDescriptorSacl(
_In_ PSECURITY_DESCRIPTOR pSecurityDescriptor,
_Out_ LPBOOL lpbSaclPresent,
_Outptr_ PACL* pSacl,
_Out_ LPBOOL lpbSaclDefaulted);根据进程 ID 显示进程所有者的示例:
bool DisplayProcessOwner(DWORD pid) {
HANDLE hProcess = ::OpenProcess(READ_CONTROL, FALSE, pid);
if (!hProcess)
return false;
BYTE buffer[1 << 10];
auto sd = (PSECURITY_DESCRIPTOR)buffer;
DWORD len;
BOOL success = ::GetKernelObjectSecurity(hProcess,
OWNER_SECURITY_INFORMATION,
sd, sizeof(buffer), &len);
::CloseHandle(hProcess);
if (!success)
return false;
PSID owner;
BOOL isDefault;
if (!::GetSecurityDescriptorOwner(sd, &owner, &isDefault))
return false;
printf("Owner: %ws (%ws)\n", GetUserNameFromSid(owner).c_str(),
SidToString(owner).c_str());
return true;
}GetNamedSecurityInfo 函数(AclAPI.h):
DWORD GetNamedSecurityInfo(
_In_ LPCTSTR pObjectName,
_In_ SE_OBJECT_TYPE ObjectType,
_In_ SECURITY_INFORMATION SecurityInfo,
_Out_opt_ PSID * ppsidOwner,
_Out_opt_ PSID * ppsidGroup,
_Out_opt_ PACL * ppDacl,
_Out_opt_ PACL * ppSacl,
_Out_ PSECURITY_DESCRIPTOR * ppSecurityDescriptor);显示文件所有者的示例:
bool DisplayFileOwner(PCWSTR filename) {
PSID owner;
DWORD error = ::GetNamedSecurityInfo(filename, SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION, &owner,
nullptr, nullptr, nullptr, nullptr);
if (error != ERROR_SUCCESS)
return false;
printf("Owner: %ws (%ws)\n", GetUserNameFromSid(owner).c_str(),
SidToString(owner).c_str());
return true;
}ACE 结构与 DACL
DACL 中的每个 ACE(Access Control Entry)包含:所应用 SID、访问掩码和 ACE 类型(允许或拒绝)。
安全检查过程:安全引用监视器(SRM)遍历 DACL 中的 ACE 以查找明确结果,一旦找到即终止遍历。
关于安全描述符/DACL 的关键规则:
- 若安全描述符为 NULL,则对象无保护,所有访问均允许
- 若 DACL 为 NULL(但描述符存在),同样无保护
- 若 DACL 为空(无 ACE),则除所有者外无人可访问
ACE 顺序至关重要。资源管理器安全对话框总是将拒绝 ACE 置于允许 ACE 之前。
ACL 结构:
typedef struct _ACL {
BYTE AclRevision;
BYTE Sbz1;
WORD AclSize;
WORD AceCount;
WORD Sbz2;
} ACL;
typedef ACL *PACL;ACE_HEADER:
typedef struct _ACE_HEADER {
BYTE AceType;
BYTE AceFlags;
WORD AceSize;
} ACE_HEADER;
typedef ACE_HEADER *PACE_HEADER;获取 ACE:
BOOL GetAce(
_In_ PACL pAcl,
_In_ DWORD dwAceIndex,
_Outptr_ LPVOID* pAce);两种最常见的 ACE 类型:
typedef struct _ACCESS_ALLOWED_ACE {
ACE_HEADER Header;
ACCESS_MASK Mask;
DWORD SidStart;
} ACCESS_ALLOWED_ACE;
typedef struct _ACCESS_DENIED_ACE {
ACE_HEADER Header;
ACCESS_MASK Mask;
DWORD SidStart;
} ACCESS_DENIED_ACE;显示 ACE 信息的函数:
void DisplayAce(PACE_HEADER header, int index) {
printf("ACE %2d: Size: %2d bytes, Flags: 0x%02X Type: %s\n",
index, header->AceSize, header->AceFlags,
AceTypeToString(header->AceType));
switch (header->AceType) {
case ACCESS_ALLOWED_ACE_TYPE:
case ACCESS_DENIED_ACE_TYPE:
{
auto data = (ACCESS_ALLOWED_ACE*)header;
printf("\tAccess: 0x%08X %ws (%ws)\n", data->Mask,
GetUserNameFromSid((PSID)&data->SidStart).c_str(),
SidToString((PSID)&data->SidStart).c_str());
}
break;
}
}安全描述符有两种格式:自相关(Self-relative)和绝对(Absolute)。可通过 MakeAbsoluteSD 和 MakeSelfRelativeSD 转换。SDDL(Security Descriptor Definition Language)字符串表示与二进制之间的转换使用 ConvertSecurityDescriptorToStringSecurityDescriptor 和 ConvertStringSecurityDescriptorToSecurityDescriptor。
默认安全描述符
SECURITY_ATTRIBUTES 结构:
typedef struct _SECURITY_ATTRIBUTES {
DWORD nLength;
LPVOID lpSecurityDescriptor;
BOOL bInheritHandle;
} SECURITY_ATTRIBUTES;未命名对象(互斥体、事件、信号量、文件映射)的安全描述符没有 DACL。命名对象有带默认 DACL 的安全描述符,该默认 DACL 来自访问令牌。
查询默认 DACL:
HANDLE hToken;
::OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &hToken);
::GetTokenInformation(hToken, TokenDefaultDacl, buffer, sizeof(buffer), &len);
auto dacl = ((TOKEN_DEFAULT_DACL*)buffer)->DefaultDacl;构建安全描述符
为事件对象构建安全描述符的完整示例(省略错误处理):
BYTE sdBuffer[SECURITY_DESCRIPTOR_MIN_LENGTH];
auto sd = (PSECURITY_DESCRIPTOR)sdBuffer;
::InitializeSecurityDescriptor(sd, SECURITY_DESCRIPTOR_REVISION);
BYTE ownerSid[SECURITY_MAX_SID_SIZE];
DWORD size;
::CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, (PSID)ownerSid, &size);
::SetSecurityDescriptorOwner(sd, (PSID)ownerSid, FALSE);
BYTE everyoneSid[SECURITY_MAX_SID_SIZE];
b = ::CreateWellKnownSid(WinWorldSid, nullptr, (PSID)everyoneSid, &size);
EXPLICIT_ACCESS ea[2];
ea[0].grfAccessPermissions = EVENT_ALL_ACCESS;
ea[0].grfAccessMode = SET_ACCESS;
ea[0].grfInheritance = NO_INHERITANCE;
ea[0].Trustee.ptstrName = (PWSTR)ownerSid;
ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
ea[1].grfAccessPermissions = SYNCHRONIZE;
ea[1].grfAccessMode = SET_ACCESS;
ea[1].grfInheritance = NO_INHERITANCE;
ea[1].Trustee.ptstrName = (PWSTR)everyoneSid;
ea[1].Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea[1].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
PACL dacl;
::SetEntriesInAcl(_countof(ea), ea, nullptr, &dacl);
::SetSecurityDescriptorDacl(sd, TRUE, dacl, FALSE);
SECURITY_ATTRIBUTES sa = { sizeof(sa) };
sa.lpSecurityDescriptor = sd;
HANDLE hEvent = ::CreateEvent(&sa, FALSE, FALSE, nullptr);
::LocalFree(dacl);简便方法:使用 SDDL 字符串调用 ConvertStringSecurityDescriptorToSecurityDescriptor。
更改现有对象安全性的 API:
BOOL SetKernelObjectSecurity(
_In_ HANDLE Handle,
_In_ SECURITY_INFORMATION SecurityInformation,
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor);
DWORD SetSecurityInfo(
HANDLE handle,
SE_OBJECT_TYPE ObjectType,
SECURITY_INFORMATION SecurityInfo,
_In_opt_ PSID psidOwner,
_In_opt_ PSID psidGroup,
_In_opt_ PACL pDacl,
_In_opt_ PACL pSacl);
BOOL SetFileSecurity(
_In_ LPCTSTR lpFileName,
_In_ SECURITY_INFORMATION SecurityInformation,
_In_ PSECURITY_DESCRIPTOR SecurityDescriptor);
DWORD SetNamedSecurityInfo(
_In_ LPTSTR pObjectName,
_In_ SE_OBJECT_TYPE ObjectType,
_In_ SECURITY_INFORMATION SecurityInfo,
_In_opt_ PSID psidOwner,
_In_opt_ PSID psidGroup,
_In_opt_ PACL pDacl,
_In_opt_ PACL pSacl);用户访问控制(User Access Control)
UAC(User Access Control)于 Windows Vista 引入。在 Vista 之前,用户被创建为本地管理员,所有执行的代码都具有管理员权限。Vista 改变了这种情况:新创建的用户不一定是管理员,且第一个管理员用户默认也不以管理员权限运行。Lsass 为本地管理员用户创建两个访问令牌——一个是完整的管理员令牌,另一个是标准用户权限令牌。进程默认使用标准用户权限令牌运行。
Vista 放宽部分操作要求使标准用户运行更容易:
- "更改时间"特权被拆分为"更改时间"和"更改时区"
- 部分以前仅管理员可进行的配置现标准用户也可进行
- 虚拟化(Virtualization)
若进程需以管理员权限运行,则请求提权(Elevation)。这会显示两个对话框之一:真正管理员看到"是/否"确认对话框,非管理员则看到要求输入用户名/密码的对话框。
- 微软签名二进制文件:浅蓝色
- 其他实体签名:浅灰色
- 未签名:明亮的橙色/黄色
UAC 对话框有 4 个级别(实际与提权相关 3 个):
- 始终通知:任何提权操作弹出提示
- 从不通知:真正管理员自动提权;否则显示输入管理员凭据对话框(管理员批准模式,Admin Approval Mode,AAM)
- 中间选项:对 Windows 组件不弹出同意对话框
Windows 组件包括任务管理器、任务计划程序、设备管理器等;不包括 cmd.exe、notepad.exe、regedit.exe。
从 Windows 8 起,UAC 无法完全关闭,因为通用 Windows 平台(UWP)进程始终使用标准令牌运行。
提升权限(Elevation)
启动提权进程的有文档记录的方法是 ShellExecute 或 ShellExecuteEx(shellapi.h):
HINSTANCE ShellExecute(
_In_opt_ HWND hwnd,
_In_opt_ LPCTSTR lpOperation,
_In_ LPCTSTR lpFile,
_In_opt_ LPCTSTR lpParameters,
_In_opt_ LPCTSTR lpDirectory,
_In_ INT nShowCmd);以管理员身份启动记事本:
::ShellExecute(nullptr, L"runas", L"notepad.exe", nullptr, nullptr, SW_SHOWDEFAULT);提权关键参数是 lpVerb 设为 "runas"。提权过程向 AppInfo service 发送消息,该 service 调用 consent.exe 显示对话框,若批准则调用 CreateProcessAsUser 使用提升后的令牌启动可执行文件。
无法在原位提升令牌权限——如果可以,UAC 就毫无用处了。
要求以管理员身份运行
通过清单文件(Manifest)通知需提权:
<trustInfo xmlns="urn:schema-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel Level="requireAdministrator" />
</requestedPrivileges>
</security>
</trustInfo>Level 可能值:
- asInvoker:默认值,父进程提升则该进程也提升,否则标准运行
- requireAdministrator:需管理员提权,无提权则无法启动
- highestAvailable:中间值,真正管理员则尝试提权,否则标准运行
UAC 虚拟化
Vista 之前的应用假定用户是管理员,会执行如写入系统目录或 HKLM\Software 等操作。UAC 虚拟化(UAC Virtualization)将此类应用的写入重定向到文件系统/注册表中用户的私有区域,使调用不致失败。
文件系统私有存储区位于:C:\Users\<用户名>\AppData\Local\VirtualStore
UAC 虚拟化自动应用于 32 位可执行文件且无表明适用于 Vista 及更高版本的清单者。任务管理器中 UAC 虚拟化列有三个值:
- 不允许(Not Allowed):系统进程和服务
- 已禁用(Disabled):未激活
- 已启用(Enabled):已激活
完整性级别(Integrity Levels)
正式名称为强制完整性控制(Mandatory Integrity Control),于 Vista 引入。其中一个原因是在同一用户下,将使用标准权限令牌运行的进程与使用提升权限令牌运行的进程区分开来。
标准完整性级别:
| 级别 | SID | 备注 |
|---|---|---|
| 系统(System) | S-1-16-16384 | 最高级别,系统进程和服务 |
| 高(High) | S-1-16-12288 | 提升权限令牌进程 |
| 中加(Medium Plus) | S-1-16-8448 | |
| 中(Medium) | S-1-16-8192 | 标准用户权限进程 |
| 低(Low) | S-1-16-4096 | UWP 进程和大多数浏览器 |
强制策略默认值为"禁止向上写入(No Write Up)"——进程试图访问完整性级别更高的对象时,不允许写入类型的访问。例如,完整性级别为"中"的进程打开"高"级别进程的句柄时,仅被授予 PROCESS_QUERY_LIMITED_INFORMATION、SYNCHRONIZE 和 PROCESS_TERMINATE 访问掩码。
所有对象(包括文件)的完整性级别默认为"中",除非通过添加类型为"强制标签(Mandatory Label)"的 ACE 来更改。可设置低于调用者令牌的级别,但设置更高级别需要 SeRelabelPrivilege(通常不授予任何人)。
UWP 进程以低完整性级别运行,无法访问用户文档或图片等常见文件位置(这些为中等级别)。如今大多数浏览器也以低完整性级别运行其进程。
启动可执行文件时,新进程的完整性级别为可执行文件的完整性级别和调用者进程令牌的完整性级别的最小值。
可使用 GetTokenInformation(TokenIntegrityLevel)读取完整性级别,使用 SetTokenInformation 设置。
完整性级别与 DACL 的关系:完整性级别优先。如果调用者的完整性级别等于或高于目标对象的完整性级别,则使用 DACL 进行正常的访问检查;否则"禁止向上写入"策略优先。
用户界面特权隔离(UIPI)
UIPI(User Interface Privilege Isolation)基于完整性级别。具有低完整性级别的进程不能向由更高完整性级别进程所拥有的窗口发送不受控制的消息。少数良性消息除外(例如:WM_NULL、WM_GETTEXT 和 WM_GETICON)。
较高完整性级别的进程可通过函数允许某些消息通过:
BOOL ChangeWindowMessageFilter(
_In_ UINT message,
_In_ DWORD dwFlag);
BOOL ChangeWindowMessageFilterEx(
_In_ HWND hwnd,
_In_ UINT message,
_In_ DWORD action,
_Inout_opt_ PCHANGEFILTERSTRUCT pChangeFilterStruct);ChangeWindowMessageFilter 的 dwFlag 取 MSGFLT_ADD(允许)或 MSGFLT_REMOVE(阻止),影响进程所有窗口。ChangeWindowMessageFilterEx(Windows 7)可对每个单独窗口进行控制,action 可取 MSGFLT_ALLOW、MSGFLT_DISALLOW 或 MSGFLT_RESET。
专用安全机制
控制流防护(Control Flow Guard,CFG)
控制流防护(Control Flow Guard,CFG)于 Windows 10 和 Server 2016 引入,用于缓解与间接调用相关的特定类型的攻击。C++ 中的虚函数调用通过虚表指针(vptr)完成,恶意代理可覆盖 vptr 并将其重定向到备用虚表。CFG 在进行任何间接调用之前进行额外检查——若目标不在任何一个模块(DLL 和 EXE)中,则进程终止。
Visual Studio 中通过在项目属性中选择 CFG 选项即可支持。
C++ 示例:
class A {
public:
virtual ~A() = default;
virtual void DoWork(int x) {
printf("A::DoWork %d\n", x);
}
};
class B : public A {
public:
void DoWork(int x) override {
printf("B::DoWork %d\n", x);
}
};
void main() {
A a;
a.DoWork(10);
B b;
b.DoWork(20);
A* pA = new B;
pA->DoWork(30);
delete pA;
}应用 CFG 后,pA->DoWork 的汇编代码调用 guard_dispatch_icall_fptr(NtDll.dll 中),该函数检查调用目标是否有效,无效则终止进程。
支持 CFG 的二进制文件在 PE(Portable Executable)中包含额外信息,列出所有有效函数。可使用 dumpbin.exe 查看:
C:\>dumpbin /loadconfig cfgdemo.exe输出包含 Guard CF function table、Guard CF function count 等信息。
CFG 工作原理:加载程序创建一个较大的保留位图(Bitmap),每个有效函数在位图中标记为"1"。检查函数是否有效是 O(1) 操作——函数指针右移以找到位图中代表它的位。
进程缓解措施(Process Mitigations)
Windows 8 引入,可单向为进程设置各种与安全相关的属性——一旦设置了某项缓解措施,就无法撤销。
四种设置方法:
- 组策略设置
- 映像文件执行选项(Image File Execution Options,IFEO)注册表项
- 通过
CreateProcess的进程属性 - 在进程内部调用
SetProcessMitigationPolicy
IFEO 键路径:HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options
缓解选项包括:
- 严格句柄检查(Strict Handle Checks):使用无效句柄则终止进程
- 禁用 Win32K 调用(Disable Win32K calls):进行 user32.dll 或 gdi32.dll 调用则引发异常
- 控制流防护(Control Flow Guard):要求加载的所有 DLL 支持 CFG
- 优先使用系统映像(Prefer System Images):确保 System32 中的 DLL 优先于其他位置的同名 DLL
通过 CreateProcess 设置 CFG 缓解措施的示例:
HANDLE LaunchWithCfgMitigation(PWSTR exePath) {
PROCESS_INFORMATION pi;
STARTUPINFOEX si = { sizeof(si) };
SIZE_T size;
DWORD64 mitigation =
PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_ON;
::InitializeProcThreadAttributeList(nullptr, 1, 0, &size);
si.lpAttributeList = (PPROC_THREAD_ATTRIBUTE_LIST)::malloc(size);
::InitializeProcThreadAttributeList(si.lpAttributeList, 1, 0, &size);
::UpdateProcThreadAttribute(si.lpAttributeList, 0,
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
&mitigation, sizeof(mitigation), nullptr, nullptr);
BOOL created = ::CreateProcess(nullptr, exePath, nullptr, nullptr, FALSE,
EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, (STARTUPINFO*)&si, &pi);
::DeleteProcThreadAttributeList(si.lpAttributeList);
::free(si.lpAttributeList);
::CloseHandle(pi.hThread);
return created? pi.hProcess : nullptr;
}在进程内设置缓解措施:
BOOL SetProcessMitigationPolicy(
_In_ PROCESS_MITIGATION_POLICY MitigationPolicy,
_In_ PVOID lpBuffer,
_In_ SIZE_T dwLength);设置加载映像策略的示例:
PROCESS_MITIGATION_IMAGE_LOAD_POLICY policy = { 0 };
policy.NoRemoteImages = true;
policy.NoLowMandatoryLabelImages = true;
::SetProcessMitigationPolicy(ProcessImageLoadPolicy,
&policy, sizeof(policy));总结
Windows 中的安全是一个很大的话题,可能需要专门写一本书来阐述。本章探讨了 Windows 安全系统中的主要概念及各种用于处理安全问题的 API。主要内容包括:登录流程(WinLogon、LogonUI、LSASS、LsaIso)、安全引用监视器和事件日志、安全标识符(SID)的结构与操作、访问令牌的创建与管理(包括二次登录服务和模拟机制)、特权系统与超级特权、访问掩码与安全描述符的构建、用户访问控制(UAC)与提权机制、完整性级别与 UIPI,以及专用安全机制(CFG 和进程缓解措施)。