频道栏目
首页 > 安全 > 系统安全 > 正文

探索Windows 10的CFG机制

2017-02-17 09:25:06      个评论      
收藏   我要投稿

探索Windows 10的CFG机制。随着操作系统开发人员一直在增强漏洞利用的缓解措施,微软在Windows 10和Windows 8.1 Update 3中默认启用了一个新的机制。这个技术称作控制流保护(CFG)。

和其他利用缓解措施机制一样,例如地址空间布局随机化(ASLR),和数据执行保护(DEP),它使得漏洞利用更加困难。毫无疑问,它将大大改变攻击者的利用技术。就像ALSR导致了堆喷射技术的出现,和DEP导致了ROP技术的出现。

为了研究这个特别的技术,我使用了Windows 10 技术预览版(build 6.4.9841)和使用Visual Studio 2015 预览版编译的测试程序。因为目前最新版的Windows 10 技术预览版(build 10.0.9926)有了一点改变,我将指出不同之处。

为了完全实现CFG,编译器和操作系统都必须支持它。作为系统层面的利用缓解措施,CFG的实现需要联合编译器、操作系统用户层库和内核模块。MSDN上面的一篇文章描述了支持CFG开发者需要做的步骤。

微软的CFG实现主要集中在间接调用保护上。考虑下面测试程序中的代码:

\

图1 - 测试程序的代码

让我们看下CFG没有启用时的代码情况。

\

图2 - 测试程序的汇编代码

在上图中,有一个间接调用。它的目标地址不在编译时决定,而是在运行时决定。一个利用如下:

\

图3 – 怎么滥用间接调用

微软实现的CFG主要关注缓解间接调用和调用不可靠目标的问题(在利用中,这是shellcode的第一步)。

不可靠的目标有明显特征:在大部分情况下,它不是一个有效的函数起始地址。微软的CFG实现是基于间接调用的目标必须是一个可靠的函数的起始位置。启用CFG后的汇编代码是怎样的?

\

图4 – 启用CFG后的汇编代码

在间接调用之前,目标地址传给_guard_check_icall函数,在其中实现CFG。在没有CFG支持的Windows中,这个函数不做任何事。在Windows 10中,有了CFG的支持,它指向ntdll!LdrpValidateUserCallTarget函数。这个函数使用目标地址作为参数,并且做了以下事情:

1. 访问一个bitmap(称为CFGBitmap),其表示在进程空间内所有函数的起始位置。在进程空间内每8个字节的状态对应CFGBitmap中的一位。如果在每组8字节中有函数的起始地址,则在CFGBitmap中对应的位设置为1;否则设置为0。下图是CFGBitmap的一部分示例:

\

图5 – CFGBitmap

2. 将目标地址转化为CFGBitmap中的一个位。让我们以00b01030为例:

\

图6 – 目标地址

高位的3个字节(蓝色圈中的24位)是CFGBitmap(单位是4字节/32位)的偏移。在这个例子中,高位的3个字节相当于0xb010。因此,CFGBitmap中指向字节单元的指针是CFGBitmap的基址加上0xb010。

同时,第四位到第八位(红色圈中的)有值X。如果目标地址以0x10对齐(目标地址&0xf==0),则X为单位内的位偏移值。如果目标地址不以0x10对齐(目标地址&0xf!=0),则X|0x1是位偏移值。

在这个例子中,目标地址是0x00b01030。X的值为6。表达式0x00b01030&0xf值为0;这意味着位偏移也是6。

3. 我们看到第二步定义的位。如果位等于1,意味着间接调用的目标是可靠的,因为它是一个函数的起始地址。如果这个位为0,意味着间接调用的目标是不可靠的,因为它不是一个函数的起始地址。如果间接调用目标是可靠的,函数将不做任何事并且直接执行。如果间接调用是不可靠的,将触发异常阻止利用代码运行。

\

图7 – CFGBitmap中的值

值X取自第4位到第8位(上面红圈中5位)。如果目标地址以0x10对齐(目标地址&0xf==0),X是单元中的位偏移值。如果目标地址不以0x10对齐(目标地址&0xf!=0),X|0x1是位偏移值。在这个例子中,目标地址是0x00b01030,X是6(图6中红色圈)。0x00b01030&0xf==0,因此位偏移是6。

在第二步中,位偏移是6。以图7为例,第6位(红圈)为1。意味着间接调用的目标是一个可靠的函数地址。

现在,我们已经有了CFG工作机制的基本认识。但是这个技巧带来了下面的问题:

1. CFGBitmap的位信息来自哪里?

2. 何时且怎么生成CFGBitmap?

3. 系统怎么处理不可靠的间接调用触发的异常?

0x01 深入CFG实现

我们能在PE文件(启用CFG的VS2015编译的)中发现另外的CFG信息。让我们看下PE文件中的图1的代码。这个信息能用VS2015的dumpbin.exe转储出来。在PE文件的Load Config Table部分,我们能找到下面的内容:

\

图8 – PE信息

Guard CF address of check-function pointer:_guard_check_icall的地址(见图4)。在Windows 10预览版中,当PE文件加载时,_guard_check_icall将被修改并指向nt!LdrpValidateUserCallTarget。

Guard CF function table:函数的相对虚拟地址(RVA)列表的指针,其包含了程序的代码。每个函数的RVA将转化为CFGBitmap中的“1”位。换句话说,CFGBitmap的位信息来自Guard CF function table。

Guard CF function count:函数RVA的个数。

CF Instrumented:表明程序中启用了CFG。

在这里,编译器完成了CFG的整个工作。剩下的是OS的支持使CFG机制起作用。

1. 在OS引导阶段,第一个CFG相关的函数是MiInitializeCfg。这个进程是system。调用堆栈如下:

\

图9 – 调用堆栈

MiInitializeCfg函数的前置工作是创建包含CFGBitmap的共享内存。调用时间可以在NT内核阶段1内存管理器初始化时找到(MmInitSystem)。如你所知,在NT内核阶段1的初始化期间,它调用MmInitSystem两次。第一个MmInitSystem将进入MiInitializeCfg。那么MiInitializeCfg做了什么?

\

图10 – 函数的主要逻辑

步骤A:注册表值来自HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel: MitigationOptions

步骤B:MmEnableCfg是一个全局变量,它被用来表示系统是否启用CFG功能

步骤C:MiCfgBitMapSection的DesiredAccess允许所有的权限;它的分配类型是“reserve”。在build 10.0.9926和build 6.4.9841中共享内存的大小是不同的。对于build 6.4.9841,它按用户模式空间大小计算。表达式是size=User Mode Space Size>>6。(>>X:右移X位)。对于build 10.0.9926,这个大小是0x3000000。CFG bitmap能表示整个用户模式空间。MiCfgBitMapSection是CFG实现的核心组件,因为它被用来包含CFGBitmap。

2. 获得压缩RVA列表信息的函数且保存到映像的Control_Area结构。

PE映像第一次加载到系统。NT内核将调用MiRelocateImage来重定位。MiRelocateImage将调用MiParseImageCfgBits。在函数MiParseImageCfgBits中,PE映像的压缩的RVA列表被计算且存储在PE映像节中的Control_Area数据结构。在系统引导期间一个PE映像只发生一次。

当PE再次加载进进程,NT内核将调用MiRelocateImageAgain。因为它的压缩的RVA列表已经保存了(且不需要再次计算),MiRelocateImageAgain不需要调用MiParseImageCfgBits保存一些进程的时间。MiParseImageCfgBits被用来计算压缩的RVA列表以便在小的空间中保存RVA列表。微软实现CFG有时间和空间的消耗。在MiRelocateImage中,它的CFG相关的部分被如下描述:

\

MiParseImageCfgBits被用来计算启用CFG编译的模块的压缩的RVA列表。在深入这个函数之前,我们将看一下这个函数调用的上下文。函数MiParseImageCfgBits将在MiRelocateImage函数中调用。

函数MiParseImageCfgBits有5个参数:

a) 映像节的Control_Area结构的指针

b) 映像文件内容的指针

c) 映像大小

d) 包含PE可选头结构的指针

e) 输出压缩的CFG函数RVA列表的指针

MiParseImageCfgBits的主要工作如下:

a) 从映像的Load Config Table获得函数RVA列表

b) 使用压缩算法压缩列表,以便在小空间保存列表

c) 创建压缩的RVA列表作为输出

3. 在CFGBitmap共享内存对象被创建后,CFGBimap共享内存对象被映射来作为两种用途:

a) 用来写共享模块(.DLL文件等)的bits。这个映射是临时的;在bits写入完成后,映射将释放。通过这个映射写入的bits信息是共享的,意味着它能被操作系统内所有的进程读取。这个映射发生在MiUpdateCfgSystemWideBitmap函数中。调用堆栈如下:

\

图11 – 调用堆栈

b) 用来写私有的bits和读取校验间接调用的bits。通过这个映射写入的bits是私有的,意味着它只能被当前进程读取。这个映射的生存周期与进程的生命周期相同。这个映射发生子MiCfgInitializeProcess中,调用堆栈如下:

\

图12 – 调用堆栈

基于调用堆栈,我们知道它在一个正在初始化的进程中被映射。Build 10.0.9926和6.4.9841的映射大小是不一样的。对于6.4.9841,大小是基于用户模式空间大小计算的。表达式为size=User Mode Sapce Size>>6(>>X:右移X位)。对于10.0.9926,这个大小是0x3000000。映射的空间在进程生命周期内总是存在的。映射的基址和长度将被保存在类型为MI_CFG_BITMAP_INFO的结构体中,且地址被修改了(在6.4.9841中,基址是0xC0802144。在10.0.9926中,是0xC080214C)。我稍后将讨论怎么将私有的bits写入映射空间中。MI_CFG_BITMAP_INFO的结构如下:

\

4. 一旦PE映像的RVA列表准备好了且CFGBitmap节也映射了,就可以将RVA列表翻译为CFGBitmap中的bits。

\

图13 – 更新CFGBitmap的bits

上一篇:深入分析Win32k系统调用过滤机制
下一篇:系统安全之如何编译Android内核Hook系统调用
相关文章
图文推荐

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训

版权所有: 红黑联盟--致力于做实用的IT技术学习网站