CFG防护机制的简要分析 您所在的位置:网站首页 checkesp CFG防护机制的简要分析

CFG防护机制的简要分析

2023-04-04 21:57| 来源: 网络整理| 查看: 265

CFG防护机制的简要分析

f01965 / 2018-08-16 13:37:52 / 浏览数 13099 社区板块 技术讨论 顶(1) 踩(0) 0.概述

本文将介绍一下Windows上的CFG防护技术。 主要会讲一个 demo,自己动手写一个有CFG保护的程序,然后跟踪调试。 接着会调试 CFG 在IE , Edge上出现的情况。 最后提一些曾经所使用的bypass方法。

1.CFG简介

微软在Windows10和Windows8.1Update3(2014年11月发布)系统中已经默认启用了一种新的机制- Control Flow Guard (控制流防护)。 这项技术通过在间接跳转前插入校验代码,检查目标地址的有效性,进而可以阻止执行流跳转到预期之外的地点, 最终及时并有效的进行异常处理,避免引发相关的安全问题。 简单的说,就是在程序间接跳转之前,会判断这个将要跳转的地址是否是合法的。 如下: ① 没有CFG保护 可以看到有段 call ecx 的间接调用,而ecx 中地址,由: mov eax , [ebp+var_8] mov ecx , [eax+edx] 得: ecx = [ [ebp+var_8] + edx ] 注:下面还有一个 call j_RTC_CheckEsp ,这个RTC_CheckEsp 函数是用来检查堆栈是否平衡的。简单的说,函数在调用之前会把 esp 保存在edi/esi 等寄存器中;当函数调用完之后,RTC_CheckEsp 会去检查这个时候的esp值与之前保存在edi/esi 中的值是否一致;不一致说明esp被改动了,堆栈上存在数据溢出,就会丢出一个错误。 ②启用 CFG 保护: 这里的间接调用是最后一句 call [ebp+var_14] 。在这个调用之前,可以看到: mov ecx , [ebp+var_14] call ds: guard_check_icall_fptr 这个里先把下面要调用的地址[ebp+var_14] 放到了 ecx 里面,然后去调用 guard_check_icall_fptr;而 guard_check_icall_fptr 就是CFG保护开启才有的保护函数;这个函数里面,将会去判断 ecx 这个地址里的调用函数是不是一个合法的函数。 然后有 RTC_CheckEsp ,最后才是间接调用。

2.demo 2.1 环境&工具&源码

前面了解了CFG的大概情况,那么这里通过自己写一个简单的程序,进行调用调试分析。 环境与工具:windows 10 pro , Visual Studio 2017 , windbg , IDA pro 源码: 出自这里

typedef int(*fun_t)(int); int foo(int a) { printf("hellow world %d\n",a); return a; } class CTargetObject { public: fun_t fun; }; int main() { int i = 0; CTargetObject *o_array = new CTargetObject[5]; for (i = 0; i > 8 = 0x0000b613 //这里右移8位,就相当于取高3字节。 edx = [ 0x00b80000 + 0x0000b613*4 ] = 0x10100444 所以:CFGbitmap = 0x10100444 ,也就是前面讲原理说到的值。

下面,来验证:

eax = ecx = 0x00b613a0 -> eax >> 3 = 0x0016c274 test cl ,0Fh // 0xa0 & 0x0F = 0 ZF = 0 // ZF 为0 ,jne不跳转,说明以0x0f对齐 bt edx, eax /* bt 指令把指定的位传送给CF标志寄存器,这里的意思就是把 edx 的某一位放到CF里,这个某一位由eax的值确定,那eax的值呢?就是 eax mod 0x20 的值。 为什么是mod 0x20呢,这是根据目的操作数来确定的,也就是edx, edx 是32位寄存器,0x20 = 32 (d)。 也就是说eax的值需要mod上对应的寄存器位数。且目的操作数只能是16/32位通用寄存器或存储单元 为什么eax 右移3位,然后用bt 指令就能达到取最低字节的前5位 作为偏移值的效果呢? eax = 0x00b613a0 0000 0000 1011 0110 0001 0011 1010 0000 eax >> 3 // 最低字节的最后8位移走3位 ,剩下5位 0000 0000 0001 0110 1100 0010 0111 0100 这剩下的5位,相当于二进制5个1: 11111 = 31 (十进制) ,就做到了 eax对 0x20 取模,模值就是偏移值。 所以: shr eax, 3 bt edx, eax 这2条指令就做到了取最低字节的前5位的作为偏移值,并放入CF标志寄存器中,然后通过一个jae跳转实现对函数地址的合法性进行检查。CF = 0,就说明地址不合法,跳转到异常处理去;反之就正常返回。 */ 4.2 错误处理

让程序断在相同的位置: 修改ecx : 00b613b0 修改ecx 为00b613b0 之后,可以发现下面的情况: 也就是说当正常的地址00b613a0,被改为 00b613b0的时候,虽然这不是一个合法的地址,但是在CFG检查的时候,第一次CF标志位被设为0,通过jae跳转到到了0x77289bfe Or eax , 1

但是,根据我们前面的分析,当地址被修改为 00b613a0 时,这不是一个正常的函数地址。CFG函数检查之后,CF标志位被置0,接下来应该是抛出异常。但根据windbg调试来看,并不是这样的流程。而是跳转到0x77289bfe ,然后继续执行:

Or eax, 1 // eax = 0x0016c276 // eax = 0x0016c276 or 1 = 0x0016c277 bt edx, eax - > CF = 0 jae 跳转

这里为什么会在进行一次or 1的操作呢? 后面笔者参考了很多其他资料,得出下面的结论(仅个人理解,若出现错误望大家斧正):

win10新增一个功能可以抑制导出,意思是现在导出函数能在CFG保护的调用位置被标记为非法目的地址。注意,导出函数本身是正常合法的函数。这一功能的实现需要使用CFG Bitmap中每一个地址的第二位,以及在初始化每个进程的bitmap时guard_fids_table中每一个RVA条目的一个标记字节。 正常的受限的导出函数现在用的2个标志位来检查的。 就像上面的情况,第一次检查CF = 0 ,这个时候检查的地址可能是非法的,但也可能是正常的受限的导出函数,若是受限的导出函数,那它在CFGbitmap中用2位标识,且这2位的情况就是 10 ,所以进行第二次检查的时候,就会检查到“1“,那这就是个正常的函数调用过程。所以,不会抛出异常。这样一些导出表将会在进行创建时以不合法的间接调用开始,但最终在运行时代码中成为合法的调用目的地址。 另外:根据上面的这种情况分析可知00b613a0 = 00b613e0 = 00b61330 这3个地址是等价的,虽然正确的地址是00b613a0 ,但它们都能通过CFG的检查,不影响程序的执行,并且打印出hello world字符串。 4.3 int 29

当我们提供一个错误的地址,我们尝试跟踪完整的中断过程,看看CFG是怎么做的。 地址:0x77289c07

跟进 ntdll!RtlpHandleInvalidUserCallTarget: 继续 ntdll!NtQueryInformationProcess: ntdll!ZwQueryInformationProcess+0x1f (7726e90f):

最终调用的函数层次比较深,用下图进行说明: 最后中断在: 0x77289d10 int 29h

5.Edge-x64 / IE 11(32/64)

下面就简单的调试下CFG在Edge,IE11下的情况。

5.1 Edge x64

使用dumpbin /headers /loadconfig xxx.exe ,查看Edge开启的保护: 在IDA的情况下: 下断点跟踪: 从上图看汇编代码,可以发现与我们的demo程序的情况是一致的,区别就是64位的系统,地址的第9-63位被用于在CFGbimap中检索一个qword,第3-10位被用于(模64)访问qword中某一指定位:

5.2 IE32

调试时候先找一个有CFG保护的函数,确定一下函数名,然后下断点:

笔者找的红框的2个函数,下面用initterm_e 说明就行。 iexplore!_initterm_e 的情况如下:

进行简单的计算一下:

iexplore!_onexit: eax = ecx = 0x73e18630 … edx = 0x80080082 =1000 0000 0000 1000 0000 0000 1000 0010 eax >> 3 = 0x0e7c30c6 0x0e7c30c6 mod 0x20 = 0x00000006 CF = 0

同样,这里也会比对CFGbitmap中的2位。道理如同上面所讲,这里不再赘述。

5.3 IE64

依然查看它的保护情况:18 h = 24d 在有CFG保护的位置下断,由于我是直接开启iexplore.exe,能断下的函数只有下图bsearch的位置,其他在开启IE过程中,都断不下来,不过并不影响什么: 跟踪情况:

从上面的跟踪可以知道,无论是demo,Edge,或是 IE ,最终都会来到ntdll中实现CFG检查,无论是32位还是64位,检查过程都是一致的。

6.bypass部分

这里bypass只列出一部分,也会简单的说明一下。

6.1 CVE-2015-0311

利用这个CVE可以先达到任意内存读写。 然后Adobe Flash Player二进制文件中有很多间接调用,但是都有CFG保护。 到目前为止,CFG在保护这个Adobe Flash Player二进制文件中的29,000多个间接调用方面做得非常出色。 但是需要找一个不受CFG保护的间接调用。 Flash Player包含一个即时(JIT)编译器,该编译器将虚拟机字节码转换为本地代码以提高执行速度。由JIT编译器生成的代码包括间接调用, 并且由于此代码是在运行时生成的,这意味着其中的间接调用不受CFG的保护。 首先有一个Vector容器,且长度很大,这个长度将允许我们从进程的地址空间内的任何内存地址读/写。 然后有一个ByteArray对象,我们的ROP链存储在Vector的第一个元素。 这个ByteArray对象+ 8的位置是一个指向VTable对象的指针 VTable_object 里面又包含很多其他指针。 在 VTable_object + 0xD4 处有一个指针,指向MethodEnv对象 MethodEnv对象第一个双字是一个指向它自己的的vtable的指针,而第二个双字是一个函数指针。 通过观察发现 MethodEnv+4处的指针,是间接调用实现的。取消引用了存储在VTable_object + 0xD4 处的 MethodEnv 对象的指针 也就是说,本应该是 通过 VTable_object + 0xD4 + 偏移 去调用 MethodEnv 第二个双字处的函数指针,然后去使用。 可以从图上看到,它是通过mov mov 然后取到了这些需要调用的位置。 此间接调用来自Flash JIT编译器生成的代码,通过调用ByteArray对象上的toString()方法,可以可靠地触发这种无保留的间接调用。

6.2 BlackHat 2015

CustomHeap::Heap对象 , 改写guard_check_icall_fptr,指向合适的函数。

利用jscript9中的CustomHeap::Heap对象将 guard_check_icall_fptr 变成可读写的:

CustomHeap::Heap::FreeAll 为每个 Bucket 对象调用 CustomHeap::Heap::FreeBucket CustomHeap::Heap::FreeBucket 遍历 Bucket 的双向链表,为每个节点的 CustomHeap::Page 对象调用CustomHeap::Heap::EnsurePageReadWrite 。 CustomHeap::Heap::EnsurePageReadWrite 。 用以下参数调用 VirtualProtect :主要是 VirtualProtect ,可以把一个只读页面改写成可读可写的。 有4个参数如下: lpAddress: CustomHeap::Page 对象的成员变量address dwSize: 0x1000 flNewProtect: PAGE_READWRITE // 请求的保护方式 flodlProtect: // 保存旧的保护方式 将内存页面标记为PAGE_READWRITE ,就能达到修改CFG函数的ptr指针的目的。从而绕过CFG。

patch:补丁如下图 微软引入了一个新的函数HeapPageAllocator::ProtectPages。 这个函数是VirtualProtect的一个封装,在调用VirtualProtect之前对参数进行校验,如下: 检查lpAddress是否是0x1000对齐的; 检查lpAddress是否大于Segment的基址; 检查lpAddress加上dwSize是否小于Segment的基址加上Segment的大小; 检查dwSize是否小于Region的大小; 检查目标内存的访问权限是否等于指定的(通过参数)访问权限; 任何一个检查项未通过,都会调用CustomHeap_BadPageState_fatal_error抛出异常而终止进程

6.3 MS16-063

微软在MS16-063 中发布的补丁:修复 jscript9.dll 与 TypedArray 和 DataView 相关的内存损坏漏洞。 这个漏洞简单来说: 就是先泄漏 vftable 的基地址,构建一个包含里 shellcode 的 rop 链, 用假的返回地址覆盖 vftable 的地址。然后 call Uint8Array.subarray ,就执行了 shellcode 但是在windows10 里面, 调用 vftable 前会检查这个地址是不是合法的,我们之前替换这个地址,所以当然不合法。就会抛出异常。那么绕过的方法:

CFG 是不会保护堆栈上的数据的 现在,要绕过CFG检查。 使用的函数是 RtlCaptureContext,它有个指向Context结构体的参数。 因为Context转储了所有寄存器,且输入值仅是一个缓冲区的指针。 因为 TypedArray 中 0x7c 处有 RtlCaptureContext 的地址, 0x20 处是 TypedArray 指向的真实数据的指针。 通过 TypedArray 去泄露 RtlCaptureContext 的地址,由于 RtlCaptureContext 在 ntdll 中存在,进而泄露 ntdll 的地址。 执行次操作的默认路径是使用的vtable地址,它是一个指向jscript9.dll的指针,这个指针往回走0x1000自己,可以找到MZ头,继而查到 kernelbase.dll 的导入表。然后做同样的操作,获得 kernelbase.dll 基地址 简单的说: 使用 TypedArray 对象 泄露 RtlCaptureContext 的地址 -> Context结构体 -> 转储了所有寄存器 根据 RtlCaptureContext -> 泄露 ntdll 的基地址 -> kernelbase.dll 的基地址 -> 泄露堆栈地址 -> 覆盖自己的函数返回地址。 泄露堆栈地址,改写返回地址。

6.4 MFC40.dll

采用 stack pivot 绕过 CFG 先让程序加载没有使用CFG保护的 dll -> MFC40.dll 通过 PEB 去寻找 MFC40.dll 的基地址 构造 rop 链 通过 mv.subarray(pivotGadgetAddr) 执行到rop

7. 参考文献

1.如何绕过Windows 10的CFG机制 http://www.freebuf.com/articles/system/126007.html 2.探索Windows 10的CFG机制 https://www.anquanke.com/post/id/85493 3.使用最新的代码重用攻击绕过执行流保护 https://bbs.pediy.com/thread-217335.htm 4.About CVE-2015-0311 https://www.coresecurity.com/blog/exploiting-cve-2015-0311-part-ii-bypassing-control-flow-guard-on-windows-8-1-update-3 http://www.freebuf.com/vuls/57925.html 5.http://blog.nsfocus.net/win10-cfg-bypass/ 6.Bypassing Control Flow Guard in Windows 10 https://improsec.com/blog/bypassing-control-flow-guard-in-windows-10 https://improsec.com/blog/bypassing-control-flow-guard-on-windows-10-part-ii 7.敏感的API绕过CFG https://blog.trendmicro.com/trendlabs-security-intelligence/control-flow-guard-improvements-windows-10-anniversary-update/

点击收藏 | 1 关注 | 1 上一篇:Windows利用技巧系列之利用任... 下一篇:CVE-2018-8373:VBS...


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有