Windows 会话、Session 0 与进程 Token 隔离身份

Windows 会话、Session 0 与进程 Token 隔离身份

开源项目参考地址 github.com/wangwei-cm/...

!\[\](i-blog.csdnimg.cn/direct/4ef9...)

目标与四步流程

这个程序的功能是读取指定进程的会话位置、进程对象 Owner 和主 Token 身份字段。它把会话、对象安全描述符和访问令牌分开记录,避免把"在哪个登录环境运行""谁拥有进程对象"和"进程以什么身份运行"混为同一个结论。

要完成这项读取,需要按四步进行:

  1. 确定目标进程的会话位置
  2. 读取进程对象 Owner
  3. 打开主 Token,读取用户和会话字段
  4. 读取隔离字段并整理独立结果

完整路径是:目标 PID -> 打开进程对象 -> 读取 Session 与 Owner -> 打开主 Token -> 读取用户、会话和隔离字段 -> 记录来源、错误码与读取时间。

Session(会话)把登录桌面、窗口站和相关进程划分为相互隔离的运行空间。进程对象是内核对象管理器维护的可访问对象,带有自己的安全描述符和 Owner(所有者)。Access Token(访问令牌)保存进程以什么用户、组、特权、完整性级别和隔离状态运行。它们都关联同一个进程,却各自回答不同问题。

正确的读取顺序先确认进程实例和 Session,再读取进程对象 Owner,随后读取主 Token 的各项字段。进程对象 Owner 查询需要 READ_CONTROL,令牌查询需要 TOKEN_QUERY。每项 API 保留独立失败状态,避免一项访问被拒绝时掩盖其它已读到的事实。

一份可复核的身份记录至少包含 PID、进程创建时间、进程 Session ID、对象 Owner SID、TokenUser SID、TokenSessionId、完整性 SID 与 RID、提升类型、AppContainer 状态、账户映射状态、使用的访问掩码和错误码。PID 可能复用,跨采样保存时还要加入创建时间。同一个数值 PID 在不同生命周期中得到的 Token 不能合并。

1. 确定目标进程的会话位置

Session 是 Windows 为登录环境划分的编号空间。一个交互用户登录控制台、远程桌面或快速用户切换环境时,系统会为该登录环境分配 Session ID。该会话包含窗口站、桌面、剪贴板、输入路由和会话内进程等资源。Session ID 主要描述进程所处的登录与图形隔离范围,不能直接说明进程拥有管理员权限,也不能直接说明它属于某个服务。

Session 0 是系统服务使用的会话。Windows Vista 以后,交互用户通常登录到非零会话,服务进程留在 Session 0,从而使服务 UI 不会直接出现在用户桌面。这项安排称为 Session 0 Isolation(Session 0 隔离)。服务尝试显示对话框时,交互用户桌面通常看不到它。服务应通过服务控制机制、日志、命名管道或其它经过授权的 IPC 与用户程序通信。

Session 0 只说明会话归属,不说明最高权限。一个 Session 0 进程可使用 LocalSystem、LocalService、NetworkService 或指定用户账户的令牌。非零会话中的进程也可能拥有高完整性令牌。Session 只是一层隔离边界,Token 权限、完整性标签、对象 DACL(Discretionary Access Control List,自主访问控制列表)和保护进程策略共同决定实际访问结果。

cpp 复制代码
// 作用:把 PID 映射到当前所属的 Windows Session ID。
// 返回:非零成功。0 失败,立即读取 GetLastError。
BOOL ProcessIdToSessionId(
    DWORD dwProcessId,   // 输入:目标进程 PID。目标可在调用前退出或被后续进程复用。
    DWORD* pSessionId    // 输出:调用方保存的 Session ID,不能为 nullptr。单位是会话编号。
);

正确流程先保存目标 PID 的创建时间,再读取 Session ID。需要长期关联时,每轮都重新确认实例身份。Session ID 为 0 时记录"Session 0",非零值时记录具体编号和读取时间。不能从 Session 数字推断用户 SID、服务账户、完整性级别或是否具有某项特权。

cpp 复制代码
DWORD sessionId = 0;
if (ProcessIdToSessionId(targetPid, &sessionId)) {
    const bool isSessionZero = sessionId == 0;
    // 保存 targetPid、创建时间、sessionId、isSessionZero 和采样时间。
} else {
    const DWORD error = GetLastError();
    // 记录失败。当前轮没有 Session 字段。
}

错误写法把会话编号当作访问判定。下面的判断会误将所有 Session 0 进程看成可完全访问对象,也会漏掉非零会话中的高权限进程。

cpp 复制代码
if (sessionId == 0) {
    // 错误:Session 0 只表示会话归属,没有授予 OpenProcess、OpenProcessToken 或对象访问权限。
}

完成第一步后,已经知道进程处于哪个登录和桌面隔离范围。会话编号没有提供进程对象安全描述符中的 Owner,也没有提供主 Token 的运行身份。接下来读取进程对象 Owner,才能把对象权限来源单独记录下来。

2. 读取进程对象 Owner

进程对象由内核对象管理器维护。它有对象类型、句柄引用、访问掩码和安全描述符。安全描述符中的 Owner SID 是该进程对象的所有者。它参与对象权限管理,例如对象 Owner 在通常规则下能调整对象 DACL。这个 Owner 是"进程对象的安全描述符字段",不能直接替代进程主令牌的用户。

主 Token 是进程默认执行身份。它保存 TokenUser SID、组 SID、特权、默认 DACL、完整性标签、会话号、AppContainer 与能力等。线程还可能临时模拟另一个令牌。OpenProcessToken 读取的是进程主令牌,不读取某个线程当下的模拟令牌。因此一个线程正在模拟时,线程访问结果可以与进程主 Token 显示的信息不同。

对象 Owner 与 TokenUser 经常相同,因为创建进程对象时系统会使用相关安全上下文设置安全描述符。它们仍然来自不同位置,允许出现差异。调查记录应分别显示"进程对象 Owner"和"TokenUser",而非只输出一个"进程所有者"标签。

cpp 复制代码
// 作用:读取当前线程紧邻失败的 Win32 API 错误码。
// 返回:DWORD 错误编号。没有参数、缓冲区、权限要求或资源所有权。
DWORD GetLastError(void);

// 作用:返回调用进程自身的数值 PID。
// 返回:非零 PID。没有参数、错误码或释放责任。
DWORD GetCurrentProcessId(void);

// 作用:按 PID 打开进程对象,建立调用方拥有的 HANDLE。
// 返回:有效 HANDLE 成功。nullptr 失败,立即读取 GetLastError。成功句柄必须 CloseHandle。
HANDLE OpenProcess(
    DWORD dwDesiredAccess, // 输入:访问掩码。本流程使用 PROCESS_QUERY_LIMITED_INFORMATION | READ_CONTROL。
    BOOL bInheritHandle,   // 输入:FALSE 表示子进程不继承此句柄。
    DWORD dwProcessId      // 输入:目标 PID。目标可能退出,PID 将来可能复用。
);

// 作用:关闭调用方拥有的普通内核对象 HANDLE。
// 返回:非零成功。0 失败,立即读取 GetLastError。成功后 hObject 失效。
BOOL CloseHandle(HANDLE hObject); // 输入:OpenProcess 或 OpenProcessToken 成功返回的真实 HANDLE,不能为 nullptr。

PROCESS_QUERY_LIMITED_INFORMATION 支持受限进程查询,READ_CONTROL 请求读取安全描述符所需的控制权限。访问受保护进程或安全描述符限制更严的进程时,OpenProcess 可能失败。也可能打开成功而 Owner 或 Token 查询被单独拒绝。最小权限使失败原因更清晰,也避免为只读检查请求多余能力。

cpp 复制代码
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | READ_CONTROL,
    FALSE, targetPid);
if (process == nullptr) {
    const DWORD error = GetLastError();
    // 保存 error。没有进程 HANDLE 时不能继续查询对象或令牌。
} else {
    // 分别查询对象 Owner、会话和主 Token。
    CloseHandle(process); // 成功打开一次,对应关闭一次。
}

从进程安全描述符读取 Owner SID

SID(Security Identifier,安全标识符)是 Windows 用来表示用户、组、服务和完整性标签的二进制身份值。SID 的显示文本形如 S-1-5-...,文本便于日志比对。访问检查使用的是二进制 SID。GetSecurityInfo 从指定对象安全描述符取得 Owner、Group、DACL、SACL 或完整安全描述符,返回值本身是 Win32 错误码,不能改用 GetLastError。

cpp 复制代码
// 作用:查询对象的安全描述符组成部分。
// 返回:ERROR_SUCCESS 成功。其它 DWORD 直接是 Win32 错误码。
DWORD GetSecurityInfo(
    HANDLE handle,                         // 输入:对象 HANDLE。本流程传进程 HANDLE,需 READ_CONTROL。
    SE_OBJECT_TYPE ObjectType,             // 输入:对象类别。进程对象使用 SE_KERNEL_OBJECT。
    SECURITY_INFORMATION SecurityInfo,     // 输入:请求部分。本流程使用 OWNER_SECURITY_INFORMATION。
    PSID* ppsidOwner,                      // 可选输出:Owner SID 指针。本流程提供有效地址,指针借自返回安全描述符。
    PSID* ppsidGroup,                      // 可选输出:Group SID。本流程传 nullptr。
    PACL* ppDacl,                          // 可选输出:DACL。本流程传 nullptr。
    PACL* ppSacl,                          // 可选输出:SACL。本流程传 nullptr,读取 SACL 还需要额外权限。
    PSECURITY_DESCRIPTOR* ppSecurityDescriptor // 可选输出:系统分配的描述符。本流程提供有效地址,成功后用 LocalFree 释放。
);

// 作用:释放 LocalAlloc 系列或指定 API 返回的本地堆内存。
// 返回:nullptr 成功。失败时返回原 HLOCAL 并读取 GetLastError。成功后原内存和内部 SID 指针失效。
HLOCAL LocalFree(
    HLOCAL hMem // 输入:要释放的本地堆指针。本流程传 GetSecurityInfo 或 ConvertSidToStringSidW 的返回分配。
);

正确流程在 GetSecurityInfo 成功后、LocalFree 之前使用 ppsidOwner。Owner SID 指向返回的安全描述符内部,释放描述符后不能继续转换、比较或保存该指针。需要长期保存时复制 SID 字节或保存字符串形式。查询失败时保留 API 直接返回的错误码。

cpp 复制代码
PSID ownerSid = nullptr;
PSECURITY_DESCRIPTOR descriptor = nullptr;
const DWORD status = GetSecurityInfo(process, SE_KERNEL_OBJECT, OWNER_SECURITY_INFORMATION,
    &ownerSid, nullptr, nullptr, nullptr, &descriptor);
if (status == ERROR_SUCCESS) {
    // 先验证并记录 ownerSid。它的生命周期由 descriptor 决定。
    // 例如,把 SID 转成文本后复制到 std::wstring。
    if (LocalFree(descriptor) != nullptr) {
        const DWORD error = GetLastError();
    }
} else {
    // status 是权威错误码,例如 ERROR_ACCESS_DENIED。不读取 GetLastError 替代它。
}

以下错误在释放安全描述符后继续使用内部 SID 指针。指针已悬空,后续账户查询可能崩溃、读取无关内存或产生错误身份结果。

cpp 复制代码
GetSecurityInfo(process, SE_KERNEL_OBJECT, OWNER_SECURITY_INFORMATION,
    &ownerSid, nullptr, nullptr, nullptr, &descriptor);
LocalFree(descriptor);
LookupAccountSidW(nullptr, ownerSid, name, &nameChars, domain, &domainChars, &use);
// 错误:ownerSid 的存储已随着 descriptor 释放而失效。

完成第二步后,已经从进程对象安全描述符取得 Owner SID,并明确它的生命周期由返回的安全描述符控制。Owner 仍无法说明进程主令牌的用户身份和令牌会话。接下来打开主 Token,读取这些独立字段。

3. 打开主 Token,读取用户和会话字段

OpenProcessToken 打开目标进程的主访问令牌。TOKEN_QUERY 是只读 Token 信息所需的权限。成功返回的 Token HANDLE 独立于进程 HANDLE。两者都由调用方关闭。进程退出后已有句柄仍可能指向退出对象,查询会得到失败、历史状态或因对象生命周期产生的边界结果,报告应带上读取时间。

cpp 复制代码
// 作用:打开目标进程的主访问令牌。
// 返回:非零成功。0 失败,立即读取 GetLastError。成功时 TokenHandle 得到调用方拥有的 HANDLE,必须 CloseHandle。
BOOL OpenProcessToken(
    HANDLE ProcessHandle,      // 输入:有效进程 HANDLE。调用方已通过 OpenProcess 获得。
    DWORD DesiredAccess,       // 输入:TOKEN_QUERY 用于只读 Token 信息。
    PHANDLE TokenHandle        // 输出:不能为 nullptr。成功时写入新 Token HANDLE。
);

// 作用:读取某个 TOKEN_INFORMATION_CLASS 对应的令牌信息。
// 返回:非零成功。0 失败,立即读取 GetLastError。零容量探测常返回 ERROR_INSUFFICIENT_BUFFER。
BOOL GetTokenInformation(
    HANDLE TokenHandle,                    // 输入:带 TOKEN_QUERY 的有效 Token HANDLE。
    TOKEN_INFORMATION_CLASS TokenInformationClass, // 输入:TokenUser、TokenSessionId、TokenIntegrityLevel 等类别。
    LPVOID TokenInformation,               // 输出:调用方缓冲区。长度探测阶段可为 nullptr。
    DWORD TokenInformationLength,          // 输入:缓冲区容量,单位字节。探测阶段为 0。
    PDWORD ReturnLength                    // 输出:实际或所需字节数,不能为 nullptr。
);

Token 信息有固定长度和可变长度两类。TokenSessionId、TokenElevationType、TokenIsAppContainer 可放入固定大小值。TokenUser 和 TokenIntegrityLevel 包含 SID,长度随 SID 子授权数量变化。通用读取器先以空缓冲区探测所需字节数,检查上限后分配本地字节数组,再用返回容量重新调用。两次调用之间令牌信息可变化,第二次若重复返回 ERROR_INSUFFICIENT_BUFFER,应有限重试并保留失败状态。

cpp 复制代码
DWORD requiredBytes = 0;
if (!GetTokenInformation(token, TokenUser, nullptr, 0, &requiredBytes)) {
    const DWORD firstError = GetLastError();
    if (firstError == ERROR_INSUFFICIENT_BUFFER && requiredBytes > 0 && requiredBytes <= 1024 * 1024) {
        std::vector<BYTE> bytes(requiredBytes);
        DWORD actualBytes = requiredBytes;
        if (GetTokenInformation(token, TokenUser, bytes.data(), actualBytes, &actualBytes) &&
            actualBytes >= sizeof(TOKEN_USER) && actualBytes <= bytes.size()) {
            const auto* user = reinterpret_cast<const TOKEN_USER*>(bytes.data());
            // user->User.Sid 只在 bytes 保持有效期间可用。先验证 SID,再转换或复制。
        }
    }
}

TOKEN_USER::User.Sid 是令牌用户 SID。它描述进程主令牌以哪个用户身份执行。若线程模拟其它令牌,当前线程实际访问使用的身份可能不同。TokenUser 映射到 LocalSystem、LocalService 或 NetworkService 等内置 SID 时,可以把它标为服务账户 SID。这个分类不能单独证明进程由服务控制管理器启动,计划任务或其它系统组件也可以使用同类账户。

将 SID 转为可读身份信息

账户映射把 SID 解析为本机或域中的"域\名称"和账户用途。映射依赖本机账户数据库、域控制器和可用缓存,因此同一有效 SID 可能在离线、删除账户或跨设备环境中没有可解析名称。记录应始终保留字符串 SID,账户名作为可失败的辅助展示。

cpp 复制代码
// 作用:把二进制 SID 转换为系统分配的 UTF-16 字符串 SID。
// 返回:非零成功。0 失败,读取 GetLastError。成功的 StringSid 由系统分配,调用方必须 LocalFree。
BOOL ConvertSidToStringSidW(
    PSID Sid,          // 输入:通过 IsValidSid 校验的 SID,不能为 nullptr。调用方只借用其字节。
    LPWSTR* StringSid  // 输出:不能为 nullptr。成功时接收 LocalFree 负责的 NUL 终止 UTF-16 字符串。
);

// 作用:把 SID 映射为账户和域名称。
// 返回:非零成功。0 失败,立即读取 GetLastError。首次探测常返回 ERROR_INSUFFICIENT_BUFFER。
BOOL LookupAccountSidW(
    LPCWSTR lpSystemName,                 // 输入:可为 nullptr,表示本机或当前可访问的账户来源。
    PSID Sid,                             // 输入:有效 SID,不能为 nullptr。
    LPWSTR ReferencedDomainName,          // 输出:域名 UTF-16 缓冲区。容量为 0 时可为 nullptr。
    LPDWORD cchReferencedDomainName,      // 输入/输出:域缓冲区容量/实际字符数,单位 wchar_t,不能为 nullptr。
    LPWSTR Name,                          // 输出:账户名 UTF-16 缓冲区。容量为 0 时可为 nullptr。
    LPDWORD cchName,                      // 输入/输出:账户名容量/实际字符数,单位 wchar_t,不能为 nullptr。
    PSID_NAME_USE peUse                   // 输出:账户用途枚举,不能为 nullptr。
);

// 作用:判断 SID 是否属于指定的内置 SID 类别。
// 返回:非零表示匹配。0 表示不匹配。它不设置"错误原因",输入 SID 必须先有效。
BOOL IsWellKnownSid(
    PSID pSid,                    // 输入:有效 SID,不能为 nullptr。
    WELL_KNOWN_SID_TYPE WellKnownSidType // 输入:WinLocalSystemSid、WinLocalServiceSid、WinNetworkServiceSid 等类别。
);

账户查询的正确流程先以空缓冲区探测名称和域的字符数,再按 wchar_t 字符数分配缓冲区。名称、域和 SID 字符串都使用 UTF-16。LookupAccountSidW 的容量单位是 wchar_t 字符数。返回的 SID_NAME_USE 可帮助区分用户、组、域和别名,却不授予访问权限。

cpp 复制代码
DWORD nameChars = 0, domainChars = 0;
SID_NAME_USE use{};
if (!LookupAccountSidW(nullptr, sid, nullptr, &domainChars, nullptr, &nameChars, &use) &&
    GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
    std::vector<wchar_t> domain(domainChars, L'\0');
    std::vector<wchar_t> name(nameChars, L'\0');
    if (LookupAccountSidW(nullptr, sid, domain.data(), &domainChars,
        name.data(), &nameChars, &use)) {
        std::wstring account = std::wstring(domain.data(), domainChars) + L"\\" +
            std::wstring(name.data(), nameChars);
    }
}

服务账户与服务程序要分开表达。LocalSystem 拥有系统本地账户的 Token,LocalService 与 NetworkService 使用受限的内置服务身份。SCM(Service Control Manager,服务控制管理器)可以用它们创建服务进程,也可以配置服务使用指定的本地或域账户。一个 TokenUser 命中内置服务 SID,记录应写为"服务账户 SID 分类命中"。是否真是 SCM 管理的服务需要结合服务数据库、父进程和启动记录另行确认。

完成第三步后,已经取得 TokenUser 和 TokenSessionId,并能把 SID 保留为可核对的文本或账户名称。用户身份与令牌会话仍无法描述令牌的完整性限制、UAC 状态和应用容器隔离。接下来读取这些隔离字段,并让每项结果保持自己的来源。

4. 读取隔离字段并整理独立结果

MIC(Mandatory Integrity Control,强制完整性控制)通过完整性标签限制跨完整性边界的访问。TokenIntegrityLevel 返回 TOKEN_MANDATORY_LABEL,其中 SID 的最后一个子授权值 RID(Relative Identifier,相对标识符)对应不受信任、低、中、中等加、高、系统或受保护进程等等级。常见中等完整性 RID 为 0x2000,高完整性 RID 为 0x3000,系统完整性 RID 为 0x4000。

完整性级别不能替代 DACL。访问检查同时考虑对象 DACL、令牌用户和组、特权、完整性标签、保护进程政策、AppContainer 能力与调用 API 的访问掩码。高完整性令牌不自动获得所有对象访问权。低完整性令牌也可能读到对象允许的内容。报告中应把"完整性标签""请求访问掩码"和"实际错误码"并列保存。

cpp 复制代码
// 作用:验证 SID 二进制格式。
// 返回:非零表示格式有效。0 表示无效。输入指针必须指向当前可访问的 SID 存储。
BOOL IsValidSid(PSID pSid);

// 作用:取得 SID 的子授权数量地址。
// 返回:PUCHAR 指向 SID 内部计数字段。调用方不释放。SID 必须已通过 IsValidSid。
PUCHAR GetSidSubAuthorityCount(PSID pSid);

// 作用:取得指定序号的 SID 子授权值地址。
// 返回:PDWORD 指向 SID 内部 DWORD。调用方不释放。nSubAuthority 必须小于子授权数量。
PDWORD GetSidSubAuthority(
    PSID pSid,             // 输入:已验证 SID。
    DWORD nSubAuthority    // 输入:零起始子授权索引。完整性标签读取最后一项。
);

正确读取完整性标签时,先用变长 Token 查询得到本地缓冲区,再确认结构长度、SID 非空和 SID 格式有效,随后读取最后一个子授权 RID。SID 指针借自 Token 信息缓冲区,缓冲区销毁后它立即失效。

cpp 复制代码
// bytes 来自成功的 GetTokenInformation(TokenIntegrityLevel) 调用,且 bytes.size() 已检查不少于结构头。
const auto* mandatoryLabel = reinterpret_cast<const TOKEN_MANDATORY_LABEL*>(bytes.data());
PSID integritySid = mandatoryLabel->Label.Sid;
if (integritySid != nullptr && IsValidSid(integritySid)) {
    const PUCHAR count = GetSidSubAuthorityCount(integritySid);
    if (count != nullptr && *count > 0) {
        const PDWORD rid = GetSidSubAuthority(integritySid, static_cast<DWORD>(*count - 1));
        // *rid 为完整性 RID。复制数值和 SID 文本后才能释放 bytes。
    }
}

UAC(User Account Control,用户账户控制)使用完整令牌和受限令牌帮助管理管理员账户的日常运行权限。TokenElevationTypeDefault 表示没有这种 UAC 成对令牌关系,TokenElevationTypeFull 表示当前使用完整令牌,TokenElevationTypeLimited 表示当前使用过滤令牌。提升类型描述 UAC 令牌关系,完整性级别描述 MIC 标签。两者相关却不能互相替换。

AppContainer 是应用隔离模型。TokenIsAppContainer 为真时,令牌带有 AppContainer 隔离身份,访问还会受包 SID、能力 SID、资源 ACL 和容器策略影响。它与 Session 隔离、完整性级别、桌面隔离并列工作。一个非 AppContainer 令牌也可能低完整性,一个 AppContainer 令牌也仍有 Session ID。

把字段、错误和资源放回同一份记录

整体读取应分别记录独立字段:先获取进程句柄,读取 PID Session 与对象 Owner。再打开主 Token,读取 TokenUser、TokenSessionId、TokenIntegrityLevel、TokenElevationType 和 TokenIsAppContainer。最后将 SID 映射为文本和账户显示名。每个字段使用自己的状态和错误码,资源句柄与 LocalAlloc 内存按成功路径配对释放。

cpp 复制代码
HANDLE process = OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION | READ_CONTROL, FALSE, targetPid);
if (process != nullptr) {
    DWORD processSession = 0;
    const BOOL sessionOk = ProcessIdToSessionId(targetPid, &processSession);
    // Owner 查询使用 GetSecurityInfo 的直接返回状态,成功时先处理 SID,再 LocalFree 描述符。

    HANDLE token = nullptr;
    if (OpenProcessToken(process, TOKEN_QUERY, &token)) {
        // 对 TokenUser、TokenSessionId、TokenIntegrityLevel、TokenElevationType、TokenIsAppContainer 分别查询。
        CloseHandle(token);
    } else {
        const DWORD tokenError = GetLastError();
    }
    CloseHandle(process);
} else {
    const DWORD processError = GetLastError();
}
  1. 混淆 TokenUser 和进程对象 Owner。二者常相同会掩盖来源差异,安全描述符重设、特殊创建上下文或不同对象类型都会导致记录解释错误。
cpp 复制代码
PSID owner = tokenUserSid; // 错误:TokenUser 来自令牌,进程对象 Owner 只能从对象安全描述符读取。
  1. 把 Session 0、LocalSystem、高完整性和服务进程写成同一个状态。这四项分别来自会话、账户 SID、强制完整性标签和服务管理关系。任意两项组合都不能自动推出第三项。
cpp 复制代码
if (sessionId == 0) {
    report("LocalSystem 高完整性服务"); // 错误:Session 0 没有提供 TokenUser、完整性或 SCM 管理证据。
}
  1. 将账户映射失败理解为 SID 无效。SID 可格式正确、可参与访问检查,同时因为账户删除、域不可达或缓存缺失而没有显示名称。日志应保留原始 SID 文本、LookupAccountSidW 错误码和采样时间。
cpp 复制代码
if (!LookupAccountSidW(nullptr, sid, name, &nameChars, domain, &domainChars, &use)) {
    deleteIdentityRecord(); // 错误:账户名不可用时仍应保留 SID 和字段状态。
}

最后需要注意时序边界。进程可在打开后退出,线程可模拟令牌,Token 也能在访问前被替换或限制。每次采样都应记录读取顺序和时间。跨轮次关联使用 PID 加创建时间。访问被拒绝、进程退出、Owner 不可读、Token 不可读、SID 无法映射和完整性标签缺失都应作为不同事实显示。只有在这些字段保持来源清晰时,Session 0、服务账户和隔离状态的判断才具备可解释性。

完整可运行程序在附件 wangweicm.lanzn.com/ipZQk4a9stj...

相关推荐
波力海苔夹心脆6752 小时前
C# 海康威视摄像头二次开发入门:HCNetSDK 登录、实时预览、云台控制、录像与抓图(WinForms 实战)
开发语言·windows·经验分享·tcp/ip·c#
爱上网的小舟2 小时前
windows无法启动windows audio服务怎么办?6个步骤排查修复windows audio无法启动
windows
SoStraw2 小时前
跨平台P2P通信SDK,Windows/Android/iOS/Linux/macOS一套API全兼容
android·linux·windows·跨平台·p2p·webrpc
爱写代码的小朋友2 小时前
Windows 下 MSYS2 + MinGW64 安装与使用 OpenCV 全指南(含踩坑记录)
c++·人工智能·windows·opencv
the3clipse2 小时前
Windows 11 对 VVC 与 AV1 编码支持全景解析:硬件、软件与开发者指南
windows·视频编解码·av1·vvc·h.266·硬件加速·视频扩展
百事牛科技12 小时前
WinRAR注释功能:给压缩包留个便签
windows·winrar
sukalot13 小时前
Windows 驱动实例分析系列:libwdi 驱动分析 - examples 篇(一)
windows·驱动开发
加载力科技13 小时前
硬盘咔咔响是坏盘信号!赶在数据丢失前克隆换盘:不重装系统、不丢文件
windows·嵌入式硬件·系统安全·策略模式·命令模式·硬盘
2601_9623649715 小时前
电脑人脸解锁的安全基石,聊聊 Windows Hello 硬件规范
windows·安全·电脑