Windows本地内核提权——Win32组件空指针漏洞(CVE-2018-8120)
目录
漏洞概述在2018年5月,微软官方公布并修复了4个win32k内核提权的漏洞,其中的CVE-2018-8120内核提权漏洞是存在于win32k内核组件中的一个空指针引用漏洞,可以通过空指针引用,对内核进行任意读写,进而执行任意代码,以达到内核提权的目的。 漏洞原理该漏洞的触发点就是窗口站tagWINDOWSTATON对象的指针成员域spklList指向的可能是空地址,如果同时该窗口站关联当前进程,那么调用系统服务函数NtUserSetImeInfoEx设置输入法扩展信息时,会间接调用SetImeInfoEx函数访问spklList指针指向的位于用户进程地址空间的零页内存。 如果当前进程的零页内存未被映射(事实上零页内存正常是不会被映射的),函数SetImeInfoEx的访问操作将引发缺页异常,导致系统BSOD;同样,如果当前进程的零页内存被提前映射成我们精心构造的数据,则有可能恶意利用,造成任意代码执行的漏洞。 漏洞复现windbg调试本地内核说明:Windbg是Microsoft公司免费调试器调试集合中的GUI的调试器,支持Source和Assembly两种模式的调试。Windbg不仅可以调试应用程序,还可以进行Kernel Debug。 该工具使得我们可以本地调试windows系统的内核,但是,本地调试内核模式不能使用执行命令、断点命令和堆栈跟踪命令等命令 1、使用管理员身份打开cmd,执行 2、使用管理员权限打开windbg(一定是管理员权限,不然不起作用),然后依次选择 3、经过上面的设置基本就可以进行相关本地内核调试 查看SSDT表和SSDTShadow表在windows操作系统中,系统服务(系统内核函数)分为两种:一种是常用的系统服务,实现在内核文件;另一种是与图形显示及用户界面相关的系统服务,实现在win32k.sys文件中。 全部的系统服务在系统运行期间都储存在系统的内存区,系统使用两个系统服务地址表KiServiceTable和Win32pServiceTable管理这些系统服务,同时设置两个系统服务描述表(SDT)管理系统服务地址表,这两个系统服务描述表 其中,前者只包含KiServiceTable表,后者包含KiServiceTable和Win32pServiceTable两个表,而且SDDT是可以直接调用访问的,SSDTShadow不可以直接调用访问。 SDT对象的结构体如下: typedef struct _KSYSTEM_SERVICE_TABLE { PULONG ServiceTableBase; // 系统服务地址表地址 PULONG ServiceCounterTableBase; PULONG NumberOfService; // 服务函数的个数 ULONG ParamTableBase; // 该系统服务的参数表 } KSYSTEM_SERVICE_TABLE,*PKSYSTEM_SERVICE_TABLE; 通过windbg本地内核调试查看相关系统服务描述表实际结构分布: 分析:图中显示的是SDDT表和SSDTShadow表中的结构,每个表中的两行分别表示系统服务地址表KiServiceTable表和Win32pServiceTable表的相关数据信息。因为上面的是SSDT表,不包含Win32pServiceTable表,所以第一个表中第二行数据为空。 结合上面的结构体可以看出,KiServiceTable的地址是 再查看系统服务地址表存储具体的内容: 分析:可以看出系统服务地址表中存储的都是四个字节的函数指针,这些指针指向的就是后面对应的系统服务函数 查看窗口站结构体信息窗口站是和当前进程和会话(session)相关联的一个内核对象,它包含剪贴板(clipboard)、原子表、一个或多个桌面(desktop)对象等。 通过windbg来查看窗口站对象在内核中的结构体实例: 分析:上图就是窗口站tagWINDOWSTATION的结构体的定义,其中在偏移 查看键盘布局的结构体定义 分析:键盘布局tagKL结构体中在偏移 当用户进程调用CreateWindowStation函数等相关函数创建新的窗口站时,最终会调用内核函数xxxCreateWindowStation执行窗口站的创建,但是在该函数执行期间,被创建的新窗口站实例的spklList指针并没有被初始化,指向的是空地址。 说明: 函数 IDA加载win32k.sys组件并手动载入符号表
分析:从上面的伪代码中可以看出,函数SetImeInfoEx首先从参数a1指向的窗口站对象中获取spklList指针(a1是窗口站地址指针,偏移0x14就是spklList指针),也就是指向键盘布局链表tagKL首节点地址的指针;然后函数从首节点开始遍历键盘布局对象链表,直到节点对象的pklNext成员指回到首节点对象为止,函数判断每个被遍历的节点对象的hkl成员是否与源输入法扩展信息对象的hkl成员相等;接下来函数判断目标键盘布局对象的piiex成员(偏移0x2c)是否为空,且成员变量 fLoadFlag(偏移0x48) 值是否为 FALSE,如果上述两个条件成立,则把源输入法扩展信息对象的数据拷贝到目标键盘布局对象的piiex成员中。 把这段伪代码变得更易读一下~ BOOL __stdcall SetImeInfoEx(tagWINDOWSTATION *winSta,tagIMEINFOEX *imeInfoEx) { [...] if ( winSta ) { pkl = winSta->spklList; while ( pkl->hkl != imeInfoEx->hkl ) { pkl = pkl->pklNext; if ( pkl == winSta->spklList ) return 0; } piiex = pkl->piiex; if ( !piiex ) return 0; if ( !piiex->fLoadFlag ) qmemcpy(piiex,imeInfoEx,sizeof(tagIMEINFOEX)); bReturn = 1; } return bReturn; } 至此我们可以看出程序的漏洞:在遍历键盘布局对象链表 spklList 的时候并没有判断 spklList 地址是否为 NULL,假设此时 spklList 为空的话,接下来对 spklList 访问的时候将触发访问异常,导致系统 BSOD 的发生。 利用Poc验证漏洞从之前的分析中,我们知道触发漏洞的条件是要将spklList指针指向空地址的窗口站关联到进程中。 具体实现就是先通过接口函数CreateWindowStation创建一个窗口站,然后调用NtUserSetImeInfoEx函数关联该窗口站和进程(NtUserSetImeInfoEx系统服务函数会调用SetImeInfoEx);因为NtUserSetImeInfoEx函数未导出,所以需要使用Malware Defender来hook得到序列号,再通过序列号计算出服务号 运行Malware Defender,选择钩子-->Win32k服务表,查看系统服务序列号 分析:NtUserSetImeInfoEx的系统服务号 = 0x1000+0x226(550的16进制) = 0x1226 ,其中 0x1000代表调用SSDTShadow中第二个表项中的系统服务函数(第一个表项的系统服务函数为0x0000) 使用windbg来查看SystemCallStub函数地址从而调用内核函数 Poc实现代码: #include <Windows.h> #include <stdio.h> __declspec(naked) void NtSetUserImeInfoEx(PVOID imeinfoex) { __asm { mov eax,0x1226 //将NtUserSetImeInfoEx函数的服务号传入eax中 mov edx,0x7ffe0300 // 将SystemCallStub函数地址传入edx中 call dword ptr[edx] //调用SystemCallStub函数 ret 0x04 } } int main() { HWINSTA hSta = CreateWindowStationW(0,READ_CONTROL,0); //使用CreateWindowStation函数创建一个窗口站 SetProcessWindowStation(hSta); char ime[0x800]; NtSetUserImeInfoEx((PVOID)&ime); //调用NtUserSetImeInfoEx函数触发漏洞,致使系统BSOD return 0; } 编译运行,成功触发漏洞,致使系统BSOD 漏洞利用
分配零页内存
NTSYSAPI NTSTATUS NTAPI ZwAllocateVirtualMemory ( IN HANDLE ProcessHandle,IN OUT PVOID BaseAddress,IN ULONG ZeroBits,IN OUT PULONG RegionSize,IN ULONG AllocationType,IN ULONG Protect ); 分析:将参数BaseAdress设置为0时,并不能在零页内存中分配空间,而是让系统寻找第一个未使用的内存块来分配使用。在AllocateType参数中有一个分配类型是MEM_TOP_DOWN,该类型表示内存分配从上向下分配内存。我们可以将参数BaseAddress指定为一个低地址同时指定分配内存的大小参数RegionSize的值大于这个地址值,如参数BaseAddress为1,参数RegionSize为8192,这样也就能成功分配,地址范围就是 0xFFFFE001(-8191)到 1把0地址包含在内了,此时再去尝试向 NULL指针执行的地址写数据,程序就不会异常了。在32位 Windows系统中,可用的虚拟地址空间共计为 2^32 字节(4 GB)。通常低地址的2GB用于用户空间,高地址的2GB 用于系统内核空间,通过这种方式我们发现在0地址分配内存的同时,也会在高地址(内核空间)分配内存。 分配零页内存,创建并设置窗口站 构造能够获取SYSTEM进程令牌的shellcode每个进程都在内核中都会有且仅有一个EPROCESS结构,其中EPROCESS结构中的Token字段记录着这个进程的Token结构的地址,进程的很多与安全相关的信息是记录在这个TOKEN结构中的,所以如果我们想获得SYSTEM权限,就需要将拥有SYSTEM权限进程的Token字段的值找到,并赋值给我们创建的程序进程中EPROCESS的Token字段。 第一步,找到拥有SYSTEM权限的进程的EPROCESS结构地址 在Ring0中,fs寄存器指向一个叫KPCR的数据结构,该结构体中偏移量为0x120的地方是一个类型为_KPRCB的成员PrcbData 结构体_KPRCB中偏移量为0x004的地方存放着指向当前线程的_KTHREAD 通过查看_KTHREAD结构体和EPROCESS组成,我们知道_KTHREAD.ApcState.Process指向的就是当前进程的EPROCESS,所以我们获取当前进程EPROCESS的汇编代码可以写成 mov edx,0x124; mov eax,fs:[edx];// Get nt!_KPCR.PcrbData.CurrentThread mov edx,0x50; mov eax,[eax + edx];// Get nt!_KTHREAD.ApcState.Process mov ecx,eax;// Copy current _EPROCESS structure 基于以上,我们已经明白如何获得自身进程的EPROCESS结构了,进一步需要做的是获得System进程的EPROCESS~ 查看EPROCESS的ActiveProcessLinks成员,它是一个_LIST_ENTRY结构,在windows系统中,每创建一个进程系统内核就会为其创建一个EPROCESS,然后使EPROCESS.ActiveProcessLinks.Flink=上一个创建的进程的EPROCESS.ActiveProcessLinks.Flink的地址,而上一个创建进程的EPROCESS.ActiveProcessLinks.Blink=新创建进程的EPROCESS.ActiveProcessLinks.Flink的地址,构成了一个双向链表。所以找到一个进程就可以通过Flink和Blink遍历全部进程EPROCESS了,由于System进程是最先创建的进程之一,因此它必然在当前进程(我们编写的这个程序进程)之前,我们可以循环访问Flink,判断其PID是否为4(EPROCESS的UniqueProcessId成员指向其所属进程的PID)来判断其是否为SYSTEM进程 第二步,将SYSTEM进程的Token字段赋值给当前进程 查找获取HalDispatchTable表地址
分析:在NtQueryIntervalProfile中调用KeQueryIntervalProfile函数 分析:从图中可以看出KeQueryIntervalProfile函数调用一个在 需要用到的是HalDispatchTable+0x4地址,那么也就是需要找到HalDispatchTable的地址即可,我们可利用另一个未文档化的函数——NtQuerySystemInformation,此函数可帮助用户进程查询内核以获取有关OS和硬件状态的信息,这个函数没有导入库,我们需要使用GetModuleHandle和GetProcAddress在‘ntdll.dll‘的内存范围内动态加载函数。 分析:
利用Bitmap任意内存读写
1. 首先创建两个Bitmap对象:gManger和个Worker; 创建一个Bitmap对象时,一个结构被附加到了进程PEB的GdiSharedHandleTable成员中, GdiSharedHandleTable是一个GDICELL结构体数组的指针 ,GDICELL结构的pKernelAddress成员指向BASEOBJECT(sizeof=0x10 我们可以用以下方式找到Bitmap对象的内核地址 addr = PEB.GdiSharedHandleTable + (handle &0xffff) *sizeof(GDICELL) ; 通过如下代码获得gManger.pvScan0和gWork.pvScan0的地址 2. 利用CVE-2018-8120的任意内存写入漏洞,将gManger对象的pvScan0值修改成gWorker对象的地址; 基本前文的漏洞分析,我们知道SetImeInfoEx函数中若想执行qmemcpy,需跳过如下所示的while循环 while ( pkl->hkl != imeInfoEx->hkl ) { pkl = pkl->pklNext; if ( pkl == winSta->spklList ) return 0; } 因此需要设置pkl->hkl = imeInfoEx->hkl,就是在零页地址位置伪造了一个和 tagIMEINFOEX 结构体 spklList 成员类型一样的 tagKL 结构体,然后把它的 hkl 字段设置为 wpv 的地址,之后再把 wpv 的地址放在 NtUserSetImeInfoEx 函数的参数 ime_info_ex 的第一个成员里面;指定pkl->piiex等于gManger.pvScan0的地址,也就是指定qmemcpy目的地址,这样执行qmemcpy之后,就可以把gWorker.pvScan0的值赋给gManger.pvScan0 注意:qmemcpy拷贝了0x15c个字节,势必会影响gManger.pvScan0之后的内存,后面调用Gdi32的 GetBitmapBits/SetBitmapBits 这两个函数就会不成功,因为这两个函数操作pvScan0的方式和SURFOBJ结构的 lDelta、iBitmapFormat、iType、fjBitmap 还有SURFACE结构的flags字段相关的,为了避免这个问题,我们需要在构造的ime_info_ex中填上一些数值进行修复 3. gManger对象调用SetBitmapBits函数将gWorker对象的pvScan0的值覆盖成HalDisptchTable+4的地址(HalDisptchTable表中对应偏移处存放着hal!HaliQuerySystemInformation() 函数指针); 4. gWorker调用GetBitmapBits函数获取HalDispatchTable+4所指内存的值,也就是hal!HaliQuerySystemInformation() 函数指针,存储起来; 5. gWork对象调用SetBitmapBits函数将HalDispatchTable+4处的函数指针覆盖成shellcode函数指针; 6. 在用户进程中调用系统API函数NtQuerySystemInformation,进而调用HalDisptchTable表中的hal!HaliQuerySystemInformation() 函数指针,也就是执行shellcode; 7. gWorker调用SetBitmapBits函数将HalDisptchTable+4的地址处的hal!HaliQuerySystemInformation() 函数指针还原,保证下面的运行不出错; Exp利用漏洞打开cmd,进入Exp-CVE-2018-8120.exe所在的目录并执行,引号内为想要执行的命令 参考资料
(编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |
- 是否可以防止.NET MSIL DLL的反编译?
- windows-server-2003 – Windows Server 2003中的
- windows-server-2008 – 如何验证是否从WSUS正确
- 在Windows上将自定义CA根证书添加到GCloud实用程
- wpf – 在Windows上绘制叠加图形的最佳方法是什么
- windows-7 – Webpack –watch在Windows上不起作
- 在Windows 7 Docker Quickstart终端中运行curl命
- 在windows server 2008 64位服务器上配置php环境
- Qt工作Windows 8风格无框自定义窗口
- windows – Hook进程