[翻译]Windows内核漏洞学习-内核池攻击原理

前言

在练习HEVD的内核池溢出漏洞之前,首先先需要了解一下内核池溢出的一些攻击方法和原理。这里对kernelpool-exploitation进行翻译阅读一下。

正文

ListEntry Flink Overwrite

Win7在从ListHeads[n]分配池块(对于给定的块大小)时,使用的安全算法验证的是ListHeads[n]的列表条目结构,而不会去验证即将unlink的实际块的结构。因此,在一个free的块中覆盖它的Flink可能会导致ListHeads[n]的地址被写为攻击者控制的地址。(如图所示)
在这里插入图片描述

这种攻击需要至少两个空闲的块存在于目标ListHeads[n]列表中。否则ListHeads [n]的Blink将验证unlink块的Flink。在上图中,ListHeads列表中池块的Flink已被攻击者选择的地址覆盖。反过来,当这个块在ExAllocatePoolWithTag中分配时,算法尝试在攻击者控制的地址(eax)上的列表条目结构的Blink写入ListHeads[n] (esi)的地址。

nt!ExAllocatePoolWithTag+0x4b7:
8296f067 897004 mov dword ptr [eax+4],esi

虽然不能从用户模式的上下文轻松地确定esi(目标写入地址)的值,但有时可以推断出它的值。例如,如果只定义了一个非分页池(如2.2中所述),esi将指向ntoskrnl的数据段中的一个固定位置(nt!NonPagedPoolDescriptor)。如果池描述符是从内存分配的,那么可以假设它在定义的池内存范围内的位置。因此,攻击者可以重写重要的全局变量或内核对象指针(例如,通过部分指针重写),以获得任意代码的执行。

攻击者还可以使用覆盖中的用户模式指针将任意写入扩展到完全控制的内核分配。这源于一个事实,即ListHeads[n]。在断开已损坏的块的链接之后,Flink被更新为指向下一个空闲块(攻击者控制的指针)。由于攻击者提供的地址的Blink被更新为指向ListHeads[n],池分配器在从空闲列表安全地解除usermode指针链接方面没有问题。

Lookaside Next Pointer Overwrite

后备列表的设计是快速和轻量级的,因此不引入与双链ListHeads列表相同的一致性检查。后备列表中的每个条目都是单链接的,它们都有一个指向下一个条目的指针。由于没有检查断言这些指针的有效性,攻击者可能会使用池损坏漏洞强制池分配器在检索下一个空闲后备块时返回任意地址。反过来,这可能允许攻击者破坏任意的内核内存。
在这里插入图片描述

为了将一个lookaside下一个指针的破坏扩展到一个n字节的任意内存覆盖,必须对目标块的大小进行分配,直到被破坏的指针返回(图2)。对于分页池分配,分配unicode字符串(如NtCreateSymbolicLinkObject)的本机api提供了一种方便的方式,可以用几乎任何字节组合填充任何大小的块。这些api还可以用于整理和操作池内存布局,以控制未初始化的指针和两次释放等可利用的原语。

在这里插入图片描述

与lookaside池块不同,lookaside池页(图3)将下一个指针存储在offset null,因为没有与它们相关联的池头。如果保持以下状态,分配的池页面将被释放到(lookaside list)后备列表中:

  • 用于分页池页面数量 =1

  • 用于未分页的池页面数量<=3

  • 目标页计数的后备列表未满

当内存管理器必须请求额外的池内存时,请求的页由由nt!MiAllocatePoolPages函数返回,这些内存内存无法从ListHeads或lookaside列表中获得。由于这通常是由许多并发系统线程执行的,因此操作内核池布局以在后备列表的空闲池页面旁边定位溢出显然是说起来容易做起来难。另一方面,在处理后备池块时,可以使用不经常请求的块大小值,以便对内存布局进行更细粒度的控制。这可以通过检查后备管理结构中的totalallocate值来完成。

PendingFrees Next Pointer Overwrite

等待释放的池条目存储在单链的pending列表中。由于在遍历这些列表时不执行任何检查,因此攻击者可以利用池损坏漏洞来损坏pending列表条目的下一个指针。反过来,这将允许攻击者将任意地址释放给所选的池描述符ListHeads列表,并可能控制后续池分配的内存(图4)
在这里插入图片描述
攻击延迟释放列表的一个值得注意的警告是,内核池非常频繁地处理这个列表(每32次释放一次)。实际上,可以将数百个线程调度到同一个内核池,也可以在多核机器上并行地处理它们。因此,池溢出所针对的块很可能已经从延迟释放列表中删除,并放到了ListHeads列表中。由于这个原因,我们很难认为这次攻击是可行的。但是,由于某些池描述符的使用频率低于其他描述符(如会话池描述符),因此在某些情况下对延迟释放列表的攻击可能是可行的。

PoolIndex Overwrite

如果为给定的池类型定义了多个池描述符,则池块的PoolIndex表示关联池描述符数组中的索引。因此,在处理ListHeads条目时,池块总是被释放到其适当的池描述符。然而,由于验证不足,格式不正确的PoolIndex可能会触发越界数组取消引用,从而允许攻击者改写任意内核内存。

在这里插入图片描述

对于分页池,PoolIndex总是表示到分页池描述符数组(nt!ExpPagedPoolDescriptor)中的索引。在已检查的构建中,索引值在与ntExpNumberOfPagedPools的比较中得到验证,以防止任何超出界限的数组访问。但是,在free的(零售)构建中,不验证索引。对于非分页池,PoolIndex表示仅当numa感知系统中存在多个节点时,才将索引引入ntExpNonPagedPoolDescriptor中。同样,在自由构建时,不会验证PoolIndex。

畸形的池索引(只需要2字节池溢出)可能会导致分配的池块被释放到空指针池描述符(图5)。通过映射虚拟空页,攻击者可以完全控制池描述符及其ListHeads条目。反过来,这可能允许攻击者在链接到列表时将池块的地址写入任意地址。这是因为当前在前面的块的Blink是用释放的块的地址更新的,例如ListHeads[n].Flink - > = FreedChunk.Blink。注意,由于释放的块没有返回到任何实际池描述符,因此不需要清理内核池(删除陈旧的条目等)。

在这里插入图片描述

如果启用了延迟池释放,那么可以通过创建一个假冒的pendingfree列表来实现类似的效果(图6)。此外,池描述符中的PendingFreeDepth值将大于或等于0x20,以触发pendingfree列表的处理。
在这里插入图片描述

示例2演示了PoolIndex重写如何可能导致将用户控制的页面地址(eax)写入任意目标地址(esi)。为了执行任意代码,攻击者可以利用这个方法用用户模式页面地址覆盖一个不常用的内核函数指针,并从相同的进程上下文中触发它的执行。

如果块的PoolType也被重写(例如,将其设置为PagedPool),那么PoolIndex重写攻击可以应用于任何池类型。由于这要求块大小也被覆盖,攻击者必须要么知道被覆盖块的大小,要么创建一个嵌入其中的假边界块。这是必须的,因为FreedBlock->块大小= NextBlock->先前的大小必须保持,由自由算法检查。此外,块大小应该大于0x20,以避免后备列表(忽略PoolIndex)。但是,请注意,嵌入式池块可能会损坏块数据中的重要字段或指针。

Quota Process Pointer Overwrite

由于可以为分配的池内存对进程进行记录,池分配必须为池算法提供足够的信息,以便将记录的配额返回给正确的进程。因此,池块可以有选择地存储指向关联流程对象的指针。在x64上,process对象指针存储在池标头的最后8个字节中(如第2.9节所述),而在x86上,指针被附加到池体。在池损坏漏洞中重写此指针(图7)可以允许攻击者释放正在使用的进程对象或损坏任意内存,以返回所占用的配额。

在这里插入图片描述

每当释放池分配时,free算法在实际将内存返回到适当的空闲列表或lookaside之前检查池类型,寻找配额位(0x8)。如果设置了该位,它将尝试通过调用nt!PspReturnQuota返回已计费的配额,然后取消对相关进程对象的引用。因此,重写流程对象指针可以允许攻击者减少任意流程对象的引用(指针)计数。随后引用计数不一致可能导致use-after-frees如果条件得到满足(如处理计数为零当引用计数降低为零)。

在这里插入图片描述

如果将process对象指针替换为指向用户模式内存的指针,则攻击者可以创建一个伪EPROCESS对象来控制指向EPROCESS配额块结构的指针(图8),其中存储了配额信息。在free时,通过减去分配的大小来更新此结构中使用的配额的值。因此,攻击者可以在返回所占用的配额时减少任意地址的值。只要设置了配额位和配额流程对象指针,攻击者就可以对任何池分配挂载这两种攻击。

明日计划

说实话俺还是没太搞明白只看了大概,还是找真实案例对着理论调试可能更好懂一些。

继续学习Windows内核漏洞

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章