首页
论坛
课程
招聘
[翻译]CVE-2012-0148: A Deep Dive Into AFD
2012-2-24 20:17 5449

[翻译]CVE-2012-0148: A Deep Dive Into AFD

2012-2-24 20:17
5449
       原文:http://mista.nu/blog/2012/02/17/cve-2012-0148-a-deep-dive-into-afd/
       翻译:http://hi.baidu.com/promised_lu/
       演示:http://www.youtube.com/embed/F0JZT5NWAM0?fs=1&feature=oembed
       欢迎大家指正翻译中的错误

       Introduction

       辅助功能驱动程序(AFD)Windows Sockets接口的内核管理层。与许多其他流行的操作系统类似,WindowsAFD把网络连接作为套接字管理。套接字模式在Windows中被改编为Windows Sockets 1.1,并支持大部分传统上能在BSD中找到的基本功能。在这一设计背后的主要目标是让Unix开发者更容易地将现有的网络应用程序移植到Windows平台。微软之后又发布了Windows Sockets 2架构以兼容Windows开放式系统体系结构。Winsock 2.0定义了应用程序编程接口(API)WS2_32.dll的导出函数和协议栈之间的标准服务提供商。这允许Winsock 2.0支持多种传输协议,然而Winsock 1.1仅支持TCP/IP协议。在Winsock标准之上,Windows还包含了主要提升性能(例如TransmitFile)和节约内存使用的额外扩展以及提供重用套接字功能的操作(例如ConnectExAcceptEx)

       Winsock Architecture

       Winsock架构被分为用户模式和内核模式。在最高层,应用程序与Windows Sockets API (ws2_32.dll)交互,它提供了例如bindlistensendrecv的常见函数。这些函数调用合适的提供商,它不是由系统定义的就是由应用程序定义的。举个例子,VMCI套接字可以被VMware虚拟机用来与其他虚拟机通信。这里,VMCI提供商(VMware Tools安装)调用VMCI套接字库(vsocklib.dll),它然后调用负责管理和呼叫主机/其他虚拟机的VMCI驱动程序(vmci.sys)。类似的,Windows为例如TCPUDP的本地支持协议注册了一个Winsock API的提供商(mswsock.dll)Winsock然后通过辅助功能驱动程序(afd.sys)进行必要的套接字管理和TCP/IP协议调用。
       虽然Windows的网络架构在Windows Vista中有了显著的变化,但应用程序与Windows Sockets APIAFD之间的交互基本保持不变。


Winsock架构


       Socket Management

       辅助功能驱动程序,即afd.sys,负责创建和管理Windows中的套接字终端。在内部,Windows用一个文件对象代表套接字,文件对象还包含了记录例如连接是否已建立的状态信息的额外属性。这允许应用程序使用标准读写API操作套接字句柄来访问和传输网络数据。
       AFD初次初始化时,它创建AFD设备(Winsock应用程序访问)并设置相关的驱动对象的IRP处理函数。当一个应用程序初次创建一个套接字时,IRP_MJ_CREATE例程(afd!AfdCreate)调用函数(afd!AfdAllocateEndpoint)分配一个终端数据结构。这是一个定义每个终端的每个属性的未公开结构,当然也包含了终端所处的状态(例如是否正在建立连接、监听或释放连接)。操作终端数据结构的AFD函数(例如sendreceive函数)在执行前通常会校验终端状态。此外,一个指向终端数据结构自身的指针被保存在套接字文件对象的FsContext字段,以便AFD可以容易地定位套接字管理数据。AFD还通过一条双向链表(afd!AfdEndpointsList)记录所有终端,因此可以判断一个地址是否已被绑定。
       AFD内部的主要功能通过设备I/O控制处理函数来被使用。AFD还提供了快速设备I/O控制例程以处理直接来自缓存管理器而不需要产生一个IRP的请求。AFD试图尽可能地使用快速I/O分发例程,特别是在读写网络数据的时候。然而,在一些需要更多扩展操作的情况下,它会回过来使用基于IRP的机制,比如在数据报大小超过2048个字节,或一个连接(终端)不在所期望的状态时。
       当研究AFD中的函数,特别是那些被分发I/O控制例程调用的函数的时候,需要重点注意直接对请求的校验。实际上,研究大部分AFD中的函数,你可能会多次注意到与例如0xAFD00xAFD10xAFD2等常量的校验(比较)。这些是描述活动套接字终端/连接的常量,它们被存放在终端数据结构的第一个16位字段中。举个例子,0xAFD1表示代表数据报套接字的终端,而0xAFD00xAFD20xAFD40xAFD6表示代表TCP套接字各种状态的终端。

       AFD!AfdPoll Integer Overflow Vulnerability (CVE-2012-0148)

  Windows Sockets API允许应用程序通过select函数查询一个或更多套接字的状态。在内部,每当AFD设备发出I/O控制号0x12024时,这些请求被AFD.sys驱动程序的afd!AfdPoll函数处理(在内部调用afd!AfdPoll32还是afd!AfdPoll64取决于产生I/O请求的进程)。这一函数操作一块用户提供的poll信息(AFD_POLL_INFO)缓冲区,它包含了所有要被查询的套接字记录(AFD_HANDLE)。这些结构的定义如下(基于ReactOS)
typedef struct _AFD_HANDLE_ {
        SOCKET Handle;
        ULONG Events;
        NTSTATUS Status;
} AFD_HANDLE, *PAFD_HANDLE;
 
typedef struct _AFD_POLL_INFO {
        LARGE_INTEGER Timeout;
        ULONG HandleCount;
        ULONG Exclusive;
        AFD_HANDLE Handles[1];
} AFD_POLL_INFO, *PAFD_POLL_INFO;

       在接收数据之前,AFD调用afd!AfdPollGetInfo分配第二块缓冲区(来自非分页内存池)用来存放返回的要查询的单独的套接字信息。特别地,每个AFD_HANDLE记录都由这块内部缓冲区结构(我们称为AFD_POLL_ENTRY)中的AFD_POLL_ENTRY记录表示。我们定义这些未公开结构如下。
typedef struct _AFD_POLL_ENTRY {
        PVOID PollInfo;
        PAFD_POLL_ENTRY PollEntry;
        PVOID pSocket;
        HANDLE hSocket;
        ULONG Events;
} AFD_POLL_ENTRY, *PAFD_POLL_ENTRY;
 
typedef struct _AFD_POLL_INTERNAL {
        CHAR Unknown[0xB8];
        AFD_POLL_ENTRY PollEntry[1];
} AFD_POLL_INTERNAL, *PAFD_POLL_INTERNAL;

       在操作用户提供的缓冲区(AFD_POLL_INFO)来查询每个单独的套接字之前,afd!AfdPoll保证这块缓冲区足够大以配合HandleCount值表示的记录数。如果大小太小,函数返回大小不足错误。这虽然阻止了用户模式代码传递伪造的HandleCount值,但它没有考虑到实际上AfdPoll函数内部分配的记录大小超过了所提供的poll信息缓冲区。举个例子,Windows 7 x64AFD_HANDLE项目的大小是0x10个字节,而内部分配的对应的项目(AFD_POLL_ENTRY)的大小是0x28。额外的0xB8个字节也被加入用来存放poll函数内部使用的元数据。当记录足够多时,这一大小差异可能导致这样一种情况:AFD认为用户模式传递的poll信息缓冲区足够大,但在计算内部缓冲区大小时触发了整数溢出。


AfdPoll64中的整数溢出

       一旦函数继续查询每个单独的套接字并填充到他们在偏小的缓冲区中对应的AFD_POLL_ENTRY记录时,再用原来的HandleCount值判断要操作的记录数就会产生内存池溢出。

       Exploitability

       一个漏洞造成的安全影响更多得依赖于它的可利用性。为了利用所描述的漏洞,我们需要理解漏洞如何触发以及受影响的模块如何与例如内核内存池分配器的系统组件进行交互这两个方面。
       为了触发漏洞,我们首先需要分配足够的内存以保证乘法/常量加法产生整数溢出。这是因为AFD在内部通过用户提供的缓冲区大小除以每个记录(AFD_HANDLE)的大小来判断所提供的计数(HandleCount)是否一致。在x64 (Win7)下,0x6666662个元素足够触发整数溢出,意味着需要传递一块大小是0x10 + (0x6666662 * 0x10)的用户模式缓冲区给驱动程序。这表示I/O管理器还需要在一块1638MB大小的内部内核缓冲区用来缓存,因为受影响的IOCTL使用了METHOD_BUFFERED。在x86 (Win7)下,必须分配一块大小是0x99999970 (((0x100000000 – 0x68 / 0x14) * 0xC) – 0x10)的用户模式缓冲区。由于这只在/3GB配置下可行,而内核需要一块相同大小的内存用来缓存,我们不认为这一漏洞能在32位系统下被实际利用。
       由于这一漏洞导致越界复制到一块偏小的内存池配额中,我们还需要了解足够多关于内存池分配器和它的内部工作原理的知识。当遇到内存池溢出时,一个最重要的问题是攻击者是否可以控制要写入所分配的缓冲区外的元素的个数。由于内核内存池被整个系统共用,任何内存破坏都可能潜在地影响系统稳定性。在大多数情况下,漏洞代码会使用刚好触发整数溢出的计数值,因此可以复制元素直到命中一个在原缓冲区或目的缓冲区中的无效页面。由于两块缓冲区都是由内核模式分配的(METHOD_BUFFERED已经缓存了用户提供的缓冲区),我们不能依赖未映射的页面来终止复制,如果说缓冲区由用户模式直接传递。然而,也存在一些情况会强制校验每个复制的元素,这允许攻击者任意地终止复制(例如参考Kernel Pool Exploitation on Windows 7中所讨论的漏洞)
       在存在漏洞的函数中,AFD复制用户提供的AFD_POLL_INFO结构到内部的潜在的一块偏小的缓冲区配额中。在随后查询每个单独的套接字状态时,这一内部结构被相同的函数操作。在每个AFD_HANDLE项目(嵌在AFD_POLL_INFO结构中)被复制到内部缓冲区之前,afd!AfdPoll64调用ObReferenceObjectByHandle校验套接字句柄并取回对应项目的文件对象。如果校验失败,这一函数就终止复制操作并忽略剩余的记录。在漏洞利用背景下,这变得非常有价值,因为我们可以在内部记录结构大小的粒度(sizeof(AFD_POLL_ENTRY))下终止内存池溢出。


套接字句柄校验

       这时,我们知道我们可以将溢出限制在0x28范围。我们还知道我们可以控制内部缓冲区结构的溢出大小,因为我们控制了0xB8 + (n * 0x28)大小中的[I]n[/I]。对于这个特定的漏洞,我们利用Kernel Pool Exploitation on Windows 7中描述的内存池索引攻击([I]PoolIndex attack[/I]),并溢出到下一个内存池配额的内存池头部。为了能可靠地完成攻击,我们必须做两件事。
       1. 灵活操作内核内存池以保证可靠且可预测的覆盖被完成。
       2. 寻找要分配的合适大小以保证溢出足够多字节并刚好破坏邻近的内存池头部。
       寻找所期望的大小基本取决于我们所能自由支配的分配单元。既然内存池溢出是在非分页内存池中,我们理想地希望使用允许我们从资源中任意分配和释放内存的API。一种可能性是使用NT对象。实际上,worker factory对象(NtCreateWorkerFactory中创建)我们特别感兴趣,因为在我们的目标平台下(Windows 7 x64),这个对象的大小是0x100个字节(0x110包含内存池头部)。通过提供一个AFD_POLL_INFO结构给AfdPoll64并使HandleCount值为0x6666668,我们将使内部缓冲区大小溢出并导致分配0xF8个字节。当内存池分配器取上整到最近的块大小时,内部缓冲区将是0x100个字节,与worker factory对象大小相同。这样,我们可以灵活操作0x100个字节的块来把AFD分配的缓冲区置于我们控制的块旁。
       当我们触发溢出时,我们只复制两个块到内部缓冲区结构中。我们通过提供一个无效的套接字句柄作为第三个AFD_HANDLE项目来做到这点。第一个记录被复制到内部缓冲区(大小现在是0x100)0xB8偏移,而第二个记录始于0xE0。因为每个AFD_POLL_ENTRY记录的大小实际上是0x24个字节(为了对齐扩展到0x28),我们溢出了4个字节到下一个内存池配额。特别地,我们溢出到了内存池头部的几位,足以发动内存池索引攻击。我们完全控制了内存池头部的前4个字节,因为在AFD_HANDLE结构中的Events(ULONG)被复制到AFD_POLL_ENTRY记录的0x20偏移。


内存池索引攻击

       当发动内存池索引攻击时,我们利用了Windows中的内存池分配器在释放内存池块前不校验内存池索引这一事实。Windows使用内存池索引来查找指向被释放内存块的内存池描述符的指针。我们因此可以引用一个越界指针()并映射空页面来完全控制内存池描述符。通过控制内存池描述符,我们也控制了延迟释放列表,它记录了等待被释放的内存池块。如果我们还认定延迟释放列表是满的(0x20个项目),那在释放到我们精心设计的内存池描述符之前就有项目被立即释放了,因此我们有能力释放任意地址到一条完全受控的链表中。简而言之,这意味着我们有能力写任意给定的可控地址到任意位置。以此,我们可以覆盖流行的nt!HalDispatchTable项目或任意其他调用自ring 0的函数指针。

[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班开始招生!!

上传的附件:
收藏
点赞0
打赏
分享
最新回复 (4)
雪    币: 83
活跃值: 活跃值 (22)
能力值: ( LV13,RANK:220 )
在线值:
发帖
回帖
粉丝
instruder 活跃值 4 2012-2-24 21:34
2
0
顶起

呵呵 调试不好弄 要1600多MB内存...
雪    币: 321
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:150 )
在线值:
发帖
回帖
粉丝
cmdhz 活跃值 3 2012-2-29 16:29
3
0
为作者的辛勤劳动而顶!!
雪    币: 100
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
leonkankan 活跃值 2012-3-4 00:19
4
0
顶起来的好东西。
雪    币: 55
活跃值: 活跃值 (10)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
寂寞如刀 活跃值 1 2012-7-13 09:06
5
0
真正的好东西,感谢楼主的共享精神。
游客
登录 | 注册 方可回帖
返回