~/posts/binary/windows-token-creation-effective-impersonation.md

Windows 令牌:创建、模拟与实际访问

从已有的 SeCreateTokenPrivilege 出发,区分新令牌属性、线程有效身份与文件访问结果。解析错误 1346、会话标识对照和 StorSvc 加载路径,并验证 SID 比较与变长数组容量。

date[31:24]
read[23:16]
7 分钟
cat[15:8]
二进制
目录
  1. 0x00起点是已有特权,不是普通账户
  2. 0x01把令牌属性放在不同坐标轴上
  3. 0x02错误 1346 出现在源文件打开阶段
  4. 0x03会话标识变化需要独立对照
  5. 0x04SID 和变长数组先检查容量
  6. 0x05写入文件之后还有一条加载链
  7. 0x06每个阶段留下独立检查点
  8. 0x07官方参考

带有管理员组的令牌,不等于线程已经获得管理员访问能力。2023 年的一组 Windows 记录同时出现“创建成功”“模拟成功”和文件访问错误 1346,差异恰好落在令牌对象、线程有效身份与对象访问检查之间。

这里把三者分开观察:先确认调用者已有的 SeCreateTokenPrivilege,再检查新令牌的各项属性,最后验证真正执行文件操作的线程。记录涉及 10.0.17134.1 与 10.0.20348.587;其他静态记录缺少完整构建信息,不将它们归并成对所有 Windows 版本的结论。

起点是已有特权,不是普通账户

旧构建的起始输出包含:

10.0.17134.1 · selected privilege output
$ whoami /priv
Privilege Name                Description                  State
SeCreateTokenPrivilege        Create a token object        Disabled
SeChangeNotifyPrivilege       Bypass traverse checking     Enabled

Disabled 表示该项已经在令牌中,只是尚未启用。AdjustTokenPrivileges 调整已有特权,未列入令牌的特权不会因为调用这个函数就出现;即使函数返回非零,也要检查 GetLastError() 是否为 ERROR_NOT_ALL_ASSIGNED。这是 API 的明确约定,而不是一般意义上的“调用成功”。Microsoft API 文档

还要确定调整的是执行受检操作的令牌。向待创建或待使用的令牌写入某项特权,与调用者在创建操作时具备该特权,是两个问题。窗口标题中的 Administrator 也不足以还原账户配置、令牌来源和提升方式。

把令牌属性放在不同坐标轴上

样本使用 ZwCreateToken 请求模拟令牌(impersonation token),保留当前用户身份,加入内置管理员组,并设置中等完整性标签。相关输出的字段应分别解释:

字段相同的数字,不代表相同语义8 行
字段 记录中的值或对象 分析意义
TokenUser 当前用户 SID 令牌代表的用户,不是默认所有者
TokenGroups S-1-5-32-544 等 组 SID 还要结合 enabled、deny-only 等属性
TokenPrivileges 6 项 包含与启用是两个状态
TokenIntegrityLevel S-1-16-8192 中等完整性,不等价于“普通用户全部权限”
TokenType 2 TokenImpersonation
TokenImpersonationLevel 2 SecurityImpersonation,本地模拟级别
AuthenticationId 0x3e7 或 0x3e6 登录会话 LUID,不是用户 SID
TokenOwner 默认所有者 SID 不应替代 TokenUser 判断身份

TokenType = 2 与 TokenImpersonationLevel = 2 来自不同枚举。SecurityIdentification 允许识别客户端,却不提供相同的本地模拟能力;SecurityDelegation 又涉及远程系统的模拟。访问令牌和模拟级别的定义分别见 Access tokens 与 SECURITY_IMPERSONATION_LEVEL。

中等完整性与管理员组同时出现,并不矛盾。完整性策略、组属性、特权和对象 DACL 共同影响具体操作;令牌自身的默认 DACL 则主要服务于新对象的安全描述符,不是对所有已有对象的通行证。

错误 1346 出现在源文件打开阶段

成功记录报告读取并写入 8704 字节。文件列表中的大小为 8.50 KB,与 8704 / 1024 = 8.5 一致;下方树只保留相关文件:

Recorded destination
  • C:\Windows\System32
    • malicious.dllsize 8704
1 个目录,1 个文件

另一次记录先输出创建令牌和模拟成功,接着出现:

Recorded failurelog
[*] Successfully impersonated the elevated token.
[-] Could not open source file by CreateFileW: [1346].
[-] Failed to exploit SeCreateTokenPrivilege.

这里失败的是 source file,还没有到目标文件写入。1346 = 0x542 对应 ERROR_BAD_IMPERSONATION_LEVEL,含义与所需模拟级别缺失或无效有关,不是一般的 DACL 拒绝,也不是 ZwCreateToken 的返回状态。Microsoft 错误码

SetThreadToken 成功后,应重新通过 OpenThreadToken 获取该线程的模拟令牌,再查询类型、级别、组属性、特权与完整性。OpenAsSelf = TRUE 只是让“打开令牌句柄”这次访问检查使用进程安全上下文,并非把线程改回进程身份,也不会自动提升返回令牌的模拟级别。OpenThreadToken 文档

会话标识变化需要独立对照

三组记录的证据边界3 行
记录 AuthenticationId 已打印模拟级别 文件结果 环境范围
早期成功 0x3e7 2 读取、写入各 8704 字节 与 10.0.17134.1 起始记录配套
失败对照 0x3e7 2 源文件打开返回 1346 静态记录没有完整构建
调整后成功 0x3e6 2 读取、写入各 8704 字节 静态记录没有完整构建

0x3e7 与 0x3e6 分别是系统和匿名登录会话常量的低位值,高位为零。修改 AuthenticationId 并不等于把 TokenUser 改成系统或匿名用户,更不等于将模拟级别改为 SecurityAnonymous。

0x3e6 记录中的访问成功值得关注,但它没有展示所有内核检查。要证明因果关系,应固定用户 SID、组及其属性、特权、完整性、模拟级别、对象和访问掩码,仅改变会话标识;成功与失败两边都保存附加前后的有效令牌。现有证据支持记录间的结果差异,不支持“全部检查被跳过”的结论。

动态演示单独显示 10.0.20348.587、组数量 13 和 AuthenticationId = 0x3e6;静态成功记录的组数量为 12。它们不是字段完全相同的同一次运行,版本与配置标签应分别保留。

SID 和变长数组先检查容量

令牌构造器容易把逻辑问题与缓冲区问题混在一起。下面的检查不创建令牌,只验证完整 SID 的区分和可变长数组的尺寸:

token-capacity-model.pypython
def required_bytes(count, first, stride, maximum):
    if first < 0 or stride <= 0 or first > maximum:
        raise ValueError("invalid layout")
    if count < 0 or count > (maximum - first) // stride:
        raise ValueError("invalid count")
    return first + count * stride

builtin_users = (1, 5, 32, 545)
unrelated_sid = (1, 5, 21, 111, 222, 333, 545)
assert builtin_users[-1] == unrelated_sid[-1]
assert builtin_users != unrelated_sid
assert required_bytes(6, 4, 12, 0xffffffff) == 76

元组是概念模型,不是 SID 的内存编码。Windows 的 EqualSid 比较完整且有效的 SID;以系统接口转换并比较这两个示例,得到不相等,长度分别为 16 与 28 字节。将一个更长 SID 原地覆盖到较短 SID 的存储中,会破坏后续数据。

本机只读验证还检查了 TOKEN_PRIVILEGES.Privileges 偏移为 4、LUID_AND_ATTRIBUTES 大小为 12,六项总长为 76。模型通过 1025 个普通数量、一个上界和两个拒绝用例;真实 C/C++ 实现应使用目标 ABI 的 offsetof、sizeof 与容量上界,不把常量当作跨结构保证。

ANYSIZE_ARRAY 为 1 并不意味着只有一项,也不自动为新增项分配空间。TOKEN_PRIVILEGES 文档明确要求为额外项分配足够内存。还应审计分配后覆盖指针、遗漏释放、短读短写,以及 NTSTATUS 与 Win32 错误码混用;这些属于构造器自身的正确性问题。

写入文件之后还有一条加载链

StorSvc 的三段反编译展示了 RPC 入口、工作对象创建与回调中的模块加载。按证据画出的流程如下,虚线处表示所展示片段没有包含工作提交位置:

flowchart TD
  accTitle: StorSvc 的入口、工作对象与回调
  accDescr: 入口经过设备与能力检查后进入初始化,创建工作对象;提交位置在已展示片段中缺失,回调另行执行模块加载。
  A["SvcRebootToFlashingMode"] --> B["设备与能力检查"]
  B --> C["InitResetPhone"]
  C --> D["CreateThreadpoolWork"]
  D -. "提交位置未展示" .-> E["ResetPhoneWorkerCallback"]
  E --> F["LoadLibraryW"]
  F --> G["GetProcAddress"]

CreateThreadpoolWork 创建工作对象;将其提交到线程池是 SubmitThreadpoolWork 的职责。看到回调地址被注册,不等于该回调已经执行。初始入口还包含设备族、多会话配置和客户端能力相关分支;未初始化和已初始化两条路径也应分开。

回调里的关键顺序可压缩为以下语义摘录,省略等待、锁与后续关机分支:

ResetPhoneWorkerCallback · semantic excerptcpp
HMODULE module = LoadLibraryW(L"SprintCSP.dll");
if (module) {
    auto factory_reset = reinterpret_cast<void (*)()>(
        GetProcAddress(module, "FactoryResetUICC"));
    if (factory_reset) {
        factory_reset();
    }
    FreeLibrary(module);
}

必须区分首次载入 DLL 的初始化执行与导出函数调用。对于新加载的 DLL,LoadLibraryW 会触发加载初始化;GetProcAddress 找不到指定导出,并不撤销已经发生的初始化。已加载模块、搜索策略、位数、签名或加载策略以及服务有效令牌,也会改变结果。LoadLibraryW 文档

10.0.20348.587 演示最终显示 whoami 为 nt authority\system,同时展示了文件写入与 RPC 客户端提示。这是该环境的结果证据,不是所有版本通用的执行保证;没有对应服务二进制、完整令牌快照和运行跟踪,就应保留加载路径中的未知项。

每个阶段留下独立检查点

复核顺序5 步
  1. 1

    调用者

    记录系统构建、起始令牌来源,以及特权存在、启用请求和启用后状态。将 API 返回值与其错误通道一并保留。

  2. 2

    新令牌

    分别记录用户、组及属性、特权、完整性、类型、模拟级别和 AuthenticationId。不要用一个字段代替整体安全上下文。

  3. 3

    线程有效令牌

    附加后重新查询线程,再执行单一对象访问。分别记录源文件打开、目标创建、读取与写入阶段。

  4. 4

    下游服务

    独立验证 RPC 可达性、条件分支、工作提交、实际加载路径和执行身份,避免把文件存在当作执行发生。

  5. 5

    结束与清理

    恢复线程身份并检查恢复结果,关闭句柄并核对文件长度。恢复身份失败时应终止该操作路径,避免后续任务继承残留上下文。

结论落在具体访问检查上:已有创建令牌特权、新令牌字段、线程模拟状态和服务加载条件,需要分别证明。本机验证仅覆盖 SID 与尺寸模型,没有创建特权令牌、写入系统目录或触发 StorSvc;历史环境的完整补丁集合与内核原因仍待单独核实。

官方参考

NORMAL~/posts/binary/windows-token-creation-effective-impersonation.md§--
0%zh-CN