现代操作系统中内核的内存公开

切刀的下面是Mateusz Jurczyk 用x86模拟和污点跟踪检测内核内存泄露 文档( 文章项目零 )的开头部分的译文


在文档的翻译部分中:


  • C编程语言特性(作为内存扩展问题的一部分)
  • Windows和Linux内核的操作细节(作为内存扩展问题的一部分)
  • 内核内存公开的重要性及其对操作系统安全性的影响
  • 检测和应对内核内存泄漏的现有方法和技术

尽管该文档侧重于OS特权内核与用户应用程序之间的通信机制,但是对于不同安全域之间的任何数据传输,问题的本质可以归纳为一般化:系统管理程序是客户机,特权系统服务(守护程序)是GUI应用程序,网络客户端是服务器等。 。


KDPV


引言


现代操作系统的任务之一是确保用户应用程序和OS内核之间的特权分离。 首先,这包括以下事实:每个程序对运行时的影响应受到某种安全策略的限制;其次,程序只能访问允许其读取的信息。 鉴于C语言(内核开发中使用的主要编程语言)的特性,第二种方法很难提供,这使得在不同安全域之间安全地传输数据极为困难。


在x86 / x86-64平台上运行的现代操作系统是多线程的,并使用客户端-服务器模型,在该模型中,独立执行用户模式应用程序(客户端)并调用OS内核(服务器),以使用由系统管理的资源。 用户模式代码( 环3 )调用一组预定义的内核函数(环0)所使用的机制称为系统调用或(简称)syscall。 典型的系统调用如图1所示:
图1:系统调用
图1:系统调用生命周期。


与用户模式程序进行交互时,避免无意中泄漏内核内存内容非常重要。 泄露敏感的内核数据存在很大的风险。 可以在安全的系统调用的输出参数中隐式传输数据(从其他角度来看)。


当OS内核返回的内存区域大于存储相应信息所需的内存区域(包含在内部)时,就会发生特权系统内存的泄露。 通常,冗余字节包含在不同上下文中填充的数据,然后未预先初始化内存,这将阻止信息在新的数据结构中传播。


C编程语言细节


在本节中,我们研究C语言的几个方面,这些方面对于内存扩展问题最为重要。


未初始化变量的未定义状态


简单类型的单个变量(例如char或int)以及数据结构的成员(数组,结构和联合)将保持未定义状态,直到第一次初始化为止(无论它们是放在堆栈还是堆上)。 C11规范的相关报价(ISO / IEC 9899:201x委员会草案N1570,2011年4月):


6.7.9初始化
...
10如果未自动初始化具有自动存储期限的对象,则其值不确定

7.22.3.4 malloc函数
...
2 malloc函数为大小由大小指定且值不确定的对象分配空间。

7.22.3.5 realloc函数
...
2 realloc函数重新分配ptr指向的旧对象,并返回指向大小由size指定的新对象的指针。 在重新分配之前,新对象的内容应与旧对象的内容相同,直到新大小和旧大小中的较小者为止。 新对象中超出旧对象大小的任何字节都具有不确定的值

适用于系统代码的部分与位于堆栈上的对象最相关,因为OS内核通常具有带有其自身语义的动态分配接口(不一定与标准C库兼容,这将在后面描述)。


据我们所知,Windows和Linux的三种最流行的C编译器(Microsoft C / C ++编译器,gcc,LLVM)均未创建可在Release-build模式(或等效模式)下预先初始化堆栈中程序员未初始化变量的代码。 有一些用于用特殊字节标记堆栈帧的编译器选项-标记(例如Microsoft Visual Studio中的RTC),但是出于性能原因,它们未在Release版本中使用。 结果,堆栈上的未初始化变量会继承相应存储区旧值。


考虑一个虚拟Windows系统调用的标准实现示例,该示例将输入整数乘以2并返回相乘的结果(清单1)。 显然,在特殊情况下(InputValue == 0),变量OutputValue保持未初始化状态,并被复制回客户端。 此错误使您可以为每个调用打开四个字节的内核堆栈内存。


NTSTATUS NTAPI NtMultiplyByTwo(DWORD InputValue, LPDWORD OutputPointer) { DWORD OutputValue; if (InputValue != 0) { OutputValue = InputValue * 2; } *OutputPointer = OutputValue; return STATUS_SUCCESS; } 

代码1:通过未初始化的局部变量扩展内存。


在实践中,未初始化的局部变量的泄漏并不常见:一方面,现代编译器经常检测到并警告此类问题,另一方面,此类泄漏是可以在开发或测试期间检测到的功能错误。 但是,第二个示例(在清单2中)显示,也可以通过structure字段发生泄漏。


在这种情况下,保留结构字段永远不会在代码中显式使用,而是仍会复制回用户模式,因此,还会向调用代码公开四字节的内核内存。 此示例清楚地表明,初始化所有返回到代码执行分支的返回给客户端的结构的每个字段并非易事。 在许多情况下,强制初始化似乎是不合逻辑的,尤其是在此字段不起作用的情况下。 但是事实上,堆栈(或堆)上的未初始化变量(或结构域)接受先前存储在该内存区域(在另一操作的上下文中)的数据内容是内核内存扩展问题的核心。


 typedef struct _SYSCALL_OUTPUT { DWORD Sum; DWORD Product; DWORD Reserved; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtArithOperations( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.Product = InputValue * 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

清单2:通过保留的结构字段扩展内存。


结构和填充字节的对齐


初始化输出结构的所有字段是避免扩展内存的良好开始。 但这还不足以保证在低级表示中没有未初始化的字节。 让我们回到C11规范:


6.5.3.4 sizeof和alignof运算符
...
4应用于具有结构或联合类型的操作数时,结果是此类对象中的字节总数, 包括内部填充和结尾填充

6.2.8对象对齐
1完整的对象类型具有对齐要求,这些条件限制了可以分配该类型对象的地址 。 对齐方式是实现定义的集成整数值,表示可以分配给定对象的连续地址之间的字节数。 [...]

6.7.2.1结构和联合规范
...
17 在结构或联合的末尾可能存在未命名的填充

也就是说,用于x86(-64)架构的C语言编译器使用结构字段(具有原始类型)的自然对齐方式:每个此类字段都按N个字节对齐,其中N是字段的大小。 此外,在数组中声明整个结构和联接时,它们也将对齐,并且满足嵌套字段对齐的要求。 为了确保对齐,在必要时将隐式填充字节插入结构中。 尽管它们不能在源代码中直接访问,但这些字节还从存储区继承了旧值,并且可以将信息传输到用户模式。


在清单3的示例中,SYSCALL_OUTPUT结构返回到调用代码。 它包含4个字节和8个字节的字段,用4个填充字节分隔,这是LargeSum字段的地址变为8的倍数所必需的。 尽管两个字段均已正确初始化,但填充字节并未显式设置,这再次导致了内核堆栈内存的扩展。 该结构在内存中的特定位置如图2所示。


 typedef struct _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; OutputStruct.LargeSum = 0; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

清单3:通过对齐结构扩展内存。


图2:对齐结构
图2:在内存中表示结构时要牢记对齐。


对齐泄漏是相对常见的,因为系统调用的很多输出参数都由结构表示。 对于64位平台,该问题尤为严重,其中指针的大小,size_t和类似类型的大小从4字节增加到8字节,这导致出现对齐此类结构的字段所需的填充。


由于填充字节无法在源代码中进行寻址,因此有必要使用memset或类似函数在初始化结构的任何字段并将其复制到用户模式之前重置结构的整个存储区,例如:


  memset(&OutputStruct, 0, sizeof(OutputStruct)); 

但是,Seacord RC在其“ CERT C编码标准,第二版:开发安全,可靠和安全的系统的98条规则。Addison-WesleyProfessional” 2014年一书中指出,这不是理想的解决方案,因为填充字节)可能在调用memset之后仍然被删除,例如,作为对相邻字段进行操作的副作用。 可以通过规范C中的以下陈述来证明关注的理由:


6.2.6类型表示
6.2.6.1概述
...
6 当将值存储在结构或联合类型的对象中 (包括在成员对象中)时,与任何填充字节相对应的对象表示形式的字节将使用未指定的值 。 [...]

但是,实际上,我们测试的所有C编译器都没有在显式声明的字段的内存区域之外进行读写操作。 似乎这种观点被使用memset的操作系统的开发人员所共有。


不同大小的联合和字段


在与特权较低的调用代码进行通信的上下文中,联接是另一种复杂的C语言构造。 考虑一下C11规范如何描述内存中的并集表示:


6.2.5类型
...
20可以从对象和函数类型构造任意数量的派生类型,如下所示: 联合类型描述成员对象的重叠非空集 ,每个成员对象都有一个可选的指定名称和可能不同的类型。

6.7.2.1结构和联合规范
...
6如6.2.5中所述,结构是由一系列成员组成的类型,其存储按有序序列分配,而联合则是由一系列成员组成的类型,这些成员的存储重叠
...
16 工会的规模足以容纳最大的工会会员 。 成员中最多一个的值可以随时存储在联合对象中。

问题在于,如果联合由不同大小的多个字段组成,并且仅显式初始化了一个较小大小的字段,则分配用于容纳大字段的其余字节将保持未初始化状态。 让我们来看一个清单4所示的假设系统调用处理程序的示例,以及图3所示的SYSCALL_OUTPUT联合内存分配。


 typedef union _SYSCALL_OUTPUT { DWORD Sum; QWORD LargeSum; } SYSCALL_OUTPUT, *PSYSCALL_OUTPUT; NTSTATUS NTAPI NtSmallSum( DWORD InputValue, PSYSCALL_OUTPUT OutputPointer ) { SYSCALL_OUTPUT OutputStruct; OutputStruct.Sum = InputValue + 2; RtlCopyMemory(OutputPointer, &OutputStruct, sizeof(SYSCALL_OUTPUT)); return STATUS_SUCCESS; } 

代码4:通过部分初始化并集来扩展内存。


图3:对齐联接
图3:内存中具有并集的联合的表示。


事实证明,SYSCALL_OUTPUT联合的总大小为8个字节(由于较大的LargeSum字段的大小)。 但是,该函数仅设置较小字段的值,未初始化4个尾随字节,随后导致其客户端应用程序泄漏。


一种安全的实现方式应仅在用户地址空间中设置Sum字段,而不应将整个对象与可能未使用的存储区一起复制。 另一个可行的解决方法是调用memset函数,以在设置内核中的任何字段并将其传输回用户模式之前,使该副本在内核内存中无效。


不安全大小


如前两节所示,使用sizeof运算符可以直接或间接有助于显示内核内存,从而导致复制的数据比以前初始化的更多。


C没有必要的设备将数据安全地从内核安全地传输到用户空间,或更普遍地,在任何不同的安全上下文之间传输。 该语言不包含可以明确指示在用于与OS内核进行交互的每个数据结构中设置了哪些字节的运行时元数据。 结果,责任在于程序员,程序员必须确定每个对象的哪些部分应该传递给调用代码。 如果正确完成,则需要为系统调用中使用的每个输出结构编写一个单独的安全复制功能。 反过来,这将导致代码的大小膨胀,可读性下降,并且通常将是一项繁琐且耗时的任务。


另一方面,通过单个memcpy调用和sizeof参数复制内核的整个内存区域,方便又简单,让客户端确定将使用输出的哪些部分。 事实证明,这种方法如今已在Windows和Linux中使用。 并且,当检测到特定情况下的信息泄漏时,带有memset调用的补丁将立即提供并由OS制造商分发。 不幸的是,这不能解决一般情况下的问题。


操作系统细节


有某些内核设计解决方案,编程方法和代码模式会影响操作系统对内存扩展漏洞的承受程度。 以下各节将介绍它们。


重用动态内存


动态内存的当前分配器(在用户模式和内核模式下)都得到了高度优化,因为它们的性能会对整个系统的性能产生重大影响。 最重要的优化之一是内存重用:释放后,很少会完全丢弃相应的内存,而是将其保存在区域列表中,以备下次分配时返回。 为了节省CPU周期,在重新分配和新分配之间不会清除默认内存区域。 结果,事实证明,内核的两个未连接部分在短时间内使用相同的内存范围。 这意味着内核动态内存内容的泄漏使您可以揭示各种OS组件的数据。


在以下各段中,我们简要概述了Windows和Linux内核中使用的分配器及其最值得注意的质量。


窗户
Windows内核池管理器的关键功能是ExAllocatePoolWithTag ,可以直接调用它,也可以通过以下可用外壳程序之一调用它:ExAllocatePool {∅,Ex,WithQuotaTag,WithTagPriority}。 这些函数都不是默认情况下或通过任何输入标志刷新返回的内存的内容的。 相反,它们在各自的MSDN文档中均具有以下警告:


注意函数分配的内存尚未初始化。 如果内核模式驱动程序要使其对用户模式软件可见,则必须首先将该内存清零(以避免泄漏潜在的特权内容)。

调用代码可以选择六种主要的池类型之一:NonPagedPool,NonPagedPoolNx,NonPagedPoolSession,NonPagedPoolSessionNx,PagedPool和PagedPoolSession。 它们每个在虚拟地址空间中都有一个单独的区域,因此分配的内存区域只能在相同的池类型内重用。 内存块的重用频率很高,通常只有在后备列表中找不到合适的记录,或者请求太大而需要新的内存页面时,才返回零区域。 换句话说,当前几乎没有任何因素可以阻止Windows中池内存的公开,并且几乎每个此类错误都可用于从内核的不同部分泄漏敏感数据。


的Linux
Linux内核具有三个用于动态分配内存的主要接口:


  • kmalloc-用于分配任意大小(在虚拟和物理地址空间中连续)的内存块的通用函数,使用slab内存分配
  • kmem_cache_createkmem_cache_alloc-一种用于分配固定大小的对象(例如结构)的专用机制,也使用平板内存分配
  • vmalloc是一种很少使用的分配函数,它返回在物理内存级别无法保证连续性的区域。

这些功能(本身)不能保证所选区域不会包含旧的(可能是机密的)数据,这使得打开内核堆的内存成为可能。 但是,调用代码可以通过几种方式请求无效的内存:


  • kmalloc函数具有kzalloc的类似物,可确保清除返回的内存。
  • 可以将可选的__GFP_ZERO标志传递给kmallockmem_cache_alloc和其他一些函数来实现相同的结果。
  • kmem_cache_create接受指向可选构造函数的指针,该函数将在将每个对象返回到调用代码之前对其进行预初始化。 构造函数可以实现为memset的包装器,以将给定的内存区域清零。

我们认为这些选项的可用性是内核安全的有利条件,因为它们鼓励开发人员做出明智的决定,并允许他们简单地使用现有的内存分配功能,而不是在每次分配动态内存后添加其他memset调用。


固定大小的数组


可以通过其测试名称来访问许多OS资源。 Windows中的命名资源种类繁多,例如:文件和目录,注册表项的键和值,窗口,字体等等。 对于其中一些名称,名称长度是受限制的,并由常量表示,例如MAX_PATH(260)或LF_FACESIZE(32)。 在这种情况下,内核开发人员通常通过声明最大大小的缓冲区并将其整体复制(例如,使用sizeof关键字)来简化代码,而不是仅处理该行的相应部分。 如果字符串是较大结构的成员,则这特别有用。 这样的对象可以在内存中自由移动,而不必担心管理指向动态内存的指针。


如您所料,大型缓冲区很少被完全使用,剩余的存储空间通常不被刷新。 这可能导致内核内存的长连续区域特别严重的泄漏。 在清单5的示例中,系统调用使用RtlGetSystemPath函数将系统路径加载到本地缓冲区中,并且,如果调用成功,则所有260个字节都会传递给调用者,而不管实际的行长如何。


 NTSTATUS NTAPI NtGetSystemPath(PCHAR OutputPath) { CHAR SystemPath[MAX_PATH]; NTSTATUS Status; Status = RtlGetSystemPath(SystemPath, sizeof(SystemPath)); if (NT_SUCCESS(Status)) { RtlCopyMemory(OutputPath, SystemPath, sizeof(SystemPath)); } return Status; } 

清单5:通过部分初始化字符串缓冲区来扩展内存。


在此示例中,复制回用户空间的内存区域如图4所示。


图4:部分初始化的串联缓冲存储器
图4:部分初始化的行缓冲区的内存。


一个安全的实现应该只返回请求的路径,而不是返回用于存储的整个缓冲区。 此示例再次说明,就内核必须传递给用户区域的实际数据量而言,使用sizeof运算符(用作RtlCopyMemory的参数)估算数据大小可能完全不正确。


任意系统调用输出大小


大多数系统调用都接受指向用户模式输出的指针以及缓冲区的大小。 在大多数情况下,大小信息仅应用于确定所提供的缓冲区是否足以接收系统调用输出。 不要使用提供的输出缓冲区的整个大小来指定要复制的内存量。 但是,在某些情况下,内核将尝试使用用户输出缓冲区的每个字节,而不计算需要复制的实际数据量。 清单6显示了这种行为的一个示例。


 NTSTATUS NTAPI NtMagicValues(LPDWORD OutputPointer, DWORD OutputLength) { if (OutputLength < 3 * sizeof(DWORD)) { return STATUS_BUFFER_TOO_SMALL; } LPDWORD KernelBuffer = Allocate(OutputLength); KernelBuffer[0] = 0xdeadbeef; KernelBuffer[1] = 0xbadc0ffe; KernelBuffer[2] = 0xcafed00d; RtlCopyMemory(OutputPointer, KernelBuffer, OutputLength); Free(KernelBuffer); return STATUS_SUCCESS; } 

清单6:通过任意大小的输出缓冲区扩展内存。


系统调用的目的是为调用代码提供三个特殊的32位值,总共占用12个字节。 尽管在函数的开头检查正确的缓冲区大小是正确的,但是OutputLength参数的使用应在此结束。 知道输出缓冲区足够大以保存结果后,内核可以分配12个字节的内存,将其填满,然后将内容复制回提供的用户模式缓冲区。 而是,系统调用分配一个池块(而且具有用户控制的长度),并将整个分配的内存复制到用户空间。 事实证明,除前12个字节外,所有字节都未初始化,并且错误地向用户打开,如图5所示。


图5:任意缓冲存储器
图5:任意大小的缓冲存储器。


本节讨论的架构在Windows中尤其常见。 一个类似的错误可以为攻击者提供一个非常有用的内存扩展原语:


  • , Windows, . , .
  • . , , . , ( — ) .

, . , , .


,


, . , Windows .



, , . , : AddressSanitizer , PageHeap Special Pool . , , - . , . , , , , , . , ( ).


, , , . , .


, API
API, Windows (Win32/User32 API). API , , , . , , , , . .



, . , . , , , . , , .


, , . , KASLR (Kernel Address Space Layout Randomization ), . : Windows, Hacking Team 2015 ( Juan Vazquez. Revisiting an Info Leak ) (derandomize) win32k.sys, . , Matt Tait' Google Project Zero ( Kernel-mode ASLR leak via uninitialized memory returned to usermode by NtGdiGetTextMetrics ) MS15-080 (CVE-2015-2433).



(/) , , (control flow), : , , , , StackGuard Linux /GS Windows . , . , , .


(/)
(/) , , , : , , , . , , . . , ( , ) , , .



KDPV#2


微软视窗



2015 Windows. 2015 Matt Tait win32k!NtGdiGetTextMetrics. Windows Hacking Team. , , , 0-day Windows.


2015, WanderingGlitch (HP Zero Day Initiative) ( Acknowledgments – 2015 ). Ruxcon 2016 ( ) "Leaking Windows Kernel Pointers" .


, 2017 fanxiaocao pjf IceSword Lab (Qihoo 360) "Automatically Discovering Windows Kernel Information Leak Vulnerabilities" , , 14 2017 (8 ). Bochspwn Reloaded, , . VMware (Bochs) . , Bochspwn Reloaded, .


, , 2010-2011 , win32k: "Challenge: On 32bit Windows7, explain where the upper 16bits of eax come from after a call to NtUserRegisterClassExWOW()" "Subtle information disclosure in WIN32K.SYS syscall return values" . Windows 8, 2015 Matt Tait , : Google Project Zero Bug Tracker .



( ), , 2017 - Windows -, : Joseph Bialek — "Anyone notice my change to the Windows IO Manager to generically kill a class of info disclosure? BufferedIO output buffer is always zero'd" . , IOCTL- .


, Visual Studio 15.5 POD- , "= {0}", . , padding- () .


的Linux


Windows, Linux , 2010 . , ( ) ( ) . , Windows Linux , — , .



, Linux . "Linux kernel vulnerabilities: State-of-the-art defenses and open problems" 2010 2011 28 . 2017- "Securing software systems by preventing information leaks" Lu K. 59 , 2013- 2016-. . : Rosenberg Oberheide 25 , Linux 2009-2010 , . Linux c grsecurity / PaX-hardened . Vasiliy Kulikov 25 2010-2011 , Coccinelle . , Mathias Krause 21 2013 50 .


, , Linux. — -Wuninitialized ( gcc, LLVM), . kmemcheck , Valgrind' . , . , KernelAddressSANitizer KernelMemorySANitizer . KMSAN syzkaller ( ) 19 , .


Linux. 2014 — 2016 Peir´o Coccinelle , Linux 3.12: "Detecting stack based kernel information leaks" International Joint Conference SOCO14-CISIS14-ICEUTE14, pages 321–331 (Springer, 2014) "An analysis on the impact and detection of kernel stack infoleaks" Logic Journal of the IGPL. , . 2016- Lu UniSan — , , : , . , 20% (350 1800), 19 Linux Android.


— (multi-variant program execution), , . , . , KASLR, -, . , 2006 DieHard: probabilistic memory safety for unsafe languages, 2017 — BUDDY: Securing software systems by preventing information leaks. John North "Identifying Memory Address Disclosures" 2015- . , SafeInit (Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities) , , . , , , Linux.



, . , : , . , , - , . .


CONFIG_PAGE_POISONING CONFIG_DEBUG_SLAB, -. -, . , , , Linux.


grsecurity / PaX . , PAX_MEMORY_SANITIZE , slab , ( — ). , PAX_MEMORY_STRUCTLEAK , ( ), . padding- (), 100% . , — PAX_MEMORY_STACKLEAK, . , , . (Kernel Self Protection Project) STACKLEAK .


Linux:


Secure deallocation, Chow , 2005

Chow,Jim和Pfa ff,Ben和Garfinkel,Tal和Rosenblum,Mendel。粉碎垃圾:通过安全的释放来减少数据寿命。在USENIX安全研讨会上,第22-22页,2005年。


, , ( ) . Linux .


Split Kernel, Kurmus Zippel, 2014

Kurmus, Anil and Zippel, Robby. A tale of two kernels: Towards ending kernel hardening wars with split kernel. In Proceedings of the 2014 ACM SIGSAC Conference on Computer and Communications Security, pages 1366–1377. ACM, 2014.


, .


SafeInit, Milburn , 2017

Milburn, Alyssa and Bos, Herbert and Giuffrida, Cristiano. SafeInit: Comprehensive and Practical Mitigation of Uninitialized Read Vulnerabilities. In Proceedings of the 2017 Annual Network and Distributed System Security Symposium (NDSS)(San Diego, CA), 2017.


, , .


UniSan, Lu , 2016

Lu, Kangjie and Song, Chengyu and Kim, Taesoo and Lee, Wenke. UniSan: Proactive kernel memory initialization to eliminate data leakages. In Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security, pages 920–932. ACM, 2016.


SafeInit , , , , .


, Linux .


( )


, , ( ). : (), , , , ( - ) . , . , , .


, :


  • Bochspwn Reloaded – detection with software x86 emulation
  • Windows bug reproduction techniques
  • Alternative detection methods
  • Other data sinks
  • Future work
  • Other system instrumentation schemes

, :) , .

Source: https://habr.com/ru/post/zh-CN415685/


All Articles