实验环境
双虚拟机调试
这里我把漏洞机和调试机都放在了虚拟机里,采用虚拟机调试虚拟机的好处就是可以实时保存快照,有空的时候在接着工作(其实是为了更好的加班,开个玩笑~~
1.漏洞机的设置
在虚拟机里安装后系统后,必须进行断网设置 ,因为我在测试调试的时候,运行poc几次后都是成功的,而后就不行了,排除了原因很久,IDA发现,它自动打了补丁(以前内核调试的时候,都没有遇到这种问题)。断网设置如下:
右键漏洞机-->虚拟机设置,这里首先把“打印机 ”给删除了(调试机也一样), 然后点击添加--> "串行端口",并根据截图填写下列信息就好:
进入漏洞机,以管理员打开cmd,分别输入下列命令:
这里写了个自动的WKD.bat脚本,管理员运行它即可:
2. 调试机的设置
调试机的虚拟机设置
在调试机里安装后windbg,设置调试符号,计算机--> 属性 --> 高级系统设置 --> 环境变量
管理员打开windbg,并进行下列nel ebug --> COM
注意:
1. win7的com端口是默认安装的,在系统安装到虚拟机的时候,自己可以查看设备管理器验证一下,都是COM1的。
2. 首先必须删除掉打印机,再进行串口的添加,这样才能保证漏洞机和调试机两边串口名的一致性,两边才能正常通信,不然windbg是附加不上的。比如在虚拟机设置--硬件--设备栏里,漏洞机的串口名为"串行端口2", 而调试机的串口名为"串行端口1",两者就不能通信。在都删除了打印机的情况下,漏洞机和调试机的串口名应该都为"串行端口",这样才能通信。
现在漏洞机和调试机都设置好了,管理员打开windbg,然后在重新打开漏洞机,选择MyDebug; 等待一段时间,windbg出现如图所示,即表示双机调试环境搭建成功。
漏洞原因
原因是在win32k.sys组件中,SetImeInfoEx函数没有对指针进行安全验证; 当指针指向零地址后,在接下的代码又对该区域进行访问,就会引发访问违规,触发蓝屏。所以该漏洞属于空指针引用漏洞。而 SetImelnfoEx 函数只有NtUserSetImeInfoEx调用它,我们就从 NtUserSetImeInfoEx函数开始分析起吧。
在win32k.sys中, NtUserSetImeInfoEx函数是用于将用户进程定义的输入法扩展信息对象设置到与当前进程关联的窗口站中。从上图可以看到NtUserSetImeInfoEx函数只有一个参数a1,而a1参数的类型其实是一个tagIMEINFOEX结构体,这个结构就是输入法的扩展信息结构。值得注意的是NtUserSetImeInfoEx函数里有调用了GetProcessWindowStation来获取当前进程所在窗口站的句柄。而与GetProcessWindowStation函数相对应的函数是在用户层使用CreateWindowStationW创建了一个窗口站,并通过SetProcessWindowStation将窗口站与当前进程相关联。而CreateWindowStationW返回的是一个tagWINDOWSTATION 句柄。
下面我们就先弄明白tagWINDOWSTATION是什么,现在根据github上的代码改写一下poc。
vs2013-->文件-->新建--项目-->Visual C++ -->Win32项目-->控制台应用程序-->空项目。新建好工程后,这里就要设置一下属性了。
编译运行后,得到crash.exe并把它赋值到漏洞机里,然后点击运行crash.exe; 调试机里windbg断下, 并切换到crash.exe进程。
可以看到没有执行CreateWindowStationW函数时,tagWINDOWSTATION->spklList时是有值的。
执行CreateWindowStationW函数后,tagWINDOWSTATION->spklList默认为空值了。
这个SetProcessWindowStation函数,如果大家有兴趣可以逆向一下,因为在我们的重点不是在这里。
那什么是tagWINDOWSTATION呢?窗口站是和当前进程和会话(session)相关联的一个内核对象,tagWINDOWSTATION 包含了剪贴板(clipboard)、原子表、一个或多个桌面(desktop)对象等。
而且这个 NtSetUserImeInfoEx 函数的参数 tagIMEINFOEX a1 是我们可以构造的,是可控的。到这里我们反汇编NtUserSetImeInfoEx函数,并在 94410030 地址处下个断点。
然后g一下,kb看下堆栈的情况,可以看到通过系统调用进入内核;
单步步过win32k!_GetProcessWindowStation;证明该函数返回的是窗口站句柄的内核地址。
到目前为止,我们知道了如下图所示,现在我们单步进入win32k!SetImeInfoEx漏洞函数分析一下:
在比较处发生异常访问。
其实SetImeInfoE就做下面这两个步骤而已,只不过在第一步骤就发生了零页内存访问异常了而已
由上可知,tagWINDOWSTATION窗口站对象的spklList成员,是关联的键盘布局tagKL(KeyBoad Latout)对象链表的头指针, SetImeInfoE函数会遍历spklList链表,直到节点对象pklNext成员回到头指针对象为止。其中判断每个被遍历的节点对象的hkl成员是否与piiex->hkl相等。如果相等会退出循环,否则继续循环遍历。接下来判断键盘布局对象的piiex成员是否为空,且成员变量的fLoadFlag值是否等于零,如果是, 则会把用户自定义的tagIMEINFOEX数据(可由我们自定义构造的)拷贝到键盘布局对象的piiex成员中。
总结—— 用户进程调用CreateWindowStation创建窗口站时, 窗口站对象splList成员会初始值为0,在调用系统服务 NtUserSetImeInfoEx 设置输入法扩展信息的时候,内核函数 SetImeInfoEx 将会访问splList指向位于用户进程地址空间中的零页内存;如果当前进程零页未被映射,函数的操作将引发缺页异常,导致系统 BSOD 的发生。到此漏洞原因分析完毕。
漏洞利用
首先,解决零页地址访问异常的问题,在未开启零页保护的系统下,我们可以通过 NtAllocateVirtualMemory申请0地址内存,来使得零页内存也是可访问的,并构造位于零页中的伪造tagKL键盘布局对象,使得tagKL->hkl也是可控的 (即在[0+14h]处存放我们构造的tagKL->hkl值)。
其次,tagIMEINFOEX结构体是我们可控的,因为NtUserSetImeInfoEx(tagIMEINFOEX a1)函数的a1参数也是我们可以构造的。
现在我们需要shellcode有ring0的权限去执行,那么我们可以修改一个具有ring0权限的函数指针为shellcode指针即可实现ring0权限执行shellcode。内核函数选择hal!HaliQuerySystemInformation函数,因为有一个调用它的ring3函数(NtQueryIntervalProfile函数)是一个未文档化的函数,也就是一个不常用的函数; 这样我们覆盖它的函数指针后对于整个程序执行造成的影响会小一些,相对来说安全些。而且NtQueryIntervalProfile函数是在ntdll.dll中导出的未公开的系统调用,可以直接在Ring3调用 。也就是Ring3调用NtQueryIntervalProfile,其内部会调用内核态nt!HalDispatchTable+0x4处的函数,而我们把nt!HalDispatchTable+0x4的地址,替换为我们的shellcode地址,就可以达到shellcode在内核态上正常执行并提取利用了。
Bitmap GDI 技术
那么我们如何把用户态shellcode的地址替换掉内核态nt!HalDispatchTable+0x4的地址呢?Bitmap GDI 技术可以实现用户态对内核态任意地址读/写(即通过CreateBitmap/GetBitmapBits/SetBitmaps这3个函数实现).
1.把gWorker.pvScan0的地址构造到tagIMEINFOEX结构体上,然后通过漏洞把gManger.pvScan0地址里存储的值替换为gWorker.pvScan0地址
2.gManger利用SetBitmapBits将gWorker.pScan0地址里存储的值设置为HalDisptchTable+4地址
3.gWorker利用GetBitmapBits获取HalDispatchTable+4地址里存储的值(也就是hal!HaliQuerySystemInformation), 存储到&pOrg里去(保存一份hal!HaliQuerySystemInformation,为后面恢复还原做准备)
4.gWorker利用SetBitmapBits将HalDispatchTable+4所指内存的值替换为shellcode的地址
5.调用NtQuerySystemInformation执行shellcode
6.gWorker利用SetBitmapBits将HalDispatchTable+4所指内存的值还原为&pOrg存放的值(即第3步骤设置的值)
1.把gManger.pvScan0的值改gWorker.pvScan0的地址
调试github上的exp,在 HANDLE gManger = CreateBitmap(0x60, 1, 1, 32, bbuf); 处下断点
下面是exp通过构造tagKL和tagIMEINFOEX来把gManger.pvScan0的值改为gWorker.pvScan0的地址。
下面是零页内存上的值
下面是完整的tagIMEINFOEX构造的逆向过程
(原图查看地址:https://imgchr.com/i/uVSse0)
现在我们跟入NtUserSetImeInfoEx函数,看它是如何把gManger.pvScan0的值改为gWorker.pvScan0的地址的。
继续跟入NtUserSetImeInfoEx漏洞函数
执行 qmemcpy 后
由上可知, 我们已经把gManger.pvScan0的值改为gWorker.pvScan0的地址,上面的图您可能看着有点奇怪,其实也不用纠结,它只不过是布局tagIMEINFOEX结构体,然后通过SetImeInfoEx漏洞函数,来实现把gManger.pvScan0的值改为gWorker.pvScan0的地址而已。至于为什么赋值给mpv-4(fdb2c75c), 而不是直接赋值给mpv(fdb2c760),是由 _SURFOBJ 结构体的特性决定的。
2. SURFOBJ 结构体
surfobj结构体在《windows Graphics Programming Win32 GDI and DirectDraw》由 Feng Yuan著是有一点讲解的,中文本叫《Windows 图形编程》 2002年出版。
mpv-4 其实就是SURFOBJ->pvBits ,mpv为SURFOBJ->pvScan0;在底层它会将 pvBits 的值更新到 pvScan0
那么这个SURFOBJ结构体是在哪里呢?当我们调用CreateBitmap函数来创建一个bitmap时,SURFOBJ结构就会被附加到进程PEB的GdiSharedHandleTable成员中。GdiSharedHandleTable是一个GDICELL结构体数组的指针(感兴趣的可以调试一下gdi32!CreateBitmap函数,这里我就不逆给你们看了)
gdicell地址的计算公式为:
通过资料和计算,我们知道gdicell->pKernelAddress刚好指向BASEOBJECT结构,在32位机中,BASEOBJECT+0x10的偏移就是SURFOBJ结构,SURFOBJ结构的pvScan0成员也就是漏洞利用的关键,pvScan0让我们能把用户态的shellcode地址,暂时存放到内核态,提供了一个很狭小的4字节的内核空间,让我们在诸多系统保护机制下,有了可乘之机。
3. gManger利用SetBitmapBits将gWorker.pScan0地址里存储的值设置为HalDisptchTable+4地址
到现在为止,总算完成了第一步骤,现在我们继续往下走,进入第二步骤。
逆向一下 SetBitmapBits((HBITMAP)gManger, sizeof(PVOID), &oaddr); 函数
NtGdiSetBitmapBits把我们的参数继续往下传
跟入GreSetBitmapBits函数,先判断一下size是否为空值,然后通过SURFREF::SURFREF函数获取pkernelAddress的地址。
可以看到 SetBitmapBits 调用 bDoGetSetBitmapBits 时,a3的值默认为零;GetBitmapBits调用时,a3为1.
跟入bDoGetSetBitmapBits ,memcpy把gWorker.pScan0里的值替换为 nt!HalDispatchTable+0x4 的地址。
到目前为止, 我们通过SetBitmapBits->NtGdiSetBitmapBits->GreSetBitmapBits->bDoGetSetBitmapBits->memcpy的连续调用,完整了第二部分的数据交换。
4. 通过GetBitmapBits把HalDispatchTable+4地址里存储的值,保存一份到&pOrg里去
我们直接来到GetBitmapBits->NtGdiGetBitmapBits->GreGetBitmapBits下的bDoGetSetBitmapBits
最后进行数据替换,如下图
5. 通过SetBitmapBits将HalDispatchTable+4所指内存的值替换为shellcode的地址
拷贝后的值:
6. 调用NtQuerySystemInformation执行shellcode
通过NtQueryIntervalProfile调用nt!HalDispatchTable+0x4,也就是被我们用shellcode地址替换掉的hal!HaliQuerySystemInformation地址。我们可以在nt!KeQueryIntervalProfile+0x23下个断点
这里的shellcode思路很简单就是把系统的Token替换掉我们当前进程的Token,那么当前进程就具备了管理员的权限,从而达到提权的目的。
7. 恢复HalDisptchTable+4的地址处的值
通过 SetBitmapBits((HBITMAP)gWorker, sizeof(PVOID), &pOrg); 把shellcode替换回hal!HaliQuerySystemInformation.
到这里所有的调试和利用就完成了,最后看下效果
心得
windows内核漏洞利用,如果内核函数不明白的,就是逆向它的逻辑,你会更加比别人理解这种利用方式;比如CreateBitmap这个函数,对于其他漏洞来说,就需要用到它来做池风水布局,而网上的公式您单看是看不明白的(如果您是追求原理本质,那就只能上手逆了,不然分析漏洞时,疑惑是很多的)。
本人的分析水平是有限的,只能尽量做到这样,所以文中很多错误的地方是有待指正和修改的。
最后,来点小菜鸡的心声(me), 想要飞的高,就请忘记地平线。全剧终...
原创文章申明:
本文为“苏州极光无限信息技术有限公司”原创,未经许可,禁止转载!