在 Microsoft 平台上引入内核清理程序

渗透技巧 1年前 (2023) admin
358 0 0

作为微软不断提高安全基线承诺的一部分,我们一直在Windows 11安全手册中概述的芯片到云安全基础上引入创新。Windows 11 Security Book .强大的基础安全性使我们能够从头构建防御,并开发设计安全的产品,这些产品能够抵御当前和未来的威胁。

这些创新使Microsoft能够在Windows和其他Microsoft产品交付给客户之前进一步提高嵌入到这些产品中的安全性。例如,在过去几年中,我们一直在不同的Microsoft平台上构建和开发对内核杀毒软件的支持,这些强大的检测功能可以发现内核模式组件中的错误。内核清理程序的覆盖范围和精度超过了其他类似功能的能力,使微软工程团队能够在软件开发周期中比以往任何时候都更早地识别和修复漏洞。

Microsoft的各个团队使用这些内核清理程序来执行模糊化、压力测试和其他开发任务。内核清理程序已经显示出它们有可能消除所有类型的内存错误,我们将继续将这些功能的实现从Windows扩展到其他平台,包括Xbox和Hyper-V。这项工作将持续改进Microsoft产品和服务的软件质量和安全性,并最终为客户提供更好、更安全的用户体验。

在这篇博客文章中,我们分享了Microsoft进攻性研究&安全工程(莫尔斯)在内核清理程序方面所做工作的技术细节、它们对Windows和其他平台的影响,以及它们为不断提高内置安全性提供的机会。

用户模式AddressSanitizer以及我们为什么进一步提高安全性

AddressSanitizer(ASAN)是一种编译器和运行时技术,最初由Google开发,用于检测C/C++程序中的几类内存错误,包括缓冲区溢出等关键安全错误。ASAN在Windows用户模式应用程序上的支持于2019年推出,并于2021年扩展到Xbox用户模式应用程序。

在微软内部,ASAN已经被用来识别和修复用户模式软件组件中的错误,现在在开发过程中经常用于用户模式组件。

但是,用户模式只是Windows的一层。Windows操作系统是一个现代化的复杂软件,它涉及在不同权限域中运行并相互交互的多个组件:

           在 Microsoft 平台上引入内核清理程序图1:Windows操作系统中的权限域

虽然ASAN在捕捉Windows用户模式组件中的错误方面很有效,但我们需要一个类似的功能来同样检测操作系统其他层中的错误。我们从Windows内核攻击面开始。

内核地址清理程序简介

Kernel AddressSanitizer(KASAN)是用户模式ASAN的一个变体,专门为Windows内核及其驱动程序设计。

实施细节

让我们深入了解实现的技术细节,重点关注在驱动程序上启用KASAN的用例。应该注意的是,当在Windows内核本身上启用KASAN时,同样的原理也适用。

跟踪逻辑:影子

KASAN的工作原理是,首先使用一个新的16TB虚拟内存区域(称为 阴影,其作用类似于指示内核虚拟地址空间的每个字节是否有效的大位图。

阴影的大小是内核虚拟地址空间大小的1/8,并且线性地支持所有内核虚拟地址空间:

在 Microsoft 平台上引入内核清理程序图2:KASAN阴影

内核地址空间中的每组8个字节由影子中的一个字节支持。由于KASAN影子是内核内存的一个区域,它驻留在内核虚拟地址空间中,因此也隐式地支持它自己:

在 Microsoft 平台上引入内核清理程序图3:地址空间中的KASAN阴影

最初,影子只是一个16TB的大型只读区域,它映射到一个4KB的全为零的物理页面。因此,即使保留了16TB的虚拟内存,也只使用了一页物理内存:

在 Microsoft 平台上引入内核清理程序图4:阴影的初始物理布局

稍后,在运行时,当内核执行内存分配时,例如通过ExAllocatePool2(),它会使支持这些分配的影子可写。它通过动态分配新的物理页面并将阴影的一部分重新映射到这些页面,同时保持其他部分不变并仍然指向初始只读零页面来实现这一点:

在 Microsoft 平台上引入内核清理程序图5:如何将物理内存划分为已使用和未使用的影子部分

这个分割阴影的过程发生在每次后端内存分配期间。总的来说,影子的内存消耗会随着其部分内容逐渐变为可写以支持内核内存分配而增加。

然后,KASAN运行时根据每个内存分配的当前状态动态初始化其影子值。在内存分配过程中,一旦影子对所分配的缓冲区可写,KASAN运行时就会将影子中所分配的缓冲区标记为有效,而将其上下的填充标记为无效:

在 Microsoft 平台上引入内核清理程序图6:分配后的影子状态

稍后,当这个缓冲区被释放时,KASAN运行时在影子中将其标记为完全无效:

在 Microsoft 平台上引入内核清理程序图7:解除分配后的影子状态

通过以这种方式更新影子内容,KASAN运行时可以保持一致的视图,即哪些内存字节是有效的,哪些是无效的。从那里, ASAN仪器,然后被用作验证逻辑的一部分,以使用影子提供的信息对存储器访问实施有效性检查。

验证逻辑:ASAN仪器

作为在目标内核模式组件(如内核驱动程序)上启用KASAN的一部分,必须使用一组特定的编译器标志重新编译该组件,这些标志会导致编译器将ASAN指令插入直接编译的二进制文件中。

编译器选择以下两种验证方法之一:

1.对 *_asan*()*的函数调用:在程序每次访问内存之前,编译器插入对_asan{load,store}{#n}(Address)函数之一的调用,根据访问的具体情况进行选择,并将将要访问的内存地址作为参数传递。例如,如果访问是两个字节的写入,则选择__asan_store2(),并因此在执行访问之前由编译的程序调用。我们不会讨论_asan*()函数的细节,因为有公开文档可用,但总的来说,这些函数计算将要访问的内存的影子地址,并验证影子是否表示从作为参数给定的地址开始的#n字节有效。否则,这些函数将暂停程序执行。

2.验证字节码 :为了提高性能,在某些情况下,编译器不插入函数调用,而是直接内联实现相同逻辑的字节码- 字节码读取名为__asan_shadow_memory_dynamic_address的全局变量的值(其必须存在于程序中),从其计算将要被访问的存储器的影子地址,并验证阴影是否表明存储器有效。如果不是,字节码将调用一个_asan_report*()函数,该函数将暂停程序执行。

虽然这个ASAN工具最初是为用户模式组件开发的,但它在KASAN中可以按原样重用,例如:

NTOS内核有一个KASAN运行时,它导出 _阿散*() 功能。这些函数可以由用KASAN编译的驱动程序导入,也可以在用KASAN编译NTOS本身时使用。

在NTOS内, __asan影子内存动态地址 声明全局变量,并在使用KASAN编译NTOS时初始化和使用,但不导出到驱动程序。对于驱动程序,基于 卡桑图书馆 被使用并在下面描述。

程序执行的暂停以内核错误检查的形式实现:当访问影子中标记为无效的内存字节时,KASAN触发 KASAN非法访问(0x1F2) 错误检查。这个错误检查的参数包括调试的有用信息,例如访问的内存类型(堆、堆栈等)、访问的字节数、访问是读还是写,以及其他元数据。

因此,ASAN仪器构成KASAN验证逻辑:它验证组件在运行时访问的每个内存字节是否在KASAN影子中被标记为有效,如果不是,则触发错误检查,以报告任何非法内存访问。

手术工具面临的挑战

让ASAN工具在KASAN中工作有几个挑战,特别是在编译器插入字节码而不是函数调用的情况下。

使用正确的计算

获取常规地址的影子地址的预期计算如下:

Shadow(Address) = ShadowBaseAddress + OffsetWithinAddressSpace(Address) / 8

在用户模式AddressSanitizer的上下文中,用户模式地址空间从地址0x0开始。因此,任何用户模式地址都等于该地址在用户模式地址空间内的偏移量:

OffsetWithinUserAddressSpace(UserAddress) = UserAddress – 0                  
                                             = UserAddress

因此,作为ASAN工具的一部分插入的验证字节码使用以下公式,其中__asan_shadow_memory_dynamic_address包含阴影的基址:

Shadow(Address) = __asan_shadow_memory_dynamic_address + (Address / 8)

下面是生成的字节码程序集的示例:

mov     rcx, cs:__asan_shadow_memory_dynamic_address                  
shr     rax, 3                  
add     rcx, rax

这里,字节码通过阅读__asan_shadow_memory_dynamic_address并将RAX右移3(意味着除以8)后的值与之相加来计算RAX的影子地址。这实现了前面提到的公式来获得地址的影子。

然而,在内核AddressSanitizer的上下文中,内核模式地址空间从地址0xFFFF80000000000开始:

OffsetWithinKernelAddressSpace(KernelAddress) = KernelAddress –                  
                                                    0xFFFF800000000000                  
                                                != KernelAddress

因此,在KASAN中按原样重复使用简化公式是不正确的:则将导致在验证存储器访问时字节码使用错误的影子地址。我们通过将__asan_shadow_memory_dynamic_address初始化为一个不同的值来解决KASAN中的这个问题,这个值并不完全是KASAN影子的基址:

__asan_shadow_memory_dynamic_address = KasanShadowBaseAddress –                  
                                                      (0xFFFF800000000000 / 8)

使用该值推导公式,得出以下结果:

Shadow(KernelAddress) = __asan_shadow_memory_dynamic_address + (KernelAddress / 8)                  
     = KasanShadowBaseAddress – (0xFFFF800000000000 / 8) + (KernelAddress / 8)                  
     = KasanShadowBaseAddress + (KernelAddress – 0xFFFF800000000000) / 8                  
     = KasanShadowBaseAddress + OffsetWithinKernelAddressSpace(KernelAddress) / 8

因此,该公式福尔斯了使用KASAN的预期计算:字节码取KASAN影子的基地址,将其加上内核地址空间内的地址偏移量除以8,这就得到了给定内核地址的正确影子地址。

使用这个技巧,我们避免了通过修改编译器来修改KASAN的字节码生成。

处理非跟踪内存

当我们描述如何映射KASAN阴影时,我们没有解释为什么要使用一种带有零页的拆分机制。原因很简单:验证字节码总是想要读取它们所验证的缓冲器的阴影,并且它们不知道缓冲器是否具有支持它的阴影。因此,内核虚拟地址内存的每个字节必须始终映射一个阴影,这要归功于拆分机制,该机制保证阴影始终存在,同时通过将非跟踪区域的阴影指向单个置零的物理页面来最小化非跟踪区域的内存消耗。

拆分机制中使用的物理页全是零,这一事实会导致KASAN始终将非跟踪内存视为有效内存。

处理用户模式指针

Windows内核及其驱动程序可以直接访问用户模式内存,例如获取传递给系统调用的用户模式参数。这就给验证字节码带来了一个问题,因为它们需要获得内核地址空间之外的地址的影子,因此没有影子。

为了处理这种情况,我们传递了一个编译器标志作为KASAN的一部分,指示编译器永远不要使用字节码,并且总是优先使用_asan*()函数调用除非编译器确定访问的是堆栈内存。

这意味着在实践中,为了验证对堆栈上局部变量的访问,编译器生成验证字节码,但对于任何其他访问,编译器使用函数调用 _阿散*()。鉴于这些 _阿散*() 函数是在KASAN运行时中实现的,我们可以完全控制它们的验证逻辑,并且可以确保通过简单的 如果条件。

通过使用这个技巧,我们再次避免了为了让插装处理用户模式指针而对编译器进行更改的需要。

告诉内核导出KASAN支持

默认情况下,内核不创建KASAN影子,也不导出KASAN运行时。换句话说,默认情况下,它不会使KASAN对驱动程序可用。为此,用户必须显式设置以下注册表项:

HKLM系统当前控制集控制会话管理器内核启用Kasan

引导加载程序在引导时读取这个键,并根据它的值决定是否指示内核使驱动程序可以使用KASAN支持。

在此基础上,下面几节将详细介绍内核如何加载用KASAN编译的驱动程序。

使用KASAN加载内核驱动程序

对于用KASAN编译的驱动程序,一个名为 卡桑图书馆链接到最终的驱动程序二进制文件中,并执行两项操作:

1.它声明一个 __asan影子内存动态地址 一个全局变量,它保持在驱动程序本身的本地,并且不导出到内核命名空间。前面描述的插入到驱动程序中的验证字节码使用这个全局变量作为KASAN影子计算的一部分。

2.它在驱动程序的PE二进制结果中发布一个名为“KASAN”的部分。本节包含信息和元数据,其格式将来可能会更改,与此处讨论无关。

加载驱动程序时,内核会验证驱动程序是否有“KASAN”部分,并且可以采用两种路径:

1.如果驱动程序具有“KASAN”部分,并且 启用Kasan 注册表项,则内核将拒绝加载驱动程序。这是为了防止系统发生故障;毕竟,驱动程序不可能工作,因为它将试图使用不是由内核创建的影子,并调用内核不导出的运行时。

2.如果驱动程序具有“KASAN”部分,并且 启用Kasan 注册表项,则内核将解析此部分,以便在驱动程序上初始化KASAN。此初始化的一部分包括设置驱动程序的 __asan影子内存动态地址 全局变量。初始化完成后,”KASAN”部分将不再使用,并从内存中丢弃,以节省内核内存。

从那时起,驱动程序可以开始执行。

所有这些是如何福尔斯在一起的:越野车驾驶员示例

我们现在已经公开了KASAN在驱动程序上工作所需的所有成分:影子是如何创建的,工具是如何操作的,内核是如何导出KASAN支持的,以及当驱动程序加载时KASAN是如何在驱动程序上初始化的。为了给予一个示例来说明所有这些是如何福尔斯一起的,让我们考虑一个使用KASAN编译的假设驱动程序。

我们已经在系统中设置了KasanEnabled注册表项,因此内核创建了一个KASAN影子并导出KASAN运行时。我们继续在系统中加载驱动程序。内核看到驱动程序的PE有一个“KASAN”部分,解析它,初始化驱动程序上的KASAN,并丢弃该部分。驱动程序最终开始执行。

让我们假设驱动程序包含以下错误代码:

PCHAR buffer;                
buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, 18, WHATEVER_TAG);                
buffer[18] = ‘a’;

这里,分配大小为18字节的堆缓冲区。在此分配过程中,KASAN运行时初始化缓冲区下方和上方的两个红色区域,如前所述。然后将’a’写入缓冲区的第19个字节。当然,这是一种越界写访问,这是不正确的,可能会导致严重的安全风险。

由于我们的驱动程序是用KASAN编译的,因此它要服从ASAN指令插入,这意味着实际编译的代码如下所示:

PCHAR buffer;                
buffer = ExAllocatePool2(POOL_FLAG_NON_PAGED, 18, WHATEVER_TAG);                
__asan_store1(&buffer[18]);                
buffer[18] = ‘a’;

编译器在这里插入了一个函数调用 __存储区1()并且不选择验证字节码,因为它不能断定“buffer”是指向堆栈存储器的指针(它不是)。

__asan_store1()是内核导出和驱动程序导入的KASAN运行时的一部分。此函数查看&缓冲区[18]的阴影,发现它被标记为无效(因为此地址的字节是缓冲区右侧红区的一部分),并继续发出KASAN_ILLEGAL_ACCESS错误检查以暂停系统执行。

作为系统的所有者,我们可以收集崩溃转储,并使用随KASAN错误检查一起提供的可操作信息调查KASAN检测到的内存安全错误是什么。

如果没有KASAN,这个bug就不容易被发现。然而,使用KASAN,它可以错误触发并转变为真实的的安全风险之前立即被检测到。因此,KASAN能够检测到整个类别的内存错误,否则可能仍然没有发现。

粒度和覆盖的内存区域

从我们到目前为止提供的细节可以推断出,KASAN以字节粒度运行。KASAN目前能够检测几种类型内存区域上的非法内存访问:

全局变量

内核堆栈

池分配器(外部分配池 *()

后备列表分配器(从查找列表中移除分配Ex()等等)

IO/连续分配器(MmMapIoSpaceEx()函数Mm分配连续节点内存()等等)

在内部,对这些区域的支持是使用同样由NTOS内核导出的KASAN API实现的。Microsoft将继续改进此API,以将其实现扩展到其他场景。

由于能够在字节粒度上检测bug,并且覆盖了大量内存区域,KASAN超过了现有bug检测技术(如Special Pool)的能力,后者通常在较粗的粒度上操作,并且不覆盖内核堆栈和其他区域。

绩效成本

当然,KASAN影子会消耗内存,ASAN工具插入的有效性检查会消耗CPU时间,并增加编译后的二进制文件的大小。

一些工作已经进入了微观优化KASAN,通过限制KASAN运行时发出的指令数量,通过使KASAN影子NUMA感知,通过压缩KASAN元数据以减少二进制大小,等等。

总的来说,KASAN目前带来了约2倍的速度下降,我们使用广泛可用的基准测试工具对此进行了测量。因此,KASAN不能被看作是一个生产特性,因为其性能成本不可忽略。但是,对于调试、开发、压力测试或与安全相关的设置来说,这个成本是可以接受的。

应当指出,这一成本高于现有技术,如特殊池,而且KASAN在未显式启用时不会对性能产生影响。也就是说,默认情况下,KASAN并不影响Windows 11的性能。

直接影响

微软生成了特殊的Windows版本,称为MegaAsan版本,生成完全可启动的Windows磁盘,这些磁盘在Windows内核和微软在Windows 11中提供的超过95%的内核驱动程序上启用了KASAN。

通过在测试、模糊处理以及简单的桌面设置中使用这些构建版本,莫尔斯已经能够识别并修复各种驱动程序和Windows内核中的35个以上的内存安全错误,这些错误以前是现有技术无法检测到的。

我们还实现了对Xbox内核及其加载的驱动程序的KASAN支持,并类似地生成了启用KASAN的Xbox系统构建版本。因此,KASAN也有助于Xbox产品线的质量和安全性。

将ASAN扩展到其他ring0域

到目前为止,我们已经讨论了Windows内核驱动程序和Windows内核上的KASAN:

在 Microsoft 平台上引入内核清理程序图8:操作系统中的KASAN

拥有KASAN是一个相当大的进步,因为它以一种以前无法实现的方式提供了对系统的大型和关键部分的内存错误的精确检测。继我们在KASAN上的工作之后,我们在该系统的其余部分开发了类似的探测能力。

介绍SKASAN…

安全内核是一种完全独立于Windows内核的不同内核,它在更高权限的域中执行,并负责系统中的许多安全操作。它是Windows上基于虚拟化的安全性的一部分。

我们开发了Secure Kernel AddressSanitizer(SKASAN),它涵盖了Secure内核和它动态加载的一些模块。

SKASAN与KASAN有许多相似之处。例如,SKASAN对安全内核模块的支持是使用“SKASAN”部分实现的,相当于Windows内核驱动程序中使用的“KASAN”部分。总的来说,SKASAN的工作方式与KASAN类似,但只适用于Secure内核域。

……还有HASAN

最后,Hyper-V是微软的虚拟机管理程序,在Windows和Azure中扮演着核心角色,它也可以从ASAN提供的功能中受益;因此,我们开发了Hyper-V地址清理器(HASAN),它是另一种ASAN实现,但是绑定到Hyper-V内核。

一样,但不同…但还是一样

KASAN、SKASAN和HASAN都是基于相同的逻辑构建的,都有一个影子和一个编译器插装,总体上在内存消耗和速度减慢方面的成本相似。

然而,确实存在一些固有的差异。首先,Windows内核、Secure内核和Hyper-V内核具有不同的分配器,*ASAN对它们的支持也相应不同。第二,这些内核的内存布局是不同的,这导致了巨大的实现差异;例如,HASAN实际上使用两个不同的阴影连接在一起。

我们将其余的技术差异作为逆向工程练习留给感兴趣的读者。

最后的画面和结果

截至2022年11月,我们已经开发并稳定了KASAN、SKASAN和HASAN。结合在一起,这些功能可精确检测 全部在Windows 11上执行的内核模式组件:

在 Microsoft 平台上引入内核清理程序

图9:操作系统中的所有 ASAN实现

我们在内部生成MegaAsan构建版本时启用了所有这些 *ASAN实现,内部团队在许多模糊和压力测试场景中使用它们。因此,我们已经能够识别并修复数十个不同严重程度的内存错误:

在 Microsoft 平台上引入内核清理程序

图10:*ASAN发现的bug类型

最后,作为 *ASAN工作的一部分,我们还对各个领域(如Windows和Hyper-V内核)以及Microsoft Visual C++(MSVC)编译器进行了大量改进和清理,以改善 *ASAN在Microsoft平台上的体验。

总的来说,这些 *ASAN功能有可能消除整个类别的内存错误,并将大大有助于确保微软产品的质量和安全性。

           

这就是我们关于内核清理程序的第一篇博文。除了 ASAN之外,我们还实现了其他几个专门用于发现其他类型bug的杀毒程序。我们将在以后的帖子中与您沟通。


翻译自网络

原文始发于微信公众号(闲聊知识铺):在 Microsoft 平台上引入内核清理程序

版权声明:admin 发表于 2023年3月6日 下午8:51。
转载请注明:在 Microsoft 平台上引入内核清理程序 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...