Symantec终端保护本地提权漏洞分析(CVE-2019-12750)(下)
本文我们将重点关注最新的Windows 10 v1909,下面,我们将介绍一种更复杂的漏洞利用方法。由于针对Windows 10 v1809及更高版本中的内核模式池分配新引入了低碎片堆(LFH),因此需要对易受攻击的产品进行进一步分析,这使我们第一部分所讲的利用方法无法正常工作。
为了在内核模式下获得代码执行,同时绕过诸如SMEP和KASLR之类的其他利用缓解措施,就需要开发新的方法。
LFH的影响(Win 10 v1809 +)
在Windows 10的最新版本中,由于内核模式LFH,不可能通过此特定漏洞泄漏令牌对象,或者至少不一致。因此,我们必须找到另一种利用方法,以便在受影响的主机上成功执行本地权限升级攻击。
虽然在Windows 10 v1803之前泄漏的内核内存页通常会包含各种大小的内核对象,但是在新版本中,内核池分配器在每个内存页中容纳尽可能多的大小相同的对象,从而降低了内存碎片。特别是,当我们必须处理较小的分配时,比如在我们的示例中(0x30字节的块),我们注意到同一内存页中的所有分配基本上都是相同的大小。
相同大小的块
如上图所示,泄漏的内核池内存页面将仅包含该大小的块,因此我们不能使用与上一篇文章中描述的方法相同的方法泄漏令牌对象。在池中进行修改,很可能会泄漏其中一些信息,但是我们决定使用其他方法。
可以清楚地看到,有各种大小相同的分配属于不同的对象。此时,我们需要仔细查看这些池块,以找出可以向userland泄漏的信息类型以及可以在userland控制的分配类型。
受到攻击的软件的各种驱动程序会在特定条件下分配大小为0x30字节的块,因此我们需要首先研究那些专有对象。
我们发现标记为“B2d2”的对象特别受关注,因为它们包含指向另一个名为“ BHDrvx64.sys”的驱动程序的指针。另外,它是指向函数指针表的指针,这使其变得更加有趣。
B2d2块(BHDrvx64.sys)
在此过程中,要注意两点:
首先,我们有一个与漏洞相关的块大小相同的对象,这意味着可以轻松地将其放置在相同的内存页中。
其次,我们有一个指向软件目标的另一个驱动程序的函数表的指针。这意味着我们还泄露了一些信息,这些信息可以帮助我们在以后找到它的内核模式基址并寻找一组要执行的指令gadget。
这是一个好的开始,但还不够,我们还需要知道我们对这类对象的控制级别。
尤其是:
1. 分配对象时;
2. 释放对象时;
3. 如何使用函数表指针;
4. B2d2对象分析;
我们可以轻松地找到“BHDrvx64.sys”函数,通过简单搜索特定的池标记来分配此类对象。
B2d2对象分配
一旦对象被分配,函数表指针就写在它的数据缓冲区的开头,其余的将以零字节初始化。
初始化B2d2对象
如果我们在分配此对象时查看调用堆栈,则可以检索有关导致此函数的执行路径以及是什么类型的事件触发了它的创建。
我们发现,各种已注册的回调函数可能负责触发此对象的分配,例如:
1. 注册表项(NtCreateKey,NtSetValueKey等);
2. 进程句柄(NtOpenProcess);
3. 进程终止(NtTerminateProcess);
4. 可执行映像加载(exe,dll等);
5. CreateFiles(NtCreateFile);
6. 关闭句柄(NtClose);
但是,关闭文件句柄是其中最重要的一项,稍后我们将提供关于这个决定的更多细节。
B2d2对象创建
至此,我们知道对象的前8个字节已填充有函数指针表的地址,其余部分最初用零进行了清除。但是,还有一个额外的细节对于利用此漏洞非常重要。实际上,将在此对象内添加一些额外的信息。
B2d2对象的额外信息
通过监控通过硬件断点对其余对象数据的访问,并检查调用堆栈,我们发现在将这些额外数据添加到对象内部之前发生了对nt!RtlAppendUnicodeStringToString函数的调用。这有助于我们跟踪执行流程,并发现额外的数据是UNICODE_STRING结构的长度和最大长度字段,后面是一个指向实际Unicode字符串缓冲区的指针。
因此,我们创建了一个测试应用程序,该应用程序将创建一个文件,然后关闭该文件的句柄。由于我们知道完整的路径长度(以字节为单位),因此我们使用了条件断点在正确的时间断开。
B2d2对象额外信息#2
请注意,驱动程序会预先分配了标记为“B2d3”的缓冲区,最多可容纳0x802字节。这意味着,当文件路径信息写入“B2d2”对象时,最大长度字段将被设置为该字节数,因为它现在引用的是另一个缓冲区,而不是从中复制路径的缓冲区。
更新的最大长度
如果完整路径超过0x800字节,则其余字符将被丢弃,长度字段将设置为该数字。
也就是说,我们使用长路径名来创建文件,以便将长度设置为0x800,最大长度设置为0x802。通过这种方式,我们成功地将“B2d2”对象与通过其他流程创建的其他对象区分开来。
你可能会问,这些额外的数据的意义是什么?
正如我们在本文的第一部分中所讨论的,该漏洞允许每个进程泄漏一个分页池内核数据的内存页,但泄露的内存页并不是很相同。然而,由于泄漏页面上的对象可能与任何正在运行的进程相关联,因此我们需要一种方法来确定漏洞利用对象应该针对哪些“B2d2”对象。
显然,破坏我们没有控制的对象的函数表指针将导致主机崩溃,此时另一个进程将试图访问它。
让我们总结一下最新发现的漏洞利用步骤:
1. 进程创建一个文件;
2. 进程关闭文件句柄->创建的B2d2对象。
3. 在B2d2对象中添加了函数表指针;
4. 在B2d2对象中添加了文件路径长度、最大长度和指向字符串缓冲区的指针。
换句话说,如果我们的进程创建了一个文件并关闭了它的句柄,则只要该进程正在运行,我们就会有一个与该文件关联的“B2d2”对象。当进程终止时,驱动程序将使用对象中指向函数表的指针来调用适当的清理函数。
有了这些信息,我们现在可以继续下一阶段的漏洞执行了。
获取控制执行权限
由于可以劫持执行流程,因此可以通过手动修改“B2d2”对象中的函数表指针来创建简单的概念证明。
设置虚拟指针
执行控制PoC
我们将其替换为虚拟指针(0xF8F8F8F8F8F8F8F8F8),并终止了该进程,以确认释放该对象后我们可以控制执行流程。
处理KASLR
我们可以通过“PfFk”分配泄漏一个指向内核模块的指针,它们的大小和与漏洞相关的池块(0x30字节)的大小相同,所以我们发现它们经常共享相同的内存页。
PfFk分配
以下就是我们泄漏的内核模块指针:
泄漏的NtosKrnl指针
不过要注意的是,这些分配并不总是包含此特定指针,而是包含任意内核模式地址。但是,有一种方法可以区分何时需要这种类型的块。
“nt!PfGlobals”符号始终位于对齐的16个字节的地址处,因此,该特定的指针将始终是该地址+ 0x299,这意味着指针值将始终以数字“9”结尾。我们在Windows 10 v1809至v1909的不同内部版本中对此进行了检查,并对其进行了部分和完全更新,并且从未失败。
通过泄漏此地址,我们可以轻松找到“ nt!PfGlobals”的地址,但是有一个问题。
这个符号不是由ntoskrnl导出的,并且由于其相对虚拟地址(RVA)在不同版本之间是不同的,因此我们需要找到一种定位它的方法,以便计算内核模块映像库。
也就是说,我们不能使用通常的LoadLibraryEx/GetProcAddress函数组合来获得这个符号的RVA。相反,我们设法通过另一种方法来实现这一目标。
通过使用IDA Pro检查内核模块,我们从位于ntoskrnl的“PAGE”部分的代码中找到了对该符号的一些引用。
对PfGlobals引用的Ntoskrnl代码
我们需要做的是用LoadLibraryEx加载userland中的ntoskrnl,解析PE标头以获取有关“PAGE”部分的信息,然后搜索此代码模式。然后,我们可以提取“PfGlobals”的地址(在x64中,“LEA”指令将对其进行相对引用),然后减去用户模式图像基数以获得该符号的RVA。一旦有了该符号的RVA,就可以从通过内存泄漏获得的内核模式地址中减去它,并最终计算ntoskrnl的图像基数。
在下一阶段,我们将需要此信息,以便计算我们的SMEP禁用gadget的内核模式地址。
禁用SMEP
回到图9和图10,我们看到我们控制来接管执行控制的指针位于“B2d2”块+ 0x10的地址处,而这也就是“RCX”寄存器所指向的位置。
然而,由于启用了SMEP,我们不能仅仅使用它来直接跳转到userland中的有效载荷,因此我们必须使用它作为内核模式代码的重定向。
由于我们可以完全控制对象的内容,因此可以使用接下来的8个字节存储指向一组指令的指针,这些指令将允许我们进一步重定向执行流,以便暂时禁用SMEP。
对象地址+ 0x10处的原始指针引用了BHDriver64.sys中存储的函数指针表,由于我们有一条引用该地址的指令,因此我们也可以使用相同的方法来计算该模块的内核模式映像库。
接下来,我们找到要执行的适当指令集,以便进一步重定向执行流程。
BHDriverx64 gadget
因此,对象中第一个被劫持的指针(偏移量为0x10)将用于将执行发送到这组指令,该指令集将提取偏移量为0x18的指针(RCX已指向对象分配地址+ 0x10 ),然后将其取消引用以获取函数指针。由于我们只读取该指针,因此可以使用偏移量为0x18的userland指针进行读取并获取要跳转的内核地址,该指针可用于读取和CR4覆盖以禁用SMEP。
例如,在基于VMWare的Windows 10 v1909客户操作系统中,CR4的值为0x3506f8,而在Windows 10 v1809中为0x1506f8。
注意,在本例中(v1909)还设置了SMAP位,但是通过EFLAGS.AC标志(对齐检查标志)对其进行了补偿,但实际上并没有使用。
注意,SMEP和SMAP分别由CR4寄存器的第20位和第21位控制。
由于我们需要将该寄存器的值更改为0x506f8,从而禁用SMEP(也使SMAP位也未设置),因此我们可以在地址0x50000分配用户模式页面,并将SMEP禁用gadget设置在地址0x506f8,因此当我们跳转到SMEP时通过取消引用RCX来禁用gadget,则该寄存器将具有该值并有效地禁用SMEP,同时保持其余位不变。
NtosKrnl SMEP启用或禁用gadget
有效载荷执行
通过清除SMEP,我们可以使用相同的方法泄漏和修改另一个“B2d2”对象,并直接跳到userland中的有效载荷,它将使用之前泄漏的所有信息来提升漏洞利用进程。
更具体地说,我们使用PsLookupProcessByProcessId获取系统进程(PID 4)对象的地址,然后使用PsReferencePrimaryToken获取其主令牌的地址。
接下来,我们使用PsLookupProcessByProcessId来获取漏洞利用进程对象的地址,并用系统进程使用的令牌指针覆盖令牌指针。
最后,有效载荷将跳转回内核模式地址空间中的SMEP gadget,以恢复原始值并使执行流程恢复正常。
总结
在利用此漏洞的第二部分中,我们将重点放在最新的Windows 10 v1909上,以了解内核池LFH的添加如何迫使我们寻找其他利用方式。