导语:在之前的文章中,我们讨论了内核池溢出漏洞,并提出了一种新的缓解措施,旨在阻止在Windows 7和8系统上使用特定的漏洞利用技术。此缓解措施后来被纳入我们的开源内核模式漏洞利用缓解工具包,称为SKREAM。

之前的文章中,我们讨论了内核池溢出漏洞,并提出了一种新的缓解措施,旨在阻止在Windows 7和8系统上使用特定的漏洞利用技术。此缓解措施后来被纳入我们的开源内核模式漏洞利用缓解工具包,称为SKREAM

尽管我们在Windows 8.1中缓解了某些恶意利用(在0xbad0b0b0中构建恶意OBJECT_TYPE结构),但是池溢出漏洞仍然是现代系统中普遍存在的问题,并且正在通过不同的、更复杂的方法积极加以利用。考虑到这一点,我们希望SKREAM的环境技术能更进一步,以一种更通用的方式缓解池溢出漏洞,这种方式可以缓解针对开源的Windows内核模式漏洞利用的攻击。

成功利用池溢出有几个关键前提,最关键的是,攻击者必须能够非常仔细的制作溢出的缓冲区,准确的知道他们计划写入哪些数据,并保持其余数据不变。这意味着任何放错位置的字节都可能以一种意想不到的方式破坏下一个池分配,从而导致可能出现蓝屏死机。

1.png

池溢出示意图,例如,在TypeIndex覆盖攻击中,漏洞会尝试将下一个池块的ObjectHeader.TypeIndex设置为0或1

为此,它必须计算ObjectHeader与溢出缓冲区开头的确切距离,以及TypeIndex成员在其中的偏移量。

考虑到代表攻击者所需的字节级精度,我们可以通过向池分配引入一定程度的随机化来打破此类的大多数漏洞。在随机化池分配时,我们有两种可能的操作方法 ,可以选择转移(或滑动)分配,也可以简单的增大池的容量。这两种方法的最终目标都是使攻击者不知道必要的溢出的大小,每种方法都有其相对的优点和缺点。

PoolSlider方案

正如WDK文档所介绍的那样,x64上内核池分配工具的一个关键属性是所有分配必须对齐并四舍五入为16个字节(或x86上的8个字节)。这意味着任何请求大小,只要不是16的整倍数的分配,将接收额外的几个字节的填充,以便将其大小进行整倍数的匹配。

2.png

请求大小(rdx)为0x68字节的池分配,最终块大小为0x80字节:池头(0x10)+请求大小(0x68)+填充(0x8)

显然,试图溢出这种分配的攻击者必须考虑到池管理器添加的额外填充字节。例如,在图2中所示的分配中,有0x70字节在到达下一个池分配之前必须被覆盖,即使调用者仅请求了0x68。

在PoolSlider缓解方案中,我们利用这个填充和“slide”(即前进),指针以随机数返回给调用者。这样,我们就会很隐蔽的在池块的开头创建一些填充,同时减少在末尾找到的填充量。这就有效的破坏了池溢出攻击的可预测性,攻击者会尝试考虑填充字节,但由于整个分配被移位,因此攻击者不会将预期的数据写入计划位置,漏洞将不会按着预期利用。

3.png

右边的是具有PoolSlider的池分配,而左边是没有PoolSlider的池分配

可以通过扩展SKREAM来监控图像加载事件并在每个新加载的驱动程序上为ExAllocatePoolWithTag放置一个IAT挂钩来实现此缓解。无论何时进行池分配,我们的钩子都会计算添加到它的填充字节数。然后它生成一个介于1和可用填充量之间的随机数,并将返回给调用者的指针向前推进。

4.png

释放处理

当然,通过推进返回给调用者的指针,我们打破了池的可预测性,但这不仅仅是针对潜在的攻击者。池管理工具做出的一个关键假设是,返回给调用者的指针紧跟在描述分配的POOL_HEADER结构之后。这意味着当尝试释放由'P'表示的池块时(例如,通过调用nt!ExFreePoolWithTag),池管理工具将在<P-sizeof(POOL_HEADER)>处搜索相关的池头。毋庸置疑,当启用PoolSlider时,此假设会完全中断,并且系统将因错误检查0x19(BAD_POOL_HEADER)而崩溃。

为了正确处理释放,我们不得不在ExFreePoolWithTag上放置一个额外的IAT钩子,并在实际释放它之前将指针重新对齐成16个字节。

5.png

其他问题

在测试PoolSlider方案时,我们遇到了一些问题。有些相对容易解决,而其他一些则面临重大挑战:

1.使用ExAllocatePoolWithTag进行分配并使用ExFreePool进行释放(反之亦然):只需挂钩ExAllocatePool和ExFreePool并进行与Ex {Allocate,Free} PoolWithTag相同的随机化或重新对齐操作即可解决此问题。

2.使用ExAllocatePool(WithTag)分配字符串并使用RtlFree{Ansi, Unicode}释放它:这是一种糟糕的编程习惯,因为字符串应使用匹配的分配例程进行分配。在内部,这些释放函数将字符串对象的“缓冲区”成员转发到ExFreePool(WithTag),如果指针未正确对齐成16个字节,则会导致系统崩溃。这可以通过简单地在RtlFree{Ansi, Unicode}字符串上放置额外的IAT钩子,并像ExFreePool(WithTag)中那样重新对齐指针来修复。

3. 在池中分配的Lookaside列表头:些驱动程序在使用nt!ExInitializeLookasideListEx进行初始化时初始化了Lookaside列表。这个函数接收的第一个参数是“PLOOKASIDE_LIST_EX Lookaside”。

SKREAM6-1.png

大多数驱动程序将此结构保留为驱动程序数据部分中的全局变量,但有些驱动程序选择在池中动态分配它。由于我们将返回给调用者的指针地址“随机化”,在这种情况下,这个结构的开始地址不是16字节对齐的,导致在调用nt!ExInitializeLookasideListEx时引发错误0x80000002(STATUS_DATATYPE_MISALIGNMENT)。

4.由一个驱动分配内存,另一个驱动释放内存:我们遇到的最复杂的情况是一个驱动程序分配池内存,而另一个驱动程序(通常是NTOS)释放它。在这种情况下,当释放驱动程序未被挂钩时,我们无法在调用ExFreePool之前重新对齐指针,从而导致BSOD出现错误检查0xC2(BAD_POOL_CALLER)。

6.png

由Blbdrive.sys分配的带有标签“Blbp”的池分配,稍后由NTOS直接释放。请注意,池块的地址未对齐到0x10,导致错误检查0xC2

到目前为止,我们还没有解决的另一个问题是池分配的情况,其请求的大小是16的倍数。由于在这些情况下没有额外的填充,因此PoolSlider无法使返回给调用者的指针向前推进。

但是,可以通过在对齐的池块的末尾,进行人为填充来解决此问题。简单的将1个字节添加到所请求的分配大小将导致池管理工具添加另外15个字节的填充,以便将块大小重新对齐成16个字节。然后,我们可以使用一些填充。

7.png

PoolSlider与HEVD的比较

我们对PoolSlider进行了测试,它利用了HEVD中一个未分页池溢出漏洞。

8.png

稍后将被利用的池分配,请注意,返回给调用者(保存在rax寄存工具中)的指针移位了5个字节

9.png

溢出前后的池块标头的比较,可以看到,这个漏洞没有保留原来的池头,在这种特殊情况下,由于PoolSlider移动了指针,溢出会被5个字节阻止

10.png

漏洞利用损坏了下一个标头,但未能保持池的完整性,这最终将导致错误检查

PoolBloater方案

减少池溢出的第二种方法简单得多,其原理就是根本不改变分配的基本地址,而是通过随机的字节量增加所请求的池分配的大小,从而破坏潜在利用的字节精度。

11.png

右边是具有PoolBloater的池分配,而左边是没有PoolBloater的池分配

与PoolSlider相比,PoolBloater的实现方式非常简单。我们用与在PoolSlider中相同的方式挂钩ExAllocatePool(WithTag),并且只更改钩子内部执行的操作过程。

12.png

这种方法的主要优点是它能避免我们在尝试PoolSlider时遇到的大多数问题,由于我们只改变了池块的大小,因此我们不必面对因指针未对齐而导致的问题。该方法的另一个明显优点是,它可以使池的喷涂技术(spray)无用,因为溢出块的大小是随机的。

13.png

13.2.png

可以看到启用PoolBloater后,分配池大小将变得更大,因此它适合于分配,这意味着攻击者无法对分配进行控制

但是,这种方法的缺点也很明显,池内存利用率可能会比平常高得多,具体取决于添加到每个分配的字节数。一般而言,这种缓解方式会表现出固有的权衡机制,为我们的随机化选择一个较高的上限将导致更强的缓解能力,代价是资源更密集。另一方面,为我们的随机化选择一个较低的下线将导致池的利用率保持相当正常,但使缓解变弱。

总结

这两种缓解方式都存在一些固有的缺点,这些缺点主要是由其他系统机制(如PatchGuard)引起的,这些机制限制了我们监视系统中每个驱动程序的能力,尤其是内核可执行文件本身(NTOSKRNL)。因此,我们目前只尝试处理池溢出攻击的场景。

目前,我们的缓解措施都受到以下限制:

1.我们只保护不属于Windows操作系统的驱动程序;

2.我们只保护在安装了SKREAM之后加载的驱动程序;

3.我们只保护由故障驱动程序通过ExAllocatePool(WithTag)直接执行的分配,系统所做的任何分配都是不受保护的,即使它稍后被转发给第三方驱动程序进行进一步处理(例如,IOCTLs的SystemBuffer),也是不可以的;

4.我们只保护与页面非常匹配的分配,跨越一个页面或更多页面的更大的分配由nt!ExpAllocateBigPool以不同的方式处理;

5.我们仍然没有实现取消挂钩,如果在启用PoolSlider的情况下编译SKREAM,则无法将其服务配置为以“system”身份启动,而只能将其设置为“auto”(否则操作系统可能会崩溃),这意味着卸载SKREAM会有系统崩溃的风险。

源链接

Hacking more

...