随笔(二):全补丁下再次利用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的数据区中的内容间接跳转)。