0x01 关于SKREAM

前一篇文章中,我们讨论了内核池溢出漏洞,并提出了一种新的缓解方法,旨在防御Windows 7和8系统上使用特定溢出技术。该技术已应用到我们的SKREAM 工具包里。

尽管我们在Windows 8.1中缓解了这种攻击手法(在0xbad0b0b0中构建恶意OBJECT_TYPE结构),但是内存溢出漏洞仍屡禁不止,道高一尺,魔高一丈。利用的手法也再不断革新。因此我们也希望SKREAM能更进一步,本文将提出两种新技术,以一种更普遍的方式来防御内存溢出漏洞,不管你使用什么手法,该技术都能让你足够头痛【手动狗头】。

溢出成功有几个必要的前提。攻击者必须能够找到一个关键的地址来构建溢出缓冲区,并且准确地知道应该写入哪些数据,哪些需要保持其余数据不变。放错字节或是放错位置都可能导致下一内存分配出错进而导致诸如蓝屏死机这样的问题。

图1 内存溢出。比如在进行类型索引覆盖攻击时,该漏洞试图设置ObjectHeader。下一个池块的类型索引为0或1。
为了实现这个目标它必须计算ObjectHeader从溢出缓冲区开始的准确距离,以及TypeIndex的偏移量。

因为攻击需要精确到每一个字节,所以可以在池分配时引入随机分配来干扰这类漏洞。这里提供两种思路,一是选择转移(或隔离)分配,二是”膨胀“分配。两种思路的最终目标都是使攻击者不知道溢出空间的大小,各有千秋。


0x02 PoolSlider

下文中将以笔者个人对这项技术的理解“内存隔离”来叙述

WDK文档里所说的一样,x64架构上的内存分配器所分配的字节长度必须四舍五入到16字节(x86架构上8字节)。这就意味着任何请求大小如果不足16个字节的整数倍的话,都需要填充几个字符串来达到要求以便对其大小进行汇总。

图2 rdx大小为0x68字节的分配请求最后的实际大小是0x80字节:即请求头(0x10) +请求大小(0x68) +填充(0x8)

很明显如果想在这种条件下实现溢出,攻击者必须要考虑到字节不足而填充的部分。比如图2中尽管开发者只请求了0x68但是有0x70字节在到达下一个池分配之前必须被覆盖。

在我们的内存池隔离保护技术中,我们可以利用了这个填充和“隔离”这两特性,让指针以随机数的形式返回给调用者。这样一来,只要我们混淆内存池的开头创建的填充字节,并且减少在结尾部分填充字节量。就能逆转溢出攻击的可预测性。整个分配被转移了,而且攻击者还需要考虑填充字节的影响,就会导致攻击者无法将特定数据写入特定的位置了。

图3 有隔离(右)和无隔离(左)。

现在我们通过SKREAM扩展来监听图像加载事件图像加载事件,并在每个新加载驱动的ExAllocatePoolWithTag上放一个IAT钩子,从而实现了防御。每当内存池分配时,我们的钩子都会计算需要填充的填充字节数添加到内存中。随即在1和可用填充量之间产生一个随机数n,并将返回给调用者的指针向前推进n位。


0x03 处理释放

通过将返回的指针向前推进后,我们破坏了内存池的可预测性,这不仅仅是攻击者。假设内存池管理器返回给调用者的指针前面还有一个描述分配的POOL_HEADER结构。这意味着当尝试释放一个由‘P’表示的内存池时(比如调用nt!ExFreePoolWithTag),内存管理器将在P – sizeof(POOL_HEADER)搜索相关的池头数据。但是当使用内存池隔离技术时,假设完全没用,BAD_POOL_HEADER会导致系统崩溃。

为了正确地处理释放,我们必须在ExFreePoolWithTag上多放一个IAT钩子,并在处理释放前将指针重新对齐到16个字节。

0x04 其他问题

在测试内存池隔离技术时,我们还遇到了一些问题。有些问题很容易搞定,而有些问题仍然对这种防御带来严重威胁:

同时在ExAllocatePoolExFreePool上放钩子,并在Ex{Allocate, Free}PoolWithTag处进行同样的随机/重新对齐处理。

这种写法很烦躁,字符串应该交给对应的进程来分配。这些内部的释放函数将字符串对象的“缓冲区”元素转给ExFreePool(WithTag),如果此时指针没有16个字节,就会导致系统崩溃。可以在RtlFree{Ansi, Unicode}String上放个IAT钩子,使用在ExFreePool(WithTag)中一样的手法来重新对齐指针。

目前遇到的最复杂的情况是一个驱动分配内存时碰上另一个驱动(通常是NTOS)释放内存。在这种情况下,当释放驱动程序没有放钩子时,不能在调用ExFreePool之前重新对齐的指针,否则会出现前面提到过的BAD_POOL_HEADER而崩溃。

图4 由Blbdrive分配的带有“Blbp”标签时的情况。NTOS直接释放了sys。由于内存地址没有与0x10对齐,导致了bugcheck 0xC2

到目前为止还有个没有解决的问题,那就是没有可填充字节的情况下内存分配需要满足请求大小为16的整数倍这个该如何实现。这个条件会导致返回给调用者的指针在内存隔离不能向前推进。目前我们选择忽略这个问题,就当是目前的技术无法解决的问题。

其实这个问题可以通过在对齐的池块的末尾人为填充来解决。将1添加到请求的分配大小里就需要内存池管理器再添加15字节的填充,的确可以填充,代价是会对内存池造成一些不必要的浪费。

0x05 内存池隔离 Vs HEVD

HEVD为HackSys Extreme Vulnerable Driver的缩写,一个用于攻击系统驱动的开源项目

我们使用了HEVD对内存池隔离技术进行了测试:

图5.1 后面将会被利用到的内存分配。需要注意的是返回给调用者的指针(保存在rax寄存器中)移动了5个字节。

图5.2 溢出之前和之后下一个池块的头。可以看到,这个漏洞没有保留原池头。在这种情况下,溢出会因为PoolSlider移动了指针而终止。

图5.3 溢出破坏了下一个标头,未能保持内存池的完整性。最终也将崩溃。

0x06 PoolBloater

又名“资源浪费者”

减少内存溢出的第二种方法要简单得多,因为我们根本不改变分配的基本地址。相反,它会随机增加请求池分配的大小(即“膨胀”),从而破坏攻击的精度。

图6 有(右)和没有(左)PoolBloater的内存池。

PoolBloater的实现手法对比起PoolSlider的来说简单得多。用相同的方式在ExAllocatePool(WithTag)处放钩子 ,只改变钩子里面的功能:

这种方法的主要优点是它避免了我们在尝试内存隔离时遇到的很多问题。因为我们只改变内存池的大小,所以我们不需要解决指针不对齐的问题。最明显优势是,它有效地避免了内存碰撞。因为溢出的大小是随机的,所以根本就不适用于内存碰撞带来的漏洞,并且会被分配到一个无法预料的位置。

图7 有SKREAM(上图)无SKREAM(下图)。

当然,这种方法也有明显的缺点,内存占用率可能会比平常高得多,随着添加的字节数而变化。我们随机化选择设置一个上限防御效果会更好,代价是资源占用也更多了。另一方面,随机化选择一个较低的下限能够避免资源占用问题,但是也会导致防御变差。

0x07 已知缺陷

因为系统机制(比如PatchGuard)的原因这两种手法各有利弊,这些机制限制了我们监视驱动的能力,最明显的就是内核可执行程序本身(NTOSKRNL)。因此,我们目前只能防御内存溢出攻击中的冰山一角,未来希望这个项目能够逐渐扩大能防御的范围。

目前,两种手法都存在以下限制:

原文:https://www.sentinelone.com/blog/skream-reloaded-randomizing-kernel-pool-allocations/

源链接

Hacking more

...