系统相关
首页 > 系统相关> > Windows本地内核提权——Win32组件空指针漏洞(CVE-2018-8120)

Windows本地内核提权——Win32组件空指针漏洞(CVE-2018-8120)

作者:互联网

目录

漏洞概述

在2018年5月,微软官方公布并修复了4个win32k内核提权的漏洞,其中的CVE-2018-8120内核提权漏洞是存在于win32k内核组件中的一个空指针引用漏洞,可以通过空指针引用,对内核进行任意读写,进而执行任意代码,以达到内核提权的目的。

漏洞原理

该漏洞的触发点就是窗口站tagWINDOWSTATON对象的指针成员域spklList指向的可能是空地址,如果同时该窗口站关联当前进程,那么调用系统服务函数NtUserSetImeInfoEx设置输入法扩展信息时,会间接调用SetImeInfoEx函数访问spklList指针指向的位于用户进程地址空间的零页内存。

如果当前进程的零页内存未被映射(事实上零页内存正常是不会被映射的),函数SetImeInfoEx的访问操作将引发缺页异常,导致系统BSOD;同样,如果当前进程的零页内存被提前映射成我们精心构造的数据,则有可能恶意利用,造成任意代码执行的漏洞。

漏洞复现

windbg调试本地内核

说明:Windbg是Microsoft公司免费调试器调试集合中的GUI的调试器,支持Source和Assembly两种模式的调试。Windbg不仅可以调试应用程序,还可以进行Kernel Debug。

该工具使得我们可以本地调试windows系统的内核,但是,本地调试内核模式不能使用执行命令、断点命令和堆栈跟踪命令等命令

1、使用管理员身份打开cmd,执行bcdedit /debug on, 开启调试模式

2、使用管理员权限打开windbg(一定是管理员权限,不然不起作用),然后依次选择File->Kernel Debugging->Local->确定

3、经过上面的设置基本就可以进行相关本地内核调试

查看SSDT表和SSDTShadow表

在windows操作系统中,系统服务(系统内核函数)分为两种:一种是常用的系统服务,实现在内核文件;另一种是与图形显示及用户界面相关的系统服务,实现在win32k.sys文件中。

全部的系统服务在系统运行期间都储存在系统的内存区,系统使用两个系统服务地址表KiServiceTable和Win32pServiceTable管理这些系统服务,同时设置两个系统服务描述表(SDT)管理系统服务地址表,这两个系统服务描述表ServiceDescriptorTable(SSDT)ServiceDescriptorTableShadow(SSDTShadow)

其中,前者只包含KiServiceTable表,后者包含KiServiceTable和Win32pServiceTable两个表,而且SDDT是可以直接调用访问的,SSDTShadow不可以直接调用访问。

SDT对象的结构体如下:

typedef struct _KSYSTEM_SERVICE_TABLE
{
        PULONG ServiceTableBase;         // 系统服务地址表地址
        PULONG ServiceCounterTableBase;   
        PULONG NumberOfService;          // 服务函数的个数
        ULONG ParamTableBase;            // 该系统服务的参数表
} KSYSTEM_SERVICE_TABLE, *PKSYSTEM_SERVICE_TABLE;

通过windbg本地内核调试查看相关系统服务描述表实际结构分布:

分析:图中显示的是SDDT表和SSDTShadow表中的结构,每个表中的两行分别表示系统服务地址表KiServiceTable表和Win32pServiceTable表的相关数据信息。因为上面的是SSDT表,不包含Win32pServiceTable表,所以第一个表中第二行数据为空。

结合上面的结构体可以看出,KiServiceTable的地址是0x83cbfd9c,包含0x191个系统服务;Win32pServiceTable的地址是0x92696000,包含0x339个系统服务。

再查看系统服务地址表存储具体的内容:

分析:可以看出系统服务地址表中存储的都是四个字节的函数指针,这些指针指向的就是后面对应的系统服务函数

查看窗口站结构体信息

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

通过windbg来查看窗口站对象在内核中的结构体实例:

分析:上图就是窗口站tagWINDOWSTATION的结构体的定义,其中在偏移0x14处的spklList指针指向关联的键盘布局tagKL对象链表首节点

查看键盘布局的结构体定义

分析:键盘布局tagKL结构体中在偏移0x2c处的piiex指针指向关联的输入法扩展信息结构体对象,这也是SetImeInfoEx函数内存拷贝的目标地址。

当用户进程调用CreateWindowStation函数等相关函数创建新的窗口站时,最终会调用内核函数xxxCreateWindowStation执行窗口站的创建,但是在该函数执行期间,被创建的新窗口站实例的spklList指针并没有被初始化,指向的是空地址。
## 分析SetImeInfoEx函数

说明: 函数SetImeInfoEx是一个win32k组件中的内核函数,主要负责将输入法扩展信息tagIMEINFOEX对象拷贝到目标键盘布局tagKL对象的结构体指针piiex指向的输入法信息对象的缓冲区。

IDA加载win32k.sys组件并手动载入符号表

分析:从上面的伪代码中可以看出,函数SetImeInfoEx首先从参数a1指向的窗口站对象中获取spklList指针(a1是窗口站地址指针,偏移0x14就是spklList指针),也就是指向键盘布局链表tagKL首节点地址的指针;然后函数从首节点开始遍历键盘布局对象链表,直到节点对象的pklNext成员指回到首节点对象为止,函数判断每个被遍历的节点对象的hkl成员是否与源输入法扩展信息对象的hkl成员相等;接下来函数判断目标键盘布局对象的piiex成员(偏移0x2c)是否为空,且成员变量 fLoadFlag(偏移0x48) 值是否为 FALSE,如果上述两个条件成立,则把源输入法扩展信息对象的数据拷贝到目标键盘布局对象的piiex成员中。

把这段伪代码变得更易读一下~

BOOL __stdcall SetImeInfoEx(tagWINDOWSTATION *winSta, tagIMEINFOEX *imeInfoEx)
{
  [...]
  if ( winSta )
  {
    pkl = winSta->spklList;
    while ( pkl->hkl != imeInfoEx->hkl )
    {
      pkl = pkl->pklNext;
      if ( pkl == winSta->spklList )
        return 0;
    }
    piiex = pkl->piiex;
    if ( !piiex )
      return 0;
    if ( !piiex->fLoadFlag )
      qmemcpy(piiex, imeInfoEx, sizeof(tagIMEINFOEX));
    bReturn = 1;
  }
  return bReturn;
}

至此我们可以看出程序的漏洞:在遍历键盘布局对象链表 spklList 的时候并没有判断 spklList 地址是否为 NULL,假设此时 spklList 为空的话,接下来对 spklList 访问的时候将触发访问异常,导致系统 BSOD 的发生。

利用Poc验证漏洞

从之前的分析中,我们知道触发漏洞的条件是要将spklList指针指向空地址的窗口站关联到进程中。

具体实现就是先通过接口函数CreateWindowStation创建一个窗口站,然后调用NtUserSetImeInfoEx函数关联该窗口站和进程(NtUserSetImeInfoEx系统服务函数会调用SetImeInfoEx);因为NtUserSetImeInfoEx函数未导出,所以需要使用Malware Defender来hook得到序列号,再通过序列号计算出服务号

运行Malware Defender,选择钩子-->Win32k服务表,查看系统服务序列号

分析:NtUserSetImeInfoEx的系统服务号 = 0x1000+0x226(550的16进制) = 0x1226 ,其中 0x1000代表调用SSDTShadow中第二个表项中的系统服务函数(第一个表项的系统服务函数为0x0000)

使用windbg来查看SystemCallStub函数地址从而调用内核函数

Poc实现代码:

  #include <Windows.h>
  #include <stdio.h>
    __declspec(naked) void NtSetUserImeInfoEx(PVOID imeinfoex)
    {
      __asm {
          mov eax, 0x1226   //将NtUserSetImeInfoEx函数的服务号传入eax中
          mov edx, 0x7ffe0300  // 将SystemCallStub函数地址传入edx中
          call dword ptr[edx]  //调用SystemCallStub函数
          ret 0x04
      }
    }
    int main()
    {
      HWINSTA hSta = CreateWindowStationW(0, 0, READ_CONTROL, 0);  //使用CreateWindowStation函数创建一个窗口站
      SetProcessWindowStation(hSta);          
      char ime[0x800];
      NtSetUserImeInfoEx((PVOID)&amp;ime);        //调用NtUserSetImeInfoEx函数触发漏洞,致使系统BSOD
      return 0;
    }

编译运行,成功触发漏洞,致使系统BSOD

漏洞利用

分配零页内存

NTSYSAPI NTSTATUS NTAPI ZwAllocateVirtualMemory (
IN HANDLE ProcessHandle,
IN OUT PVOID BaseAddress,
IN ULONG ZeroBits,
IN OUT PULONG RegionSize,
IN ULONG AllocationType,
IN ULONG Protect
);

分析:将参数BaseAdress设置为0时,并不能在零页内存中分配空间,而是让系统寻找第一个未使用的内存块来分配使用。在AllocateType参数中有一个分配类型是MEM_TOP_DOWN,该类型表示内存分配从上向下分配内存。我们可以将参数BaseAddress指定为一个低地址同时指定分配内存的大小参数RegionSize的值大于这个地址值,如参数BaseAddress为1,参数RegionSize为8192,这样也就能成功分配,地址范围就是 0xFFFFE001(-8191)到 1把0地址包含在内了,此时再去尝试向 NULL指针执行的地址写数据,程序就不会异常了。在32位 Windows系统中,可用的虚拟地址空间共计为 2^32 字节(4 GB)。通常低地址的2GB用于用户空间,高地址的2GB 用于系统内核空间,通过这种方式我们发现在0地址分配内存的同时,也会在高地址(内核空间)分配内存。

分配零页内存,创建并设置窗口站

构造能够获取SYSTEM进程令牌的shellcode

每个进程都在内核中都会有且仅有一个EPROCESS结构,其中EPROCESS结构中的Token字段记录着这个进程的Token结构的地址,进程的很多与安全相关的信息是记录在这个TOKEN结构中的,所以如果我们想获得SYSTEM权限,就需要将拥有SYSTEM权限进程的Token字段的值找到,并赋值给我们创建的程序进程中EPROCESS的Token字段。

第一步,找到拥有SYSTEM权限的进程的EPROCESS结构地址

在Ring0中,fs寄存器指向一个叫KPCR的数据结构,该结构体中偏移量为0x120的地方是一个类型为_KPRCB的成员PrcbData

结构体_KPRCB中偏移量为0x004的地方存放着指向当前线程的_KTHREAD

通过查看_KTHREAD结构体和EPROCESS组成,我们知道_KTHREAD.ApcState.Process指向的就是当前进程的EPROCESS,所以我们获取当前进程EPROCESS的汇编代码可以写成

mov edx, 0x124;
mov eax, fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread
mov edx, 0x50;
mov eax, [eax + edx];// Get nt!_KTHREAD.ApcState.Process
mov ecx, eax;// Copy current _EPROCESS structure

基于以上,我们已经明白如何获得自身进程的EPROCESS结构了,进一步需要做的是获得System进程的EPROCESS~

查看EPROCESS的ActiveProcessLinks成员,它是一个_LIST_ENTRY结构,在windows系统中,每创建一个进程系统内核就会为其创建一个EPROCESS,然后使EPROCESS.ActiveProcessLinks.Flink=上一个创建的进程的EPROCESS.ActiveProcessLinks.Flink的地址,而上一个创建进程的EPROCESS.ActiveProcessLinks.Blink=新创建进程的EPROCESS.ActiveProcessLinks.Flink的地址,构成了一个双向链表。所以找到一个进程就可以通过Flink和Blink遍历全部进程EPROCESS了,由于System进程是最先创建的进程之一,因此它必然在当前进程(我们编写的这个程序进程)之前,我们可以循环访问Flink,判断其PID是否为4(EPROCESS的UniqueProcessId成员指向其所属进程的PID)来判断其是否为SYSTEM进程

第二步,将SYSTEM进程的Token字段赋值给当前进程

查找获取HalDispatchTable表地址

分析:在NtQueryIntervalProfile中调用KeQueryIntervalProfile函数

分析:从图中可以看出KeQueryIntervalProfile函数调用一个在HalDispatchTable+0x4处的指针,我们可以覆盖该指针使其指向shellcode,那么当调用NtQueryIntervalProfile时shellcode也就间接的可以在内核层0运行

需要用到的是HalDispatchTable+0x4地址,那么也就是需要找到HalDispatchTable的地址即可,我们可利用另一个未文档化的函数——NtQuerySystemInformation,此函数可帮助用户进程查询内核以获取有关OS和硬件状态的信息,这个函数没有导入库,我们需要使用GetModuleHandle和GetProcAddress在‘ntdll.dll‘的内存范围内动态加载函数。

分析:

利用Bitmap任意内存读写

1. 首先创建两个Bitmap对象:gManger和个Worker;

创建一个Bitmap对象时,一个结构被附加到了进程PEB的GdiSharedHandleTable成员中, GdiSharedHandleTable是一个GDICELL结构体数组的指针 ,GDICELL结构的pKernelAddress成员指向BASEOBJECT(sizeof=0x10
)结构,BASEOBJECT结构后面的紧跟着SURFOBJ结构, SURFOBJ结构中偏移量为0x20处即为pvScan0字段

我们可以用以下方式找到Bitmap对象的内核地址

addr = PEB.GdiSharedHandleTable + (handle &0xffff) *sizeof(GDICELL) ;

通过如下代码获得gManger.pvScan0和gWork.pvScan0的地址

2. 利用CVE-2018-8120的任意内存写入漏洞,将gManger对象的pvScan0值修改成gWorker对象的地址;

基本前文的漏洞分析,我们知道SetImeInfoEx函数中若想执行qmemcpy,需跳过如下所示的while循环

 while ( pkl->hkl != imeInfoEx->hkl )
    {
      pkl = pkl->pklNext;
      if ( pkl == winSta->spklList )
        return 0;
    }

因此需要设置pkl->hkl = imeInfoEx->hkl,就是在零页地址位置伪造了一个和 tagIMEINFOEX 结构体 spklList 成员类型一样的 tagKL 结构体,然后把它的 hkl 字段设置为 wpv 的地址,之后再把 wpv 的地址放在 NtUserSetImeInfoEx 函数的参数 ime_info_ex 的第一个成员里面;指定pkl->piiex等于gManger.pvScan0的地址,也就是指定qmemcpy目的地址,这样执行qmemcpy之后,就可以把gWorker.pvScan0的值赋给gManger.pvScan0

注意:qmemcpy拷贝了0x15c个字节,势必会影响gManger.pvScan0之后的内存,后面调用Gdi32的 GetBitmapBits/SetBitmapBits 这两个函数就会不成功,因为这两个函数操作pvScan0的方式和SURFOBJ结构的 lDelta、iBitmapFormat、iType、fjBitmap 还有SURFACE结构的flags字段相关的,为了避免这个问题,我们需要在构造的ime_info_ex中填上一些数值进行修复

3. gManger对象调用SetBitmapBits函数将gWorker对象的pvScan0的值覆盖成HalDisptchTable+4的地址(HalDisptchTable表中对应偏移处存放着hal!HaliQuerySystemInformation() 函数指针);

4. gWorker调用GetBitmapBits函数获取HalDispatchTable+4所指内存的值,也就是hal!HaliQuerySystemInformation() 函数指针,存储起来;

5. gWork对象调用SetBitmapBits函数将HalDispatchTable+4处的函数指针覆盖成shellcode函数指针;

6. 在用户进程中调用系统API函数NtQuerySystemInformation,进而调用HalDisptchTable表中的hal!HaliQuerySystemInformation() 函数指针,也就是执行shellcode;

7. gWorker调用SetBitmapBits函数将HalDisptchTable+4的地址处的hal!HaliQuerySystemInformation() 函数指针还原,保证下面的运行不出错;

Exp利用漏洞

打开cmd,进入Exp-CVE-2018-8120.exe所在的目录并执行,引号内为想要执行的命令

参考资料

标签:函数,Windows,漏洞,地址,内核,进程,8120,EPROCESS,2018
来源: https://www.cnblogs.com/fyss/p/11071780.html