一.前言
1.漏洞描述
由于win32kfull中的NtUserSetWindowFNID在对窗口对象的fnid进行设置的时候,没有判断该窗口是否已经释放,这样就可以对一个已经释放的窗口进行fnid的设置。而在xxxSBTrackInit和xxxFreeWindow中都存在用户层的回调,通过对函数的劫持可以在回调中释放掉xxxSBTrackInit函数中使用的tagSBTRACK结构体,这样当xxxSBTrackInit释放该结构体的时候就会因为双重释放导致BSOD的产生。通过xxxSBTrackInit函数释放结构体之前会对结构体中的部分成员进行解引用的操作,在相应的内存地址中放置PALETTE的cEntries成员的地址来利用解引用扩大该值,实现越界地址写入相邻PALETTE的pFirstColor来实现任意地址读写,最终实现提权。
2.实验环境
操作系统:Win10 x64 1709 专业版
编译器:Visual Studio 2017
调试器:IDA Pro, WinDbg
二.漏洞分析
1.关键结构体
由于在Win10系统中,很多符号并没有导出,所以一些结构体成员只能通过分析得到。对于窗口对象tagWND,和本次漏洞有关的成员定义如下:
3: kd> dt tagWND
+0x000 head : _THRDESKHEAD
+0x052 fnid : Uint2B
+0x0A8 pcls : Ptr64 tagCLS
+0x180 cbwndExtra : Uint8B
3: kd> dt _THRDESKHEAD
+0x000 h : Ptr64 Void
+0x008 cLockObj : Uint4B
+0x010 pti : Ptr64 tagTHREADINFO
+0x018 rpdesk : Ptr64 tagDESKTOP
+0x020 pSelf : Ptr64 UChar
tagWND偏移0x52的fnid表明了窗口的状态,当值包含0x8000的时候,表示窗口被释放:
#define FNID_DELETED_BIT 0x00008000
tagWND偏移0x10的pti指向tagTHREADINFO结构体,改结构体保存了线程信息,定义如下:
3: kd> dt tagTHREADINFO
+0x198 pq : Ptr64 tagQ
+0x2B0 pSBTrack : Ptr64 tagSBTRACK
tagTHREADINFO偏移0x198的pq指向tagQ结构体,结构体定义如下:
1: kd> dt tagQ
+0x068 spwndCapture : Ptr64 tagWND
tagTHREADINFO偏移0x2B0指向tagSBTRACK结构体,当鼠标在一个滚动条按下左键的时候,系统会通过该结构体用来标记鼠标的当前状态,结构体定义如下:
3: kd> dt tagSBTRACK -v
struct tagSBTRACK, 17 elements, 0x68 bytes
+0x000 fHitOld : Bitfield Pos 0, 1 Bit
+0x000 fTrackVert : Bitfield Pos 1, 1 Bit
+0x000 fCtlSB : Bitfield Pos 2, 1 Bit
+0x000 fTrackRecalc : Bitfield Pos 3, 1 Bit
+0x008 spwndTrack : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
+0x010 spwndSB : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
+0x018 spwndSBNotify : Ptr64 to struct tagWND, 170 elements, 0x128 bytes
+0x020 rcTrack : struct tagRECT, 4 elements, 0x10 bytes
+0x030 xxxpfnSB : Ptr64 to void
+0x038 cmdSB : Uint4B
+0x040 hTimerSB : Uint8B
+0x048 dpxThumb : Int4B
+0x04c pxOld : Int4B
+0x050 posOld : Int4B
+0x054 posNew : Int4B
+0x058 nBar : Int4B
+0x060 pSBCalc : Ptr64 to struct tagSBCALC, 16 elements, 0x40
2.xxxFreeWindow函数分析
内核通过xxxFreeWindow来释放窗口,函数会将要释放的窗口对象的fnid与0x8000进行或运算,表示窗口被释放。接着判断窗口对象是否有扩展内存,即cbwndExtra是否为0,如果不为0,则执行xxxClientFreeWindowClassExtraBytes来释放扩展内存:
.text:00000001C0050A10 mov r8, [rdi+180h] ; r8 = tagWND->cbwndExtra
.text:00000001C0050A17 mov eax, 8000h
.text:00000001C0050A1C or [rdi+52h], ax ; tagWND->fnid |= 0x8000
.text:00000001C0050A20 lea rax, [r8-1]
.text:00000001C0050A24 cmp rax, 0FFFFFFFFFFFFFFFDh
.text:00000001C0050A28 ja short loc_1C0050A6D ; 判断r8是否为0
.text:00000001C0050A2A test dword ptr [rdi+130h], 800h
.text:00000001C0050A34 jnz loc_1C00513AD
.text:00000001C0050A3A call cs:__imp_PsGetCurrentProcess
.text:00000001C0050A40 mov ecx, [rax+304h]
.text:00000001C0050A46 test ecx, 40000008h
.text:00000001C0050A4C jnz short loc_1C0050A66
.text:00000001C0050A4E mov eax, [r15+1D0h]
.text:00000001C0050A55 test r14b, al
.text:00000001C0050A58 jnz short loc_1C0050A66
.text:00000001C0050A5A mov rcx, [rdi+180h] ; rcx = tagWND->cbwndExtra
.text:00000001C0050A61 call xxxClientFreeWindowClassExtraBytes
xxxClientFreeWindowClassExtraBytes函数会执行KeUserModeCallback来返回用户层:
.text:00000001C00B6D25 mov r8d, 8
.text:00000001C00B6D2B lea rax, [rsp+48h+arg_10]
.text:00000001C00B6D30 lea r9, [rsp+48h+var_18]
.text:00000001C00B6D35 mov [rsp+48h+var_28], rax
.text:00000001C00B6D3A lea rdx, [rsp+48h+arg_18]
.text:00000001C00B6D3F lea ecx, [r8+76h] ; ecx = 0x76 + 0x8 = 0x7E
.text:00000001C00B6D43 call cs:__imp_KeUserModeCallback
xxxClientFreeWindowClassExtraBytes函数执行完成之后,函数会判断窗口对象的fnid值的低12位是否在0x2A0到0x2AA之间:
.text:00000001C00509EF movzx eax, word ptr [rdi+52h] ; eax = tagWND->fnid
.text:00000001C00509F3 mov edx, 3FFFh
.text:00000001C00509F8 movzx ecx, ax
.text:00000001C00509FB and cx, dx
.text:00000001C00509FE mov edx, 29Ah
.text:00000001C0050A03 lea r8d, [rdx+6] ; r8d = 0x29A + 0x6 = 0x2A0
.text:00000001C0050A07 cmp cx, dx
.text:00000001C0050A0A jnb loc_1C005117A ; 此处会跳转
; 省略部分代码
.text:00000001C005117A loc_1C005117A:
.text:00000001C005117A mov ebx, 4000h
.text:00000001C005117F test bx, ax
.text:00000001C0051182 jnz loc_1C0050A10 ; r8 = tagWND->cbwndExtra
.text:00000001C0051188 cmp cx, r8w ; r8w = 0x2A0
.text:00000001C005118C jbe loc_1C00514B7 ; fnid <= 0x2A0跳转
.text:00000001C0051192 mov eax, 2AAh
.text:00000001C0051197 cmp cx, ax
.text:00000001C005119A ja short loc_1C00511AC ; fnid >= 0x2AA则跳转
.text:00000001C005119C mov eax, [r15+1D0h]
.text:00000001C00511A3 test r14b, al
.text:00000001C00511A6 jz loc_1C00515D8 ; 这里需要跳转
如果fnid在0x2A0到0x2AA之间,则会调用SfnDWORD函数:
.text:00000001C00515D8 loc_1C00515D8:
.text:00000001C00515D8 mov rax, cs:__imp_gpsi
.text:00000001C00515DF xor r9d, r9d ; r9d = 0
.text:00000001C00515E2 movzx ecx, cx
.text:00000001C00515E5 xor r8d, r8d
.text:00000001C00515E8 mov [rsp+100h+var_C8], r13
.text:00000001C00515ED mov dword ptr [rsp+100h+var_D0], r14d
.text:00000001C00515F2 mov rax, [rax]
.text:00000001C00515F5 lea edx, [r9+70h] ; edx = 0 + 0x70 = 0x70
.text:00000001C00515F9 mov rax, [rax+rcx*8-1210h]
.text:00000001C0051601 mov rcx, rdi
.text:00000001C0051604 mov qword ptr [rsp+100h+var_D8], rax
.text:00000001C0051609 mov qword ptr [rsp+100h+var_E0], r13
.text:00000001C005160E call SfnDWORD
.text:00000001C0051613 jmp loc_1C00511AC
SfnDWORD函数也会调用KeUserModeCallback函数返回用户层:
.text:00000001C006F85A lea r9, [rsp+108h+arg_18] ;
.text:00000001C006F862 mov r8d, 30h ; '0' ;
.text:00000001C006F868 lea rdx, [rsp+108h+var_C8]
.text:00000001C006F86D lea ecx, [r8-2Eh] ; ecx = 0x30 - 0x2E = 0x2
.text:00000001C006F871 call cs:__imp_KeUserModeCallback
3.xxxSBTrackInit函数分析
xxxSBTrackInit是用来执行鼠标左键按下滚动条进行拖动的函数,该函数的部分代码如下,函数首先申请一块内存用来保存pSBTrack结构体,对这块内存进行初始化,并对部分成员进行引用;接着函数调用xxxSBTrackLoop用来处理拖动滚动条要处理的消息;最后函数对相关成员进行解引用后,释放掉pSBTrack结构体:
__int64 __fastcall xxxSBTrackInit(struct tagWND *tagWND, __int64 a2, int a3, int a4)
{
// 申请内存并进行初始化
pSBTrack = Win32AllocPoolWithQuota(0x68, 'tssU');
pSBTrack_1 = pSBTrack;
if ( !pSBTrack )
return pSBTrack;
// 对成员进行初始化
*(_DWORD *)pSBTrack &= 0xFFFFFFFE;
*(_QWORD *)(pSBTrack + 0x40) = 0i64;
*(_QWORD *)(pSBTrack + 8) = 0i64;
*(_QWORD *)(pSBTrack + 0x10) = 0i64;
*(_QWORD *)(pSBTrack + 0x18) = 0i64;
*(_QWORD *)(pSBTrack + 0x30) = xxxTrackBox;
// 将pSBTrack存储与tagTHRAEDINFO->pSBTrack中
*(_QWORD *)(*((_QWORD *)tagWND + 2) + 0x2B0i64) = pSBTrack;
// 对spwndTrack,spwndSB,spwndSBNotify进行引用
arr[0] = pSBTrack_1 + 8;
arr[1] = tagWND;
HMAssignmentLock(arr);
arr[0] = pSBTrack_1 + 0x10;
arr[1] = tagWND;
HMAssignmentLock(arr);
arr[0] = pSBTrack_1 + 0x18;
arr[1] = *((_QWORD *)tagWND + 0xD);
HMAssignmentLock(arr);
xxxCapture(*(_QWORD *)gptiCurrent, tagWND, 3i64);
pti = *((_QWORD *)tagWND + 2);
if ( pSBTrack == *(_QWORD *)(pti + 0x2B0) )
{
// 消息分发
xxxSBTrackLoop(tagWND, a2, (struct tagSBCALC *)v17);
// 释放pSBTrack对象
pti = *((_QWORD *)tagWND + 2);
pSBTrack = *(_QWORD *)(pti + 0x2B0);
if ( pSBTrack )
{
// 解引用
HMAssignmentUnlock(pSBTrack + 0x18);
HMAssignmentUnlock(pSBTrack + 0x10);
HMAssignmentUnlock(pSBTrack + 8);
// 释放pSBTrack
Win32FreePool(pSBTrack);
pti = *((_QWORD *)tagWND + 2);
*(_QWORD *)(pti + 0x2B0) = 0i64;
return pti;
}
}
}
xxxSBTrackLoop会调用xxxTranslateMessage和xxxDispatchMessage来分发处理消息,xxxDispatchMessage函数会调用上面说的SfnWORD函数来返回用户层:

4.NtUserSetWindowFNID函数分析
该函数用来设置窗口对象的fnid增加指定的值,但是,这里增加的时候,函数没有判断窗口是否已经被释放,即是否具备0x8000。这就会导致,进行设置的时候很有可能会对一个已经释放的窗口的fnid值进行设置:
__int64 __fastcall NtUserSetWindowFNID(__int64 a1, __int16 fnid)
{
hwnd = ValidateHwnd(a1);
if ( hwnd )
{
if ( *(_QWORD *)(*(_QWORD *)(hwnd + 0x10) + 400i64) == PsGetCurrentProcessWin32Process(v5) )
{
// 判断要设置的fnid是否满足要求
if ( fnid == 0x4000 || fnid - 0x2A1 <= 9 && (*(_WORD *)(hwnd + 0x52) & 0x3FFF) == 0 )
{
// 设置tagWND->fnid
*(_WORD *)(hwnd + 0x52) |= fnid;
}
}
}
}
5.漏洞成因
这个漏洞的成因比较复杂,要将上面的几个函数都联系起来看,成因如下:
当向滚动条控件发送WM_LBUTTONDOWN(左键按下)的消息时候,xxxSBTrackInit函数就会被调用,xxxSBTrackInit函数会调用xxxDispatchMessage,该函数又会调用SfdDWORD来返回用户层
如果用户HOOK了用户层对应的处理函数,就可以在该函数中调用DestroyWindow来释放拥有该滚动条控件的窗口。这样就会执行xxxFreeWindow,该函数会首先将窗口的fnid标记为删除的窗口,接着在该窗口存在扩展对象的时候,调用xxxClientFreeWindowClassExtraBytes函数返回用户层
如果用户HOOK了用户层对应的处理函数,就可以在处理函数中调用NtUserSetWindowFNID,将窗口的fnid加入0x2A1的标记。这样xxxClientFreeWindowClassExtraBytes函数返回以后,会因为被修改的窗口的fnid值的低12位为0x2A1导致再次调用SfdDWORD返回用户层,此时在对应的处理函数中释放掉xxxSBTrackInit函数申请的pSBTrack结构体,这块内存就会处于释放状态
当xxxFreeWindow函数返回后,就会返回到xxxSBTrackInit继续执行,而xxxSBTrackInit会在最后释放pSBTrack结构体,而这个结构体已经被释放,此时如果在释放就会产生BSOD错误
三.漏洞触发
要成功触发这个漏洞,就需要在SfdDWORD在用户层的处理函数中释放pSBTrack结构体,此时只需要通过向滚动条发送WM_CANCELMODE消息,该函数会导致xxxEndScroll函数来释放内存,该函数的主要代码如下:
__int64 __fastcall xxxEndScroll(struct tagWND *pwnd, int a2)
{
// 要释放pSBTrack结构体的三个条件
pti = *((_QWORD *)pwnd + 2);
pSBTrack = *(_QWORD *)(pti + 0x2B0);
if ( !pSBTrack ) // pSBTrack != NULL
return pti;
pq = *(_QWORD *)(*(_QWORD *)gptiCurrent + 0x198i64);
if ( *(struct tagWND **)(pq + 0x68) != pwnd ) // pq->spwndCapture == pwnd
return pti;
if ( !*(_QWORD *)(pSBTrack + 0x30) ) // pSBTrack->xxxpfnSB != NULL
return pti;
// 释放掉pSBTrack结构体
pti = *((_QWORD *)pwnd + 2);
if ( pSBTrack == *(_QWORD *)(pti + 0x2B0) )
{
spwndSB = *(struct tagWND **)(pSBTrack + 0x10);
if ( !spwndSB || (zzzShowCaret(spwndSB), pti = *((_QWORD *)pwnd + 2), pSBTrack == *(_QWORD *)(pti + 0x2B0)) )
{
*(_QWORD *)(pSBTrack + 0x30) = 0i64;
HMAssignmentUnlock(pSBTrack + 0x10);
HMAssignmentUnlock(pSBTrack + 0x18);
HMAssignmentUnlock(pSBTrack + 8);
Win32FreePool(pSBTrack);
pti = *((_QWORD *)pwnd + 2);
*(_QWORD *)(pti + 0x2B0) = 0i64;
}
}
return pti;
}
其中第二处的限制需要窗口一个新得滚动条对象,并对其调用SetCapture。整个触发漏洞的流程如下:
创建一个带有八字节额外内存的窗口对象,并将该对象的句柄赋值到额外内存中供之后使用。同时,在该窗口中在创建一个滚动条对象用来触发漏洞
HOOK SfdDWORD和xxxCreateFreeWindowClassExtraBytes返回到用户层会执行的函数
向创建的窗口发送WM_LBUTTIONDWORD函数来调用xxxSBTrackInit函数
在用户层定义的xxxClientFreeWindowClassExtraBytes会根据要释放的内存中保存的是否是第一步中窗口的窗口句柄,来判断是否要修改fnid和调用SetCapture
在用户层定义的SfdDWORD的处理函数中,会判断如果是第一次调用就会通过DestroyWindow来调用xxxFreeWindow。如果是第二处调用,则发送WM_CANCELMODE来是否pSBTrackInit函数
当xxxSBTrackInit函数最后释放pSBTrack结构体的时候就会因为双重释放导致BSOD
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
最后于 2022-8-11 20:53
被1900编辑
,原因: