说说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,最常用的是OpenProcessToken、AdjustTokenPrivileges、GetTokenInformation、SetTokenInformation。
获取当前进程令牌的基本流程是: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获取所有详细信息,需要指定不同的信息类,如TokenUser、TokenGroups、TokenPrivileges等。
总结
在进行DLL注入、调试HookAPI、CodePatch等工具的时候,也需要设置号相应的令牌。