第11章 安全性(Security)
# 第11章 安全性(Security)
安全性是Windows系统的基石,对任何现代操作系统而言都至关重要。本章将探讨与安全性相关的原生API(native API)。我们将首先进行简要概述,随后深入分析访问令牌(access token)——进程和线程的安全凭据(security credential),接着探讨安全描述符API(security descriptor API)——用于保护内核对象(kernel object)的访问权限。
本章包含以下内容:
- 概述(Overview)
- 安全标识符(SID)
- 令牌(Tokens)
- 安全描述符(Security Descriptors)
- 访问控制列表(Access Control Lists)
# 11.1 概述(Overview)
主体(principal)是安全系统中可被引用的实体,例如用户(user)或组(group)。每个主体通过安全标识符(Security Identifier,SID)进行标识,SID是一种可变大小的结构体(variable-sized structure)。系统定义了一组众所周知的安全标识符(Well Known SID),它们在所有系统上代表相同的概念实体。例如,“所有人”组(Everyone group,SID为S-1-1-0,也称为World SID)和管理员别名(Administrators alias,SID为S-1-5-32-544)。
访问令牌(access token,简称令牌)是存储进程或线程安全上下文(security context)的内核对象。该上下文包括用户的SID、特权(privilege)、所属组(group)以及其他详细信息。令牌分为两种类型:主令牌(primary)和模拟令牌(impersonation)。每个进程创建时都会附带一个不可替换的主令牌。线程访问安全对象(secure object)时,会自动使用其所属进程的令牌,除非线程获取了自己的(模拟)令牌。
特权代表可绕过安全性的操作。例如,将驱动程序(driver)加载到系统中需要特定特权(SeLoadDriver*),默认情况下该特权仅授予管理员别名的成员。特权存储在令牌中,无法直接添加或删除。管理员可在用户数据库中为用户添加或移除特权;用户下次注销并重新登录后,基于该用户账户创建的令牌会应用这些更改。
安全描述符(security descriptor)用于保护各类内核对象。安全描述符的核心元素包括对象的所有者SID(默认情况下为创建者的SID)和自主访问控制列表(Discretionary Access Control List,DACL)——后者指定了谁可以对该对象执行何种操作。
# 11.2 安全标识符(SID)
SID是一种可变大小的二进制结构体,其结构如图11-1所示。

图11-1:SID结构
为便于可读性和持久化存储,SID可转换为字符串形式。通过RtlConvertSidToUnicodeString函数可将二进制SID转换为字符串:
NTSTATUS RtlConvertSidToUnicodeString(
_Inout_ PUNICODE_STRING UnicodeString,
_In_ PSID Sid,
_In_ BOOLEAN AllocateDestinationString);
2
3
4
可预先为传入的UNICODE_STRING分配足够大的内存,并将AllocateDestinationString设为FALSE;也可将AllocateDestinationString设为TRUE,由函数自动分配UNICODE_STRING的内部缓冲区(Buffer)。若为后者,需调用RtlFreeUnicodeString释放已分配的缓冲区。
PSID 的类型定义为 PVOID,因此无需解引用。 |
|---|
以下是示例代码片段:
SE_SID sid{};
DWORD size = sizeof(sid);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &sid, &size);
UNICODE_STRING ssid;
RtlConvertSidToUnicodeString(&ssid, &sid.Sid, TRUE);
printf("管理员组(Administrators)的SID:%wZ\n", &ssid);
RtlFreeUnicodeString(&ssid);
2
3
4
5
6
7
二进制形式的SID最大长度定义为SECURITY_MAX_SID_SIZE(68字节),字符串形式的最大长度定义为SECURITY_MAX_SID_STRING_CHARACTERS(187个字符)。
SE_SID是<WinNt.h>中定义的辅助联合(helper union):
typedef union _SE_SID {
SID Sid;
BYTE Buffer[SECURITY_MAX_SID_SIZE];
} SE_SID, *PSE_SID;
2
3
4
可通过RtlLengthSidAsUnicodeString函数获取SID字符串的预期长度:
NTSTATUS RtlLengthSidAsUnicodeString(
_In_ PSID Sid,
_Out_ PULONG StringLength);
2
3
部分简单的SID相关API包括:验证SID有效性(RtlValidSid)、比较两个SID是否相等(RtlEqualSid和RtlEqualPrefixSid):
BOOLEAN RtlValidSid(_In_ PSID Sid);
BOOLEAN RtlEqualSid(
_In_ PSID Sid1,
_In_ PSID Sid2);
BOOLEAN RtlEqualPrefixSid(
_In_ PSID Sid1,
_In_ PSID Sid2);
2
3
4
5
6
7
RtlEqualPrefixSid会比较两个SID的所有组件,但忽略最后一个子授权ID(sub-authority ID)。
# 11.2.1 创建和销毁SID(Creating and Destroying SIDs)
有多个API专门用于创建SID,最直接的是RtlAllocateAndInitializeSid:
typedef struct _SID_IDENTIFIER_AUTHORITY {
BYTE Value[6];
} SID_IDENTIFIER_AUTHORITY, *PSID_IDENTIFIER_AUTHORITY;
NTSTATUS RtlAllocateAndInitializeSid(
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount,
_In_ ULONG SubAuthority0,
_In_ ULONG SubAuthority1,
_In_ ULONG SubAuthority2,
_In_ ULONG SubAuthority3,
_In_ ULONG SubAuthority4,
_In_ ULONG SubAuthority5,
_In_ ULONG SubAuthority6,
_In_ ULONG SubAuthority7,
_Outptr_ PSID *Sid);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
该函数接受一个6字节数组形式的授权机构(authority)。<Winnt.h>中定义的一个常见授权机构是所谓的NT授权机构(NT Authority):
#define SECURITY_NT_AUTHORITY {0,0,0,0,0,5}
SID中最多可包含15个子授权(sub-authority),但该函数仅支持8个——这通常已足够,因为超长SID并不常见。SubAuthorityCount参数指定调用中有效的子授权数量。
函数返回的SID(最后一个Sid参数)由函数分配,因此必须通过调用RtlFreeSid显式释放:
PVOID RtlFreeSid(_In_ _Post_invalid_ PSID Sid);
扩展APIRtlAllocateAndInitializeSidEx支持任意数量的子授权:
NTSTATUS RtlAllocateAndInitializeSidEx(
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount,
_In_reads_(SubAuthorityCount) PULONG SubAuthorities,
_Outptr_ PSID *Sid);
2
3
4
5
SubAuthorities是一个包含SubAuthorityCount个ULONG值的数组,用于初始化SID。与RtlAllocateAndInitializeSid相同,需调用RtlFreeSid释放该函数分配的SID内存。
RtlInitializeSid(及RtlInitializeSidEx)可用于初始化已分配的SID缓冲区:
NTSTATUS RtlInitializeSid(
_Out_ PSID Sid,
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount);
NTSTATUS RtlInitializeSidEx( // Windows 10及以上版本
_Out_writes_bytes_(SECURITY_SID_SIZE(SubAuthorityCount)) PSID Sid,
_In_ PSID_IDENTIFIER_AUTHORITY IdentifierAuthority,
_In_ UCHAR SubAuthorityCount,
...);
2
3
4
5
6
7
8
9
扩展函数RtlInitializeSidEx接受可变数量的参数(…)用于初始化子授权;而RtlInitializeSid仅初始化授权机构值和子授权计数。
可为Windows服务(Windows Service)创建SID,以便即使服务运行在通用账户(如本地系统账户Local System、网络服务账户Network Service或本地服务账户Local Service)下,也能为其指定特定访问权限。这可通过RtlCreateServiceSid函数实现:
NTSTATUS RtlCreateServiceSid(
_In_ PUNICODE_STRING ServiceName,
_Out_writes_bytes_opt_(*ServiceSidLength) PSID ServiceSid,
_Inout_ PULONG ServiceSidLength);
2
3
4
该函数将服务名称(ServiceName,转换为大写)作为SHA-1算法的输入,生成格式为S-1-5-80-hash1-hash2-hash3-hash4-hash5的最终SID。ServiceSidLength是输入/输出参数:输入时指定SID缓冲区可容纳的字节数,成功返回时指示实际写入的字节数。
以下是示例代码:
SE_SID sid{};
UNICODE_STRING name;
RtlInitUnicodeString(&name, L"MyService");
ULONG len = sizeof(sid);
RtlCreateServiceSid(&name, &sid, &len);
// 转换为字符串显示
UNICODE_STRING ssid;
RtlConvertSidToUnicodeString(&ssid, &sid.Sid, TRUE);
printf("MyService的SID:%wZ\n", &ssid);
// 输出示例:S-1-5-80-517257762-1253276234-605902578-3995580692-1133959824
RtlFreeUnicodeString(&ssid);
2
3
4
5
6
7
8
9
10
11
| 有关服务SID(Service SID)的更多信息,请参阅Windows SDK文档。 |
|---|
# 11.3:令牌(Tokens)
令牌对象必须附加到进程或线程才能生效。获取令牌的典型方式是通过NtOpenProcessToken(或NtOpenProcessTokenEx)从进程中打开:
NTSTATUS NtOpenProcessToken(
_In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_Out_ PHANDLE TokenHandle);
NTSTATUS NtOpenProcessTokenEx(
_In_ HANDLE ProcessHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ ULONG HandleAttributes,
_Out_ PHANDLE TokenHandle);
2
3
4
5
6
7
8
9
Windows API函数OpenProcessToken直接调用了NtOpenProcessToken。 |
|---|
NtOpenProcessToken本质上是调用NtOpenProcessTokenEx,并将HandleAttributes设为0。
ProcessHandle是目标进程的句柄,该句柄必须具备PROCESS_QUERY_LIMITED_INFORMATION或PROCESS_QUERY_INFORMATION访问掩码,才能成功获取进程的令牌。DesiredAccess可以是任何标准访问掩码(standard access mask)或令牌特定访问掩码(token-specific access mask),常见值为TOKEN_QUERY(用于查询令牌信息),也可使用其他值。但部分访问掩码需要特定特权才能使用,例如TOKEN_ADJUST_SESSIONID。HandleAttributes指定额外属性(OBJ_系列标志),如OBJ_INHERIT,但通常设为0。最终,返回的令牌句柄存储在*TokenHandle中。
也可通过NtOpenThreadToken(或NtOpenThreadTokenEx)从线程中打开令牌:
NTSTATUS NtOpenThreadToken(
_In_ HANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ BOOLEAN OpenAsSelf,
_Out_ PHANDLE TokenHandle);
NTSTATUS NtOpenThreadTokenEx(
_In_ HANDLE ThreadHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_ BOOLEAN OpenAsSelf,
_In_ ULONG HandleAttributes,
_Out_ PHANDLE TokenHandle);
2
3
4
5
6
7
8
9
10
11
ThreadHandle必须具备THREAD_QUERY_LIMITED_INFORMATION或THREAD_QUERY_INFORMATION访问掩码。DesiredAccess、HandleAttributes和TokenHandle的含义与NtOpenProcessTokenEx中的相同。OpenAsSelf参数指定:对请求令牌的访问检查(access check)应基于调用线程的当前安全上下文(若线程正在模拟其他主体,则此参数有实际意义),还是基于调用者的进程令牌。设为FALSE时,使用前者。
Windows API函数OpenThreadToken调用了NtOpenThreadToken。 |
|---|
获取令牌的另一种方式是通过NtDuplicateToken复制现有令牌(并可进行一些修改):
NTSTATUS NtDuplicateToken(
_In_ HANDLE ExistingTokenHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ BOOLEAN EffectiveOnly,
_In_ TOKEN_TYPE TokenType,
_Out_ PHANDLE NewTokenHandle);
2
3
4
5
6
7
ExistingTokenHandle是要复制的源令牌句柄,该句柄必须具备TOKEN_DUPLICATE访问掩码。DesiredAccess是新令牌所需的访问类型(如TOKEN_QUERY、TOKEN_ALL_ACCESS等)。ObjectAttributes是常见的属性结构体(若提供),用于指定以下内容:
- 句柄属性(handle attributes)
- 可选的安全描述符(security descriptor)
- 服务质量安全(security quality of service),包括新令牌的模拟级别(SECURITY_IMPERSONATION_LEVEL)。有关更多信息,请参阅
DuplicateTokenEx的文档。
EffectiveOnly参数指定是复制源令牌的全部内容(FALSE),还是仅复制令牌的启用部分(TRUE),例如仅启用的特权。该参数通常设为FALSE。
Windows API函数
DuplicateTokenEx始终将EffectiveOnly设为FALSE。
最后,TokenType指定新令牌的类型:TokenPrimary(主令牌)或TokenImpersonation(模拟令牌)。主令牌可附加到进程,模拟令牌可附加到线程。调用NtDuplicateToken的一个常见原因是获取另一种类型的令牌——例如,Windows API函数CreateProcessAsUser需要主令牌才能成功执行。
新令牌句柄通过*NewTokenHandle返回,最终需通过NtClose正常关闭。
可通过移除特权、禁用SID和/或限制SID,从现有令牌创建新令牌,这正是NtFilterToken的功能:
NTSTATUS NtFilterToken(
_In_ HANDLE ExistingTokenHandle,
_In_ ULONG Flags,
_In_opt_ PTOKEN_GROUPS SidsToDisable,
_In_opt_ PTOKEN_PRIVILEGES PrivilegesToDelete,
_In_opt_ PTOKEN_GROUPS RestrictedSids,
_Out_ PHANDLE NewTokenHandle);
2
3
4
5
6
7
另有
NtFilterTokenEx函数,但该API目前尚未实现。
NtFilterToken是Windows API函数CreateRestrictedToken的核心实现,有关更多信息,请参阅该函数的文档。CreateRestrictedToken的参数在概念上与NtFilterToken一致,但使用的结构体略有不同:NtFilterToken利用TOKEN_GROUPS和TOKEN_PRIVILEGES中的计数字段(count field),而Windows API使用SID_AND_ATTRIBUTES结构体,并为长度单独设置参数。除此之外,两者功能完全相同。新创建的令牌通过*NewTokenHandle返回。
# 11.3.1 令牌信息(Token Information)
获取令牌句柄(token handle)后,可通过调用 NtQueryInformationToken 函数查询令牌的各类属性:
NTSTATUS NtQueryInformationToken( _In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_Out_writes_bytes_to_opt_ (Length, *ReturnLength) PVOID TokenInformation,
_In_ ULONG Length,
_Out_ PULONG ReturnLength);
2
3
4
5
Windows API 中的 GetTokenInformation 函数会使用完全相同的参数调用 NtQueryInformationToken。有关该 API 的所有可用选项,请参阅 Windows SDK 文档。
对应的设置函数同样存在:
NTSTATUS NtSetInformationToken( _In_ HANDLE TokenHandle,
_In_ TOKEN_INFORMATION_CLASS TokenInformationClass,
_In_reads_bytes_(TokenInformationLength) PVOID TokenInformation,
_In_ ULONG TokenInformationLength);
2
3
4
Windows API 中的 SetTokenInformation 函数会使用相同的参数调用 NtSetInformationToken。有关详细信息,请参阅 Windows SDK 文档。
NtSetInformationToken 可修改令牌的多个属性,但其他一些修改操作需要使用专用函数。其中一个是 NtAdjustPrivilegesToken:
NTSTATUS NtAdjustPrivilegesToken( _In_ HANDLE TokenHandle,
_In_ BOOLEAN DisableAllPrivileges,
_In_opt_ PTOKEN_PRIVILEGES NewState,
_In_ ULONG BufferLength,
_Out_writes_bytes_to_opt_ (BufferLength, *Length) PTOKEN_PRIVILEGES PreviousState,
_Out_opt_ PULONG Length);
2
3
4
5
6
NtAdjustPrivilegesToken 允许启用或禁用权限(privileges)。权限永远无法从令牌中添加或移除,只能启用或禁用。Windows API 中的 AdjustTokenPrivileges 函数会直接调用此函数,有关详情,请参阅 Windows SDK 中该函数的文档。
有一个差异需要说明:Windows API 中权限标识符(privilege identifiers)以字符串形式暴露。例如,SE_DEBUG_NAME 被定义为字符串 “SeDebugPrivilege”。而 AdjustTokenPrivileges 和 NtAdjustPrivilegesToken 需要 PTOKEN_PRIVILEGES 类型参数,其中权限通过数字标识。Windows API 函数 LookupPrivilegeValue 用于获取与权限字符串对应的数字。
在原生 API(native API)中,权限标识符定义如下:
#define SE_CREATE_TOKEN_PRIVILEGE (2L)
#define SE_ASSIGNPRIMARYTOKEN_PRIVILEGE (3L)
#define SE_LOCK_MEMORY_PRIVILEGE (4L)
#define SE_INCREASE_QUOTA_PRIVILEGE (5L)
#define SE_MACHINE_ACCOUNT_PRIVILEGE (6L)
#define SE_TCB_PRIVILEGE (7L)
#define SE_SECURITY_PRIVILEGE (8L)
#define SE_TAKE_OWNERSHIP_PRIVILEGE (9L)
#define SE_LOAD_DRIVER_PRIVILEGE (10L)
#define SE_SYSTEM_PROFILE_PRIVILEGE (11L)
#define SE_SYSTEMTIME_PRIVILEGE (12L)
#define SE_PROF_SINGLE_PROCESS_PRIVILEGE (13L)
#define SE_INC_BASE_PRIORITY_PRIVILEGE (14L)
#define SE_CREATE_PAGEFILE_PRIVILEGE (15L)
#define SE_CREATE_PERMANENT_PRIVILEGE (16L)
#define SE_BACKUP_PRIVILEGE (17L)
#define SE_RESTORE_PRIVILEGE (18L)
#define SE_SHUTDOWN_PRIVILEGE (19L)
#define SE_DEBUG_PRIVILEGE (20L)
#define SE_AUDIT_PRIVILEGE (21L)
#define SE_SYSTEM_ENVIRONMENT_PRIVILEGE (22L)
#define SE_CHANGE_NOTIFY_PRIVILEGE (23L)
#define SE_REMOTE_SHUTDOWN_PRIVILEGE (24L)
#define SE_UNDOCK_PRIVILEGE (25L)
#define SE_SYNC_AGENT_PRIVILEGE (26L)
#define SE_ENABLE_DELEGATION_PRIVILEGE (27L)
#define SE_MANAGE_VOLUME_PRIVILEGE (28L)
#define SE_IMPERSONATE_PRIVILEGE (29L)
#define SE_CREATE_GLOBAL_PRIVILEGE (30L)
#define SE_TRUSTED_CREDMAN_ACCESS_PRIVILEGE (31L)
#define SE_RELABEL_PRIVILEGE (32L)
#define SE_INC_WORKING_SET_PRIVILEGE (33L)
#define SE_TIME_ZONE_PRIVILEGE (34L)
#define SE_CREATE_SYMBOLIC_LINK_PRIVILEGE (35L)
#define SE_DELEGATE_SESSION_USER_IMPERSONATE_PRIVILEGE (36L)
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
为当前进程令牌(current process token)启用或禁用单个权限是一项常见操作,NtAdjustPrivilegesToken 完全能够实现此功能。不过,原生 API 提供了一个更便捷的专用 API(内部调用 NtAdjustPrivilegesToken):
NTSTATUS RtlAdjustPrivilege( _In_ ULONG Privilege,
_In_ BOOLEAN Enable,
_In_ BOOLEAN Client,
_Out_ PBOOLEAN WasEnabled);
2
3
4
Privilege 参数为上述常量之一。Enable 参数指定权限应启用还是禁用。Client 参数指定调整当前进程令牌(FALSE)还是当前线程令牌(TRUE)。如果指定为 TRUE,但当前线程没有令牌,则函数调用失败。最后,WasEnabled 参数返回权限的先前状态,这对于后续恢复权限状态可能有用。
与调整权限类似,NtAdjustGroupsToken 允许调整令牌中的组(groups):
NTSTATUS NtAdjustGroupsToken( _In_ HANDLE TokenHandle,
_In_ BOOLEAN ResetToDefault,
_In_opt_ PTOKEN_GROUPS NewState,
_In_opt_ ULONG BufferLength,
_Out_writes_bytes_to_opt_ (BufferLength, *Length) PTOKEN_GROUPS PreviousState,
_Out_opt_ PULONG Length);
2
3
4
5
6
Windows API 中的 AdjustTokenGroups 函数会调用此函数,参数基本相同(唯一区别是 Windows API 使用 BOOL 类型而非 BOOLEAN 类型)。有关详细信息,请参阅 Windows SDK 文档。
# 11.3.2 模拟(Impersonation)
线程可以通过将令牌附加到自身来进行模拟,该令牌必须是模拟令牌(impersonation token)。获取此类令牌后(例如通过调用 NtDuplicateToken),调用线程即可进行模拟。
有一种无需显式令牌的模拟方式,适用于匿名用户(anonymous user):
NTSTATUS NtImpersonateAnonymousToken(_In_ HANDLE ThreadHandle);
线程句柄必须具有 THREAD_IMPERSONATE 访问掩码(access mask),如果使用当前线程句柄(NtCurrentThread),则该访问掩码始终可用。
要模拟任意令牌(即向线程分配模拟令牌),需调用 NtSetInformationThread 函数,并指定 ThreadImpersonationToken 信息类。例如:
HANDLE hToken = ...
NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hToken, sizeof(hToken), nullptr);
2
线程句柄(上述示例中为 NtCurrentThread())必须具有 THREAD_SET_THREAD_TOKEN 访问掩码。
要将线程恢复为使用进程令牌(process’ token),需再次调用 NtSetInformationThread,但将提供的令牌句柄设置为 NULL,如下所示:
HANDLE hToken = nullptr; // 恢复(reverting)
NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hToken, sizeof(hToken), nullptr);
2
# 11.3.2.1:其他模拟函数(Other Impersonation Functions)
NtImpersonateThread 允许一个线程直接模拟另一个线程:
NTSTATUS NtImpersonateThread(
_In_ HANDLE ServerThreadHandle,
_In_ HANDLE ClientThreadHandle,
_In_ PSECURITY_QUALITY_OF_SERVICE SecurityQos);
2
3
4
“服务器”线程(server thread)是执行模拟的线程,其句柄必须具有 THREAD_IMPERSONATE 访问掩码。“客户端”线程(client thread)是被模拟的线程,其句柄必须具有 THREAD_DIRECT_IMPERSONATION 访问掩码。SecurityQos 参数提供模拟所需的更多详细信息:
typedef struct _SECURITY_QUALITY_OF_SERVICE
{
DWORD Length;
SECURITY_IMPERSONATION_LEVEL ImpersonationLevel;
SECURITY_CONTEXT_TRACKING_MODE ContextTrackingMode;
BOOLEAN EffectiveOnly;
} SECURITY_QUALITY_OF_SERVICE, *PSECURITY_QUALITY_OF_SERVICE;
2
3
4
5
6
7
该结构体在 Windows SDK 中有文档说明。简要来说,Length 必须设置为该结构体的大小。ImpersonationLevel 可能是最重要的成员,它指示模拟线程的“权限级别”(power):
typedef enum _SECURITY_IMPERSONATION_LEVEL
{
SecurityAnonymous,
SecurityIdentification,
SecurityImpersonation,
SecurityDelegation
} SECURITY_IMPERSONATION_LEVEL, *PSECURITY_IMPERSONATION_LEVEL;
2
3
4
5
6
7
它表示授予模拟者的“信任度”(trust)——即模拟者可以代表被模拟线程执行哪些操作。ContextTrackingMode 指示模拟者获取被模拟线程安全上下文的快照(静态跟踪,SECURITY_STATIC_TRACKING=0),还是动态更新上下文(动态跟踪,SECURITY_DYNAMIC_TRACKING=1)。
最后,EffectiveOnly 指示是否允许模拟者启用或禁用权限。
RtlImpersonateSelf 是 Windows API ImpersonateSelf 的核心实现函数,它获取进程令牌的副本作为模拟令牌,并将其分配给当前线程:
NTSTATUS RtlImpersonateSelf(_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel);
这对于线程私下启用/禁用权限非常有用,不会影响可能被其他线程使用的进程令牌。
此外,还有一个扩展版本:
NTSTATUS RtlImpersonateSelfEx(
_In_ SECURITY_IMPERSONATION_LEVEL ImpersonationLevel,
_In_opt_ ACCESS_MASK AdditionalAccess,
_Out_opt_ PHANDLE ThreadToken);
2
3
4
其功能与 RtlImpersonateSelf 相同,但允许为新令牌请求更多访问权限(超出 TOKEN_IMPERSONATE),并可选择直接接收令牌句柄。
ALPC 端口(ALPC ports)也提供了一些自身的模拟函数(有关 ALPC 的更多信息,请参阅第 10 章):
NTSTATUS NtImpersonateClientOfPort(
_In_ HANDLE PortHandle,
_In_ PPORT_MESSAGE Message);
#define ALPC_IMPERSONATE_ALLOW_ANONYMOUS (PVOID)(ULONG_PTR)1
#define ALPC_IMPERSONATE_LEVEL (PVOID)(ULONG_PTR)2
NTSTATUS NtAlpcImpersonateClientOfPort( _In_ HANDLE PortHandle,
_In_ PPORT_MESSAGE Message,
_In_ PVOID Flags);
NTSTATUS NtAlpcImpersonateClientContainerOfPort( _In_ HANDLE PortHandle,
_In_ PPORT_MESSAGE Message,
_In_ ULONG Flags);
2
3
4
5
6
7
8
9
10
11
12
13
14
NtImpersonateClientOfPort 由 ALPC 服务器(ALPC server)用于模拟连接的客户端(connecting client),以便服务器能在客户端的安全上下文(security context)中执行操作。PortHandle 是接收客户端消息(Message)的端口。
NtAlpcImpersonateClientOfPort 与 NtImpersonateClientOfPort 功能相同,均用于模拟发送指定消息的给定端口的客户端,但提供了额外选项。Flags 参数通常为 0,但可设置为 ALPC_IMPERSONATE_ALLOW_ANONYMOUS,以允许匿名用户模拟应用容器(AppContainers,以低完整性级别运行)中的端口。此外,如果指定了 ALPC_IMPERSONATE_LEVEL 标志,则模拟级别(SECURITY_IMPERSONATION_LEVEL)可通过左移两位进行按位或(OR)操作。
NtAlpcImpersonateClientContainerOfPort(Windows 10+)允许模拟端口客户端的容器(容器也称为服务器隔离区,server silos)。仅当当前进程是 ALPC 服务器时,此函数才能生效,这意味着 ALPC 端口必须归当前进程所有。
# 11.3.3 创建令牌(Creating Tokens)
我们已经了解了基于现有令牌的令牌创建方式——NtDuplicateToken 和 NtFilterToken。通过 NtCreateToken 和 NtCreateTokenEx 函数,也可以创建不基于现有令牌的新令牌,其定义如下:
NTSTATUS NtCreateToken(
_Out_ PHANDLE TokenHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ TOKEN_TYPE TokenType,
_In_ PLUID AuthenticationId,
_In_ PLARGE_INTEGER ExpirationTime,
_In_ PTOKEN_USER User,
_In_ PTOKEN_GROUPS Groups,
_In_ PTOKEN_PRIVILEGES Privileges,
_In_opt_ PTOKEN_OWNER Owner,
_In_ PTOKEN_PRIMARY_GROUP PrimaryGroup,
_In_opt_ PTOKEN_DEFAULT_DACL DefaultDacl,
_In_ PTOKEN_SOURCE TokenSource);
2
3
4
5
6
7
8
9
10
11
12
13
14
NTSTATUS NtCreateTokenEx(
_Out_ PHANDLE TokenHandle,
_In_ ACCESS_MASK DesiredAccess,
_In_opt_ POBJECT_ATTRIBUTES ObjectAttributes,
_In_ TOKEN_TYPE TokenType,
_In_ PLUID AuthenticationId,
_In_ PLARGE_INTEGER ExpirationTime,
_In_ PTOKEN_USER User,
_In_ PTOKEN_GROUPS Groups,
_In_ PTOKEN_PRIVILEGES Privileges,
_In_opt_ PTOKEN_SECURITY_ATTRIBUTES_INFORMATION UserAttributes,
_In_opt_ PTOKEN_SECURITY_ATTRIBUTES_INFORMATION DeviceAttributes,
_In_opt_ PTOKEN_GROUPS DeviceGroups,
_In_opt_ PTOKEN_MANDATORY_POLICY TokenMandatoryPolicy,
_In_opt_ PTOKEN_OWNER Owner,
_In_ PTOKEN_PRIMARY_GROUP PrimaryGroup,
_In_opt_ PTOKEN_DEFAULT_DACL DefaultDacl,
_In_ PTOKEN_SOURCE TokenSource);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
使用 CreateToken(Ex) 创建令牌需要 SeCreateToken 权限,该权限通常仅 Lsass.exe 进程可用。获取此权限的一种方式是复制 Lsass 的令牌并用于模拟,这仅能由管理员级别的调用者(admin level caller)实现。另一种方式是由管理员将此权限添加到用户账户,该用户下次登录时,其令牌中将包含此权限。
以下是 NtCreateToken 函数的参数详细说明:
• TokenHandle(令牌句柄):成功调用后返回的令牌句柄。 • DesiredAccess(期望访问权限):返回的令牌句柄应具备的访问掩码。最简单的设置是 TOKEN_ALL_ACCESS(完全访问权限),允许返回的令牌用于任何操作。 • TokenAttributes(令牌属性):标准的 OBJECT_ATTRIBUTES(对象属性)结构,本场景中为可选参数。 • TokenType(令牌类型):指定所需的令牌类型——TokenPrimary(主令牌)或 TokenImpersonation(模拟令牌)。 • AuthenticationId(身份验证 ID):生成的令牌应关联的登录会话 ID。它必须对应一个已存在的登录会话。部分登录会话是始终存在的: • 999(0x3e7)——供 Local System 账户(本地系统账户)使用 • 997(0x3e5)——供 Local Service 账户(本地服务账户)使用 • 996(0x3e4)——供 Network Service 账户(网络服务账户)使用
其他登录会话会根据需要创建。要查看所有登录会话列表,可使用 Sysinternals 工具集中的 logonsessions 命令行工具,或通过我的 System Explorer 工具(系统/登录会话菜单选项)以图形化方式查看。以下是 logonsessions 工具的示例输出:
[0] Logon session 00000000:000003e7:
User name: WORKGROUP\PAVEL7760$
Auth package: NTLM
Logon type: (none)
Session: 0
Sid: S-1-5-18
Logon time: 5/7/2024 2:47:52 PM
Logon server:
DNS Domain:
UPN:
...
[2] Logon session 00000000:00021d05:
User name: Font Driver Host\UMFD-0
Auth package: Negotiate
Logon type: Interactive
Session: 0
Sid: S-1-5-96-0-0
Logon time: 5/7/2024 2:47:52 PM
Logon server:
DNS Domain:
UPN:
[3] Logon session 00000000:000003e5:
User name: NT AUTHORITY\LOCAL SERVICE
Auth package: Negotiate
Logon type: Service
Session: 0
Sid: S-1-5-19
Logon time: 5/7/2024 2:47:52 PM
Logon server:
DNS Domain:
UPN:
[4] Logon session 00000000:000003e4:
User name: WORKGROUP\PAVEL7760$
Auth package: Negotiate
Logon type: Service
Session: 0
Sid: S-1-5-20
Logon time: 5/7/2024 2:47:53 PM
Logon server:
DNS Domain:
UPN:
...
[6] Logon session 00000000:0002d1b9:
User name: Window Manager\DWM-1
Auth package: Negotiate
Logon type: Interactive
Session: 1
Sid: S-1-5-90-0-1
Logon time: 5/7/2024 2:47:53 PM
Chapter 11: Security 307
Logon server:
DNS Domain:
UPN:
...
[8] Logon session 00000000:00042bbc:
User name: PAVEL7760\Pavel
Auth package: NTLM
Logon type: Interactive
Session: 1
Sid: S-1-5-21-3968166439-3083973779-398838822-1001
Logon time: 5/7/2024 2:47:53 PM
Logon server: PAVEL7760
DNS Domain:
UPN:
...
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
可通过 LsaEnumerateLogonSessions 函数获取登录会话列表。
你可以将登录会话理解为令牌的“来源”。换句话说,令牌会指向其对应的登录会话。
图 11-2 展示了这一关系。

图 11-2:登录会话与令牌
继续介绍 NtCreateToken 的参数:
• ExpirationTime(过期时间):指定生成的令牌何时过期。如果 LARGE_INTEGER 类型的值为 0,则令牌永不过期。注意,此处传入 NULL 作为参数是无效的。 • User(用户):生成的令牌所属的用户,以标准 TOKEN_USER(令牌用户)结构指针的形式指定,该结构包含用户的 SID(安全标识符)。有关 TOKEN_USER 及相关结构的定义,请参考 GetTokenInformation 函数的文档。 • Groups(组):令牌应包含的组列表,以 TOKEN_GROUPS(令牌组)指针的形式提供,相关说明见 Windows API 文档。 • Privileges(权限):指定要添加到生成的令牌中的权限列表。我们可以在此处添加任何希望令牌具备的权限。 • Owner(所有者):使用该令牌创建安全属性时要设置的默认所有者(对应 TOKEN_OWNER 结构)。该参数为可选,但建议设置。简单的设置方式是使用与 User 参数相同的 SID。 • PrimaryGroup(主组):该令牌的主组——必须是 Groups 列表中列出的组之一。主组在 Windows 中意义不大,其设计初衷是为了兼容 POSIX 子系统;尽管如此,该参数为必填项。 • DefaultDacl(默认自由访问控制列表):用于保护令牌的可选 DACL(自由访问控制列表)。 • TokenSource(令牌来源):令牌的来源,由一个简单字符串和一个 LUID(本地唯一标识符)标识。这两个值可任意设置——用于标识与返回令牌相关的日志条目。
NtCreateTokenEx 函数额外增加了 4 个参数,可用于指定以下内容:
• UserAttributes(用户属性):可选的用户声明集,相关说明见 Windows SDK 文档。其类型为原生 API 和内核使用的 TOKEN_SECURITY_ATTRIBUTES_INFORMATION(令牌安全属性信息)结构,但实际上与用户模式下的 CLAIM_SECURITY_ATTRIBUTES_INFORMATION(声明安全属性信息)结构相同。有关详细信息,请查阅 SDK 文档。 • DeviceAttributes(设备属性):可选的设备声明集,相关说明见 Windows SDK 文档。 • DeviceGroups(设备组):用户所使用设备所属的可选组集。 • TokenMandatoryPolicy(令牌强制策略):可选的完整性级别策略,默认值为 TOKEN_MANDATORY_POLICY_NO_WRITE_UP。有关更多信息,请查阅 SDK 文档。
NtCreateToken 函数会调用 NtCreateTokenEx 函数,并将上述 4 个参数设置为 NULL。
# 11.3.4 综合应用:从零开始创建令牌对象
在以下演示中,我们将使用 NtCreateToken 函数从零开始创建一个令牌。完整的源代码位于 CreateToken 项目中。
由于使用 NtCreateToken 函数需要具备 SeCreateToken 权限(创建令牌权限),因此我们将采用上一节中提到的第一种方法——复制 Lsass(本地安全授权子系统)的令牌。
第一步是找到 Lsass 进程并打开其句柄。我们首先获取其完整路径名,用于后续对比:
HANDLE FindLsass()
{
WCHAR path[MAX_PATH];
GetSystemDirectory(path, ARRAYSIZE(path));
wcscat_s(path, L"\\lsass.exe");
UNICODE_STRING lsassPath;
RtlInitUnicodeString(&lsassPath, path);
2
3
4
5
6
7
接下来,我们需要遍历所有进程,查找 Lsass 进程。一种方法是使用 NtQuerySystemInformation 函数并指定 SystemProcessInformation 参数,但这种方法过于繁琐。我们需要打开 Lsass 进程的句柄,因此不妨直接使用 NtGetNextProcess 函数同时实现进程遍历和句柄获取:
BYTE buffer[256];
HANDLE hProcess = nullptr, hOld;
while (true)
{
hOld = hProcess;
//
// 获取下一个进程句柄
//
auto status = NtGetNextProcess(hProcess,
PROCESS_QUERY_LIMITED_INFORMATION, 0, 0, &hProcess);
if (hOld)
NtClose(hOld);
if (!NT_SUCCESS(status))
break;
//
// 获取 Lsass 可执行文件的路径
//
if (NT_SUCCESS(NtQueryInformationProcess(hProcess,
ProcessImageFileNameWin32, buffer, sizeof(buffer), nullptr)))
{
auto name = (UNICODE_STRING*)buffer;
if (RtlEqualUnicodeString(&lsassPath, name, TRUE))
return hProcess;
}
}
return nullptr;
}
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
我们会调用 NtGetNextProcess 函数,直到找到目标进程。最后,将句柄返回给调用者。下一步是打开 Lsass 进程的令牌并进行复制。首先打开令牌:
HANDLE DuplicateLsassToken()
{
auto hProcess = FindLsass();
if (hProcess == nullptr)
return nullptr;
HANDLE hToken = nullptr;
NtOpenProcessToken(hProcess, TOKEN_DUPLICATE, &hToken);
if (!hToken)
return nullptr;
2
3
4
5
6
7
8
9
10
我们需要 TOKEN_DUPLICATE(令牌复制)访问掩码,以便能够复制令牌用于模拟(不能直接使用原始令牌,因为它是主令牌)。
现在可以调用 NtDuplicateObject 函数获取我们自己的令牌,并将其设置为模拟令牌:
HANDLE hNewToken = nullptr;
OBJECT_ATTRIBUTES tokenAttr;
InitializeObjectAttributes(&tokenAttr, nullptr, 0, nullptr, nullptr);
//
// 设置该令牌以支持模拟
//
SECURITY_QUALITY_OF_SERVICE qos{ sizeof(qos) };
qos.ImpersonationLevel = SecurityImpersonation;
tokenAttr.SecurityQualityOfService = &qos;
NtDuplicateToken(hToken, TOKEN_ALL_ACCESS, &tokenAttr,
FALSE, TokenImpersonation, &hNewToken);
NtClose(hToken);
return hNewToken;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
关键在于要意识到,若要使用新令牌进行模拟,仅将其创建为模拟令牌是不够的——模拟令牌仅表示它可以附加到线程。这是少数需要设置 OBJECT_ATTRIBUTES 结构的 SecurityQualityOfService(安全服务质量)成员的场景之一,而 InitializeObjectAttributes 宏并不支持该操作。
现在可以开始编写主函数。首先,需要启用 SeDebug 权限(调试权限),否则无法访问 Lsass 进程(当然,调用者本身必须是管理员):
int main()
{
BOOLEAN enabled;
auto status = RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
if (!NT_SUCCESS(status))
{
printf("Failed to enable the debug privilege! (0x%X)\n", status);
return status;
}
2
3
4
5
6
7
8
9
接下来,复制 Lsass 的令牌:
auto hDupToken = DuplicateLsassToken();
if (!hDupToken)
{
printf("Failed to duplicate Lsass token\n");
return 1;
}
2
3
4
5
6
然后,为调用 NtCreateToken 函数做准备,首先设置我们希望在新创建的令牌中包含的组。我们先创建要使用的 SID(因为任何组都通过 SID 表示):
SE_SID systemSid;
DWORD size = sizeof(systemSid);
CreateWellKnownSid(WinLocalSystemSid, nullptr, &systemSid, &size);
SE_SID adminSid;
size = sizeof(adminSid);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &adminSid, &size);
SE_SID allUsersSid;
size = sizeof(allUsersSid);
CreateWellKnownSid(WinWorldSid, nullptr, &allUsersSid, &size);
SE_SID interactiveSid;
size = sizeof(interactiveSid);
CreateWellKnownSid(WinInteractiveSid, nullptr, &interactiveSid, &size);
SE_SID authUsers;
size = sizeof(authUsers);
CreateWellKnownSid(WinAuthenticatedUserSid, nullptr, &authUsers, &size);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
如果之后希望使用新令牌创建进程(我们后续会这样做),上述部分组是必需的。
最后要创建的组是完整性级别,它并非简单的已知组:
PSID integritySid;
SID_IDENTIFIER_AUTHORITY auth = SECURITY_MANDATORY_LABEL_AUTHORITY;
status = RtlAllocateAndInitializeSid(&auth, 1,
SECURITY_MANDATORY_MEDIUM_RID, 0, 0, 0, 0, 0, 0, 0, &integritySid);
assert(SUCCEEDED(status));
2
3
4
5
之后需要使用 RtlFreeSid 函数释放分配的 SID。
组必须存储在 TOKEN_GROUPS 结构中,但该结构是可变大小的。默认情况下,它只能直接容纳一个组:
typedef struct _TOKEN_GROUPS {
DWORD GroupCount;
SID_AND_ATTRIBUTES Groups[ANYSIZE_ARRAY]; // = 1
} TOKEN_GROUPS, *PTOKEN_GROUPS;
2
3
4
由于我们需要多个组,因此需要动态分配一些内存,将其强制转换为 TOKEN_GROUPS* 后使用。不过,还有一种更便捷(且更快)的方法是静态分配所需大小的内存。为方便实现,我定义了以下模板辅助结构:
template<int N>
struct MultiGroups : TOKEN_GROUPS
{
MultiGroups()
{
GroupCount = N;
}
SID_AND_ATTRIBUTES _Additional[N - 1]{};
};
2
3
4
5
6
7
8
9
该结构使用整数作为模板参数,用于分配静态大小的 SID_AND_ATTRIBUTES(SID 和属性)结构数组。通过此定义,我们可以静态分配所需数量的组,并将数组填充到分配的限制范围内:
MultiGroups<6> groups;
groups.Groups[0].Sid = &adminSid;
groups.Groups[0].Attributes = SE_GROUP_DEFAULTED | SE_GROUP_ENABLED | SE_GROUP_OWNER;
groups.Groups[1].Sid = &allUsersSid;
groups.Groups[1].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[2].Sid = &interactiveSid;
groups.Groups[2].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[3].Sid = &systemSid;
groups.Groups[3].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[4].Sid = integritySid;
groups.Groups[4].Attributes = SE_GROUP_INTEGRITY | SE_GROUP_INTEGRITY_ENABLED;
groups.Groups[5].Sid = &authUsers;
groups.Groups[5].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
2
3
4
5
6
7
8
9
10
11
12
13
14
接下来,需要设置权限。我们将添加两个权限:SeChangeNotify(“绕过遍历检查”权限,始终需要)和 SeTcb(仅作演示用)。这是你添加任何所需权限的机会!这也是从零开始创建令牌的主要原因之一。
权限的添加方式与组类似——TOKEN_PRIVILEGES(令牌权限)结构默认只能容纳一个权限,如需添加多个则需要额外分配内存。我们将使用相同的技巧静态分配权限:
template<int N>
struct MultiPrivileges : TOKEN_PRIVILEGES
{
MultiPrivileges()
{
PrivilegeCount = N;
}
LUID_AND_ATTRIBUTES _Additional[N - 1]{};
};
2
3
4
5
6
7
8
9
现在可以填写权限详情:
MultiPrivileges<2> privs;
privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED_BY_DEFAULT;
privs.Privileges[0].Luid.LowPart = SE_CHANGE_NOTIFY_PRIVILEGE;
privs.Privileges[1].Luid.LowPart = SE_TCB_PRIVILEGE;
2
3
4
有关各种权限的描述,请查阅微软官方文档。
接下来,需要初始化主组:
TOKEN_PRIMARY_GROUP primary;
primary.PrimaryGroup = &adminSid;
2
该令牌的用户将设为 Local System 账户(当然也可以是任何其他用户):
TOKEN_USER user{};
user.User.Sid = &systemSid;
2
我们几乎已准备好调用 NtCreateToken 函数。现在需要模拟复制的 Lsass 令牌,以便获得 SeCreateToken 权限:
status = NtSetInformationThread(NtCurrentThread(), ThreadImpersonationToken, &hDupToken, sizeof(hDupToken));
if (!NT_SUCCESS(status))
{
printf("Failed to impersonate! (0x%X)\n ", status);
return status;
}
2
3
4
5
6
7
最后的初始化步骤是登录会话ID(有时也称为身份验证ID,authentication Id)。我们必须选择一个现有的登录会话——我们将选择本地系统账户(Local System account)使用的会话,其固定编号为999:
LUID authenticationId = RtlConvertUlongToLuid(999);
现在我们可以调用NtCreateToken函数了。我们还需要一个TOKEN_SOURCE结构,它表示创建令牌(token)的逻辑实体——由一个短字符串和一个数字组成。我们还将使创建的令牌没有过期时间:
TOKEN_SOURCE source{ "Ch11", 777 };
LARGE_INTEGER expire{};
HANDLE hToken;
status = NtCreateToken(&hToken, TOKEN_ALL_ACCESS, nullptr, TokenPrimary, &authenticationId, &expire, &user, &groups, &privs, nullptr,
&primary, nullptr, &source);
2
3
4
5
我们创建的新令牌是主令牌(primary token),因此我们可以在进程创建函数中使用它,这一点我们很快就会看到。该令牌创建时拥有所有可能的权限(TOKEN_ALL_ACCESS),因此我们可以根据需要修改其属性。
让我们用这个令牌做一些有趣的事情——创建一个附加到用户控制台会话(console session)的进程,这样新进程的任何用户界面(UI)都将是可见的。
我们可以获取活动控制台会话ID,然后在令牌中替换它。修改令牌中存储的会话ID需要SeTcb权限(SeTcb privilege),幸运的是我们拥有该权限,因为本地安全授权子系统服务(Lsass)也拥有它;我们只需要在进行修改之前启用它:
if (NT_SUCCESS(RtlAdjustPrivilege(SE_TCB_PRIVILEGE, TRUE, TRUE, &enabled)))
{
ULONG session = WTSGetActiveConsoleSessionId();
NtQueryInformationProcess(NtCurrentProcess(), ProcessSessionInformation, &session, sizeof(session), nullptr);
NtSetInformationToken(hToken, TokenSessionId, &session, sizeof(session));
}
2
3
4
5
6
接下来,我们将准备进程创建中使用的STARTUPINFO和PROCESS_INFORMATION结构,并选择记事本(notepad)作为要启动的可执行文件:
STARTUPINFO si{ sizeof(si) };
PROCESS_INFORMATION pi;
WCHAR desktop[] = L"winsta0\\Default";
WCHAR name[] = L"notepad.exe";
si.lpDesktop = desktop;
2
3
4
5
现在我们可以尝试使用以下Windows API之一创建进程:CreateProcessAsUser或CreateProcessWithTokenW。尽管这些函数看起来相似,但它们并不相同。CreateProcessAsUser是一个“本地”函数,而CreateProcessWithTokenW会联系一个服务来执行实际的创建操作。另一方面,CreateProcessAsUser需要SeAssignPrimary权限(SeAssignPrimary privilege)才能工作。
我们将首先尝试使用CreateProcessAsUser,如果失败,则调用CreateProcessWithTokenW:
status = RtlAdjustPrivilege(SE_ASSIGNPRIMARYTOKEN_PRIVILEGE, TRUE, TRUE, &enabled);
BOOL created = FALSE;
if (NT_SUCCESS(status))
{
created = CreateProcessAsUser(hToken, nullptr, name, nullptr, nullptr,
FALSE, 0, nullptr, nullptr, &si, &pi);
}
if (!created)
{
created = CreateProcessWithTokenW(hToken, LOGON_WITH_PROFILE, nullptr,
name, 0, nullptr, nullptr, &si, &pi);
}
if (!created)
{
printf("Failed to create process (%u)\n ", GetLastError());
}
else
{
printf("Process created: %u\n ", pi.dwProcessId);
NtClose(pi.hProcess);
NtClose(pi.hThread);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
以下是完整的main函数,方便参考:
int main()
{
BOOLEAN enabled;
auto status = RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
if (!NT_SUCCESS(status))
{
printf("Failed to enable the debug privilege! (0x%X)\n ", status);
return status;
}
auto hDupToken = DuplicateLsassToken();
if (!hDupToken)
{
printf("Failed to duplicate Lsass token\n ");
return 1;
}
union Sid
{
BYTE buffer[SECURITY_MAX_SID_SIZE];
SID Sid;
};
SE_SID systemSid;
DWORD size = sizeof(systemSid);
CreateWellKnownSid(WinLocalSystemSid, nullptr, &systemSid, &size);
DisplaySid(&systemSid);
SE_SID adminSid;
size = sizeof(adminSid);
CreateWellKnownSid(WinBuiltinAdministratorsSid, nullptr, &adminSid, &size);
DisplaySid(&adminSid);
SE_SID allUsersSid;
size = sizeof(allUsersSid);
CreateWellKnownSid(WinWorldSid, nullptr, &allUsersSid, &size);
DisplaySid(&allUsersSid);
SE_SID interactiveSid;
size = sizeof(interactiveSid);
CreateWellKnownSid(WinInteractiveSid, nullptr, &interactiveSid, &size);
DisplaySid(&interactiveSid);
SE_SID authUsers;
size = sizeof(authUsers);
CreateWellKnownSid(WinAuthenticatedUserSid, nullptr, &authUsers, &size);
DisplaySid(&authUsers);
PSID integritySid;
SID_IDENTIFIER_AUTHORITY auth = SECURITY_MANDATORY_LABEL_AUTHORITY;
status = RtlAllocateAndInitializeSid(&auth, 1,
SECURITY_MANDATORY_MEDIUM_RID, 0, 0, 0, 0, 0, 0, 0, &integritySid);
assert(SUCCEEDED(status));
//
// 设置组(groups)
//
MultiGroups<6> groups;
groups.Groups[0].Sid = &adminSid;
groups.Groups[0].Attributes = SE_GROUP_DEFAULTED | SE_GROUP_ENABLED
| SE_GROUP_OWNER;
groups.Groups[1].Sid = &allUsersSid;
groups.Groups[1].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[2].Sid = &interactiveSid;
groups.Groups[2].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[3].Sid = &systemSid;
groups.Groups[3].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
groups.Groups[4].Sid = integritySid;
groups.Groups[4].Attributes = SE_GROUP_INTEGRITY | SE_GROUP_INTEGRITY_ENABLED;
groups.Groups[5].Sid = &authUsers;
groups.Groups[5].Attributes = SE_GROUP_ENABLED | SE_GROUP_DEFAULTED;
//
// 设置权限(privileges)
//
MultiPrivileges<2> privs;
privs.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED_BY_DEFAULT;
privs.Privileges[0].Luid.LowPart = SE_CHANGE_NOTIFY_PRIVILEGE;
privs.Privileges[1].Luid.LowPart = SE_TCB_PRIVILEGE;
TOKEN_PRIMARY_GROUP primary;
primary.PrimaryGroup = &adminSid;
TOKEN_USER user{};
user.User.Sid = &systemSid;
//
// 模拟(impersonate)
//
status = NtSetInformationThread(NtCurrentThread(),
ThreadImpersonationToken, &hDupToken, sizeof(hDupToken));
if (!NT_SUCCESS(status))
{
printf("Failed to impersonate! (0x%X)\n ", status);
return status;
}
LUID authenticationId = RtlConvertUlongToLuid(999);
TOKEN_SOURCE source{ "Ch11", 777 };
LARGE_INTEGER expire{};
HANDLE hToken;
status = NtCreateToken(&hToken, TOKEN_ALL_ACCESS, nullptr, TokenPrimary, &authenticationId, &expire, &user, &groups, &privs, nullptr,
&primary, nullptr, &source);
if (NT_SUCCESS(status))
{
printf("Token created successfully.\n ");
if (NT_SUCCESS(RtlAdjustPrivilege(SE_TCB_PRIVILEGE, TRUE, TRUE, &enabled)))
{
ULONG session = WTSGetActiveConsoleSessionId();
NtQueryInformationProcess(NtCurrentProcess(),
ProcessSessionInformation, &session, sizeof(session), nullptr);
NtSetInformationToken(hToken, TokenSessionId, &session, sizeof(session));
}
STARTUPINFO si{ sizeof(si) };
PROCESS_INFORMATION pi;
WCHAR desktop[] = L"winsta0\\Default";
WCHAR name[] = L"notepad.exe";
si.lpDesktop = desktop;
status = RtlAdjustPrivilege(SE_ASSIGNPRIMARYTOKEN_PRIVILEGE,
TRUE, TRUE, &enabled);
BOOL created = FALSE;
if (NT_SUCCESS(status))
{
created = CreateProcessAsUser(hToken, nullptr, name,
nullptr, nullptr, FALSE, 0, nullptr, nullptr, &si, &pi);
}
if (!created)
{
created = CreateProcessWithTokenW(hToken, LOGON_WITH_PROFILE, nullptr, name, 0, nullptr, nullptr, &si, &pi);
}
if (!created)
{
printf("Failed to create process (%u)\n ", GetLastError());
}
else
{
printf("Process created: %u\n ", pi.dwProcessId);
NtClose(pi.hProcess);
NtClose(pi.hThread);
}
RtlFreeSid(integritySid);
}
else
{
printf("Failed to create token (0x%X)\n ", status);
}
return status;
}
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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
# 11.3.5 其他令牌操作(Other Token Operations)
要检查某个令牌是否拥有一组特定的权限,可以调用NtPrivilegeCheck函数:
NTSTATUS NtPrivilegeCheck(
_In_ HANDLE ClientToken,
_Inout_ PPRIVILEGE_SET RequiredPrivileges,
_Out_ PBOOLEAN Result);
2
3
4
Windows API中的PrivilegeCheck函数直接使用了这个函数。有关更多详细信息,请参阅文档。可以使用NtCompareTokens函数比较两个令牌:
NTSTATUS NtCompareTokens(
_In_ HANDLE FirstTokenHandle,
_In_ HANDLE SecondTokenHandle,
_Out_ PBOOLEAN Equal);
2
3
4
该函数从访问检查(access check)的角度判断给定的两个令牌是否等效。显然,如果两个句柄(handle)指向同一个令牌对象,那么它们是等效的。该函数通过检查令牌的以下几个部分来确定是否等效:
- 一个令牌中存在的每个安全标识符(SID)必须在另一个令牌中存在,包括它们的属性(例如SE_GROUP_ENABLED)。
- 两个令牌必须拥有相同的权限列表。
- 两个令牌要么都是受限令牌(restricted tokens),要么都不是。
# 11.4 安全描述符(Security Descriptors)
安全描述符(SDs,Security Descriptors)是可以附加到内核对象(kernel objects)的对象,用于指示“谁可以对该对象执行什么操作”。安全描述符有两种格式:绝对格式(absolute)和自相对格式(self-relative)。绝对格式使用绝对指针(absolute pointers)指向安全描述符的各个部分,而自相对格式使用偏移量(offsets)指向其各个部分。自相对格式的安全描述符可以轻松地在内存中移动,而绝对格式的安全描述符则难以移动,但如果其某些部分与其他安全描述符共享,则可能更节省空间。
绝对格式的安全描述符由SECURITY_DESCRIPTOR结构描述:
typedef struct _SECURITY_DESCRIPTOR {
BYTE Revision;
BYTE Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control;
PSID Owner;
PSID Group;
PACL Sacl;
PACL Dacl;
} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
2
3
4
5
6
7
8
9
自相对格式的安全描述符由SECURITY_DESCRIPTOR_RELATIVE结构描述:
typedef struct _SECURITY_DESCRIPTOR_RELATIVE {
BYTE Revision;
BYTE Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control;
DWORD Owner;
DWORD Group;
DWORD Sacl;
DWORD Dacl;
} SECURITY_DESCRIPTOR_RELATIVE, *PISECURITY_DESCRIPTOR_RELATIVE;
2
3
4
5
6
7
8
9
如你所见,SECURITY_DESCRIPTOR结构中的指针被SECURITY_DESCRIPTOR_RELATIVE结构中的偏移量所取代。无论如何,为了兼容性和一致性,安全描述符通常被视为不透明的PVOID指针。实际上,PSECURITY_DESCRIPTOR类型被定义为PVOID(请注意上述定义中指针类型带有“PI”前缀)。
图11-3展示了绝对格式安全描述符的布局,而图11-4展示了自相对格式安全描述符的布局。

图11-3:绝对格式安全描述符(Absolute Security Descriptor)

图11-4:自相对格式安全描述符(Self-relative Security Descriptor)
可以使用以下API在两种格式之间进行转换:
NTSTATUS RtlAbsoluteToSelfRelativeSD(
_In_ PSECURITY_DESCRIPTOR AbsoluteSD,
_Out_writes_bytes_to_opt_(*Length, *Length) PSECURITY_DESCRIPTOR SelfSD,
_Inout_ PULONG Length);
NTSTATUS RtlSelfRelativeToAbsoluteSD(
_In_ PSECURITY_DESCRIPTOR SelfRelativeSD,
_Out_writes_bytes_to_opt_(*SDSize, *SDSize) PSECURITY_DESCRIPTOR AbsoluteSD,
_Inout_ PULONG SDSize,
_Out_writes_bytes_to_opt_(*DaclSize, *DaclSize) PACL Dacl,
_Inout_ PULONG DaclSize,
_Out_writes_bytes_to_opt_(*SaclSize, *SaclSize) PACL Sacl,
_Inout_ PULONG SaclSize,
_Out_writes_bytes_to_opt_(*OwnerSize, *OwnerSize) PSID Owner,
_Inout_ PULONG OwnerSize,
_Out_writes_bytes_to_opt_(*PrimaryGroupLen, *PrimaryGroupLen) PSID PrimaryGroup,
_Inout_ PULONG PrimaryGroupLen);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这些函数是 Windows API(应用程序编程接口)MakeSelfRelativeSD 和 MakeAbsoluteSD 的核心底层实现。有关详细信息,请参阅 SDK(软件开发工具包)文档。此外,原生 API 还提供了 RtlMakeSelfRelativeSD 函数,可将原始安全描述符(Security Descriptor,SD)从任意格式转换为自相对格式(无论原始格式如何,都会创建一个副本):
NTSTATUS RtlMakeSelfRelativeSD(
_In_ PSECURITY_DESCRIPTOR AbsoluteSD,
_Out_writes_bytes_(*BufferLength) PSECURITY_DESCRIPTOR SelfRelativeSD,
_Inout_ PULONG BufferLength);
2
3
4
如果提供的缓冲区过小,该函数会执行失败,并在 *BufferLength 中返回所需的缓冲区大小。
如第 11-3 图和第 11-4 图所示,安全描述符(SD)包含以下组成部分:
- 控制标志(Control flags)
- 所有者安全标识符(Owner SID)—— 对象的所有者。
- 主组安全标识符(Primary Group SID)—— 过去用于 POSIX 子系统应用程序中的组安全机制。
- 自主访问控制列表(Discretionary Access Control List,DACL)—— 访问控制项(Access Control Entries,ACE)的列表,用于指定哪些主体可以对对象执行哪些操作。
- 系统访问控制列表(System Access Control List,SACL)—— 访问控制项(ACE)的列表,用于指示哪些操作应触发在安全日志中写入审计记录。
从保护角度来看,最重要的组成部分是所有者和自主访问控制列表(DACL)。
许多与安全描述符(SD)相关的 Windows API 都是相应原生 API 的轻量级包装器。表 11-1 总结了其中的大部分 API。这些 API 的参数实际上是相同的,不同之处在于原生 API 的返回值为 NTSTATUS 类型,而 Windows API 的返回值为 BOOL 类型。此外,Windows API 中的 BOOL 输入参数在原生 API 中被替换为 BOOLEAN 类型。
表 11-1:基于原生 API 封装的安全描述符(SD)相关 Windows API
| Windows API | 原生 API(Native API) |
|---|---|
| InitializeSecurityDescriptor | RtlCreateSecurityDescriptor |
| IsValidSecurityDescriptor | RtlValidSecurityDescriptor |
| IsValidRelativeSecurityDescriptor | RtlValidRelativeSecurityDescriptor |
| GetSecurityDescriptorLength | RtlLengthSecurityDescriptor |
| MakeSelfRelativeSD | RtlMakeSelfRelativeSD |
| MakeAbsoluteSD | RtlSelfRelativeToAbsoluteSD |
| GetSecurityDescriptorControl | RtlGetControlSecurityDescriptor |
| SetSecurityDescriptorControl | RtlSetControlSecurityDescriptor |
| SetSecurityDescriptorOwner | RtlSetOwnerSecurityDescriptor |
| SetSecurityDescriptorGroup | RtlSetGroupSecurityDescriptor |
| GetSecurityDescriptorDacl | RtlGetDaclSecurityDescriptor |
| SetSecurityDescriptorDacl | RtlSetDaclSecurityDescriptor |
| SetSecurityDescriptorRMControl | RtlSetSecurityDescriptorRMControl |
| GetSecurityDescriptorSacl | RtlGetSaclSecurityDescriptor |
| SetSecurityDescriptorSacl | RtlSetSaclSecurityDescriptor |
| SetKernelObjectSecurity | NtSetSecurityObject |
| GetKernelObjectSecurity | NtQuerySecurityObject |
| IsValidAcl | RtlValidAcl |
| InitializeAcl | RtlCreateAcl |
| GetAclInformation | RtlQueryInformationAcl |
| SetAclInformation | RtlSetInformationAcl |
| AddAce | RtlAddAce |
| DeleteAce | RtlDeleteAce |
| GetAce | RtlGetAce |
| AddAccessAllowedAce | RtlAddAccessAllowedAce |
| AddAccessAllowedAceEx | RtlAddAccessAllowedAceEx |
| AddMandatoryAce | RtlAddMandatoryAce |
| AddResourceAttributeAce | RtlAddResourceAttributeAce |
| AddAccessDeniedAce | RtlAddAccessDeniedAce |
| AddAccessDeniedAceEx | RtlAddAccessDeniedAceEx |
| AddAuditAccessAce | RtlAddAuditAccessAce |
| AddAuditAccessAceEx | RtlAddAuditAccessAceEx |
| AddAccessAllowedObjectAce | RtlAddAccessAllowedObjectAce |
| AddAccessDeniedObjectAce | RtlAddAccessDeniedObjectAce |
| FindFirstFreeAce | RtlFirstFreeAce |
有关这些函数的描述,请参阅 Windows SDK 文档。
# 11.4.1 演示 查看安全描述符(SD)
让我们使用表 11-1 中的一些函数。我们将编写一个应用程序,用于显示所选内核对象的安全描述符(SD):进程、线程、任何命名内核对象,或任何进程中的任何句柄(假设可访问)。
首先,我们编写一个 DisplaySD 函数,重点关注安全描述符(SD)的两个重要组成部分:所有者和自主访问控制列表(DACL)。
void DisplaySD(const PSECURITY_DESCRIPTOR sd)
{
auto len = RtlLengthSecurityDescriptor(sd);
printf("SD 长度: %u (0x%X) 字节\n ", len, len);
SECURITY_DESCRIPTOR_CONTROL control;
DWORD revision;
if (NT_SUCCESS(RtlGetControlSecurityDescriptor(sd, &control, &revision)))
{
printf("版本(Revision): %u 控制标志(Control): 0x%X (%s)\n ", revision,
control, SDControlToString(control).c_str());
}
2
3
4
5
6
7
8
9
10
11
12
上述代码获取安全描述符(SD)的长度(通过 RtlLengthSecurityDescriptor 函数),然后获取控制标志(通过 RtlGetControlSecurityDescriptor 函数)。SDControlToString 函数是一个辅助函数,用于以人类可读的形式显示控制标志。
完整的源代码位于 sd 项目中。
接下来,我们获取所有者(如果存在):
PSID sid;
BOOLEAN defaulted;
if (NT_SUCCESS(RtlGetOwnerSecurityDescriptor(sd, &sid, &defaulted)))
{
if (sid)
printf("所有者(Owner): %ws (%ws) 默认(Defaulted): %s\n ",
SidToString(sid).c_str(), GetUserNameFromSid(sid).c_str(),
defaulted ? "是(Yes)" : "否(No)");
else
printf("无所有者(No owner)\n ");
}
2
3
4
5
6
7
8
9
10
11
SidToString 函数将二进制安全标识符(SID)转换为其字符串表示形式(使用前面讨论的 RtlConvertSidToUnicodeString 函数),而 GetUserNameFromSid 函数则查找该安全标识符(SID)对应的友好名称(如果可用)。
下一步是获取自主访问控制列表(DACL)(如果存在),并遍历所有访问控制项(ACE),显示每个项的内容:
BOOLEAN present;
PACL dacl;
if (NT_SUCCESS(RtlGetDaclSecurityDescriptor(sd, &present, &dacl, &defaulted)))
{
if (!present)
printf("空自主访问控制列表(NULL DACL)- 对象未受保护(object is unprotected)\n ");
else
{
printf("自主访问控制列表(DACL): 访问控制项(ACE)数量: %d\n ", (int)dacl->AceCount);
PACE_HEADER header;
for (int i = 0; i < dacl->AceCount; i++)
{
if (NT_SUCCESS(RtlGetAce(dacl, i, (PVOID*)&header)))
{
DisplayAce(header, i);
}
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
RtlGetAce 函数获取每个访问控制项(ACE),而 DisplayAce 函数显示其内容:
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: // 具有相同的二进制布局(have the same binary layout)
{
auto data = (ACCESS_ALLOWED_ACE*)header;
printf("\t访问权限(Access): 0x%08X %ws (%ws)\n ", data->Mask,
SidToString((PSID)&data->SidStart).c_str(),
GetUserNameFromSid((PSID)&data->SidStart).c_str());
}
break;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
一个访问控制项(ACE)包含三个部分:访问掩码(access mask)、类型(最常见的是允许(Allow)或拒绝(Deny)),以及其适用的安全标识符(SID)。
现在我们已经准备好 DisplaySD 函数,接下来需要为其提供一个安全描述符(SD)。可以使用 NtQuerySecurityObject 函数获取内核对象的安全描述符(SD):
NTSTATUS NtQuerySecurityObject(
_In_ HANDLE Handle,
_In_ SECURITY_INFORMATION SecurityInformation,
_Out_writes_bytes_opt_(Length) PSECURITY_DESCRIPTOR SecurityDescriptor,
_In_ ULONG Length,
_Out_ PULONG LengthNeeded);
2
3
4
5
6
SECURITY_INFORMATION 是一组标志,用于指示请求的信息类型。
在调用 NtQuerySecurityObject 函数之前,我们需要获取目标对象的句柄。对于具有名称的对象,我们将创建一个辅助函数,尝试各种类型的对象,直到找到对象或耗尽对象类型列表:
HANDLE OpenNamedObject(PCWSTR path)
{
UNICODE_STRING name;
RtlInitUnicodeString(&name, path);
OBJECT_ATTRIBUTES objAttr;
InitializeObjectAttributes(&objAttr, &name,
OBJ_CASE_INSENSITIVE, nullptr, nullptr);
HANDLE hObject = nullptr;
2
3
4
5
6
7
8
上述代码准备了一个(基本为空的)对象属性(OBJECT_ATTRIBUTES),其中包含 OBJ_CASE_INSENSITIVE 标志,因此按名称搜索时不区分大小写,方便调用者使用。
命名对象有对应的“打开”函数可供使用。以下是其中一些:
// 事件(event)
NtOpenEvent(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 互斥体(mutex)
NtOpenMutant(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 作业(job)
NtOpenJobObject(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 内存区域(section)
NtOpenSection(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 注册表项(registry key)
NtOpenKey(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 信号量(semaphore)
NtOpenSemaphore(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 计时器(timer)
NtOpenTimer(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
// 对象管理器目录(object manager directory)
NtOpenDirectoryObject(&hObject, READ_CONTROL, &objAttr);
if (hObject)
return hObject;
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
READ_CONTROL 通用访问掩码是获取对象安全描述符(SD)所需的权限。如果捕获返回状态并与 STATUS_ACCESS_DENIED 进行比较,代码可以变得更加高效。如果返回该值,则可以提前退出,因为已找到正确的对象,但无法授予访问权限。
如果我们对某个命名对象调用了错误类型的“打开”函数,会发生什么情况?返回的状态将是 STATUS_OBJECT_TYPE_MISMATCH(0xC0000024),这是一个我们会忽略并继续尝试下一种对象类型的错误。
最后一种对象类型是文件,需要特殊处理。一方面,NtOpenFile 函数有额外的参数。另一方面,客户端可能会指定标准的 Win32 路径,例如 "c:\temp",但原生 API 无法正确解析该路径,我们需要在路径前添加 "\??\",以便将符号链接目录用作查找驱动器号的基础。以下是相关代码:
IO_STATUS_BLOCK ioStatus;
NtOpenFile(&hObject, FILE_GENERIC_READ, &objAttr, &ioStatus,
FILE_SHARE_READ | FILE_SHARE_WRITE, 0);
if (hObject)
return hObject;
//
// 对驱动器号的特殊处理(special handling for a drive letter)
//
if (name.Length > 4 && path[1] == L':')
{
UNICODE_STRING name2;
name2.MaximumLength = name.Length + sizeof(WCHAR) * 4;
name2.Buffer = (PWSTR)RtlAllocateHeap(RtlProcessHeap(), 0,
name2.MaximumLength);
UNICODE_STRING prefix;
RtlInitUnicodeString(&prefix, L"\\??\\ ");
RtlCopyUnicodeString(&name2, &prefix);
RtlAppendUnicodeStringToString(&name2, &name);
InitializeObjectAttributes(&objAttr, &name2,
OBJ_CASE_INSENSITIVE, nullptr, nullptr);
NtOpenFile(&hObject, FILE_GENERIC_READ, &objAttr, &ioStatus,
FILE_SHARE_READ | FILE_SHARE_WRITE, 0);
RtlFreeHeap(RtlProcessHeap(), 0, name2.Buffer);
}
return hObject;
}
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
主要工作是在给定路径前添加 "\??\"。为了灵活性,代码使用 RtlAllocateHeap 动态分配内存(malloc 也可以正常工作,但如果可能的话,我更倾向于使用原生 API),然后通过操作 UNICODE_STRING 对象来获得所需的结果。最后,在释放分配的缓冲区之前,使用新路径再次调用 NtOpenFile 函数。
为了将所有部分整合在一起,我们需要实现主函数。首先,获取命令行参数:
int wmain(int argc, const wchar_t* argv[])
{
if (argc < 2)
{
printf("用法(Usage): sd [[-p pid [handle] | [-t tid] | [object_name]]\n ");
printf("如果未指定任何参数,则显示当前进程的安全描述符(SD)(If no arguments are specified, shows the current process SD)\n ");
}
2
3
4
5
6
7
该应用程序接受进程ID(-p标志)、线程ID(-t标志)或对象名称作为输入。如果指定了-p选项,还可以额外指定一个句柄(handle),作为目标进程中的目标句柄。
接下来,我们将启用SeDebug权限(如果调用者的令牌中可用),以便应用程序在尝试打开对象句柄时获得更多权限:
BOOLEAN enabled;
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
2
3
然后,我们将准备一个空的OBJECT_ATTRIBUTES(对象属性)结构,用于打开进程和线程:
OBJECT_ATTRIBUTES emptyAttr;
InitializeObjectAttributes(&emptyAttr, nullptr , 0 , nullptr , nullptr);
2
之后,我们开始处理传入的参数,首先处理-p选项:
bool thisProcess = argc == 1;
HANDLE hObject = argc == 1 ? NtCurrentProcess() : nullptr;
if (argc > 2)
{
if (_wcsicmp(argv[1], L"-p") == 0)
{
CLIENT_ID cid{ ULongToHandle(wcstol(argv[2], nullptr , 0)) };
HANDLE hProcess = nullptr;
NtOpenProcess(&hProcess, argc > 3 ? PROCESS_DUP_HANDLE : READ_CONTROL, &emptyAttr, &cid);
if (hProcess && argc > 3)
{
NtDuplicateObject(hProcess,
ULongToHandle(wcstoul(argv[3], nullptr , 0)),
NtCurrentProcess(), &hObject, READ_CONTROL, 0 , 0);
NtClose(hProcess);
}
else
{
hObject = hProcess;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
如果指定了进程ID,将调用NtOpenProcess(打开进程的原生API)来获取该进程的句柄。如果提供了额外的参数(一个句柄值),则表示调用者关注的是该句柄而非进程本身。为了访问任意进程中的句柄,我们必须将其复制到当前进程中,这正是NtDuplicateObject(复制对象的原生API)的作用。如果没有提供额外参数,则将进程句柄赋值给hObject。
如果请求的对象是线程,我们必须打开该线程的句柄:
else if (_wcsicmp(argv[1], L"-t") == 0)
{
CLIENT_ID cid{ nullptr, ULongToHandle(wcstol(argv[2], nullptr , 0)) };
NtOpenThread(&hObject, READ_CONTROL, &emptyAttr, &cid);
}
}
2
3
4
5
6
如果没有指定任何选项,则输入的是对象名称:
else if (argc == 2)
{
hObject = OpenNamedObject(argv[1]);
}
2
3
4
OpenNamedObject是我们之前看到的函数,用于尝试打开命名对象。
如果无法获取有效的句柄,则程序结束:
if ( !hObject)
{
printf("Error opening object\n " );
return 1;
}
2
3
4
5
如果获得了有效的句柄,我们就可以获取该对象的安全描述符(Security Descriptor,SD)(如果存在):
PSECURITY_DESCRIPTOR sd = nullptr;
BYTE buffer[1 << 12];
ULONG needed;
auto status = NtQuerySecurityObject(hObject,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
buffer, sizeof(buffer), &needed);
if ( !NT_SUCCESS(status))
{
printf("No security descriptor available (0x%X)\n " , status);
}
else
{
DisplaySD((PSECURITY_DESCRIPTOR)buffer);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
该代码采用了一种简单的方案,假设安全描述符的大小不超过4KB(这是一个合理的假设)。更健壮的代码应该先查询安全描述符的大小,然后分配缓冲区再获取它。这部分内容留给读者作为练习。
最后需要执行清理操作。以下是完整的wmain函数,方便参考:
int wmain(int argc, const wchar_t* argv[])
{
if (argc < 2)
{
printf("Usage: sd [[-p pid [handle] | [-t tid] | [object_name]]\n " );
printf("If no arguments are specified, shows the current process SD\n " );
}
BOOLEAN enabled;
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, TRUE, FALSE, &enabled);
OBJECT_ATTRIBUTES emptyAttr;
InitializeObjectAttributes(&emptyAttr, nullptr , 0 , nullptr , nullptr);
bool thisProcess = argc == 1;
HANDLE hObject = argc == 1 ? NtCurrentProcess() : nullptr;
if (argc > 2)
{
if (_wcsicmp(argv[1], L"-p") == 0)
{
CLIENT_ID cid{ ULongToHandle(wcstol(argv[2], nullptr , 0)) };
HANDLE hProcess = nullptr;
NtOpenProcess(&hProcess, argc > 3 ? PROCESS_DUP_HANDLE : READ_CONTROL, &emptyAttr, &cid);
if (hProcess && argc > 3)
{
NtDuplicateObject(hProcess,
ULongToHandle(wcstoul(argv[3], nullptr , 0)),
NtCurrentProcess(), &hObject, READ_CONTROL, 0 , 0);
NtClose(hProcess);
}
else
{
hObject = hProcess;
}
}
else if (_wcsicmp(argv[1], L"-t") == 0)
{
CLIENT_ID cid{ nullptr, ULongToHandle(wcstol(argv[2], nullptr , 0)) };
NtOpenThread(&hObject, READ_CONTROL, &emptyAttr, &cid);
}
}
else if (argc == 2)
{
hObject = OpenNamedObject(argv[1]);
}
if ( !hObject)
{
printf("Error opening object\n " );
return 1;
}
PSECURITY_DESCRIPTOR sd = nullptr;
BYTE buffer[1 << 12];
ULONG needed;
auto status = NtQuerySecurityObject(hObject,
OWNER_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION,
buffer, sizeof(buffer), &needed);
if ( !NT_SUCCESS(status))
{
printf("No security descriptor available (0x%X)\n " , status);
}
else
{
DisplaySD((PSECURITY_DESCRIPTOR)buffer);
}
if (hObject && !thisProcess)
NtClose(hObject);
return 0;
}
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
以下是sd.exe工具针对不同对象的输出示例:
>sd -p 540
SD Length: 116 (0x74) bytes
Revision: 1 Control: 0x8004 (DACL Present, Self Relative)
Owner: S-1-5-32-544 (BUILTIN\Administrators) Defaulted: No
DACL: ACE count: 3
ACE 0: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001FFFFF S-1-5-32-544 (BUILTIN\Administrators)
ACE 1: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001FFFFF S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 2: Size: 28 bytes, Flags: 0x00 Type: ALLOW
Access: 0x00121411 S-1-5-5-0-268513 (NT AUTHORITY\LogonSessionId_0_268513)
2
3
4
5
6
7
8
9
10
11
>sd c:\windows\system32
SD Length: 392 (0x188) bytes
Revision: 1 Control: 0x9404 (DACL Present, DACL Auto Inherited, DACL Protected, Self\ Relative)
Owner: S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464 (NT SERVICE\TrustedInstaller) Defaulted: No
DACL: ACE count: 13
ACE 0: Size: 40 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F01FF S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464 (NT SERVICE\TrustedInstaller)
ACE 1: Size: 40 bytes, Flags: 0x0A Type: ALLOW
Access: 0x10000000 S-1-5-80-956008885-3418522649-1831038044-1853292631-2271478464 (NT SERVICE\TrustedInstaller)
ACE 2: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001301BF S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 3: Size: 20 bytes, Flags: 0x0B Type: ALLOW
Access: 0x10000000 S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 4: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001301BF S-1-5-32-544 (BUILTIN\Administrators)
ACE 5: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0x10000000 S-1-5-32-544 (BUILTIN\Administrators)
ACE 6: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001200A9 S-1-5-32-545 (BUILTIN\Users)
ACE 7: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0xA0000000 S-1-5-32-545 (BUILTIN\Users)
ACE 8: Size: 20 bytes, Flags: 0x0B Type: ALLOW
Access: 0x10000000 S-1-3-0 (\CREATOR OWNER)
ACE 9: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001200A9 S-1-15-2-1 (APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES)
ACE 10: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0xA0000000 S-1-15-2-1 (APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES)
ACE 11: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001200A9 S-1-15-2-2 (APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES)
ACE 12: Size: 24 bytes, Flags: 0x0B Type: ALLOW
Access: 0xA0000000 S-1-15-2-2 (APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES)
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
>sd \kernelobjects\memoryerrors
SD Length: 156 (0x9C) bytes
Revision: 1 Control: 0x8004 (DACL Present, Self Relative)
Owner: S-1-5-32-544 (BUILTIN\Administrators) Defaulted: No
DACL: ACE count: 5
ACE 0: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x00120001 S-1-1-0 (\Everyone)
ACE 1: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F0003 S-1-5-32-544 (BUILTIN\Administrators)
ACE 2: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F0003 S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 3: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x00120001 S-1-15-2-1 (APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES)
ACE 4: Size: 24 bytes, Flags: 0x00 Type: ALLOW
Access: 0x00120001 S-1-15-2-2 (APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>sd -p 57716 8
SD Length: 20 (0x14) bytes
Revision: 1 Control: 0x8000 (Self Relative)
No owner
NULL DACL - object is unprotected
2
3
4
5
>sd -p 57716 0x48
SD Length: 140 (0x8C) bytes
Revision: 1 Control: 0x8004 (DACL Present, Self Relative)
Owner: S-1-5-21-3968166439-3083973779-398838822-1001 (PAVEL7760\Pavel) Defaulted: No
DACL: ACE count: 3
ACE 0: Size: 20 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F0001 S-1-5-18 (NT AUTHORITY\SYSTEM)
ACE 1: Size: 36 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F0001 S-1-5-21-3968166439-3083973779-398838822-1001 (PAVEL7760\Pavel)
ACE 2: Size: 28 bytes, Flags: 0x00 Type: ALLOW
Access: 0x001F0001 S-1-5-5-0-268513 (NT AUTHORITY\LogonSessionId_0_268513)
2
3
4
5
6
7
8
9
10
11
# 11.5 总结
本章探讨了一些与安全相关的原生API(Native API)。许多Windows系统中与安全相关的API都是这些原生函数的简单封装,但也有一些原生API更为特殊,没有对应的Windows API等效实现,例如NtCreateToken(创建令牌的原生API)。