说说Windows进程的令牌(token)

说说Windows进程的令牌(token)

在Windows系统中,每个进程都有一个关键的安全凭证------访问令牌,通常简称为令牌。这个看似简单的概念,实际上承载着整个Windows安全模型的核心机制。

什么是令牌

令牌是Windows内核中的一个数据结构,它包含了与安全相关的一切信息。当一个用户登录系统时,会创建一个主令牌。之后启动的每个进程都会继承或获得一个令牌,这个令牌决定了进程能做什么、不能做什么。

可以把令牌想象成现实世界中的身份证加门禁卡。身份证证明你是谁,门禁卡决定你能进哪些房间。Windows令牌同样包含这两类信息:身份信息和权限信息。

令牌的结构

在技术实现上,令牌主要包含以下几个关键部分:

用户SID:这是令牌的核心。SID是安全标识符,Windows用它唯一标识一个用户或组。进程令牌中的用户SID决定了进程以哪个用户的身份运行。

组SID列表:除了主用户,令牌还包含该用户所属的所有组。管理员用户运行时,令牌中既包含用户SID,也包含Administrators组的SID。

权限列表 :这是最实用的部分。每个权限都是一个字符串常量,对应特定的操作能力。比如SeDebugPrivilege允许调试其他进程,SeShutdownPrivilege允许关机。

完整性级别:从Vista开始引入的重要概念。分为低、中、高、系统等级别,用于实现强制完整性控制。浏览器通常运行在低完整性级别,限制其对系统的修改。

会话ID:标识进程属于哪个用户会话。这对于终端服务环境特别重要,确保不同用户的进程相互隔离。

其他属性:如默认DACL、源令牌、限制信息等,提供了更精细的安全控制。

令牌的类型

Windows中有两种主要令牌:主令牌和模拟令牌。

主令牌是进程的默认令牌,在进程创建时分配。当你在桌面上双击一个程序,该进程获得的就是主令牌,基于当前登录用户的凭证。

模拟令牌则不同。当进程A需要代表另一个用户执行操作时,可以创建一个模拟令牌。比如服务进程处理客户端请求时,需要以客户端的身份访问资源,就会使用模拟令牌。

模拟有几个级别:匿名模拟只能访问Everyone权限的资源,标识级别能获取用户身份但无法以该用户行事,模拟级别可以完全以该用户身份运行,委托级别甚至可以在网络间传递身份。

令牌的创建和继承

进程创建时,默认继承父进程的令牌。这就是为什么从命令提示符启动的程序,如果有管理员权限,启动的程序也有管理员权限。

但有个重要例外:UAC用户账户控制。当标准用户启动需要管理员权限的程序时,系统会弹出UAC提示。如果用户确认,系统会创建一个新的管理员令牌,并用它启动程序。这时父进程和子进程的令牌就不同了。

服务进程的令牌又有特殊规则。服务可以在系统账户、网络服务、本地服务等内置账户下运行,这些账户有预定义的权限集。

令牌操作API

Windows提供了一系列操作令牌的API,最常用的是OpenProcessTokenAdjustTokenPrivilegesGetTokenInformationSetTokenInformation

获取当前进程令牌的基本流程是:OpenProcess打开进程句柄,然后OpenProcessToken打开令牌,接着可以用GetTokenInformation查询各种信息。

调整权限需要小心操作。首先要查找权限的LUID本地唯一标识符,然后准备TOKEN_PRIVILEGES结构,调用AdjustTokenPrivileges。启用权限需要SE_PRIVILEGE_ENABLED标志,禁用则是移除这个标志。

示例代码:启用SeDebugPrivilege权限

c 复制代码
#include <windows.h>
#include <stdio.h>

BOOL EnableDebugPrivilege(BOOL bEnable) {
    HANDLE hToken = NULL;              // 令牌句柄
    TOKEN_PRIVILEGES tp = {0};         // 权限结构
    LUID luid = {0};                   // 本地唯一标识符
    
    // 打开当前进程的令牌
    // TOKEN_ADJUST_PRIVILEGES - 允许调整权限
    // TOKEN_QUERY - 允许查询令牌信息
    if (!OpenProcessToken(GetCurrentProcess(), 
                         TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, 
                         &hToken)) {
        printf("OpenProcessToken failed: %lu\n", GetLastError());
        return FALSE;
    }
    
    // 查找SeDebugPrivilege的LUID
    // 第一个参数NULL表示在本地系统查找
    // SE_DEBUG_NAME是"SeDebugPrivilege"的字符串常量
    if (!LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &luid)) {
        printf("LookupPrivilegeValue failed: %lu\n", GetLastError());
        CloseHandle(hToken);
        return FALSE;
    }
    
    // 设置权限结构
    tp.PrivilegeCount = 1;                // 只调整一个权限
    tp.Privileges[0].Luid = luid;         // 设置权限的LUID
    // 根据参数决定启用还是禁用权限
    // SE_PRIVILEGE_ENABLED表示启用权限
    // 设置为0表示禁用权限
    tp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
    
    // 调整令牌权限
    // 第二个参数FALSE表示禁用所有其他权限
    // 第三个参数tp指定要设置的权限
    // 第四、五个参数NULL表示不获取之前的权限状态
    if (!AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL)) {
        printf("AdjustTokenPrivileges failed: %lu\n", GetLastError());
        CloseHandle(hToken);
        return FALSE;
    }
    
    // 注意:即使AdjustTokenPrivileges返回成功,还需要检查GetLastError
    // 如果返回ERROR_NOT_ALL_ASSIGNED,表示部分权限未分配成功
    DWORD dwError = GetLastError();
    if (dwError == ERROR_NOT_ALL_ASSIGNED) {
        printf("Warning: Not all privileges were assigned\n");
    }
    
    CloseHandle(hToken);
    return TRUE;
}

权限提升实战

在安全研究和漏洞利用中,令牌操作是常见技术。如果进程有SeDebugPrivilege权限,就可以打开系统进程,复制其令牌,然后用这个高权限令牌创建新进程。

典型的提权步骤是:首先启用SeDebugPrivilege,然后打开目标高权限进程如winlogon.exe,获取其令牌,最后用CreateProcessAsUser以这个令牌创建新进程。

但这需要满足多个条件:不仅要有相应权限,还要在正确的会话中,有合适的完整性级别等。现代Windows增加了更多保护,使得这种攻击越来越难。

令牌和UAC

UAC用户账户控制的核心就是令牌管理。标准用户登录时,系统创建两个令牌:一个过滤过的标准用户令牌,一个完全的管理员令牌。

标准用户令牌移除了管理员组的大部分权限。当需要管理员权限时,系统使用管理员令牌启动新进程。这就是为什么管理员账户运行程序时,有时也会看到UAC提示,因为默认使用的是过滤后的令牌。

从编程角度,可以在清单文件中指定执行级别。asInvoker使用调用者的令牌,requireAdministrator需要管理员权限,highestAvailable请求尽可能高的权限。

令牌的检查过程

当进程访问资源时,系统如何检查权限?这是一个多步骤过程。

首先检查完整性级别。低完整性的进程不能写入高完整性的对象,无论其他权限如何。这是强制完整性控制的体现。

然后检查用户和组SID。访问令牌中的SID与对象DACL中的ACE访问控制项进行比较,查看是否明确允许或拒绝。

最后检查特权。有些操作不需要对象权限,而是需要特定特权。比如调试进程需要SeDebugPrivilege,关机需要SeShutdownPrivilege

令牌在安全中的应用

在安全软件中,监控令牌操作是重要防御手段。检测异常的令牌复制、权限提升尝试,可以发现攻击行为。

应用白名单技术会检查进程的令牌,确保只有授权用户和权限可以运行特定程序。

在虚拟化和容器环境中,令牌隔离是重要的安全边界。每个容器实例应有独立的令牌空间,防止逃逸攻击。

令牌的限制令牌

Windows还支持限制令牌,这是一种沙箱机制。通过创建限制令牌,可以移除某些组或特权,或添加限制SID。

限制SID是特殊的SID,即使原始令牌有权限,但如果限制SID列表不允许,访问也会被拒绝。这实现了双重检查机制。

谷歌的Chromium浏览器就使用限制令牌实现沙箱。浏览器进程有完整令牌,但渲染进程使用限制令牌,大大降低了攻击面。

示例代码:查询令牌完整性级别

c 复制代码
#include <windows.h>
#include <sddl.h>
#include <stdio.h>

void PrintTokenIntegrityLevel(HANDLE hToken) {
    DWORD dwLength = 0;                  // 返回的缓冲区大小
    TOKEN_MANDATORY_LABEL* pTml = NULL;  // 令牌完整性级别结构指针
    
    // 第一次调用GetTokenInformation获取所需缓冲区大小
    // 参数5传入NULL,函数返回需要的缓冲区大小到dwLength
    if (!GetTokenInformation(hToken, TokenIntegrityLevel, NULL, 0, &dwLength)) {
        DWORD dwError = GetLastError();
        // 期望的错误是ERROR_INSUFFICIENT_BUFFER
        if (dwError != ERROR_INSUFFICIENT_BUFFER) {
            printf("First GetTokenInformation failed: %lu\n", dwError);
            return;
        }
    }
    
    // 根据获取的大小分配内存
    // LPTR表示分配固定内存并初始化为0
    pTml = (TOKEN_MANDATORY_LABEL*)LocalAlloc(LPTR, dwLength);
    if (!pTml) {
        printf("LocalAlloc failed\n");
        return;
    }
    
    // 第二次调用GetTokenInformation获取实际数据
    if (GetTokenInformation(hToken, TokenIntegrityLevel, pTml, dwLength, &dwLength)) {
        // 获取SID的子权限数量
        PDWORD pSubAuthorityCount = GetSidSubAuthorityCount(pTml->Label.Sid);
        if (!pSubAuthorityCount) {
            printf("GetSidSubAuthorityCount failed\n");
            LocalFree(pTml);
            return;
        }
        
        // 获取最后一个子权限,即完整性级别RID
        // 完整性级别SID格式: S-1-16-xxxx,xxxx就是完整性级别
        DWORD dwIntegrityLevel = *GetSidSubAuthority(pTml->Label.Sid, 
            (DWORD)(*pSubAuthorityCount - 1));
        
        printf("Integrity Level RID: 0x%lX\n", dwIntegrityLevel);
        printf("Integrity Level: ");
        
        // 根据RID值判断完整性级别
        if (dwIntegrityLevel < SECURITY_MANDATORY_LOW_RID)
            printf("Untrusted (0)\n");
        else if (dwIntegrityLevel < SECURITY_MANDATORY_MEDIUM_RID)
            printf("Low (0x1000)\n");
        else if (dwIntegrityLevel < SECURITY_MANDATORY_HIGH_RID)
            printf("Medium (0x2000)\n");
        else if (dwIntegrityLevel < SECURITY_MANDATORY_SYSTEM_RID)
            printf("High (0x3000)\n");
        else if (dwIntegrityLevel < SECURITY_MANDATORY_PROTECTED_PROCESS_RID)
            printf("System (0x4000)\n");
        else
            printf("Protected Process (>=0x5000)\n");
    } else {
        printf("GetTokenInformation failed: %lu\n", GetLastError());
    }
    
    // 释放分配的内存
    LocalFree(pTml);
}

令牌的持久性

令牌通常只在进程运行时存在。但服务可以保存令牌,在需要时使用。这就是为什么服务可以重新启动后仍然保持身份。

在攻击中,攻击者会尝试转储令牌并重用,这就是传递哈希攻击的原理。获取用户令牌的哈希后,可以在其他系统上重用它。

Windows现在有Credential Guard等防护,在虚拟化安全环境中保护凭证,防止这种攻击。

令牌查看工具

Sysinternals的Process Explorer可以查看进程令牌。在进程属性页的Security选项卡,能看到用户、组、完整性级别等信息。

WhoAmI命令行工具可以显示当前进程的令牌信息,包括用户、组、特权。

在编程中,可以用GetTokenInformation获取所有详细信息,需要指定不同的信息类,如TokenUserTokenGroupsTokenPrivileges等。

总结

在进行DLL注入、调试HookAPI、CodePatch等工具的时候,也需要设置号相应的令牌。

相关推荐
包饭厅咸鱼3 小时前
小龙虾openclaw----Windows+Wsl+Docker 安装openclaw 并接入飞书
windows·docker·openclaw·小龙虾
※※冰馨※※3 小时前
【QT】TortoiseGit配 SSH 克隆 Codeup
开发语言·c++·windows
今夕资源网4 小时前
坚果手机直连Windows,打开软件实现键鼠操作TNT系统 视频教程+所需软件(今夕存档)
windows·智能手机·tnt·smartisan·smartisan tnt·锤子系统·坚果手机
alphaTao6 小时前
LeetCode 每日一题 2026/3/16-2026/3/22
linux·windows·leetcode
阿昭L6 小时前
说说VirtualAlloc的第三个参数
windows
翱翔的苍鹰6 小时前
什么是 Deep Agents?
人工智能·windows·语言模型·自然语言处理·langchain·开源
今夕资源网10 小时前
windows11无法启用投屏功能 无线显示器无法添加可选功能 解决办法 Miracast修复脚本
windows·计算机外设·miracast·系统修复·无线显示器·投屏功能·投屏功能无法添加
内卷焦虑人士18 小时前
Windows安装WSL2+Ubuntu 22.04
linux·windows·ubuntu
NGBQ1213819 小时前
4DDiG Partition Manager.exe 全解析:Windows 端专业磁盘分区管理工具深度指南
windows