随笔(二):全补丁下再次利用CPU漏洞攻破KASLR

author : https://weibo.com/jfpan

  12月初微博提到微软RS4的内核修改,介绍了其KVA Shadowing方案消除了多种已知硬件边信道攻击,无意中成了当时尚未公开的meltdown CPU漏洞补丁的最早(?)粗略分析。漏洞公布后本想补充写个详细分析的blog,但忙于保障部门驱动与补丁的兼容性故而推迟。几天后发现网上已经遍布翻译的、原创的meltdown/spectre相关文章,再写重复的内容就没什么意义了。所以这篇blog主要是写一些大家没有提到的内容。

  之前短文提到了操作系统抵御meltdown的方案是用户态使用另一份不映射内核绝大多数地址空间的页表(Windows上的KVA Shadowing和Linux上的KPTI,它们源自KAISER),那么已有方案是否完美呢?答案是否定的,下面以微软补丁方案为例介绍一个导致全补丁下KASLR Bypass的简单缺陷。(注意虽说原理极为简单,但为了确认是否能公开,两周前已将缺陷报给了MSRC,刚得到微软确定答复。小小吐槽一下,微软认为其威胁不大、不归于漏洞这点在意料之中,但给的理由又是常用的一个:“This is by design”,给人的感觉就是专门留下这点设计来废掉KASLR,其实KAISER原本就是设计用于防止针对KASLR的边信道攻击,本质上还是算方案设计有遗漏)

  言归正传,这个缺陷的原理在于KVA Shadowing虽然不在用户态映射绝大多数内核地址空间,但为了保证应用层、内核层之间能正常切换,依然必须有少量的内核代码与数据映射在用户层的页表中。比如,我们可以看到在补丁生效时的syscall入口KiSystemCall64Shadow并不在.text节里,而是和KiDivideErrorFaultShadow等中断处理入口一起放入了KVASCODE节,该节内容集中放置了CPU状态转换时所需的切换页表的代码,其必须映射在用户态的Shadow address space。同理,KPCR这样的重要数据区也是被映射的。前述代码数据区域虽被映射,但地址是随机的。那么有没有既必须被映射、又能被用户层知晓位置的重要数据呢?不幸的是在目前的设计下存在这样的数据区:IDT与GDT(未使用UMIP时用户层可获取地址)。其中IDT中有各个中断处理函数在前述的KVASCODE节中,可通过meltdown的攻击方法在打完全补丁(包括meltdown/spectre补丁)下直接泄露NT内核模块地址。不过并不是指定内核地址随意使用meltdown攻击就能轻易读出内容,看起来内核地址所存储的数据需要在L1缓存中Meltdown攻击才更有可能成功,因此可以使用prefetch指令去预读,不过实验中找一些实际触碰目标内存的操作成功率会大一些,例如:读取IDT内容前故意触发一个中断,读取GDT前如下修改段寄存器内容使CPU访问GDT数据填入段寄存器的影子寄存器:

1
2
3
4
5
6
mov ax, es
push rax
mov ax, fs
mov es, ax ; Let cpu touch GDT.
pop rax
mov es, ax

  实验中IDT内容的读取相对不那么稳定,不过通过阈值的调整在笔者多台机器上可正确获取NT内核模块地址。PoC代码就不贴出了,简单原理已经说清楚了,附图中是读取IDT(中断处理函数)。

  要修补该缺陷也很简单,对支持UMIP(User-Mode Instruction Prevention)的CPU可直接使用该特性;更通用的方案则是将中断处理入口改为随机化地址同时又映射在user shadow address space的代码片段中,该段代码切换页表后跳转至nt内核中实际处理函数(为防止理论上攻击者可读取该段代码内容分析出跳转目标地址,可使最后跳转指令在未被映射到user的页面上,或者读取未被映射到user的数据区中的内容间接跳转)。

随笔

author : https://weibo.com/jfpan

  这是一篇随笔,Win10对虚拟化实施拦截的产品设的障碍越来越大,忍不住吐槽下。话说RS3改进PatchGuard的针对性很明显,但为什么昨天提到Dual-CR3呢?因为它虽对功能实现没什么影响,但对性能造成不小麻烦(实际上,虚拟化拦截类项目,其拦截功能本身的实现是非常简单的,而能否大规模产品化、商业化的根本核心难点与重点在于完美兼容性与极高实时性能的要求:1、兼容性——除去极端软件,即“用我时就别运行其他虚拟化或硬件相关程序”的软件——必须实现对GUEST展现实际CPU全部硬件特性且GUEST确实可使用这些特性,否则在一些场景一定有兼容问题。兼容性的一些入门测试有不少,比如虚拟化功能开启时运行vmware workstation在里面各跑一个32bit Guest和64bit Guest、跑一个Bluestacks模拟器玩玩Android游戏、给Intel CPU打一个微码补丁等等;2、性能的要求是几乎不造成性能下降,而#VMEXIT的性能损耗是巨大的,因此至少需要实现未嵌套工作时在支持unrestricted guest的CPU上几乎不产生#VMEXIT。这两点可探讨的细节和实例太多,就不写了,一个小广告——可参考360HVM)。

  那么微软为什么要在RS4引入Dual-CR3,这要从内核地址空间随机化(KASLR)说起了,Win10 KASLR随机化了模块的加载基址、内核对象地址、页表地址等,缓解了内核漏洞的利用。不过之前微软对各种基于硬件的边信道攻击(double page fault、prefetch side-channel、TSX-based side-channel等等)依然是没有防护的,这次引入Dual-CR3至少目标中包含增加该种防护。学术圈对该类攻击和防御手段研究已经多时了,今年《KASLR is Dead: Long Live KASLR》这篇论文为Linux设计实现的内核地址隔离方案KAISER号称性能损失仅有0.28%,当初看到的时候只凭感觉每次系统调用都切换CR3、把非Global的TLB项清除(何况为了实现内核地址强隔离应该是没有Global项),这性能损失怎么会这么小(论文里倒是提供了一下解释:首先Global没什么用”Surprisingly, we found the performance impact of disabling global bits to be entirely negligible”;其次现代CPU对TLB管理的优化使得频繁切CR3也没什么大损失了)。没想到没几个月微软就直接在Win10上完全照搬了这套方案(不是每个进程都切换)。这套方案原理简单可行,参见附图一(论文附图)就一目了然了。微软在进程—_KPROCESS中增加了UserDirectoryTableBase配合原有DirectoryTableBase即提供论文中描述的CR3 Pair的内容。线程运行时,_KPRCB中的KernelDirectoryTableBase、RspBaseShadow、UserRspShadow、ShadowFlags用于模式转换时的隔离切换,需要加入的代码很少,附图二是Intel CPU的系统调用入口的代码,返回时自然也有相应的处理。

  回到一开始,微软的强隔离对虚拟化拦截项目有什么影响呢?首先对一些拦截了MOV-CR3操作的情况乐子就大了,增加大量的#VMEXIT;其次微软仅保留映射了极少的内核页面在所谓Shadow address space中,比如KiSystemCall64Shadow需要被映射,但KiSystemCall64- KiSystemServiceUser都未被映射,更别说虚拟机在GUEST中的HOOK代码了。如果强制在GUEST中映射自己的代码,这相当不优美又对强隔离有所破坏且带来风险。有事要忙随笔先写到这里。

参考

https://cmaurice.fr/pdf/essos17_gruss.pdf

附图1

附图2