解决Windows上未初始化的内核池内存

  • A+
所属分类:安全闲碎

MSRC安全研究与防御/ ( /

这篇博客文章概述了Microsoft为消除Windows中未初始化的内核池内存漏洞所做的工作,以及我们为何走这条路。

有关未初始化的内存为何重要以及过去曾使用过哪些选项来解决此问题的背景信息,请参见我们以前的博客文章简短的回顾是,未初始化的内核池漏洞占2017年至2018年中向Microsoft报告的所有未初始化的内存问题的不到一半。

这篇博客文章分为几个部分,您可以跳转至:

  1. 了解解决未初始化的池内存的可行性

  2. 潜在的实施方案

  3. 新的Windows内核池API

  4. 性能优化

  5. 部署计划

  6. 对客户的影响

  7. 前瞻性计划

没有Windows组织和MSRC的密切合作,这项工作将无法实现。

了解解决未初始化的池内存的可行性

解决未初始化内核池内存的工作始于解决未初始化堆栈内存的工作。

就像未初始化的堆栈存储器一样,我们需要一种可以确定性地防止漏洞,而不是仅依赖于静态分析,模糊测试或代码审查的解决方案。对我们来说,理想的最终状态是知道我们的代码在构造时没有未初始化的内核池问题。

最初,解决未初始化的池内存似乎比未初始化的堆栈内存更具挑战性。请考虑以下差异:


堆栈分配 内核池分配
典型尺寸 小(内核堆栈为20KB,无法增长) 可以很多MB。平均池分配大小大于堆栈。
快取 活动堆栈通常在L1高速缓存中或即将被使用(当变量被代码使用时)。 池分配可能由根本不在高速缓存中的内存满足,并且可能无法立即使用(尽管池分配通常在分配后不久就使用)。
优化能力 MSVC很好地完成了消除冗余存储以堆叠变量的工作。 如果在池API中将分配清零,则编译器不可能自动优化掉初始清零。MSVC将需要自定义优化逻辑来识别“此API为零,因此,如果调用者在分配后立即将其memset设置为零,则可以消除该memset”。MSVC通常很难优化冗余存储以池/堆存储。
分配时间 通过调整堆栈指针进入函数后,即时有效地批量分配了堆栈。 快速但不是即时的。涉及分支逻辑,读取多个内存以查询结构和元数据,并可能获取锁或调用内存管理器以获得其他虚拟地址空间。
初始化花费的“总分配时间”百分比 如果无法优化强制初始化,则“分配时间”将从零变为初始化所需的时间。这纯粹是开销。 堆分配已经有开销,希望在memset中花费的时间将是总分配时间的个位数百分比。对于非常大的内存集,我们期望内存集成本将主导总分配时间。

简而言之,我们期望池分配平均比堆栈分配大,将很难(或不可能)优化冗余存储,而不是平均分配,并且将在较慢的CPU高速缓存中处理内存。唯一的赎回因素是,进行堆分配的时间要比堆分配的时间长,因此,尽管我们可能花更多时间初始化堆分配,但希望它会与“花费在堆分配上的时间”的杂波混在一起。

为了验证这一理论,我们进行了两组测试。

真实的Windows性能测试

在此测试中,我们修改了现有池API,以无条件将所有分配归零,而不能选择退出行为。然后,我们使用现有的性能门基础设施来衡量此更改在关键情况下的影响。

这些测试结果非常积极。大多数基准测试均未显示可衡量的性能下降。

Web基础(网络服务器性能的一种衡量标准)是我们的关键基准之一,它在估计整个系统性能方面做得很出色,并且众所周知,它对内核池分配器的性能非常敏感。当我们之前用运行在用户模式下的段堆实现替换了旧的内核池分配器时,Web基础知识最初看到的回归约为15%(请注意:我们修复了这些回归)。这里的要点是,Web基础知识对池分配器的性能非常敏感。

在池归零的情况下,一项网络基础测试显示噪声水平回归约为1%。其余的网络基础测试表明没有回归。

这使我们相信,池零归零是可行的,只要我们能够让开发人员选择退出引起回归的热分配即可。

微基准

我们还建立了微基准,以帮助我们了解清零的开销对于不同大小的分配是什么。请注意,这些微基准确实有一些噪音。如果您在单一尺寸下看到大量尖峰,则可能只是测试中的噪音。还要注意,在进行了一些性能调整后,这些基准并不代表当前的性能。这些是初始性能数字。

测试1:使用相同大小的多个分配分配8GB内存

以下基准测试测量单个线程重复分配某个固定大小时,池归零引起的回归。请注意,对于这些测试,进行了8GB的分配。

这种情况有些不切实际,因为在正常的堆操作中,我们期望分配并释放分配(因此可以重新使用虚拟地址空间)。在此测试中,堆需要定期进入内存管理器以请求其他内存(速度较慢)。

解决Windows上未初始化的内核池内存

    

解决Windows上未初始化的内核池内存

合理化绩效数据

现实世界的数据看起来不错,但是一些微基准测试数据看起来确实令人担忧。这是我们可以合理化所见内容的方法:

  1. 与较大的分配相比,在热路径中更可能看到较小的分配。例如,您通常不会在热路径中看到几兆甚至几千字节的分配。通常在有限的地方进行较大的分配,并且热路径是使用分配的代码,而不是进行分配的代码。

  2. 小分配性能不是超级回归。仍有影响,但并非不合理。

  3. 许多现有的代码路径在进行分配后已经为零分配。我们的实际测试设置是在Pool API内部无条件地将分配清零,因此我们导致许多分配被双重清零。如果我们确保分配仅被清零一次,我们可以赢回一些性能。

  4. 微型基准测试仍然不够准确。该基准通过分配相同大小的分配来工作,这意味着该API内部的分支预测变量将受到良好的训练。在现实世界中,这有时是正确的(即有时应用程序连续进行一堆相同大小的分配),但通常也并非如此。如果未对分支预测变量进行充分的训练,则正常的池分配代码将具有这些测试未反映的其他分支错误预测开销。

  5. 如果归零行为最终成为他们代码中的瓶颈,我们总是可以允许开发人员选择退出分配。

您可能已经猜到了,我们最终根据收集到的数据进行了池清零项目。

潜在的实施方案

我们考虑了三种方法来获取初始化的池内存:

  1. 默认情况下,创建新的池API会将内存归零。

  2. 使用一些编译器魔术将初始化后未完全初始化的池分配归零。

  3. 默认情况下,使现有池API的内存为零,并提供新的标志以允许退出。

我们排除了#2,因为它将涉及一次性编译器逻辑,以识别池分配,检测它们是否已完全初始化以及是否未插入初始化。这也只会使使用MSVC编译驱动程序的开发人员受益,而第一种方法还将帮助使用其他编译器编译驱动程序的开发人员。

我们排除了#3,因为它为现有池API造成了重大变化。许多公司编写的驱动程序可以在所有Windows版本上运行。如果我们更改了现有的池API,那么如果驱动程序编写者的驱动程序需要在较旧版本的Windows上运行,则他们将面临一个难题:

  1. 继续使用现有池API返回的零分配,这样,在没有此行为的Windows版本上运行时,其驱动程序在功能上将正确。

  2. 编写驱动程序,使其仅在具有新池API行为的Windows版本上运行。

请注意,即使我们发布了此变更级别,驱动程序开发人员也将无法依赖它。一些客户不安装更新或花费很长时间安装更新。

我们花了一些时间研究可以使我们“升级”现有API以使其具有归零行为的解决方案,但是,我们无法提供一个满足以下要求的好的解决方案:

  1. 对于开发人员而言,使用调零API必须比非调零API更方便(即,我们更愿意强迫某人选择退出调零而不是选择加入)

  2. 不得在任何支持平台上导致功能正确性问题

  3. 不得在任何支持平台上要求双归零

  4. 如果需要性能,必须能够禁用调零

新的Windows内核池API

Windows 10版本2004 API

对于Windows 10版本2004发行版,我们引入了新的池API(默认情况下为零)。

这些API是:

  • ExAllocatePool2

  • ExAllocatePool3

ExAllocatePool2使用较少的参数,使其更易于使用。它涵盖了最常见的情况。

需要更灵活参数的不太常见的方案(例如优先级分配)通过ExAllocatePool3进行。这两个API的目的都是为了将来可扩展,因此我们无需继续添加新的API。

下层兼容API

我们还引入了一套新的包装器API,它们可在所有受支持的下层操作系统上使用。这些实现为forceinline函数,需要驱动程序开发人员:

  1. 在拉入任何Windows标头之前,请在其驱动程序中定义POOL_ZERO_DOWN_LEVEL_SUPPORT(使用#define)。

  2. 在使用这些API之前,请调用ExInitializeDriverRuntime。

旧API 调零包装 未初始化的包装器
ExAllocatePoolWithTag ExAllocatePoolZero ExAllocatePool未初始化
ExAllocatePoolWithQuotaTag ExAllocatePoolQuota归零 ExAllocatePoolQuota未初始化
ExAllocatePoolWithTagPriority ExAllocatePoolPriority0 ExAllocatePoolPriority未初始化

当这些API在本机支持池清零的操作系统上使用时,它们仅调用池API并允许其进行清零。当它们在本机不支持池清零的操作系统(即Windows 10版本2004之前的操作系统)上使用时,它们将进行池分配,然后将分配内存设置为零。

目的是使驱动程序开发人员可以更明确地了解其在程序中的工作方式。由于该行为是在API名称中明确指定的,因此开发人员是否真正打算将分配初始化或归零将永远不会有任何问题。

ExAllocatePool2 / 3对旧API的改进

投掷行为

旧的池API具有令人困惑的错误路径行为。

除非将POOL_QUOTA_FAIL_INSTEAD_OF_RAISE标志传递给它,否则ExAllocatePoolWithQuotaTag会在出错时引发异常,在这种情况下,出错时返回NULL。除非将POOL_RAISE_IF_ALLOCATION_FAILURE标志传递给他们,否则ExAllocatePoolWithTag和ExAllocatePoolWithTagPriority将在失败时返回NULL,在这种情况下,它们将引发异常。一组具有不同语义的API有点令人困惑。

除非指定了POOL_FLAG_RAISE_ON_FAILURE标志,否则ExAllocatePool2 / 3在失败时将返回NULL,在这种情况下将引发异常。

标签行为

旧的池API接受的池标记为零。这会使调试更加困难。新的池API不接受零池标记。

默认情况下,不可执行的非分页池

使用POOL_FLAGS_NON_PAGED默认为不可执行的内存。POOL_FLAGS_NON_PAGED_EXECUTABLE必须用于可执行的非页面缓冲池内存。通过使更方便的分配类型成为更安全的分配类型,开发人员不太可能意外地执行不安全的操作。

注意:分页池在x86体系结构上始终是可执行的,而在所有其他体系结构上则不能执行。

 默认清零

默认情况下,新池API的零分配。如果调用者需要未初始化的分配,则必须指定POOL_FLAGS_UNINITIALIZED标志。

性能优化

由于该性能在现实世界中的测试中看起来不错,因此并没有做太多事情来进一步优化它。有一些事情值得一提。

  1. 在请求清零的情况下进行大型分配时,堆可能需要从内存管理器中检索内存。在这种情况下,堆将请求置零的页面,并且内存管理器将尝试使用后台置零线程已置零的内存来提供这些页面。这允许快速分配大量的零位内存。

  2. 对于非常大的分配,开发人员可以使用适当的标志手动从零中退出分配。通常不会在热路径中进行很大的分配,因此通常不需要使用此分配。

  3. 创建了一个特定于堆的归零函数,该函数优于常规memset实现。我们计划将来发布有关此的另一篇博客文章。该功能利用了特定对齐方式的保证,即堆可以为其分配进行分配。

无需其他优化。

部署计划

与InitAll不同,新的池清零API需要更改代码才能使用。

对于Windows 10版本2004,整个Windows内存管理器已转换为使用新的池清零API。除了一个地方(位图分配可能很大)以外的所有位置都使用归零分配。

我们还对Hyper-V和许多网络组件进行了更改(将在将来的版本中发布),以使用这些新的API。我们目前的计划是在不久的将来使用自动错误归档工具将所有内核模式代码转换为新的API,以帮助确保所有内容都得到转换。

到目前为止,对新API的反馈是肯定的。没有发现性能问题,并且减少了代码大小,因为如果开发人员需要零分配,则不再需要调用pool API和内存集。

我们也希望在我们如何能够帮助3聚会司机摆脱旧池API的路程。在这方面,我们还没有任何可共享的计划,但是工作正在进行中。

对客户的影响

一旦我们完成了将代码过渡到新的池API的操作,当前会影响客户的大多数未初始化的内存漏洞将在Windows上得到缓解。当然,仍可能存在未初始化的内存漏洞,但是在保护堆栈的InitAll和使用归零标志的大多数分配之间,这些问题潜伏的机会要小得多。

还有可能的是,在初始化内存时,不会将其初始化为程序有意义的值(即,内存初始化为0,但应将其初始化为其他值才能使程序正确)。在这些情况下,我们至少会在程序中产生确定性的行为(即,由于错误地初始化了值,所以程序总是做错事),而不是随机的行为(即,根据未初始化的值是什么,程序会做完全不同的事情) )。因为我们总是知道内存的用途,所以这使问题更易于分类,并且更直接地评估错误的影响。在大多数情况下,即使零不是一个正确的值,但从安全角度来看,它仍然是最安全的自动值。

我们仍然希望这些缓解措施将在很大程度上消除漏洞类别的威胁,该漏洞类别近年来占所有Microsoft CVE的5-10%。

前瞻性计划

虽然池清零对我们来说是一个不错的开始,但我们仍然需要研究以下几个方面:

  1. 如何处理用户模式堆。我们应该禁止malloc并强制使用calloc吗?还有吗

  2. 使用构造函数处理C ++类的方法。是否应该选择退出调零,但需要在构造函数中完全初始化该类?那内部的填充字节呢?

我们还计划在将来的博客文章中发布有关如何为内核池创建新的专用内存集以用于零分配以及如何使所有Windows应用程序实现更高性能的内存集实现的博客文章。

本文翻译自:https://msrc-blog.microsoft.com/2020/07/02/solving-uninitialized-kernel-pool-memory-on-windows/

解决Windows上未初始化的内核池内存

本文始发于微信公众号(Ots安全):解决Windows上未初始化的内核池内存

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: