前言
本文的主要内容来源于2018NDSS的文章Back To The Epilogue: Evading Control Flow Guard via Unaligned Targets.
通常,攻击者可以通过利用内存漏洞来截获控制流。但是研究人员也提出了各种各样的防御手段避免发生这样的问题,例如地址空间随机化(ALSR),异或执行(XOR Execute),控制流完整性(CFI)等各种各样的防御措施。CFI通过强制控制流完整性保证程序的执行不会出现问题,目前部署最为广泛的CFI是windows提供的control flow guard(CFG)。CFG目前部署在最新的Windows 8.1,Windows 10上,已经超过了5亿使用量。因此一旦在CFG上出现问题,可能导致非常严重的后果。
Windows CFG实现
Windows的CFI实现称为Control Flow Guard(CFG),因为实际的性能要求,不可能做到非常精确的CFI,因此,实际在windows上部署的CFI是粗粒度的、前向CFI。首先,粗粒度的CFI是:所有的有效跳转地址为一个全局的集合,即不精确的为每一个间接跳转指定一个有效跳转地址;其次,什么叫做前向CFI:只考略call,jump的直接跳转和间接跳转,没有计算ret的情况。此外,Windows CFG实现还依赖于bitmap表,该表存储的信息是关于目标地址是否有效的一张表。bitmap表中的两位与实际地址的16byte一一对应,因此有四种情况:
00:该地址范围没有有效的跳转地址
01:地址范围包含导出抑制表目标
10:只有16位对其的地址有效(该范围的第一个地址)
11:地址范围的所有地址均有效
因此,可以看出一个非常明显的漏洞:
在编码为11的情况在,整个16位地址均为有效跳转地址,此时,如果存在间接跳转漏洞,例如:jump [eax] (eax = 0x1007),当eax的值可以修改的时候,跳转地址可以跳转到0x1007地址的上面的指令,(add rsp, 0x40)这是Windows CFG的设计漏洞之一。其次,由于Windows CFG是前向的CFI防御,因此不能阻止重写return address的情况,则利用该特点,如果可以改写ret addr,也可以达到绕过CFG的目的。
Bypass CFG
在论文中提到了一种绕过CFG的方法,该方法和以往的绕过相比较:
需要更少的栈控制,只需要控制栈顶的区域即可
灵活性更大,在实际攻击场景中可以复现
所需gadget在Windows系统中广为存在
文章定义了两种gadget,通过两种gadget的配合可以实现绕过CFG。定义P(p)R(r) gadget:
该gadget是一个有效的CFG目标
该gadget是一条 add {e, r}sp, m 指令,可以不存在
该gadget是n条 pop 指令,可以不存在
该gadget是一条 ret r 指令,r可以为0
p = m + wn(w是一个字的长度,32-bit是4,64-bit是8)
这样一系列定义其实描述了程序函数调用结束的操作。例如上图中有效代码段
add rsp, 0x40
pop rdi
pop rbx
ret
根据定义,该gadget可以定义为P(80)R(0),p = 64 + 8 * 2, r = 0。该gadget链主要目的是更改栈帧的位置,以便修改ret addr。在32位系统中,参数传递是通过栈来传递,通过改变参数即可以影响栈中的内容,通过这种方式可以达到修改ret addr的目的。然而,在64位系统中参数传递是通过寄存器完成的,因此,栈中的内容一般不会被影响,所以重新定义了 S gadget 以便可以修改栈中内容。通常一个尾调用优化可能产生S gadget。
定义S gadget:
该gadget是一个有效的CFG目标
该gadget溢出n个寄存器到寄存器参数区域(RPA)
该gadget以一个受控制的间接跳转结束
S(2) gadget DEMO
mov [rsp + 0x8], rcx
mov [rsp + 0x10], rdx
sub rsp, 0x40
...
mov rax, [rcx]
mov rax, [rax + 0x20]
add rsp, 0x40
jmp [dispatch_fptr]
rcx, rdx一般在Windows 64-bit函数调用中是作为第一个参数和第二个参数,假设参数可以被攻击者控制,则在这段gadget中,一个被攻击者控制的参数被传入栈中,即有机会修改ret addr。
下图展示了一个PR-P链的连接过程:
Attack
如何利用PR,P gadget进行攻击,论文中以Edge作为攻击目标,实施远程攻击。首先需要知道object在内存中的地址,因此需要有地址泄露的过程,其次,我们需要伪造一些数据,要有一定的写内存操作。在攻击的Demo中,利用CVE-2016-7200和CVE-2016-7201两个Edge漏洞,达到地址泄露和任意内存读写的权限。
S(2) gadget
1 ; @ chakra+0x31f0000
2 chakra!ScriptEngine::EnumHeap:
3 mov r11, rsp
4 ; Spill arguments to RPA
5 mov [r11+0x10], rdx
6 mov [r11+0x8], rcx
7 ; Allocate stack frame
8 sub rsp, 0x28
9 ; Prepare call to rcx->__vfptr[10]
10 mov rax, [rcx]
11 mov r8, rdx
12 xor edx, edx
13 mov rax, [rax+0x50]
14 ; Deallocate stack frame
15 add rsp, 0x28
16 ; Perform indirect call via CFG
17 jmp cs:__guard_dispatch_icall_fptr
P(16)R(0) gadget
pop rdi
pop rsi
ret
目标函数是JavascriptFunction::HasInstance虚函数,首先定位JavascroptFunction object在内存中的位置,通过修改VTable pointer指向一个伪造的VTable,其中instanceof函数在VTable中的位置为0x200,将该函数修改为S(2) gadget 地址。当instanceof函数调用时即执行S(2) gadget。
S gadget 得到一个指向JavascroptFunction object的指针作为第一个参数,指向Var的指针做为第二个参数,S gadget 在第5行和第6行将参数放入栈中。第13行将JavascroptFunction偏移0x50的放入rax中,之后jmp rax执行P(16)R(0) gadget。
我们只需要设置一个自己构造的Var指针,即可截获控制流。
截获控制流之后,便可以通过传统的ROP进行攻击。
总结
论文中提出了一种新的绕过Windows CFG的方法,具有一定的灵活性,并且文章中提到在Windows系统中存在较多的可用gadget,因此,如果进行系统性的扫描Windows所有常用动态库,可以在很多场景下进行利用。
*本文作者:jaguoooooo,转载请注明来自FreeBuf.COM