文章本该以倒叙的方式,从漏洞函数到调用漏洞的函数,类似于栈回溯的方式开始编写。
但是为了方便大家理解,我准备以exp的执行流程为主线开始讲解
调试过程中用到的exp地址
https://github.com/unamer/CVE-2019-1458
漏洞存在于函数为xxxPaintSwitchWindow
其交叉引用如下
xxxWrapSwitchWndProc->xxxSwitchWndProc->xxxPaintSwitchWindow
要想调用xxxWrapSwitchWndProc函数,我们需要调用NtUserMessageCall
该函数原型如下
NtUserMessageCall(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam, ULONG_PTR ResultInfo, DWORD dwType)
下面我们从NtUserMessageCall开始分析,以及怎么经过层层bypass最终调用漏洞函数
可以看到NtUserMessageCall内部会调用gapfnMessageCall表
通过控制其参数NtUserMessageCall将会调用NtUserfnINLPCREATESTRUCT,该函数内部会通过gpsi,调用gapfnMessageCall表从而调用触发漏洞的函数xxxWrapSwitchWndProc
我们可以看到该表的调用跟dwType有关系
因此可以通过控制传入dwType的值为0xE0,调用xxxWrapSwitchWndProc函数(公式:(gpsi+ 8 * ((dwType + 6) & 0x1F)+ 16))
xxxSwitchWndProc函数分析
红色部分是第一次判断,我们新创建的窗口期,fnid为0,可以顺利进入判断内部
黄色部分是第二次判断,在这里前面已经不符合要求,因此我们要进行绕过,绕过的方法为 不满足 cbwndExtra+0x128 < [gpsi+0x154]
蓝色部分是第三次判断,在这里我们可以直接将消息设置为WM_CREATE即可绕过
我们要对NtUserMessageCall进行两次调用,第一次调用目的修改wnd.fnid为2a0(过掉红色和黄色部分,蓝色部分)
黑色部分是我们最终想要得到的结果,令wnd->fnid为 0x2a0,这样我们才能走到下面的switch语句
第二次调用是目的是为了调用xxxPaintSwitchWindow函数,我们只需要让msg为0x14 或者0x3a
在对应的 Window Messages中,可以查询到
#define WM_ERASEBKGND 0x0014
xxxPaintSwitchWindow函数分析
如下图,要进行第一次bypass
我们可以通过创建一个特殊的窗口,则可以让(gpsi+0x154) = 0x130,只要事先把窗口的cbwndExtra=0x8即可bypass
printf("[*] Creating switch window #32771, this has a result of setting (gpsi+0x154) = 0x130\n"); HWND switchWnd = CreateWindowEx(0, (LPCWSTR)0x8003, L"", 0, 0, 0, 0, 0, NULL, NULL, hself, NULL);
走到这里,离胜利已经非常近了。我们可以看到,wndExtra的值是可以通过
SetWindowLongPtr设置的,这样我们便可以实现一个任意内存破坏的漏洞了
第一次调用NtUserMessageCall
第一次判断
[rcx+0x42]为fnid ,判断其是否为0x2a0
第二次判断 rcx为[gpsi+0x154],rax为wnd->cbwndExtra + 0x128
成功将wnd.fnid赋值为0x2a0
通过SetWindowLongPtr更改wndExtra值
红色框会设置前后,蓝色框为要设置的值
第二次调用NtUserMessageCall,在过掉几层判断之后
可以看到,实现了任意内存的破坏,成功让tagcls.cbClsExtra变成了一个比较大的数
此时我们已经通过xxxSetClassLongPtr函数进行越界写
要写的地址为pclsBase[1].pclsNext,等价于 pclsBase+sizeof_tagCLS==tagCLS+sizeof_tagCLS
该漏洞的利用方式为更改tagWND的StrName指针,搭配InternalGetWindowText 和 NtUserDefSetText即可实现任意读写,实现token的替换
因此在此处控制offset为
offset=tagWND - tagCLS - sizeof_tagCLS + off_tagWND_strName
最后的要写的地址
dwDestAddr=tagCLS+sizeof_tagCLS+offset
dwDestAddr=tagCLS+sizeof_tagCLS+tagWND - tagCLS - sizeof_tagCLS + off_tagWND_strName
dwDestAddr=tagWND + off_tagWND_strName
通过InternalGetWindowText读取
通过NtUserDefSetText实现写
exp中有封装好的函数,我们只需要根据不同的系统,构造不同的offset即可
有了任意读写,替换token已经很简单了,不再阐述
值得注意的是我们在破坏内存的时候不仅仅破坏了pcls的cbClsExtra的字段,因此要对被破坏的字段进行修复
总体来讲,该漏洞的利用还是比较简单的
exp中并没有针对2016 r2的提权。我们下面对2016 R2进行分析
左面为2016 右面 win7 的xxxSwitchWndProc 函数对比,可以看到除了偏移不一样,都可以让Fnid设置为2a0
左面为2016 右面 win7 的xxxPaintSwitchWindow函数对比
可以看到2016的会获取tagwnd 0x168偏移的位置,这里其实就是wndExtra。理论上来讲也是存在漏洞的
但是2016和win7的xxxSetWindowLongPtr函数是有区别的
2016对tagWnd的某个标志位进行了检测
只有当tagWnd的偏移0x2a的bit位为1时,才会走win7所走的流程,才会使得 tagwnd的0x168大小偏移处写上我们的数据
动态验证,将0x2a的bit位,置1 ,可以看到返回值会是tagWnd的内核地址
2016的tagWND微软已经不公开了,但是前边跟win7是一样的,可以参考如下
那么现在的关键问题就来了
是否可以在tagwnd.fnid位为0的情况下,又使bDialogWindow位为1呢?