Pwn2Own大赛之后,HP的0day防御计划(Zero Day Initiative, ZDI) 并不要求研究者给我们提供相关攻击利用。ZDI的分析师对每个提交的案例进行评估,可能也会挑选一些进行全面的攻击利用。
在内核级的漏洞(操作系统本身或者硬件驱动)中,有一项就是我们常说的’任意写(write-what-where)’[1]。 为便于分析, 很有必要写个基本的框架,对给定的’任意写(write-what-where)’漏洞进行封装,并验证其对操作系统的利用。
要把任意的写操作转换为攻击利用,需要三个基本的步骤, 我们将来逐一介绍。
禁用Windows访问权限检查的负载
Windows访问控制系统的核心是nt!SeAccessCheck函数。它决定着我们是否有权访问操作系统中的对象(文件,进程等等)。
我们将采用的手法,于1992年由Greg Hoglund[2]首次提及, 2006年由John Heasman改进使用[3]。 这里将采用后者作为我们的起点。
关键点在于高亮标注的内容。 如果是出于对用户态进程的检查,那么操作系统就会检查确认是否具有正确的权限。 但如果是内核态的,就会永远成功。 那么我们的目标,就是对Windows内核进行动态补丁,使其根据设置的AccessMode认为该访问检查调用是针对内核的。
在Windows XP上,这是非常简单的。 如果我们在IDA里检查内核(这里,我们处理的是ntkrnlpa.exe), 在SeAccessCheck实现中发现如下的代码:
PAGE:005107BC xor ebx, ebx
PAGE:005107BE cmp [ebp+AccessMode], bl
PAGE:005107C1 jnz short loc_5107EC
由于在wdm.h中KernelMode被定义为0,我们只需将比较语句之后的条件跳转Nop掉,这样所有的访问权限检查就能顺利通过了。
在XP之后的版本中,情况变得有点复杂了。Nt!SeAccessCheck函数调用了nt!SeAccessCheckWithHint, 后者正是我们要处理的。在后面我们收集用于攻击的信息时,就会看到这如何使得问题复杂化了。在Windows8.1中,会看到没有跳转到内核函数,而是这样的分支:
.text:00494613 loc_494613:
.text:00494613 cmp [ebp+AccessMode], al
.text:00494616 jz loc_494B28
仅需将条件跳转替换为无条件跳转,即可使得对SeAccessCheck的调用好像来自内核一样。
现在目标清楚了,但还有一个小问题要解决。 我们要改写的内存地址在只读页中。 我们可以更改页的属性设置,但有个更简单的解决方法。在x86和x64处理器上,在控制寄存器0 (Control Register 0)上有个标志决定着管态下的代码(即Ring 0代码,我们的攻击利用代码)是否在乎内存的只读状态。 引用Barnaby Jack[4]的话:
禁用CR0中的写保护位。
执行代码和内存改写。
再恢复写保护位。
到这里,我们实际的攻击利用核心代码如下所示:
我们对写保护位的处理还有一个问题。我们要将攻击利用和处理器进行关联,确保我们始终处于设定的处理器的核心上。 我们对写保护位的处理似乎是非必须的, 但在更复杂的,要求我们禁用SMEP(稍后介绍)的攻击利用中,这着实是个事。 无论哪种方式,都可以的,我们要做的,就是一个简单的调用:
1SetProcessAffinityMask(GetCurrentProcess(), (DWORD_PTR)1);
此时,你会注意到在攻击代码中,我们实际上并不知道具体的补丁信息。该攻击代码仅是获取我们提供的信息,然后应用它。我们将在实际的漏洞研究中提供这些信息。#p#
攻击:将控制转给攻击代码
我们有了让代码在Ring 0运行的条件了。 现在需要两件事来使其运行:关于操作系统配置的信息,以及当处理器运行在管态时如何将控制权转移给我们的代码。
由于最初是想处理’任意写(write-what-where)’问题, 这里我们将改写HalDispatchTable中的一个函数。 该方法,在2007年由Ruben Santamarta[5]提及, 允许我们将执行流转向我们的攻击利用代码。
我们将拦截处理的是hal!HalQuerySystemInformation函数。该函数由未文档化的NtQueryIntervalProfile[5][6]函数调用,而且调用函数也不常用。 要理解它为啥很重要,我们就需要详细的谈下windows中的内存布局。
Windows将内存划分为两个大区间:内核态的内存空间在MmUserProbeAddress之上, 其下是用户态的内存空间。 内核态的内存是所有进程共用的(虽然进程代码无法访问这些空间),而用户态的内存空间是因进程而异的。由于我们的攻击利用代码要运行在用户进程空间中,而我们却在挂钩一个内核函数指针, 如果其他进程调用了NtQueryIntervalProfile,很可能就会导致操作系统的崩溃。 因此,我们攻击利用的第一步就是恢复原始的函数指针。
在我们之前的代码里,你可以看到我们依赖额外的信息来确定函数指针入口所在,以及原始值。
现在,我们实际的攻击利用触发函数如下所示:
为了灵活性,我们的WriteWhatWhere()函数原型也包括了地址的原始值。最后一步就是找到利用点和攻击利用挂钩的地址。#p#
研究:确定操作系统的配置
这里,我们假设进行本地提权。我们可以在系统上以某用户权限执行任意代码, 最终的目标是取得对系统的完全控制权。 在对内核漏洞的远程攻击中,确定操作系统的配置会更复杂。
已经确定需要知道如下的信息:
1) nt!HalDispatchTable的地址
2) hal!HaliQuerySystemInformation的地址
3) nt!SeAccessCheck 或者相关函数中要打补丁的代码的地址
4) 要补丁的值
此外,我们也需要查找原始的值,以便我们恢复原始函数功能,进行不同的利用。毕竟,一旦我们完成了所需做的,干嘛还把门敞着呢?
我们需要知道两个内核模块的基址——硬件抽象层(HAL)和NT内核自身。为了获取这些,我们还需要一个未文档化的函数——NtQuerySystemInformation[6][7]。 因为我们需要知道两个函数,我们将提前创建函数原型,并从NTDLL中直接加载它们:
下一步就是确定所使用模块的版本信息,以及它们在内存中的实际位置。利用NtQuerySystemInformation获取加载的模块,然后遍历查询所需的模块名称。
下一步,在大多数情况是很坏的主意,但在这里不是。使用LoadLibraryEx已经过时的特性, 加载我们找到的模块的拷贝(duplicate copies)。
设置了这个标志, 我们就不会加载任何引用的模块, 不执行任何的代码, 但仍然可以使用GetProcAddress()来搜寻模块。这正是我们想要的, 因为我们要把这些加载的模块当作我们的源码,来查询在实际运行的内核代码中所需的。
此时,我们已经具备了查找偏移所需的一切。有了拷贝的内核模块的基址,系统的实际基址, 所以我们可以将拷贝中的相对虚拟地址(RVA)转换为实际的系统地址。同时我们又拥有对拷贝代码的读取权,所以可以扫描代码查询所需的函数。 所剩的最后一件事,就是一个windows调用,用GetVersionEx()来决定运行的windows的版本。
有些很容易, 因为地址已经导出了:
但我们所需的大部分,还是要通过搜索获取。要搜索的两个函数,其中hal!HaliQuerySystemInformation没有导出符号, 另一个是nt!SeAccessCheck或者它直接调用的函数。
再看最后一个例子,来看看是如何处理导出函数和那些完全私有函数的。首先是nt!SeAccessCheck:
然后看一下将要进行补丁处理的nt!SeAccessCheckWithHint:
这里,两个函数看上去是相邻的, 但我们还是要使用公开的函数来跟踪对这些内部函数的引用,然后据此确定我们的补丁位置。 代码如下所示:
这里的PatternScan函数是一个简单的辅助处理,根据给定的指针、扫描的大小范围,要扫描的模式及其大小,来查找模式匹配的起始点(如果没有找到就是NULL)。
在上面的代码中,首先搜索到nt!SeAccessCheckWithHint的相对跳转,并获取到偏移。
将其用于计算我们的拷贝模块中nt!SeAccessCheckWithHint的实际起始位置,然后扫描查找我们要替换的条件分支的模式部分。 一旦定位成功, 就可以确定它的实际地址——首先将其转换为相对虚拟地址(RVA),然后根据实际加载的内核映像进行重定位。最后,相应的替换值还是依赖于具体的操作系统版本的。这里对JZ(0x0f 0×84)的替换是NOP(0×90)和JMP(0xe9)。
通过从拷贝的系统模块中收集所需信息,对多个不同版本的Windows操作系统的处理都可以放在同一个框架下进行。 通过在目标函数中搜索有关模式, 只要不是我们要查找的函数发生了变化,其他的我们都可以有效的应对。#p#
最后的麻烦
目前我们所做的一切都可以正常运行,直到Windows 8, 更确切的说,是NT6.2内核。为方便起见,我们将实际的攻击利用代码运行在用户态中。
在Ivy-Bridge架构中,Intel引入了一种叫做管态执行保护(Supervisor Mode Execute Protection, SMEP)[9]的功能。如果开启了此项功能,当处理器在管态模式下,我们试图执行用户态地址空间中的指令时,处理器就会出错。这样将控制权从内核中拦截的函数指针转换给我们的代码时,就会发生异常。Windows在Windows8/Server 2012中支持SMEP,而且在支持的处理器中是默认开启的。要解决这个问题,我们要么把攻击利用代码移到内核空间中(这在NT6.2中也比较难), 要么就是禁用SMEP[11][12]。
最后存在的问题出现在Windows 8.1中。 要获取Windows 8.1的真正版本号, 需要额外的处理,根据MSDN[13]:
包含了这个,就能正确的检测Windows 8.1了,在确定偏移时适当的调整一下我们的搜索参数即可。
结束语
当然了,这里有缺少的部分。利用框架来验证攻击利用是很有帮助的,我们还需要一个‘任意写(write-what-where)’的案例来对对此进行验证。
我们内部就是在使用该框架在验证那些漏洞,如果你有啥新的发现,可以通过http://www.zerodayinitiative.com/提交给我们(有没有攻击负载都可以)。期待你的消息。
原文:http://h30499.www3.hp.com/t5/HP-Security-Research-Blog/Verifying-Windows-Kernel-Vulnerabilities/ba-p/6252649#.UyZi2T-Sy0f
注解
[1] 我对此能找到的最早使用是在Gerardo Richarte的论文“About Exploits Writing(GCON 1, 2002)”他将其划分为“将任意的内容写到某处”(write-anything-somewhere)和“将任意的内容写到任意地方”(write-anything-anywhere)。这里,我们的“任意写”(write-what-where)是指“将任意内容写到任意地方”(write-anything-anywhere)。
[2] Greg Hoglund, “A *REAL* NT Rootkit, patching the NT Kernel” (Phrack 55, 1999)
[3] John Heasman, “Implementing and Detecting an ACPI BIOS Rootkit” (Black Hat Europe, 2006)
[4] Barnaby Jack, “Remote Windows Kernel Exploitation – Step In To the Ring 0” (Black Hat USA, 2005) [White Paper]
[5] Ruben Santamarta, “Exploiting Common Flaws in Drivers” (2007)
[6] Windows NT/2000 Natvie API Reference (Gary Nebbett, 2000) 虽然没有囊括较新的Windows 操作系统版本,但它对内部Windows API函数和结构仍然是一份很棒的参考
[7] Alex Ionescu, “I Got 99 Problems But a Kernel Pointer Ain’t One” (RECon, 2013)
[8] Raymond Chen, “LoadLibraryEx(DONT_RESOLVE_DLL_REFERENCES) is fundamentally flawed” (The Old New Thing)
[9] Varghese George, Tom Piazza, and Hong Jiang, “Intel Next Generation Microarchitecture Codename Ivy Bridge” (IDF, 2011)
[10] Ken Johnson and Matt Miller, “Exploit Mitigation Improvements in Windows 8” (Black Hat USA, 2012)
[11] Artem Shishkin, “Intel SMEP overview and partial bypass on Windows 8” (Positive Research Center)
[12] Artem Shisken and Ilya Smit, “Bypassing Intel SMEP on Windows 8 x64 using Return-oriented Programming” (Positive Research Center)
[13] MSDN, “Operating system version changes in Windows 8.1 and Windows Server 2012 R2”
参考读物
Enrico Perla and Massimiliano Oldani, A Guide to Kernel Exploitation: Attacking the Core, (Syngress, 2010)
bugcheck and skape, “Kernel-mode Payloads on Windows”, (Uninformed Volume 3, 2006)
skape and Skywing, “A Catalog of Windows Local Kernel-mode Backdoor Techniques”, (Uninformed Volume 8, 2007)
mxatone, “Analyzing local privilege escalations in win32k”, (Uninformed Volume 10, 2008)