查看原文
其他

【Part I】CVE-2018-8897 原理深度漫游、漏洞利用、调试实战

shayi 看雪学院 2019-09-18
简介


CVE-2018-8897 是一种本地权限提升(
Local Privilege Escalation,LPE)型漏洞,可从模式(Ring3;CPL=3写任意内核内存、以 Ring0 权级执行用模式代,危害性非常大。


成因是 Intel 文档中段(SS)寄存器的指令—— pop/mov ss——中断、系统调的交互行,描述不明确和完整,致多数 OS 内核实现#DB调试异常)handler 错误地假先前的境,者有机会绕过段(CS)DPL 检查,取得LPE

任何基于 Intel x86/x64 体系结构的 OS,比如 Windows、Linux、MacOS、FreeBSD、某些配置的 Xen,都受此漏洞影响。

让我们首先快速浏览一下关键的脆弱逻辑,以 64 位 Windows 10 内核映像中的KiDebugTrapOrFault() 函数为例(它是 #DB,亦即 INT 1 中断向量的处理程序),如下图所示。


第一个红框中的 test / jz 指令比较当前代码段的 DPL(TrapFrame.SegCS)是否为 1, 如果 DPL 为 0 表示 previous mode 为内核,则跳转到分支【FromKernelMode】,这样就不 会执行 swapgs 指令(把当前 GS 段寄存器的内容——GS 段基址——从用户模式的 _TEB 交换为内核模式的 _KPCR。)


换言之,假若能够骗取
KiDebugTrapOrFault() 信任先前的执行模式为内核,它就把我们控制的 _TEB 误当 _KPCR 使用。


(在后面的 exploit POC 源码分析中,会看到如何通过精心构造的汇编指令序列,来绕过这个不健壮的逻辑,以及避免用户—内核栈段切换)



《时间表》


1.
CVE-2018-8897Nick Peterson 与 Nemanja Mulasmajic 两名安全研究员发现,并在2018/03/21 前后提交给 CVE 归档:https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2018-8897。

2. 微软在 2018/5/8 前后发布相应的安全更新修补:

针对 Windows 10 Version 1607:http://www.catalog.update.microsoft.com/Search.aspx?q=KB4103723,后面的调试章节会额外分析安装了该补丁的 1607版内核映像,对比修改之处,了解部署的防御手段;


在此 2 个多月的空白期,它成为 0-day,但由于成功利用该漏洞需要掌握不少的硬件、系统软件知识,还要将多核编程环境纳入考虑。

3. 因此两名发现者也仅仅在 2018/5/11 左右释出一个利用此漏洞的简易 Local Dos(本地拒绝服务。蓝屏在国外文献中常有两种表述法,其一是 BSOD;Blue Screen Of Death,另一 种就是 Local Dos)POC

4. 在 2018/12/3 举办的全球最顶尖水平黑帽大会 Black Hat 2018 上,两名发现者演示了一个完整的 exploit POC 程序,实现以Ring0 特权级执行任意用户模式代码。


他们将源码放在托管网站:https://github.com/nmulasmajic/syscall_exploit_CVE-2018-8897 (后面的源码分析章节全数基于此)。

注意,在相关安全更新出现前(2018/5/8),发行的所有 Windows 10 版本都受到危害,如下图所示。


所以若你要调试此漏洞,建议选用图中红框内的 Windows 10 版本,下载点可以从这里找到:http://windowsiso.net/windows-10-iso/windows-10-anniversary-update-1607-download-build- 14393-0/windows-10-anniversary-update-1607-iso-download-standard/



前置知识与全局概览


为了能够顺利理解后面的源码分析与调试章节,我在这里会快速介绍一下 CVE-2018-8897 exploit 用到的核心概念。


首先必解决一个最棘手的问题(它也是此漏洞的实际成因):如何在用构造指令序列来绕过内核模式入口点的特权级检查


思路如下: 


第一步,在用户空间设置一个数据访问断点(硬件断点),可用程序内的全局变量来引用这个硬件断点;

第二步,调用指令序列


mov ss, [全局变量名]
syscal
l


这样,当访问变量引用的用户地址时,将抛出一个 #DB 异常,但
mov ss 会禁用所有中断、NMI(不可屏蔽中断)、以及搁置(挂起)所有 #DB 异常;

第三步,直到 syscall 指令执行完毕,CPU 才重新识别 #DB 异常。syscall 是 64 位系统调用指令,它负责从用户模式切换到内核模式;

第四步,syscall 仅仅从一个 DPL = 3 的用户 CS 转换到一个 DPL = 0 的内核 CS (在 64 位Windows 上是 KiSystemCall64() 所在的 CS),按照 Intel 文档,若涉及 CPL 小于等于 DPL的段间转移,则需要从用户 SS 切换到内核 SS。


但是此刻触发的 #DB 异常让KiSystemCall64() 未能完成这一关键的内核栈段切换,控制流就转移到了 #DB 异常handler——KiDebugTrapOrFault() 的内部。

结果就是,我们不仅成功让 KiDebugTrapOrFault() 误认为 previous mode 是内核态(两个 例程位于同一个代码段,其 DPL 和 CPL 都为 0),而且这些内核例程调用链还会延用我们可 控制的用户栈段,这样我们可以将链中压栈的关键返回地址替换成用户空间的实际 payload,取得以 Ring0 权限执行 Ring3 代码。

不仅如此,由于 swapgs 是特权指令,正常情况只能在 Ring0 下调用(如下图):



从上图你可以看到:


如果 swapgs 的执行不在 64 位模式下,则抛出 #UD 异常;如果当前的CPL 不为 0(不在内核模式下),则抛出 #GP(0) 通用保护异常;


若在内核模式下执行swapgs ,则当前的 GS 寄存器包含 _KPCR 结构,而 IA32_KERNEL_GS_BASE MSR 地址C0000102H 则包含发起切换线程的 TEB 结构。所以再次执行 swapgs 就换回来了。整个交换机制如下图所示:



借由前述的 mov ss, [variable name] && syscall 漏洞,我们就能够在用户空间执行 swapgs,把 GSBASE(GS 段基址) 的内容从 TEB 变为 _KPCR,实现用 gs:xxxx 语法读写敏感的内核内存区域。比如,用 gs:188h 读取 _KPCR.Prcb.CurrentThread 字段。

第三项需要掌握的知识点就是,调试寄存器 DR0~DR7 ,以及如何在指定的内存地址设置数 据访问断点,以便触发 #DB 异常。由于这事关漏洞利用能否成功(第一步),所以我还是不厌其烦地叙述一次。


在后面的源码分析中你会看到,如何通过编程方式设定硬件断点。

  • 对于 x64 体系结构,调试寄存器 DR0–DR7 为 64 位;

  • 在 64 位模式下,MOV DR(0~7) 指令读取或写入所有 64 位。 MOV 指令的操作数大小前缀被忽略。

  • 在 64 位模式下,DR6 和 DR7 的高 32 位保留,必须用零写入;

  • DR0~DR3 的所有 64 个位,都可由系统软件写入;

  • 将 1 写入(DR6 和 DR7 的)任一高 32 位,都会导致 #GP(0)异常。

  • MOV DR(0~7) 指令仅能在 Ring0 下被调用,但它又是触发此漏洞的第一步工作,这意味着我们需要一次初始的 Ring0 访问来设置相关寄存器内容,看起来似乎进入了鸡与蛋的死循环。

幸运的是,exploit 的两位作者找到一个 Windows 用户模式 API调用链——kernel32!SetThreadContext() → ntdll!NtSetContextThread()提供接口,允许我们在 Ring3 设置相关寄存器的内容。我把它留待源码分析章节再讲。 


下图取自 Intel 文档,描述 64 位 DR0~DR7 的各字段作用,其中:

  • DR0~DR3 是调试地址寄存器,保存断点的 64 位线性地址;

  • DR6 是调试状态寄存器,含有 #DB 触发时的【状态】,异常 handler 可以检查调试寄存器 以确定导致异常的条件;

  • DR7 是调试控制寄存器,启用或禁用断点并设置断点条件;

Black Hat 2018 大会上展示的 exploit POC 只用到了 DR0DR7,所以我将忽略讨论不相关的寄存器。


在继续本文之前,我想梳理一个常被各种【技术文档】混淆的概念——导致 #DB 的原因,以及它们之间的关系、分类(FaultTrap)。用图来总结最直观:


所以,mov ss, [variable name] 会在访问 variable name 处的内容后,触发一个数据访问断点 ,即俗称的硬件断点。(这也是 KiDebugTrapOrFault() 的名称由来,因为它要处理的 #DB异常情况既有陷阱又有错误)

最后一项【基础知识】有关于 current privilege level (CPL) 不变情况下的中断/异常 handler对栈的使用情况,这也是 CVE-2018-8897 exploit POC 成功利用时的内核栈状态,如下图所示,改图对于源码分析和调试都有极大帮助:


从上图你可以看到,不进行栈段切换的后果就是——用户程序与内核例程共享同一个 SS、RSP,在其上存储返回地址(saved EIP)信息,这种做法极易受到危害!仅仅应用以上基础知识还不够,我们还需确保不发生一些非预期情况,导致漏洞利用失败:

  • exploit 主进程的数据和代码页必须常驻内存,不被换出至磁盘上的 pagefile.sys,否则在执行 mov ss, [variable name] 期间的一个 #PF(缺页异常)+ #DB 会导致控制流转移到#DF(双误异常)handler,使得漏洞利用失败!

  • 需要一个 Worker 线程运行在另一个 CPU 核上,持续地向用户—内核共享的栈段上覆盖恶意的返回地址,维护它不被众多内核例程改回合法值。

  • 需要一种多处理器之间的同步机制,在 exploit 主线程和 Worker 线程之间根据信号同步执行流。

我将相应的解决方案留待源码分析章节中讨论。


第二部分,明日继续!




- End -



看雪ID:shayi  

https://bbs.pediy.com/user-533967.htm  



本文由看雪论坛 shayi 原创

转载请注明来自看雪社区




⚠️ 注意


2019 看雪安全开发者峰会门票正在热售中!

长按识别下方二维码即可享受 2.5折 优惠!





热门文章阅读

1、VMProtect 3.3.1虚拟机&代码混淆机制入门

2、攻破 Windows AMD 64 平台的 PatchGuard - 清除执行中的 PatchGuard

3、VMProtect 3.31的OEP之旅




公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com



↙点击下方“阅读原文”,查看更多干货

Modified on

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存