Windows win32k空指针解引用内核信息泄露漏洞分析(CVE-2019-1169)

一、漏洞详情

近期,VS-Labs针对微软在2019年8月补丁更新中修复的win32k.sys NULL指针解引用漏洞进行了研究。利用该漏洞,攻击者可以创建一个漏洞利用程序,从而从受影响Windows 7计算机上的任何内核地址中泄露数据。

在本文中,我们将展示VS-Labs如何编写这一漏洞利用程序,我们首先建立了测试环境,然后使用DIaphora分析补丁,最后使用C++语言编写漏洞利用程序。希望通过阅读这篇文章,能够使读者对这个典型的漏洞产生深刻的理解。

要阅读本文,需要读者事先具备以下知识:

C/C++相关知识(必备);

X86编译知识(必备);

Windows用户空间漏洞利用经验(推荐)。

二、漏洞分析

2.1 漏洞公告分析

在分析CVE-2019-1169之前,我们首先分析了两份单独的漏洞公告,其中一份是来自Microsoft的,另一份是来自ZDI。经过初步分析,我们发现了这两份公告之间的差异。

Microsoft在CVE-2019-1169的漏洞公告页面上将该漏洞描述为影响Windows 7、Windows 7 SP1、Windows Server 2008和Windows Server 2008 R2的任意代码执行漏洞。在这里,Microsoft没有列出不再受支持的漏洞操作系统,例如Windows XP,但这些操作系统也存在该漏洞。

另一方面,ZDI的ZDI-19-709漏洞公告将该漏洞描述为xxxMNDragOver()中的NULL指针解引用漏洞。

在这份通告中还提到,可以通过在回调期间破坏菜单来触发漏洞,从而使攻击者能够从用户模式代码读取内核内存。

在这里,我们必须要提出一个问题——在两份通告中,究竟哪一个是正确的?

经过进一步的研究,我们已经能够确定,ZDI的公告实际上是正确的,而Microsoft的公告是将多个公告组合在了一起,并使用其中最严重的一个漏洞描述为其贴上标签,这样可能会为用户带来错误的印象,会让人们觉得CVE-2019-1169可以导致攻击者实现特权提升。

在我们开展分析之前,有必要描述一些有关NULL指针解引用漏洞工作原理的背景知识。

2.2 空指针解引用的原因和影响

之所以会产生NULL指针解引用漏洞,是因为开发人员在解除对指针的引用并检索其指向的数据之前没有检查指针是否为NULL。这通常是由于开发人员忘记了其代码中的一个代码路径会将对象或指针更改为意外状态而导致的。这可能导致开发人员做出错误的假设,也就是必须进行哪些检查才能适当地保护其程序免受恶意输入的侵害。

NULL指针解引用漏洞的严重性,取决于解引用后应用程序如何使用这个指针。如果将指针指向的数据用于写操作的位置,就能导致攻击者可以写入任意内存,从而可能会导致代码执行。

同样,如果使用指针来确定从哪里读取数据,那么攻击者就只能实现任意读取,最终导致的潜在危害就是信息泄露。在这两种情形中,攻击者都必须要分配NULL页,以便在解应用指针时,受影响的程序会在分配的NULL页中引用攻击者控制的数据。然后,这些数据将在受影响的应用程序中使用,这将会导致攻击者控制程序的行为。

在制作NULL页时,攻击者必须确保NULL页的内容与NULL指针指向的数据的结构相匹配,否则他们将无法控制应用程序的数据。我们应该指出,这表明没有通用方法可以为NULL页制作数据。每个受影响的应用程序可能都需要用一个你哟功能程序唯一的数据来填充NULL页。

最后,需要特别说明的是,我们在本文中仅会讨论内核模式NULL指针解引用漏洞,但同样的概念也适用于用户模式NULL指针解引用漏洞。二者之间唯一的区别在于,内核模式NULL指针解引用漏洞更有可能导致特权提升,因为内核模式代码可以读取系统上的任何地址,而对于用户模式的NULL指针解引用漏洞,则只能在用户模式地址空间内读取或写入地址。

三、漏洞影响范围

我们再次阅读Microsoft的公告,发现其中表示,该漏洞不会影响Windows 8、Windows 8.1和Windows 10。

这些版本之所以不受漏洞影响的原因,可以在2012年MSRC上Matt Miller的演讲中找到。该研究人员的研究成果指出,在Windows 8和更高版本上,用户尝试使用前64KB内存(0x00000000 – 0x0000FFFF)会阻止其使用,以防止出现NULL指针解引用漏洞。

这个缓解措施的工作原理就如同内核模式尝试在该区域中分配内存一样,将会引发访问冲突,这会导致BSOD,从而向系统管理员表明网络内部可能存在攻击者。

同样,用户模式应用程序将无法在这些区域中分配内存,而是会返回错误。

这里要注意的一点是,这个缓解措施已经反向移植到Windows Vista及更高版本的64位版本中。因此,只能在大多数Windows操作系统的32位版本上利用NULL指针解引用。但是,如何验证这一点呢?我们需要进行一些开发工作。

3.1 验证是否已回传NULL页面缓解

为了测试NULL页缓解措施是否已经反向移植到Windows 7 x64中国呢,我们使用了以下代码,该代码尝试使用ZwVirtualAllocMemory()分配NULL页。

NULLPageAllocation.cpp:
// NULLPageAllocation.cpp: This file contains the 'main' function. 
// Program execution begins and ends there.

#include <stdio.h>
#include <Windows.h>
#include <bcrypt.h>  
// Needed to solve the issue "Function returning function 
// is not allowed”. Might be because it defines 
// NTSTATUS and some other data structures.

// Definition taken from NtAllocateVirtualMemory function (ntifs.h) -Windows drivers
typedef NTSTATUS(WINAPI* ZwAllocateVirtualMemory)(HANDLE ProcessHandle, PVOID* BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect); 

#define STATUS_SUCCESS 0

int main()
{
        // Get the address of NtAllocateVirtualMemory()
        // which is exported from ntdll.dll
        ZwAllocateVirtualMemory pZwAllocateVirtualMemory = (ZwAllocateVirtualMemory)GetProcAddress(GetModuleHandle(L"ntdll.dll"), "ZwAllocateVirtualMemory"); 
        DWORD baseAddress = 0x1;
        SIZE_T sizeOfAllocation = 1024;
        NTSTATUS result = pZwAllocateVirtualMemory(GetCurrentProcess(), (PVOID*)&baseAddress, 0, &sizeOfAllocation, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);

        if (result == STATUS_SUCCESS) {
                printf("[*] Successfully allocated the NULL page!rn");
        }
        else {
                printf("[!] Could not allocate the NULL page...rn");
        }
}

在这个示例中,将0x1作为基址,因为ZwVirtualAllocMemory()不允许address参数为0x0。但是,如果提供了0x1,那么ZwVirtualAllocMemory()会在内部将0x1向下舍入到下一个页面对齐的地址0x0。因此,攻击者可以请求ZwVirtualAllocMemory()尝试分配NULL页,而不会违反ZwVirtualAllocMemory()的参数要求。

这里需要注意的是,如果尝试使用其他内存分配函数,将无法实现这一点,因为ZwVirtualAllocMemory()和其NT等效函数NtVirtualAllocMemory()具有在NULL页上分配内存的独特功能,而其他调用(例如VirtualAlloc())则可以舍入到最接近的页边界,也拒绝小于某个地址的地址。下图展示了我们尝试在Windows 7 SP1 x64计算机上运行该程序的结果。

需要关注的是,返回的错误代码保存在result变量中,该代码为0xC00000F0或STATUS_INVALID_PARAMETER_2。通过参考Microsoft的NTSTATUS页面,我们确定这个错误代码表明第二个参数会传递给ZwAllocateVirtualMemory(),这是要分配的内存的基址,是无效的。我们也许想弄清楚,如果尝试在NULL页之外、前64KB内存中分配内存,将会发生什么情况。实际上,这样的尝试也会失败,但错误代码会有所不同。在下面展示的示例中,我们尝试在位于前64KB内存中的0xF80C地址处分配内存。

这会导致ZwAllocateVirtualMemory()返回相同的错误代码0xC00000F0或STATUS_INVALID_PARAMETER_2,表示指定的内存地址无效,如下图所示。

最后,如果用户尝试在内存前64KB之外的较低位内存地址上分配内存,那么ZwAllocateVirtualMemory()会成功,如下图所示。

从最后三个测试的结果来看,可以确认ZwAllocateVirtualMemory()的内部已经修改,但改动仅是为了确保它不能在NULL页的前64KB内存中分配内存。

在NtVirtualAllocateMemory()进行相同的测试,展示出类似的结果,表明它也以相同的方式进行了修复,因此我们就可以确认CVE-2019-1169仅影响Windows 7 x86和更低版本,而不会影响Windows 7 x64、Windows 8和更高版本。

四、目标设定

4.1 环境设置:快照和文件夹

在我们掌握了哪些系统受到CVE-2019-1169漏洞的影响之后,我们就可以创建一个环境来分析补丁。在这里,我们使用了运行Windows 7 SP1 x86的VirtualBox VM。然后,在纯净版映像的状态拍摄两个快照。

第一个快照命名为“Windows 7 2019年7月补丁”,将CVE-2019-1169之前的所有补丁安装在纯净版本的Windows 7 SP1映像上。这一过程,是通过安装2019年7月每月汇总补丁(KB4507449)来实现的。

在每月汇总补丁中,包含当月和前几个月的所有补丁程序,这样使得研究人员可以轻松获得已经安装了正确补丁集合的系统。

第二个快照命名为“Windows 7 2019年8月补丁”,是在安装KB4512486后拍摄的,其中包含CVE-2019-1169的修复。

为了确认这是正确的更新程序包,我们查看了Microsoft CVE-2019-1169的漏洞公告页面,该页面上说明,修复了CVE-2019-1169漏洞的2019年8月安全更新程序包编号为KB4512486。下图展示了执行这些操作后,VirtualBox的快照视图。

一旦安装补丁并拍摄快照后,我们就可以从其中的两个快照中提取win32k.sys文件,并将其复制到主机上。

未修复的win32k.sys被放置在名为Old的文件夹中,而修复后的win32k.sys被放置在名为New的文件夹中。该文件夹的结构如下图所示:

然后,我们将这两个win32k.sys文件加载到IDA Pro中进行分析,并将生成的IDA Pro数据库文件(.i64文件)保存在相应的本地文件夹中。

4.2 符号路径设置

我们需要执行的下一项工作,是在主机上配置符号路径。符号路径是一个名为_NT_SYMBOL_PATH的环境变量,它通常包含两部分:系统上本地文件夹的路径,该路径用作已下载PDB文件的缓存,以及一个URL,指向服务器以下载缓存中目前没有的PDB文件。

对于大多数用户来说,这个路径将直接指向C:Symbols,而URL将指向Microsoft的符号服务器。

我们需要将这个环境变量设置为不包含它的Windows程序(例如WinDBG),该Windows程序依靠这个环境变量来找到各种二进制文件所对应的PDB文件,这时将无法找到适当的PDB文件。PDB是Microsoft的Unix符号文件版本,在没有相关函数名、参数、返回值、数据类型和与特定二进制文件关联的结构布局信息的情况下,要尝试调试程序会非常困难。在进行内核研究时更是如此,因为在PDB文件中经常没有记录组件并定位相应信息。

为了设置符号路径,我们在管理员级别的PowerShell命令提示符中,使用了以下命令行。

PowerShell符号路径设置脚本:

mkdir c:MySymbols

[Environment]::SetEnvironmentVariable("_NT_SYMBOL_PATH", "cache*c:MySymbols;srv*https://msdl.microsoft.com/download/symbols", "Machine")

其中,第一个命令创建了文件夹C:MySymbols。在创建这个文件夹后,第二个命令创建了一个名为_NT_SYMBOL_PATH的系统环境变量,其值为 cache*c:MySymbols;srv*https://msdl.microsoft.com/download/symbols ,以指定系统应将PDB文件存储在C:MySymbols中,并且对于任何未在本地存储的PDB文件,都应该先从Microsoft Symbol Server下载,然后再保存到C:MySymbols中。

4.3 设置VirtualBox进行内核调试

一旦设置了符号路径,我们的下一步就是配置VirtualBox进行内核调试。

有几种方式可以实现这一点。其中,最为常见的方法是COM和KDNET。KDNET允许通过兼容的网络适配器进行内核调试,它所提供的调试必须比COM的效率更高(参阅MSDN),并且设置和延迟更少,但是它需要操作系统时Windows 8或更高版本。

但遗憾的是,由于CVE-2019-1169不影响Windows 8系统,因此我们只能在命名管道上使用普通的COM通信来对所选的Windows 7计算机进行内核调试。

为了在VirtualBox中设置命名管道,我们首先导航到VirtualBox Manager。在选择Windows 7虚拟机后,我们选择了设置图标(黄色齿轮),然后出现以下的屏幕内容:

我们选择“Serial Ports”(串行端口)选项,并在“Port 1”菜单选项下选择“启用串行端口”的复选框,以及以下选项:

端口:COM1

端口模式:Host Pipe(主机管道)

取消选中“Connect to Existing Pipe/Socket”(连接到现有管道/套接字),以便让VirtualBox创建管道,而非连接到现有管道。

路径/地址:.pipeWin7Kernel

下图展示了正确设置后的截图:

这些设置将要求VirtualBox在计算机启动时创建一个名为.pipeWin7Kernel的COM管道。该COM管道将与串行端口COM1关联。完成此操作后,将会启动要调试的快照,并在管理员权限的命令提示符中输入以下命令。

在目标上启用内核调试的bcdedit命令:

bcdedit /debug on

bcdedit /dbgsettingsserial debugport:1 baudrate:115200

这些命令在目标计算机上启用了调试模式,该模式允许远程计算机对目标计算机进行内核调试,并进行调试设置,以便其使用串行端口1(COM 1),其信号/波特率为115200,这也是通常使用的信号/波特率。

完成此操作后,我们将正常关闭虚拟机(不通过VirtualBox关机),然后启动WinDBG Preview并选择“附加到内核”。我们随后检查“管道”、“重新连接”和“初始中断”按钮,并将“波特率”设置为115200,将“端口”设置为.pipeWin7Kernel。下图展示了正确设置后的截图:

然后,按下OK按钮,WinDBG Preview会显示一个窗口,表明该窗口正在等待目标计算机重新连接到调试器,如下图所示。

出现这个消息后,我们在VirtualBox中启动目标计算机,并看到WInDBG Preview的确认,即客户端已连接,并且符号已经正确配置,如下图所示。

五、原始版本与补丁修改版本分析

5.1 Diaphora分析

在设置好环境并创建IDA Pro 64位(.i64)文件后,我们接下来就要分析原始版本与补丁修改后版本的区别,以确认win32k.sys的两个版本之间存在什么样的变化。

这个步骤需要一个比较程序。我们比较常用的补丁分析工具是Diaphora,这是一个知名的补丁比较工具,它具有多种启发式算法,可以比其他方式更为准确地识别Windows二进制文件中的更改。这个工具可以从GitHub上免费获取。

在从GitHub上克隆Diaphora后,我们在IDA Pro中打开与补丁的win32k.sys文件相对应的.i64文件,并通过选择“文件”-“运行脚本”,导航到文件diaphora.py的位置,该文件位于Diaphora存储库被克隆的文件夹根目录中。完成后,将出现下图所示的对话框。

在大多数情况下,分析人员可以直接使用Diaphora提供的默认设置。需要注意的是,选项“忽略自动生成的名称”将会导致IDA忽略任何以sub_开头的函数,对于没有提供名称的函数来说会出现这种情况。

对于Windows 7 SP1 x86,其中存在win32k.syssince符号,该符号将会填充IDA Pro使用函数名称检测到的所有函数。但是,当符号不可用或者不完整时,建议禁用这个选项,以确保对所有函数都进行了恰当的分析。为了完整起见,我们决定取消选中“忽略自动生成的名称”选项,以确保所有函数都会被分析。

在完成上述操作后,按OK按钮,Diaphora开始与当前加载的.i64文件相对应的函数信息导出到.sqlite文件中。我们可以在下图中看到。

分析完成后,Diaphora将关闭这个对话框,以表明导出过程已经完成,同时会将一些信息输出到输出日志中,确认已经成功将数据库信息导出到.sqlitedatabase文件。

一旦发生这种情况,我们关闭IDA Pro,使用与Win32k.sys修复版本相对应的.i64文件重新打开编辑,如下图所示,再次使用相同的设置对这个文件运行Diaphora工具。

在完成对两个文件的分析后,我们得到了两个.sqlite文件:一个是未修补的win32k.sys文件,另一个是已修补的win32k.sys文件。现在,我们可以重新打开IDA Pro,加载与win32k.sys未修补版本相对应的.i64文件。

然后,再次运行DIaphora,但这次将SQLite数据库更改为diff againstoption,以使其指向与win32k.sysfile已修补版本相对应的.sqlite文件,如下图所示。

完成这一操作后,按下OK按钮,这时会显示出一个弹出菜单,询问是否覆盖现有的.sqlitefile。此时,我们选择了No,因为Diaphorahad已经导出了两个版本的win32k.systo .sqlitefiles所需的信息,没有必要再重复这项工作。

随后,Diaphora开始运行多个线程,来分析两个文件之间的差异,并在这个过程中应用了各种启发式方法和算法。

大约20分钟后,分析完成,并成功弹出了一个窗口,询问是否要立即保存结果。由于我们这时还不知道哪些结果是有用的,因此按下OK按钮可以忽略此窗口,并继续显示结果。

下面是示例中的输出结果:

我们立即意识到,Diaphoramay已经检测到又一些额外的函数被更改,因为出现的几个sub_XXX函数被标记为已更改,但是代码改动的概率非常小。

发生这种情况的原因在于,即使Diaphora可能具有功能相似的代码,它们有时也会将函数标记成不同类别。例如,当一个函数使用不同的缓存器执行相同的操作时,或者当代码被更改以使其执行相同的一组操作时。

由于Diaphora当前无法自动检测到这些类型的更改,因此逆向工程必须人工将这些无效的函数标记为误报。

经过快速分析后,我们确定Diaphora“部分匹配”选项卡中的sub_XXX函数为误报,因为每个函数在功能上都是相同的,但两个win32k.sys文件都使用不同的方式引用了相同的全局变量。

我们继续比较二者之间的差异,查看每个sub_XXX函数,使其以蓝色突出显示,然后按DEL将其删除。需要注意的是,由于存在一些小BUG,更新可能不会立即显示出来。如果在单击“删除”之后没有更新,建议右键单击,并选择“刷新”以更新结果并查看更改。在去除所有sub_XXX函数后,我们就可以在Diaphora中看到干净的Partial Matchestab,如下图所示。

5.2 xxxMNDragOver()补丁分析

在我们查看ZDI的通告之后,我们注意到通告中特别提到了受影响的函数是xxxMNDragOver(),该函数是在上图中检测到有改动的函数之一。我们右键点击xxxMNDragOver(),,选择Diff组件,就可以得到如下图所示的结果。

需要关注的是,上面的输出展示了经过我们多次分析迭代后添加的注释,这些内容不是由IDA Pro或Diaphora自动添加的。

截图的右侧展示了未修补的代码,而左侧展示的是更新的代码。通过查看右侧的代码,我们注意到这里调用了_safe_cast_fnid_to_PMENUWND()。

然后,将这个调用的结果与EDI进行比较,该结果在测试过程中被设置为0x0。

在两个版本之间,这个代码尚未进行更改,但是有几行代码对_safe_cast_fnid_to_PMENUWND()的结果进行了操作,而在安装补丁后,似乎对这个函数进行了更改。

由于函数名称为_safe_cast_fnid_to_PMENUWND(),所以我们认为_safe_cast_fnid_to_PMENUWND()的输出将会是PMENUWND结构。PMENUWND结构的定义如下:

typedef struct tagMENUWND {
WND wnd; 
PPOPUPMENU ppopupmenu;
} MENUWND, *PMENUWND;

在对于函数可能操作哪些数据这方面有所了解后,我们再次检查了该代码的旧版本,旧版本在上图右侧以红色标出。

这部分代码首先将ECX设置为_safe_cast_fnid_to_PMENUWND()返回的PMENUWND对象内ppopupmenu指针的值。

完成这一操作之后,使用下面的指令将EAX设置为ppopupmenu指向的POPUPMENU结构内部的值spmenufield。然后将EAX与EDI进行比较,EDI的值为0x0,从而确保spmenufield不会为NULL。我们通过查看WinDBG中的标签POPUPMENU结构,来确认spmenufield在PPOPUPMENU中的位置:

1: kd> dt win32k!tagPOPUPMENU
        ...
        +0x014 spmenu           : Ptr32 tagMENU
        ...

细心的读者可能已经注意到,这里的代码存在问题,因为没有进行检查以确保_safe_cast_fnid_to_PMENUWND()返回的PMENUWND对象包含的一个ppopupmenufield字段不为NULL。

因此,ppopupmenu可能是一个NULL指针,这将导致EAX被设置为内存0x14位置的32位值。从而导致攻击者可以控制EAX的值,并有可能改变xxxMNDragOver()的执行。

攻击者所需要做的,就是能够在内存中分配NULL页,用户可以在Windows 7 x86上通过调用诸如ZwAllocateVirtualMemory()或NtAllocateVirtualMemory()这样的函数,然后将此页的偏移量0x14设置为特定值来执行此操作。然后,当EAX设置为ppopupmenu的spmenufield的值时,就允许攻击者控制EAX。

修补后的代码通过确保_safe_cast_fnid_to_PMENUWND()返回的PMENUWND对象中包含一个不为NULL的ppopupmenu字段的方式,来修复这一漏洞。如果ppopupmenu字段为NULL,则xxxMNDragOver()将会停止处理该对象,并提前终止,从而防止攻击者控制该程序的行为。

六、总结

到目前为止,我们已经明确,攻击者可以利用CVE-2019-1169在某种程度上获取对xxxMNDragOver()执行方式的控制。

但是,这里仍然存在一些问题,例如“攻击者可以完全控制哪些地方”、“这是否会导致关键的信息泄露”?

后续,我们还将研究这些问题,并进一步确定攻击者可以如何利用这一漏洞,同时验证攻击者能够泄露的内核地址。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章