英文:
Token created via LogonUserW while impersonating another user is denied from accessing resources
问题
以下是翻译的内容:
我有一个程序,通过LogonUserW
创建令牌,并在调用RegOpenKeyW
时模拟它们。如果我只做一次RegOpenKeyW
调用,它将成功。如果我模拟第一个令牌,然后使用LogonUserW
创建第二个令牌并模拟它,对RegOpenKeyW
的相同调用将失败。以下是演示此行为的示例代码(注意:宏USER
和PASSWORD
未包含在其中)。
#include <Windows.h>
#include <iostream>
void Test() {
HKEY key;
auto status{ RegOpenKeyW(HKEY_CLASSES_ROOT, L"*", &key) };
std::wcout << L"RegOpenKeyW: ";
if (status == ERROR_SUCCESS) {
std::wcout << L"Succeeded";
RegCloseKey(key);
}
else {
std::wcout << L"Failed (0x" << std::hex << status << L")";
}
std::wcout << std::endl;
}
int main() {
// 登录用户,模拟他们,然后运行测试
HANDLE token1;
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token1)) {
if (ImpersonateLoggedOnUser(token1)) {
Test();
// 在仍模拟用户的情况下,运行相同的操作
HANDLE token2;
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token2)) {
if (ImpersonateLoggedOnUser(token2)) {
Test();
}
CloseHandle(token2);
}
}
CloseHandle(token1);
}
}
这会生成以下输出。
RegOpenKeyW: Succeeded
RegOpenKeyW: Failed (0x5)
有人知道为什么第二次对Test
的调用失败并返回访问被拒绝(0x5
)吗?我期望如果第一次调用成功,第二次调用也会成功,但实际情况并非如此。
如果我在两次调用LogonUserW
时使用相同的帐户,我可以使用TokenViewer检查两个令牌,并且它们似乎是相同的。但是,如果在调用LogonUserW
时使用不同的帐户,行为将相同。
如果我选择不同的登录类型或登录提供程序作为LogonUserW
的参数,行为将相同。
如果我在第二次调用LogonUserW
之前调用RevertToSelf
,则两次调用RegOpenKeyW
都将成功,但我不确定为什么这种更改会导致它成功,而原始示例代码则不会。
我还修改了Test
以访问除注册表之外的资源(例如LsaOpenPolicy
),并遇到了相同的行为。第一次尝试访问资源将成功,而第二次尝试将失败。
英文:
I have a program which creates tokens via LogonUserW
and impersonates them while calling RegOpenKeyW
. If I do this once the call to RegOpenKeyW
will succeed. If I impersonate the 1st token, then make a 2nd token with LogonUserW
and impersonate it, the same call to RegOpenKeyW
will fail. Here is example code which demonstrates the behavior (note: the macros USER
and PASSWORD
are not included).
#include <Windows.h>
#include <iostream>
void Test() {
HKEY key;
auto status{ RegOpenKeyW(HKEY_CLASSES_ROOT, L"*", &key) };
std::wcout << L"RegOpenKeyW: ";
if (status == ERROR_SUCCESS) {
std::wcout << L"Succeeded";
RegCloseKey(key);
}
else {
std::wcout << L"Failed (0x" << std::hex << status << L")";
}
std::wcout << std::endl;
}
int main() {
// Logon a user, impersonate them, then run a test
HANDLE token1;
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token1)) {
if (ImpersonateLoggedOnUser(token1)) {
Test();
// While still impersonating the user, run the same actions
HANDLE token2;
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token2)) {
if (ImpersonateLoggedOnUser(token2)) {
Test();
}
CloseHandle(token2);
}
}
CloseHandle(token1);
}
}
That generates the following output.
RegOpenKeyW: Succeeded
RegOpenKeyW: Failed (0x5)
Does anyone know why the 2nd call to Test
fails with access denied (0x5
)? I would expect that if the 1st call succeeded that the 2nd call would succeed as well but that is not the case.
If I use the same account for both calls to LogonUserW
, I can inspected both tokens with TokenViewer and they appear to effectively be the same. However, I can use different accounts when calling LogonUserW
and the same behavior will occur.
If I chose a different logon type or logon provider as the arguments to LogonUserW
the same behavior will occur.
If I call RevertToSelf
before the 2nd call to LogonUserW
then both calls to RegOpenKeyW
will succeed, but I am unsure why that change would allow it to succeed and the original example code will not.
I have also modified Test
to access resources other than the registry (ex. LsaOpenPolicy
) and have experienced the same behavior. The 1st attempt at accessing the resource will succeed and the 2nd attempt will fail.
答案1
得分: 3
在线索 SECURITY_IMPERSONATION_LEVEL
分配给线程令牌中的关键点。ImpersonateLoggedOnUser
在内部调用 SetThreadToken
-> [PsImpersonateClient
][2] 时使用 SecurityImpersonation
级别,但在内部此 API 调用 SeTokenCanImpersonate
并检查某些条件是否不满足:
> 该例程通过检查各种条件来确保客户端模拟实际发生,包括以下条件:... 如果没有满足任何条件,该例程将复制传递给调用的现有令牌,并将新复制的令牌分配为模拟令牌,尽管具有有限的安全模拟级别;也就是说,服务器线程只能获取关于客户端的信息。
换句话说,SecurityImpersonation
被替换为 SecurityIdentification
。
条件列表可能会发生变化,但通常情况下会发生以下情况:
- 允许模拟匿名令牌
- 如果进程令牌具有 SeImpersonatePrivilege 则允许
- 如果进程令牌的 AuthenticationId 等于客户端令牌的 OriginatingLogonSession 则允许
- 如果令牌具有相同的安全标识符(SIDs)则允许
当您调用 LogonUserW
时不进行模拟 - OriginatingLogonSession 客户端令牌将等于进程令牌的 AuthenticationId。结果是,在第一次调用 ImpersonateLoggedOnUser
后,您将始终具有 SecurityImpersonation
级别。但是,如果您在进行模拟时再次调用 LogonUserW
,则 token2
将具有另一个OriginatingLogonSession,如果您的进程没有SeImpersonatePrivilege,并且您模拟另一个用户(而不是当前用户,您的进程以其身份启动),则在第二次调用后,您将具有SecurityIdentification。这意味着您只能检查令牌属性,但任何安全检查都将失败。opnkey、文件、具有 SD 的对象 - 都会失败。
用于测试/检查的 POC 代码:
PCSTR ToString(TOKEN_TYPE TokenType)
{
switch (TokenType)
{
case TokenPrimary: return " Primary";
case TokenImpersonation: return "Impersonation";
}
__debugbreak();
return 0;
}
PCSTR ToString(SECURITY_IMPERSONATION_LEVEL ImpersonationLevel)
{
switch (ImpersonationLevel)
{
case SecurityAnonymous: return "Anonymous ";
case SecurityIdentification: return "Identification";
case SecurityImpersonation: return "Impersonation ";
case SecurityDelegation: return "Delegation ";
}
__debugbreak();
return 0;
}
void PrintTokenInfo(HANDLE hToken)
{
TOKEN_ORIGIN to;
TOKEN_STATISTICS ts;
ULONG cb;
if (GetTokenInformation(hToken, TokenStatistics, &ts, sizeof(ts), &cb) &&
GetTokenInformation(hToken, TokenOrigin, &to, sizeof(to), &cb))
{
DbgPrint("%s:%s: %08x -> %08x\n",
ToString(ts.TokenType), ToString(ts.ImpersonationLevel),
to.OriginatingLogonSession.LowPart, ts.AuthenticationId.LowPart);
}
}
void PrintTokenInfo()
{
HANDLE hToken;
if (OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken) ||
OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, FALSE, &hToken))
{
PrintTokenInfo(hToken);
CloseHandle(hToken);
}
else
{
DbgPrint("OpenThreadToken=%u\n", GetLastError());
}
}
void m() {
HANDLE token1, token2;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &token1))
{
const TOKEN_PRIVILEGES tp = { 1, { { { SE_IMPERSONATE_PRIVILEGE }, SE_PRIVILEGE_ENABLED } } };
AdjustTokenPrivileges(token1, FALSE, const_cast<TOKEN_PRIVILEGES*>(&tp), sizeof(tp), 0, 0);
DbgPrint("SE_IMPERSONATE_PRIVILEGE=%u\n", GetLastError());
PrintTokenInfo(token1);
CloseHandle(token1);
}
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token1)) {
PrintTokenInfo(token1);
if (ImpersonateLoggedOnUser(token1)) {
PrintTokenInfo();
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token2)) {
PrintTokenInfo(token2);
if (ImpersonateLoggedOnUser(token2)) {
PrintTokenInfo();
}
CloseHandle(token2);
}
}
CloseHandle(token1);
}
RevertToSelf();
}
如果在提升的管理员进程下运行此代码:
SE_IMPERSONATE_PRIVILEGE=0
Primary:Anonymous : 000003e7 -> 0003dafa
Primary:Impersonation : 0003dafa -> 0107cb0a
Impersonation:Impersonation : 0003dafa -> 0107cb0a
Primary:Impersonation : 0107cb0a -> 0107cbba
Impersonation:Impersonation : 0107cb0a -> 0107cbba
如果在受限制的(非提升的)用户下运行:
SE_IMPERSONATE_PRIVILEGE=1300 // ERROR_NOT_ALL_ASSIGNED
Primary:Anonymous : 000003e7 -> 0003dbc5
Primary:
<details>
<summary>英文:</summary>
here key point in [`SECURITY_IMPERSONATION_LEVEL`][1] assigned to thread token. `ImpersonateLoggedOnUser` internally call `SetThreadToken `
-> [`PsImpersonateClient`][2] with `SecurityImpersonation` level, but internally this api call `SeTokenCanImpersonate` and if some conditions not meet :
> *The routine ensures whether client impersonation can actually occur by checking various conditions, including the following: ... If none
> of the conditions are met, the routine makes a copy of the existing
> token passed to the call and assigns the newly copied token as
> impersonation token albeit with **limited** security impersonation
> level; that is, the server thread can **only** obtain information
> about the client.*
in other words, the `SecurityImpersonation` replaced to `SecurityIdentification `
the list of conditions can be subject to change,but in general it next
- allow impersonating anonymous tokens
- allow if process token have ***SeImpersonatePrivilege***
- allow if ***AuthenticationId*** of process token equal to
***OriginatingLogonSession*** client token
- allow if tokens have equal security identifiers (SIDs)
when you call `LogonUserW` while no impersonate - the ***OriginatingLogonSession*** client token will be equal to ***AuthenticationId*** of process token. as result you will be always have `SecurityImpersonation` level after first `ImpersonateLoggedOnUser`. but if you make second call to `LogonUserW` while you impersonating - the `token2` will be have **another *OriginatingLogonSession*** and if your process have no *SeImpersonatePrivilege* and you impersonate another user (not current, under which your process started) - after second call you will be have **SecurityIdentification**. with this - you can only check token properties but any security check will fail. opnkey, file, object with SD - all fail.
the POC code for test/check:
PCSTR ToString(TOKEN_TYPE TokenType)
{
switch (TokenType)
{
case TokenPrimary: return " Primary";
case TokenImpersonation: return "Impersonation";
}
__debugbreak();
return 0;
}
PCSTR ToString(SECURITY_IMPERSONATION_LEVEL ImpersonationLevel)
{
switch (ImpersonationLevel)
{
case SecurityAnonymous: return "Anonymous ";
case SecurityIdentification: return "Identification";
case SecurityImpersonation: return "Impersonation ";
case SecurityDelegation: return "Delegation ";
}
__debugbreak();
return 0;
}
void PrintTokenInfo(HANDLE hToken)
{
TOKEN_ORIGIN to;
TOKEN_STATISTICS ts;
ULONG cb;
if (GetTokenInformation(hToken, TokenStatistics, &ts, sizeof(ts), &cb) &&
GetTokenInformation(hToken, TokenOrigin, &to, sizeof(to), &cb))
{
DbgPrint("%s:%s: %08x -> %08x\n",
ToString(ts.TokenType), ToString(ts.ImpersonationLevel),
to.OriginatingLogonSession.LowPart, ts.AuthenticationId.LowPart);
}
}
void PrintTokenInfo()
{
HANDLE hToken;
if (OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, TRUE, &hToken) ||
OpenThreadToken(GetCurrentThread(), TOKEN_QUERY, FALSE, &hToken))
{
PrintTokenInfo(hToken);
CloseHandle(hToken);
}
else
{
DbgPrint("OpenThreadToken=%u\n", GetLastError());
}
}
void m() {
HANDLE token1, token2;
if (OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES|TOKEN_QUERY, &token1))
{
const TOKEN_PRIVILEGES tp = { 1, { { { SE_IMPERSONATE_PRIVILEGE }, SE_PRIVILEGE_ENABLED } } };
AdjustTokenPrivileges(token1, FALSE, const_cast<TOKEN_PRIVILEGES*>(&tp), sizeof(tp), 0, 0);
DbgPrint("SE_IMPERSONATE_PRIVILEGE=%u\n", GetLastError());
PrintTokenInfo(token1);
CloseHandle(token1);
}
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token1)) {
PrintTokenInfo(token1);
if (ImpersonateLoggedOnUser(token1)) {
PrintTokenInfo();
if (LogonUserW(USER, L".", PASSWORD, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, &token2)) {
PrintTokenInfo(token2);
if (ImpersonateLoggedOnUser(token2)) {
PrintTokenInfo();
}
CloseHandle(token2);
}
}
CloseHandle(token1);
}
RevertToSelf();
}
if run this code under elevated admin process:
SE_IMPERSONATE_PRIVILEGE=0
Primary:Anonymous : 000003e7 -> 0003dafa
Primary:Impersonation : 0003dafa -> 0107cb0a
Impersonation:Impersonation : 0003dafa -> 0107cb0a
Primary:Impersonation : 0107cb0a -> 0107cbba
Impersonation:Impersonation : 0107cb0a -> 0107cbba
and if run under restricted(not elevated) user
SE_IMPERSONATE_PRIVILEGE=1300 // ERROR_NOT_ALL_ASSIGNED
Primary:Anonymous : 000003e7 -> 0003dbc5
Primary:Impersonation : 0003dbc5 -> 01064877
Impersonation:Impersonation : 0003dbc5 -> 01064877
Primary:Impersonation : 01064877 -> 010648ee
OpenThreadToken=1346 // ERROR_BAD_IMPERSONATION_LEVEL
last error ( ERROR_BAD_IMPERSONATION_LEVEL ) say that real impersonation level of our thread is `SecurityIdentification`. this also can be checked by another, elevated tool
[1]: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/wdm/ne-wdm-_security_impersonation_level
[2]: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-psimpersonateclient
</details>
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论