这个CVE-2018-8897是针对x86平台上所有操作系统的漏洞。这是个两年前的漏洞了,写这篇文章是因为:
- 这是个x86处理器上所有OS都有可能出现的漏洞,作为OS与VM研究学者,既然看到了不能不总结经验。
- 这个漏洞攻击方向是从Ring3向Ring0提权,可以算是最强的提权了。
- 百度了一圈没啥科普性的中文资料,有的也是些只描述现象不解释逻辑的翻译。
- 试试看论坛的Markdown编辑,毕竟是我第一次玩。
背景知识介绍
在x86平台的十个段寄存器中,栈段(ss)
选择子是相当特殊的,当你使用mov ss,xx
或pop ss
指令修改ss
之后,当前逻辑处理器会屏蔽外部中断(ExInt)
,不可屏蔽中断(NMI)
,以及单步和硬断调试异常(Single-Step #DB/Hardware Breakpoint #DB)
,直到下一条指令执行完毕后才会停止屏蔽。
修改栈段选择子同时屏蔽中断是为了在你切换栈的时候避免与中断产生竞态条件(中断有压栈操作,而栈指针没改好,栈段却已经被改了。这三点就会构成竞态条件)。更具体的说,当你在
mov ss,xx
mov rsp,xxx
中开始修改rsp
的时候,因为你没有改好rsp
,这就会导致此时产生中断的时候处理器对栈的操作会形成竞态条件。于是为了规避这一竞态条件,x86处理器就选择在你修改ss
时临时屏蔽ExInt
, NMI
以及#DB
。
至于为什么不屏蔽别的,这是因为只要你写的没毛病,就不会出别的中断和异常。
难道不能让OS程序员在这里用cli
指令么?不能,虽然cli
指令可以屏蔽ExInt
,但是不能屏蔽NMI
和#DB
。
早年的intel手册就没有明说这一点,估计是漏洞被披露之后才在手册里加上的。
至于AMD,搜了一圈没找到老版本文档,不知道是早就指出过这一条还是跟Intel一样以前没明确指出。。。
不过有趣的是:AMD手册上推荐用lss
指令来同时修改ss
段选择子和rsp
寄存器,此时不会造成中断阻塞。简单说就是:
lss rsp,xx:yyyy
估计是因为Intel的x86工程师在给mov ss,xx
和pop ss
指令里来这么一出的时候忘了自己曾设计过lss
指令了,我这么推测是因为lss
是一条8086指令,属于“基本指令”。
如果我是当年的x86工程师,我就仿照mov cs,xx
的处理方法,在mov ss,xx
或pop ss
时直接丢个#UD
异常。
我感觉超威半导体在设计AMD64架构的指令集时还是有些畏手畏脚。尽管主动在长模式里废掉了pop sreg
指令,但没有主动废掉mov ss,xx
指令。如果废掉它,至少能避免这个漏洞的影响力覆盖到用户态长模式上。
漏洞的利用原理
首先讲一些写OS时的逻辑。在权限跃迁(Ring3转向Ring0)的时候,通常会访问一些当前逻辑处理器相关的变量。
这时通常需要切换一下fs/gs
段,使得其段基址从指向用户态使用的当前线程的结构体(比如Windows里的TEB
结构体),切换到指向内核态使用的当前逻辑处理器的结构体(比如Windows里的KPCR
结构体)。
如果在切换fs/gs
之前发生了一个中断,处理器在中断时就会将cs/ss
段选择子压到栈上,被压到栈上的cs/ss
段选择子中,RPL
域(段选择子的低2位)就会等于0,也就是说中断处理函数会认为触发中断的时候CPL=0
,于是就不会切换fs/gs
段。
问题就出在,如果触发中断的指令实际上发生在应用层,而这个中断处理函数错误地把用户态的东西当成内核态的东西来用,那么就会造成提权,也就是提权漏洞。
再说说为什么有可能用户态触发的中断会让内核态的中断处理函数收到RPL=0
。举个例子,我们来看以下用户态的代码:
mov ss,word ptr [rax]
syscall
其中rax
包含的地址是硬件访问断点的地址(设置调试寄存器可以实现读写某个地址时产生#DB
异常)。但是由于这是mov ss,xx
指令,因此就会造成#DB
异常被屏蔽,开始执行syscall
指令之前不会触发中断。于是乎syscall
指令执行完毕并进入内核之后,#DB
的屏蔽被解除,立即产生了由于mov ss,word ptr [rax]
指令所造成的#DB
异常,但是此时已经进入内核态,syscall
依据STAR
寄存器切换了cs
和ss
段(详见我写的【处理器】简介AMD64体系的系统调用),因此#DB
异常就会收到RPL=0
,那么#DB
的处理函数就会认为触发中断时CPL=0
。说了这么长一串,考验逻辑的时候到了,这时的fs/gs
其实是给哪个模式用的,#DB
处理函数认为它是给哪个模式用的?
答案是:此时fs/gs
其实是给用户模式用的,而#DB
处理函数认为是给内核模式用的(用鼠标左键“三击”本行以显示答案)
若不是因为ss
如此特殊,#DB
在syscall
指令之前就触发了,哪能等到进入了内核态才触发#DB
呢?
于是乎提权漏洞的使用者可以恶搞一个fs/gs
段的内容出来,丢给#DB
中断处理函数当做内核模式的玩意来用。因此如果操作系统不强制切换fs/gs
到内核专用的fs/gs
,那么恶意软件不难使得#DB
处理函数以内核模式跑恶意用户模式代码。
漏洞的其他性质
这个漏洞不能攻击虚拟机,不过不能用“虚拟机监管器位于Ring-1”之类的说法来解释说“这个漏洞里Ring3攻击不到Ring-1”。不论Intel还是AMD都从未定义过CPL=-1这个概念,Ring-1只能是一种形象的说法。
当Guest向Host切换时,即VM-Exit,Guest内的mov ss,xx
或pop ss
造成中断延迟不会影响到Host,而是在VM-Entry之后顺延到Guest中。因此这个漏洞不能攻击使用硬件虚拟化的虚拟机软件。
这个漏洞不属于微码级漏洞,因此不需要更新处理器微码。只需要操作系统强制在存在权限跃迁的可能时切换好fs/gs
段即可。
漏洞实战
本论坛非黑客交流平台,不讲如何实战以免水表。不过可以讲讲如何确定操作系统的确存在漏洞。
一般来讲,用户模式用的fs/gs
段和内核模式用的fs/gs
段所用的结构体肯定是不一样的,因此操作系统若是忘记切换了fs/gs
段的话,其运行逻辑必然出错。放到未修复该漏洞的Windows里就是蓝屏。
而在虚拟机中,使用处理器虚拟化技术的虚拟机软件可以主动拦截调试异常等各类中断。但若是拦截中断,由于Intel VT-x关于ss
中断阻塞的特殊性,若是不加以正确处理,可以导致虚拟机软件在处理VM-Exit
时逻辑产生错误。其结果或许是崩掉Guest(说明写VMM的程序员考虑到了自己写的VM-Exit
处理函数有可能存在BUG,于是进行了灾难最小化处理),或许干脆整个系统都GG了(说明程序员根本没想过自己写的VM-Exit
处理函数能出BUG)。
如何规避此漏洞
至于“内核模式跑恶意用户模式代码”这一点,可以去CR4
寄存器将SMEP
位(Supervisor Mode Execution Prevention Bit
)置位,使得内核模式执行用户模式的代码时产生#PF
异常,那么OS检测到了恶意软件,杀掉它的进程就完事了。
与SMEP
位相关的还有SMAP
位(Supervisor Mode Access Prevention Bit
)。把SMAP
置位后,内核模式访问用户页的时候就会产生#PF
异常。不过这里就需要认真思量了,因为不是什么时候都可以禁止内核模式访问用户页的。
此外,SMEP
和SMAP
能力并不是所有CPU都支持的,需要用cpuid
指令检测CPUID[EAX=7,ECX=0].EBX
,其中第20位对应SMAP
,第7位对应SMEP
。那么就得考虑一个不依赖SMEP
与SMAP
能力的规避策略。
这个漏洞说白了就是利用了程序员的企图优化的心理,为了优化掉切换段带来的性能损失而尽可能减少段切换。那么规避它的方法就是在#DB
处理函数中强制切换到内核fs/gs
段即可。由于#DB
是调试用的,可以认为在这上面搞出来的性能损失,只要不离谱就都可以接受。