[翻译]CVE-2012-0148: A Deep Dive Into AFD
2012-2-24 20:17
5929
[翻译]CVE-2012-0148: A Deep Dive Into AFD
原文: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 接口的内核管理层。与许多其他流行的操作系统类似, Windows 与 AFD 把网络连接作为套接字管理。套接字模式在 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 ) 和节约内存使用的额外扩展以及提供重用套接字功能的操作 ( 例如 ConnectEx 和 AcceptEx ) 。
Winsock Architecture
Winsock 架构被分为用户模式和内核模式。在最高层,应用程序与 Windows Sockets API (ws2_32.dll) 交互,它提供了例如 bind 、 listen 、 send 和 recv 的常见函数。这些函数调用合适的提供商,它不是由系统定义的就是由应用程序定义的。举个例子, VMCI 套接字可以被 VMware 虚拟机用来与其他虚拟机通信。这里, VMCI 提供商 ( 由 VMware Tools 安装 ) 调用 VMCI 套接字库 (vsocklib.dll) ,它然后调用负责管理和呼叫主机 / 其他虚拟机的 VMCI 驱动程序 (vmci.sys) 。类似的, Windows 为例如 TCP 和 UDP 的本地支持协议注册了一个 Winsock API 的提供商 (mswsock.dll) 。 Winsock 然后通过辅助功能驱动程序 (afd.sys) 进行必要的套接字管理和 TCP/IP 协议调用。
虽然 Windows 的网络架构在 Windows Vista 中有了显著的变化,但应用程序与 Windows Sockets API 和 AFD 之间的交互基本保持不变。
Winsock架构
Socket Management
辅助功能驱动程序,即 afd.sys ,负责创建和管理 Windows 中的套接字终端。在内部, Windows 用一个文件对象代表套接字,文件对象还包含了记录例如连接是否已建立的状态信息的额外属性。这允许应用程序使用标准读写 API 操作套接字句柄来访问和传输网络数据。
当 AFD 初次初始化时,它创建 AFD 设备 ( 被 Winsock 应用程序访问 ) 并设置相关的驱动对象的 IRP 处理函数。当一个应用程序初次创建一个套接字时, IRP_MJ_CREATE 例程 (afd!AfdCreate ) 调用函数 (afd!AfdAllocateEndpoint ) 分配一个终端数据结构。这是一个定义每个终端的每个属性的未公开结构,当然也包含了终端所处的状态 ( 例如是否正在建立连接、监听或释放连接 ) 。操作终端数据结构的 AFD 函数 ( 例如 send 和 receive 函数 ) 在执行前通常会校验终端状态。此外,一个指向终端数据结构自身的指针被保存在套接字文件对象的 FsContext 字段,以便 AFD 可以容易地定位套接字管理数据。 AFD 还通过一条双向链表 (afd!AfdEndpointsList ) 记录所有终端,因此可以判断一个地址是否已被绑定。
AFD 内部的主要功能通过设备 I/O 控制处理函数来被使用。 AFD 还提供了快速设备 I/O 控制例程以处理直接来自缓存管理器而不需要产生一个 IRP 的请求。 AFD 试图尽可能地使用快速 I/O 分发例程,特别是在读写网络数据的时候。然而,在一些需要更多扩展操作的情况下,它会回过来使用基于 IRP 的机制,比如在数据报大小超过 2048 个字节,或一个连接 ( 终端 ) 不在所期望的状态时。
当研究 AFD 中的函数,特别是那些被分发 I/O 控制例程调用的函数的时候,需要重点注意直接对请求的校验。实际上,研究大部分 AFD 中的函数,你可能会多次注意到与例如 0xAFD0 、 0xAFD1 、 0xAFD2 等常量的校验 ( 比较 ) 。这些是描述活动套接字终端 / 连接的常量,它们被存放在终端数据结构的第一个 16 位字段中。举个例子, 0xAFD1 表示代表数据报套接字的终端,而 0xAFD0 、 0xAFD2 、 0xAFD4 和 0xAFD6 表示代表 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 x64 下 AFD_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 的函数指针。
[2022夏季班]《安卓高级研修班(网课)》月薪三万班招生中~
上传的附件: