Windows kernel local privilege escalation --Win32 assembly null pointer vulnerability (CVE-2018-8120)

Vulnerability Overview

In May 2018, Microsoft officially announced and fix vulnerabilities four win32k mention the right kernel, which CVE-2018-8120 kernel privilege escalation vulnerability is present in the core components of a win32k null pointer dereference vulnerability through a null pointer reference to read and write arbitrary kernel, and further to execute arbitrary code, the right to achieve the purpose mentioned core.

Vulnerabilities principle

Trigger the vulnerability is a pointer member of a domain spklList window station tagWINDOWSTATON object points may be empty address, if at the same time associated with the window station of the current process, extended information then call the system service functions NtUserSetImeInfoEx set input method, would be an indirect call SetImeInfoEx function access spklList pointer in the user process address space of zero page memory.

If the current process is not a zero-page memory map (in fact zero page will not be normal memory mapped), function SetImeInfoEx access operation will lead to missing page exception cause the system BSOD; Similarly, if the current process is a zero-page memory data mapped to advance our carefully constructed, it is possible malicious use, resulting in arbitrary code execution vulnerability.

Vulnerability reproduction

Local kernel debugging windbg

Description : Windbg debugging of Microsoft Corporation in the collection free debugger GUI debugger that supports debugging Source and Assembly two modes. Windbg can not debug the application, you can also Kernel Debug.

This tool allows us to local kernel debugging windows system, however, can not use local debugging kernel mode execution command, command breakpoints and stack trace commands Command

1, using an Administrator, open cmd, execution bcdedit /debug on, debug mode is turned on

2, with administrator privileges to open windbg (must be the administrator rights, or does not work), then selectFile->Kernel Debugging->Local->确定

3. After the above basic settings can be related to local kernel debugging

View SSDT tables and table SSDTShadow

In the windows operating system, system service (kernel function) is divided into two types: one is a common system services, implemented in the kernel file; the other is the graphical user interface display and related service system, implemented in win32k. sys file.

All system services during system operation are stored in the memory area of the system, the system uses two systems and services address table KiServiceTable Win32pServiceTable management of these system services, set both the System Service Descriptor Table (SDT) address table management system services, which Both systems service description table ServiceDescriptorTable(SSDT)andServiceDescriptorTableShadow(SSDTShadow)

The former contains only KiServiceTable table, which contains KiServiceTable and Win32pServiceTable two tables, but you can call SDDT is accessed directly, SSDTShadow can not directly access the call.

SDT structure object is as follows:

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

View the System Service Descriptor Table by local kernel debugging windbg actual structure of distribution:

Analysis: shown in the figures and tables are SDDT SSDTShadow table structure, two rows of each table represent the address table system service table and the data-related information KiServiceTable Win32pServiceTable table. Because of the above table that the SSDT is not included Win32pServiceTable table, the first table in the second data is empty.

Conjunction with the above structure can be seen, KiServiceTable addresses are 0x83cbfd9ccontained 0x191 service systems; Win32pServiceTable addresses are 0x92696000contained 0x339 service systems.

View store specific content service system address table:

Analysis: As can be seen all four bytes of the function pointer address table stored in the service system, and pointers to the rear of the system is the corresponding service functions

See station configuration-information window

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

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

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

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

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

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

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

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

  • 选择File-->loadfile-->pdbfile,然后点击弹出窗口的OK选项
  • 在函数框中使用Ctrl+F查找SetImeInfoEx函数,并使用F5反编译出函数的伪代码

分析:从上面的伪代码中可以看出,函数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

漏洞利用

  • 原理:内核提权的常见方法是将当前进程的EPROCESS对象指针成员域Token替换为系统进程的Token指针,这相关的shellcode并不难写。但是所有进程的EPROCESS结构体都处于内核空间中,我们能控制的用户进程属于Ring3,并不能达到运行shellcode的要求,因此难点是需要使用Ring0权限去执行这段shellcode修改内核内存地址,这也就是我们利用CVE-2018-8120这个漏洞的原因。

分配零页内存

  • X86的Windows系统中,进程地址空间中从0x000000000x0000FFFF的闭区间被称为空指针赋值分区,也就是我们上面说的零页内存,正常情况下未被映射,强行对其访问则会出现漏洞Poc的情况,系统BOSD。
  • 为了函数SetImeInfoEx能够顺利向下执行,我们需要提前映射零页内存,这里我们利用ZwAllocateVirtualMemory函数对其进行映射,ZwAllocateVirtualMemory函数作用是在指定进程的虚拟空间中申请一块内存,该块内存默认以64kb大小对齐。以下是ZwAllocateVirtualMemory函数的函数原型:
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表地址

  • 我们需要shellcode有ring0的权限去执行,可以修改一个具有ring0权限的函数指针为shellcode指针即可实现ring0权限执行shellcode。
  • 内核函数选择hal!HaliQuerySystemInformation函数,因为有一个调用它的函数(NtQueryIntervalProfile函数)是一个未文档化的函数,也就是一个不常用的函数这样我们覆盖它的函数指针后对于整个程序执行造成的影响会小一些,相对来说安全些。而且NtQueryIntervalProfile函数是在ntdll.dll中导出的未公开的系统调用,可以直接在Ring3调用

分析:在NtQueryIntervalProfile中调用KeQueryIntervalProfile函数

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

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

分析:

  • NT内核文件的名字会因为单处理器和多处理器以及不同位数的操作系统版本以及是否支持PAE(Physical Address Extension)而不同,所以需要编程获取。

  • HalDispatchTable在内核中真正的地址需要使用加载模块的基地址+HalDispatchTable在该模块中的偏移来获取的。我们通过NtQuerySystemInformation获取了nt模块的基址kernelimageBase,通过计算用户空间中HalDispatchTable的地址-用户空间中nt模块的地址可以获得偏移。

利用Bitmap任意内存读写

  • 这是一种编写Exp对任意内存进行读写的方法技巧,越来越多地被应用于Exp的编写。简单的来说,这种技巧就是利用系统函数GetBitmapBitsSetBitmapBits可以对Bitmap内核对象中的pvScan0字段指向的内存地址进行读写操作,这样就可以通过pvScan0字段实现对任意内存的读写操作。

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所在的目录并执行,引号内为想要执行的命令

参考资料

Guess you like

Origin www.cnblogs.com/fyss/p/11071780.html