在《Chrome漏洞调试笔记1-CVE-2019-5768》中,笔者介绍了2019年出现的一个Chrome浏览器在野漏洞的原理和利用方法。文末通过关闭Chrome沙箱的方法(–no-sandbox)演示了渲染进程中Shellcode的执行。但是在实际利用中,由于Chrome浏览器的渲染进程受到沙箱模块的保护,像blink或者v8这些在渲染进程中模块的漏洞即使被利用,也难以穿过沙箱。因此在实际利用中,仍需要寻找其他方法穿过Chrome的沙箱。
我们知道,Chrome基于多进程架构,主要包括浏览器进程和渲染进程,进程间通过IPC通信(Mojo):
其中渲染进程运行着不可信的HTML和JS代码,浏览器中的每一个tab为一个独立的进程,运行在Untrusted的低权限等级,并通过沙箱引擎隔离。因此像CVE-2019-5768这样的渲染进程中的远程代码执行漏洞仍需要结合其他高权限漏洞实现沙箱逃逸。一般来说有几种思路:1)利用浏览器进程的漏洞,比如IndexedDB,Mojo等; 2)利用操作系统内核漏洞,比如与CVE-2019-5768组合的win32k.sys内核提权漏洞CVE-2019-0808。
0x0 漏洞原理分析
CVE-2019-0808是win32k.sys中的一个空指针解引用漏洞。根据360的Blog,漏洞存在于win32k!xxxMNMouseMove函数中:
首先分析xxxMNMouseMove函数:
可以看到,xxxMNMouseMove函数中首先通过xxxMNFindWindowFromPoint函数根据当前输入坐标返回对应的窗口对象,然后通过xxxMNUpdateDraggingInfo函数更新窗口拖动信息。跟进分析xxxMNFindWindowFromPoint函数:
这里通过xxxSendMessage函数像用户态发送WM_MN_FINDMENUWINDOWFROMPOINT(0x1EB)消息获取窗口句柄,并通过HMValidateHandleNoSecure函数将窗口句柄转为内核态窗口对象返回xxxMNMouseMove函数。
继续跟进xxxMNUpdateDraggingInfo函数:
xxxMNUpdateDraggingInfo函数内部首先调用MNGetpItem根据pMENUSTATE_->uDraggingIndex返回对应的tagITEM,跟进MNGetpItem函数:
MNGetpItem函数参数中pPopupMenu指向的就是xxxMNFindWindowFromPoint返回的窗口对象tagWND(tagMENUWND)+0xb0保存的弹出菜单tagPOPUPMENU,tagPOPUPMENU的spmenu属性指向其对应的菜单对象tagMenu。注意到pPopupMenu是通过xxxSendMessage返回的,这里在访问pPopupMenu->spmenu的cItems属性时,并没有对pPopupMenu->spmenu指针进行空指针检查。如果可以通过xxxSendMessage返回一个用户态伪造的pPopupMenu->spmenu = NULL,那么这里就存在一处空指针解引用漏洞。而空指针解引用漏洞在Win7环境中是可以通过分配零页内存利用的。
那么如何返回一个(tagMENUWND +0xb0)->pPopupMenu->spmenu = NULL的窗口对象呢,关键在于xxxMNFindWindowFromPoint函数中的xxxSendMessage函数调用。Windows API提供了SetWindowsHookEx和SetWinEventHook这样的消息/事件钩子API用来截获窗口的消息/事件,因此可以通过设置SetWindowsHookEx和SetWinEventHook函数利用xxxSendMessage向用户态发送WM_MN_FINDMENUWINDOWFROMPOINT消息的机会,创造一个内核态到用户态的回调,从而打破内核态中xxxMNMouseMove中的原子操作,返回用户态伪造的窗口对象。
具体PoC构造如下:
- 获取user32! NtUserMNDragOver函数地址
win32k!xxxMNMouseMove由win32k!xxxMNDragOver调用,而win32k!xxxMNDragOver由用户态user32! NtUserMNDragOver调用。由于user32! NtUserMNDragOver未导出,因此可以通过user32! NtUserMNDragOver附近导出函数地址+偏移的方式获取。这里选择user32! NtUserMenuItemFromPoint:
可以看到,user32! NtUserMNDragOver在user32! NtUserMenuItemFromPoint + 0x3A处,对应PoC代码:
当然也可以通过内联汇编的方式直接调用syscall:
这里11ED是NtUserMNDragOver对应的syscall编码,[7FFE0300h]保存的是ntdll!KiFastSystemCall函数地址。
- 创建两个非模态可拖放的弹出菜单
这里分别创建两个非模态可拖放的弹出菜单Root和子菜单Sub。
- 创建伪造菜单窗口对象
根据MSDN,”#32768″为系统使用的ClassName,直接调用后创建的菜单窗口对象属性tagPOPUPMENU->spmenu = NULL,满足漏洞触发条件:
- 创建主窗口
- Hook WH_CALLWNDPROC消息和EVENT_SYSTEM_MENUPOPUPSTART事件
- 显示弹出菜单窗口
TrackPopupMenuEx会调用内核函数xxxTrackPopupMenuEx。xxxTrackPopupMenuEx首先会调用xxxCreateWindowEx创建菜单对象tagMENU对应的窗口对象tagWND(tagMENUWND)和弹出菜单窗口对象tagPOPUPMENU,其中tagWND+0xb0 -> tagPOPUPMENU,tagPOPUPMENU + 0x8 -> tagWND(spwndPopupMenu), tagPOPUPMENU + 0x14 -> tagMENU(spmenu);接着调用xxxSetWindowPos设置菜单窗口的坐标;最后调用xxxWindowEvent发送EVENT_SYSTEM_MENUPOPUPSTART事件表示菜单弹出开始。
- 处理窗口消息循环,等待条件触发漏洞
当主线程进入窗口消息循环后,因为xxxTrackPopupMenuEx发送事件EVENT_SYSTEM_MENUPOPUPSTART,且EVENT_SYSTEM_MENUPOPUPSTART事件被SetWinEventHook函数Hook,用户态函数DisplayEventProc第一次被调用:
此时iMenuCreated = 0,触发case 0分支,发送鼠标左键按下消息,从而子菜单sub弹出,再次触发EVENT_SYSTEM_MENUPOPUPSTART事件并第二次调用DisplayEventProc函数。第二次iMenuCreated = 1,触发case 1分支,发送鼠标移动消息,返回前iMenuCreated++ = 2满足消息循环中的if (iMenuCreated >= 1)分支。由于两次调用DisplayEventProc形成了鼠标拖动的操作,最终进入xxxMNMouseMove函数触发漏洞。
根据前面的分析,xxxMNMouseMove函数内部的xxxMNFindWindowFromPoint函数通过xxxSendMessage发送WM_MN_FINDMENUWINDOWFROMPOINT消息。由于窗口消息WH_CALLWNDPROC被SetWindowsHookEx函数Hook,从而触发WindowHookProc中if分支代码的调用:
该if分支中通过SetWindowLongPtr替换窗口的默认过程函数DefWindowProc为SubMenuProc,从而进入SubMenuProc:
SubMenuProc最终返回用户态构造的窗口对象句柄hWndFakeMenu作为xxxMNFindWindowFromPoint函数调用xxxSendMessage的返回值,最终得到一个pPopupMenu->spmenu = NULL的空指针。
动态调试过程如下:
用户态构造的弹出菜单窗口句柄:
在win32k!xxxMNFindWindowFromPoint函数call win32k!xxxSendMessage前下断点,检查xxxSendMessage调用后的返回值:
可以看到返回的正是用户态构造的窗口对象句柄0x00020284。继续步过,win32k!HMValidateHandleNoSecure返回对应的内核对象tagWND地址0xfea22310。根据之前的分析,tagWND+0xb0 -> tagPOPUPMENU=0xfda091c0 ,而tagPOPUPMENU+0x14->spmenu = NULL满足漏洞触发要求:
继续执行,进入xxxMNUpdateDraggingInfo函数的MNGetpItem函数,因为此时spmenu(ecx)= NULL,从而触发空指针解引用异常,最终导致BSOD:
0x1 寻找利用点
对于内核态的空指针解引用漏洞,Win7中用户态可以通过ntdll!NtAllocateVirtualMemory函数分配零页内存利用。分配零页内存的原理比较简单,不再详述:
具体分析分配零页内存后代码执行的流程,寻找后续可利用点:
可以看到在分配零页内存后,pMenu->cItems [0x00000020] = 0x00000000不再触发内存访问异常,但是因为if判断中uDraggingIndex < pMenu->cItems=0 不成立,导致返回pItem=0。我们需要控制程序流程走到红框分支,返回可控数据:
由于零页内存可控,考虑设置[0x00000020]=0xffffffff:
再次运行利用程序,控制流程进入if分支:
这次uDraggingIndex < pMenu->cItems=0xffffffff 成立,进入if成立分支,并注意到函数返回值eax = uDraggingIndex * 0x6c + [0x00000034],其中[0x00000034]可控。如果uDraggingIndex的值也可以泄露的话,MNGetpItem函数的返回值就用户态可控。
继续执行,返回xxxMNUpdateDraggingInfo函数:
继续向后寻找,下面一处函数调用是xxxMNSetGapState,注意到xxxMNSetGapState函数中存在位修改操作:
幸运的是,这里再次出现了之前分析的MNGetpItem函数,并且MNGetpItem函数的第一个参数就是从用户态返回的tagPOPUPMENU,其属性spmenu正指向零页内存,用户态可控。而根据之前对MNGetpItem函数返回值的分析知道,返回值eax = (uDraggingIndex – 1) * 0x6c + [0x00000034],其中[0x00000034]可控,uDraggingIndex可以从tagMSG的wParam取到,因此MNGetpItem函数返回的pItem值用户态可控。从而可以通过pItem->fState |= 0x40000000u 实现指定地址的值修改为与0x40000000或的功能,计算过程如下(addressToWrite表示写入地址):
- pItem = eax = (uDraggingIndex – 1) * 0x6c + [0x00000034]
- [pItem->fState] = [eax + 0x4] = [(uDraggingIndex – 1) * 0x6c + [0x00000034] + 0x4] |= 0x40000000
- [addressToWrite] = [(uDraggingIndex – 1) * 0x6c + [0x00000034] + 0x4] |= 0x40000000
- [0x00000034] = addressToWrite – (uDraggingIndex – 1) * 0x6c – 0x4
这里addressToWrite就是期望写入的地址,首先设置为0,查看是否可以利用漏洞修改成功:
动态调试发现,未进入xxxMNSetGapState函数前就发生了内存访问异常,静态分析这段代码:
因此这里需要将[0x0000004c]*0x6C + [0x00000034] + 0x28地址指向的稳定可读写的内存区域,Exodus的exp中选择了零页内存0x180附近的地址,并设置其值为0xF0F0F0F0从而进入pMENUSTATE_->uDraggingFlags = 1的else分支:
再次调试,成功步过之前的crash点,最终将0x00000000的值修改为0x40000000
这样,我们就实现了指定地址值的有限修改功能。
0x2 窗口对象喷射
那么获得了指定地址值的修改功能后,如何进一步利用呢?Exodus的blog中选择了窗口对象喷射:
简单的说就是通过创建大量窗口对象tagWND,寻找到两个相近的tagWND。因为tagWND+0x90->cbwndExtra表示窗口附加数据长度(tagWND+0xB0开始),从而可以通过漏洞修改第一个窗口的cbwndExtra = 0x40000000实现第一个窗口附加数据越界读写功能。而第一个窗口对象附加数据越界读写目标是修改第二个窗口对象的strName.Buffer指针,从而通过设置第二个窗口的strName实现指定地址数据修改功能。
窗口对象喷射有一个需要解决的问题是如何在用户态泄露内核态窗口对象指针。user32!HMValidateHandle可以用来泄露内核对象地址,user32! HMValidateHandle用来返回窗口句柄的THRDESKHEAD结构体,而THRDESKHEAD.pSelf属性保存了该句柄的内核对象地址:
但是user32!HMValidateHandle并未导出,因此需要借助其他导出函数寻找,这里选择的是user32! IsMenu:
IsMenu间接调用函数HMValidateHandle。查找HMValidateHandle函数地址代码如下:
得到HMValidateHandle函数地址后,就可以进行窗口喷射,找到满足要求的两个相近窗口对象,窗口喷射部分代码说明:
0x3 内核态Shellcode执行
通过窗口对象喷射,我们得到两个相邻的窗口对象,并可以利用漏洞修改第一个窗口对象的cbwndExtra实现第一个窗口对象附加数据越界读写功能。我们最终的目的是在内核态执行用户态的shellcode,窗口对象tagWND. bServerSideWindowProc标志位可以帮助实现此功能。如果tagWND. bServerSideWindowProc 被置位则窗口过程函数直接在内核上下文执行,而通过之前的窗口喷射我们已经可以通过第一个窗口的附加数据越界修改第二个窗口的strName.Buffer字段,通过将第二个窗口的strName.Buffer指向第二个窗口的bServerSideWindowProc,最终可以利用第二个窗口strName修改第二个窗口对象的bServerSideWindowProc标志位:
通过漏洞修改第二个窗口对象tagWND. bServerSideWindowProc 标志位后,就可以直接在内核态执行用户态shellcode:
注意这里和一般内核提权替换token利用方法稍有不同的是,这里首先清空了进程的Job对象指针,这是因为在Chrome的渲染进程中,即使shellcode替换了system进程的token,当前进程的token依然会继承自Job对象,并且Job不允许Chrome渲染进程产生新进程,因此需要先清空当前进程的Job对象指针:
修改tagWND. bServerSideWindowProc标志位的部分代码说明如下:
Shellcode执行成功后,当前进程提升为system权限,最后可以通过WinExec(“cmd.exe”, 1); 创建一个system权限的cmd。动态调试过程:
窗口对象喷射后,找到满足条件的两个相邻窗口对象:
触发漏洞后,第一个窗口的cbwndExtra被修改为0x40000000:
通过第一个窗口cbwndExtra越界修改第二个窗口的strName.Buffer,将其指向第二个窗口的bServerSideWindowProc标志位:
通过SetWindowTextA(hSecondaryWindow, “\x06”)修改第二个窗口的bServerSideWindowProc标志位:
最后执行shellcode,成功替换system进程token,实现提权:
0x4 Chrome执行dll
在完成了内核提权exp后,接下来需要考虑如何结合Chrome渲染进程的漏洞实现沙箱逃逸。首先可以考虑将内核提权exp以dll的形式编译,然后加载到目标进程,执行提权操作。但是由于Chrome渲染进程运行在Untrusted权限,无法直接利用漏洞获取shellcode执行权限后注入提权dll,需要考虑其他方法。
反射型dll注入就是一个比较好的方法,github中有相关项目可以直接使用:
以一个简单的dll程序为例:
vs编译选择最大优化并选择/MT模式生成payload.dll。使用sRDI的python模块生成bin文件:
再通过python脚本转换为js,替换CVE-2019-5768里的shellcode,关闭sandbox可以看到dll被成功执行:
同样方法编译CVE-2019-0808的exp,可以实现沙箱逃逸,完整的利用代码可以参考Exodus的github。感兴趣的同学可以自行尝试。
0x5 参考文献
- https://www.chromium.org/developers/design-documents/multi-process-architecture
- https://www.anquanke.com/post/id/194351/
- https://blog.exodusintel.com/2019/05/17/windows-within-windows/
- https://github.com/exodusintel/CVE-2019-0808
- http://blogs.360.cn/post/RootCause_CVE-2019-0808_CH.html
- https://github.com/monoxgas/sRDI