windows现代漏洞缓解机制-CFG&CET&XFG
1.CFG/KCFG
控制流防护CFG,内核中的实现称为KCFG,在windows10和windows8.1中默认启用的一种新的漏洞缓解机制.最低的编译器版本是VS2015,它的出现将会改变漏洞利用技术,就像ALSR通过堆喷绕过,DEP通过ROP技术进行绕过一样.
CFG的实现对函数间接的调用进行了保护.
通常来说调用一个对象的内部函数时,通过虚函数表进行调用,它所调用的目标地址不是在编译时就确定的,而是在运行时决定的.漏洞利用者可以通过一些漏洞对函数地址进行覆盖,当发生调用时就会执行到shellcode.
1.开启/未开启CFG对比
我使用的环境是visual studio 2022,测试代码如下,我创建了一个函数指针数组fun_array,存储了2个函数test1,test2,后面通过对fun_array对这两个函数进行了间接的调用.
1 |
|
1.未开启CFG
直接编译后,调用过程如下,可以看到在间接调用时没有任何的防护,如果使用任意地址读写原语对fun_array进行篡改可以很轻松的完成利用.
2.开启CFG
下面我开起了控制流防护,重新编译
我编译时出现了不兼容的情况,需要把调试信息格式修改成”无”
编译后如下,通过__guard_dispath_icall_fptr进行了调用,实际内部调用的函数ntdll!LdrpDispatchUserCallTarget对调用函数目标地址进行了验证,这跟我之前文章分析的是一致的.
这里调用的是_guard_dispath_icall_fptr,需要讲下__guard_dispath_icall_fptr和_guard_check_icall.
_guard_check_icall是执行检查的函数,检查通过才会调用到我们的函数,调用的操作再_guard_check_icall函数外.
因为函数执行现在直接放到了__guard_dispath_icall_fptr里面,而不用先check再调用了,少了一个call,验证通过后会直接jmp eax,跳转到目标函数,其实没什么区别.
2.原理
下图是一个开启了CFG防护的程序它的调用过程.
在调用之前,调用目标的地址esi,会被传递到_guard_check_icall函数中,这也是CFG防护执行的位置,这个函数指向ntdll!LdrpValidateUserCallTarget函数.
在LdrpValidateUserCallTarget函数中,会首先获取一个位图CFGBitmap,这个位图表示进程空间中所有函数的起始位置,进程空间中每8个字节的状态对应CFGBitmap的一个位.如果8个字节中有一个函数起始的地址,则CFGBitmap中的相应位会被置1.
所以它是用这个CFGBitmap进行调用地址的验证的,如果对应的值位1,那么表示调用的目标地址是有效的,如果为0那么就是无效的.有效不做处理,无效调用LdrpHandleInvalidUserCallTarget.
这其中涉及到了将调用地址转换为CFGBitmap中某一位的过程,最高的3个字节是CFGBitmap中的偏移量.最后的1个字节中的4-8位,如果目标地址未与0x10对齐,也就是0xf&目标地址!=0,那么4-8位的值|1就是位偏移值.
3.实现
使用dumpbin,在PE文件的LoadConfigTable中可以看到关于CFG的一些信息
Guard CF function table:指向了函数RVA列表的指针,其中每个RVA被转换为CFGBitmap中的1,CFGBitmap中的位信息,都是来源于这张表.
Guard CF Function count:表示RVA列表中记录的数量
Guard Flags:CF instrumented表示该程序已经启用CFG
在操作系统启动阶段,会通过执行MilnitializeCfg对CFG位图的共享内存进行创建.
它首先会查询注册表项SYSTEM\CurrentControlSet\Control\Session\Manager\kernel中MitigationOptions并设置全局变量MmEnableCfg,控制系统是否开启CFG功能.
然后创建共享内存MiCfgBitMapSection,这个共享内存是根据用户模式空间大小计算得出的,这意味着CFG位图可以表示整个用户空间.MiCfgBitMapSection包含了CFGBitmap.
后面内核会对加载到系统的PE文件,进行读取和计算的操作,写入CFGBitmap.
在初始化和处理完成后,CFGBitmap的地址需要映射到用户层进行使用,内核会调用PspPrepareSystemDllInitBlock函数,将CFGBitmap映射地址和长度写入到全局变量的字段中,全局变量的数据结构就是PsSystemDllInitBlock结构.它是一个固定地址,可以从用户模式和内核模式中访问.
在用户模式下,可以获取PspSystemDllInitBlock全局变量中的CFGBitmap字段硬编码.
最后在调用LdrpCfgProcessLoadConfig中将_guard_check_icall指向ntdll.dll!LdrpValidateUserCallTarget.
如果检查到地址无效,会调用函数LdrpHandleInvalidUserCallTarget,该函数主要作用检查dep,执行0x29中断,对应的中断处理函数是KiRaiseSecurityCheckFailure,作用是停止进程.
影响判断结果的最直接方法是修改CFGBitmap,但是从上面分析的结果来看,CFGBitmap是内核映射过来的只读内存,无法修改.
4.存在的一些绕过方式
1.通用的任意地址读写的绕过方法
如果漏洞可以构造任意地址读写的原语,那么可以修改掉堆栈的返回地址,劫持控制流
在CFG中,返回的地址并没有受到CFG的保护,只是保护了间接调用的目标地址.
2.通过RPC的方式绕过
在我之前分析的CVE-2021-26411的exp中,其中使用了RPC的方式去绕过CFG的保护,通过构造RPC_MESSAGE去调用一些函数完成RCE,不过前提是需要任意地址读写原语.
3.通过合法的函数地址进行覆盖
在前面讲过CFG会通过检查CFGBitmap确认是否是一个合法的地址,但是在调用时函数时,使用一个记录在CFGBitmap中的函数地址对它进行覆盖,是不会被中断的,依然可以正常执行.如果使用WriteProcessMemeory的函数地址或者其他敏感的函数地址进行覆盖,可能会造成一些成功的利用,当然,这些前提是,函数地址是要记录在CFGBitmap里面的.
关于这种情况,其实微软也考虑到了,并进行了缓解,这种技术称为XFG,在后面会进行介绍.
2.CET
CET控制流保护技术是intel引入的基于硬件的缓解机制,它弥补了CFG的缺陷,对函数的返回地址进行了保护.
最初微软使用的缓解方式名为RFG,与CET类似.
在win10 19h1开始支持了CET技术,但是必须在11代以上intel cpu上才能使用.
CET的技术主要有2部分组成shadow stack和indirect branch tracking
ROP/JOP/COP
在CET引入后,ROP的攻击手法被完全缓解,因为ROP需要依赖ret指令执行堆栈上的后续指令的地址,就不能在堆栈中构造ROP链.
JOP表示Jump Oriented Programming
COP表示Control Oriented Programming
这两种漏洞利用技术其实和ROP非常类似,ROP借助ret指令作为控制流的控制.
JOP通过跳转JMP指令进行控制流的控制,比如通过一个任意地址读写的原语修改jmp指令的跳转目标,使程序跳转到shellcode处.
COP通过call指令进行控制流的控制,通过任意地址读写原语修改call的目标进行劫持控制流.
shadow stack
在shadow stack中存储了每个函数的返回地址,当程序执行函数调用时,会将返回地址记录到shadow stack中,这个shadow stack是记录在内核中的,ring3无法访问,在函数的末尾处,会将shadow stack中记录的返回地址映射与当前返回地址进行对比,如果返回地址被篡改了,这里就会直接检测到.
indirect branch tracking
间接分支跟踪,简称IBT,防止利用者修改间接分支指令指向任意地址.间接分支指令指的是从内存位置获取目标地址的指令.通过指令endbr32/endbr64实现.
它的工作原理是通过endbr32和endbr64在跳转之间做一个标记,在进行跳转时,cpu会判断下条指令是否为endbr32/endbr64,若并不是则会触发异常.
在win11中启用了该功能,在注册表项HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\DeviceGuard\Virtualization\SecurityEnhancements\IsolateProcess\CET IBTEnabled
3.XFG
前面在讲CFG的绕过方式时,提到了XFG技术.
在调用函数时,使用一个记录在CFGBitmap中的函数地址对它进行覆盖,是不会被中断的,依然可以正常执行.这是CFG的一个缺陷.即使有了CFG的保护,函数地址仍然可能会被其他函数所覆盖,造成无法控制和预知的情况,可能会对漏洞利用提供帮助.
XFG就是用来解决这个问题的,它对调用的函数通过函数的类型生成一个hash,在调用时进行匹配,如果匹配不成功,就会发生崩溃.
首先,我来编译一个启用了CFG的程序,代码如下,我新增了一个与前面类型不同的函数test3进行调用.
1 |
|
我在编译这里遇到了问题,我使用了visual studio 2022 preview版本,系统是win11 22h2,通过Visual studio编译或者通过ci编译,虽然都可以编译成功,也会生成hash,但是内部调用的却不是xfg对应的验证函数.我觉得是因为我的系统不是preview版本导致的.预览版本我无法下载,只能静态分析了.
编译和链接选项中都配置/guard:xfg
就可以了.
编译后的程序如下,__guard_xfg_dispatch_icall_fptr肯定就是xfg保护的验证函数,在调用之前,它传递了2个值,在r10寄存器传入了一个hash,在rax寄存器传递了调用目标地址.可以很明显的看到在调用相同类型的函数时,test1和test2 hash生成的是一样的,test3函数的hash与test1,test2不同.
内部调用的函数未ntdll!LdrpDispatchUserCallTargetXFG
//todo 暂时搁置.