如何在用户态下绕过LSA保护机制(Part 2)

阅读量364349

|

发布时间 : 2021-05-12 15:30:57

x
译文声明

本文是翻译文章,文章原作者 scrt,文章来源:blog.scrt.ch

原文地址:https://blog.scrt.ch/2021/04/22/bypassing-lsa-protection-in-userland/

译文仅供参考,具体内容表达以及含义原文为准。

 

0x03 利用DefineDosDevice

现在我们开始深入分析DefineDosDevice函数,我们可以看到该函数中存在哪种脆弱性,以及如何利用这个脆弱点来实现我们的目标。

DefineDosDevice内部原理

JF在博客中逆向了BaseSrvDefineDosDevice函数,提供了相应的伪代码(参考此处)。如果我们重复该操作,应当注意到第4步存在一些错误,应该为CsrImpersonateClient(),而不是CsrRevertToSelf()。无论如何,我不会复制粘贴他的代码,而是使用图表来展示整体流程。

在如上流程图中,我使用不同颜色高亮出了一些单元。其中,模拟函数为橙色,符号链接创建步骤为蓝色。最后,我用红色突出了我们需要注意的关键路径。

首先,可以看到CSRSS服务在模拟调用者(比如RPC客户端)时,会尝试打开\??\DEVICE_NAME,主要目的是先删除符号链接(如果已经存在)。除此之外,服务还会检查符号链接是否为“全局”链接,这里会使用一个内部函数,检查对象的“实际”路径是否以\GLOBAL??\开头。如果满足该条件,则会在后续执行中禁用身份模拟,服务在调用NtCreateSymbolicLinkObject()之前不会模拟客户端,这意味着符号链接会由CSRSS服务自己创建。最后,如果该操作成功,服务会将对象标记为“永久”。

是否为漏洞?

大家可能已经意识到,这里存在某种TOCTOU(Time-of-Check Time-of-Use)漏洞。用来打开符号链接的路径以及用来创建符号链接的路径相同,都为\??\DEVICE_NAME。然而,“打开”操作始终通过模拟用户来执行,而如果禁用模拟,那么“创建”操作可能直接由SYSTEM来完成。前面我提到过,\??代表用户的本地DOS设备目录,因此会根据用户的身份解析为不同的路径。因此,尽管这两种情况下都使用相同的路径,但实际上可能会指向完全不同的路径。

为了利用这种行为,我们首先必须解决一个挑战:我们需要找到一个“设备名”。当服务模拟客户端身份时,该名称会解析成我们可控制的一个“全局对象”。并且当禁用模拟时,这个“设备名”必须解析成\KnownDlls\FOO.dll。这听上去有点棘手,但我们来逐步搞定它。

先从最简单的部分开始,我们需要确定\??\DEVICE_NAME中的DEVICE_NAME,以便当调用者为SYSTEM时,这个路径可以解析为\KnownDlls\FOO.dll。这里我们可知\??会被解析为\GLOBAL??

如果检查\GLOBAL??\目录的内容,可以看到里面有一个非常方便的对象。

在这个目录中,GLOBALROOT对象是指向空路径的一个符号链接。这意味着类似\??\GLOBALROOT\的路径会被翻译为\,也就是对象管理器的根(因此称为“全局根”)。如果我们将这个原则应用于我们的“设备名”,可知当调用者为SYSTEM时,\??\GLOBALROOT\KnownDlls\FOO.DLL会解析为\KnownDlls\FOO.dll。我们已经解决了部分问题!

现在我们知道,我们需要为DefineDosDevice函数调用提供GLOBALROOT\KnownDlls\FOO.DLL作为“设备名”(注意\??\会自动加到这个值作为前缀)。如果我们希望CSRSS服务禁用模拟,还知道符号链接对象必须被视为“全局”,这样其路径必须以\GLOBAL??\开头。因此,这里的问题在于:我们如何将类似\??\GLOBALROOT\KnownDlls\FOO.DLL的路径转换为\GLOBAL??\KnownDlls\FOO.dll?答案其实非常简单,因为这几乎就是符号链接的定义。当服务模拟用户时,我们知道\??指向的是这个特定用户的本地DOS设备目录。因此我们所要做的就是创建类似\??\GLOBALROOT的符号链接,让该链接指向\GLOBAL??,仅此而已。

总结以下,当用户而不是SYSTEM打开路径时,映射关系如下:

\??\GLOBALROOT\KnownDlls\FOO.dll
-> \Sessions\0\DosDevices\00000000-XXXXXXXX\GLOBALROOT\KnownDlls\FOO.dll

\Sessions\0\DosDevices\00000000-XXXXXXXX\GLOBALROOT\KnownDlls\FOO.dll
-> \GLOBAL??\KnownDlls\FOO.dll

另一方面,如果SYSTEM打开相同的路径:

\??\GLOBALROOT\KnownDlls\FOO.dll
-> \GLOBAL??\GLOBALROOT\KnownDlls\FOO.dll

\GLOBAL??\GLOBALROOT\KnownDlls\FOO.dll
-> \KnownDlls\FOO.dll

这里还有最后一个点需要考虑。在检查对象是否为“全局”对象之前,该对象首先必须存在,否则最初的“打开”操作将会失败。因此,我们需要确保在调用DefineDosDevice之前,\GLOBAL??\KnownDlls\FOO.dll是已经存在的一个符号链接对象。

这里还有个小问题。管理员无法在\GLOBAL??中创建对象或者目录。这并不是一个真正的问题,我们只需要在攻击过程中,将权限临时提升为SYSTEM即可。作为SYSTEM,我们首先可以在\GLOBAL??\中创建一个虚假的KnownDlls目录,然后在其中创建一个虚拟符号链接对象,使用我们希望劫持的DLL名。

完整利用过程

前面内容较多,因此在讨论最后内容之前,我们先简单概述一下利用步骤。在利用过程中,我们假设以管理员权限来执行操作:

1、提升至SYSTEM权限,否则我们无法在\GLOBAL??中创建对象。

2、创建对象目录\GLOBAL??\KnownDlls,模拟实际的\KnownDlls目录。

3、创建符号链接\GLOBAL??\KnownDlls\FOO.dll,其中FOO.dll为我们希望劫持的DLL名。这里要记住一点,重要的是链接本身的名称,而不是链接指向的目标。

4、释放SYSTEM权限,恢复到我们的管理员用户上下文。

5、在当前用户的DOS设备目录中创建一个符号链接GLOBALROOT,指向\GLOBAL??。不能以SYSTEM执行这个步骤,因为我们想在自己的DOS目录中创建一个虚假的GLOBALROOT链接。

6、这是攻击的核心步骤。调用DefineDosDevice,使用GLOBALROOT\KnownDlls\FOO.dll作为设备名。这个设备的目标路径为DLL的位置,后面我们再分析这一点。

在最后一个步骤中,CSRSS服务内部的处理过程如下。首先该服务会收到GLOBALROOT\KnownDlls\FOO.dll值,然后在前面加上\??\,生成的设备名为\??\GLOBALROOT\KnownDlls\FOO.dll。然后,服务会模拟客户端,尝试打开对应的符号链接对象。

\??\GLOBALROOT\KnownDlls\FOO.dll
-> \Sessions\0\DosDevices\00000000-XXXXXXXX\GLOBALROOT\KnownDlls\FOO.dll
-> \GLOBAL??\KnownDlls\FOO.dll

由于对象存在,服务会检查对象是否为全局对象。对象的“实际”路径以\GLOBAL??\开头,因此的确会被认为是全局对象,因此会在后续执行中禁用模拟。当前链接会被删除,新创建一个链接,但此时RPC客户端没有被模拟,所以操作会在CSRSS服务上下文中,以SYSTEM执行:

\??\GLOBALROOT\KnownDlls\FOO.dll
-> \GLOBAL??\GLOBALROOT\KnownDlls\FOO.dll
-> \KnownDlls\FOO.dll

大功告成,现在服务会创建符号链接\KnownDlls\FOO.dll,并且目标路径我们可控。

 

0x04 通过Known DLLs实现DLL劫持

现在我们已经知道如何在\KnownDlls目录中添加任意条目,那么应该回到我们最初的问题,以及漏洞利用方面的限制。

DLL劫持目标

我们想在PPL中执行任意代码,理想情况下是使用WinTcb签名者类型。因此我们首先需要找到合适的可执行程序。在Windows 10上,据我所知,有4个内置的程序可以以这种保护级别来执行:wininit.exeservices.exesmss.exe以及csrss.exesmss.execsrss.exe无法在Win32模式下执行,所以可以排除。我针对wininit.exe做了一些测试,但发现以具有debug权限的管理员来运行这个程序不是一个好主意。实际上,这个程序很大概率会将自己标记为关键进程,意味着当程序结束时,系统很可能会出现BSOD。

这样我们只剩下一个目标:services.exe。事实证明,这的确是最佳目标,其主函数非常容易反编译,对应的伪代码如下:

int wmain()
{
    HANDLE hEvent;
    hEvent = OpenEvent(SYNCHRONIZE, FALSE, L"Global\\SC_AutoStartComplete");
    if (hEvent) {
        CloseHandle(hEvent);
    } else {
        RtlSetProcessIsCritical(TRUE, NULL, FALSE);
        if (NT_SUCCESS(RtlInitializeCriticalSection(&CriticalSection))
            SvcctrlMain();
    }
    return 0;
}

该程序首先会尝试打开全局Event对象,如果成功,则关闭句柄。这种简单的同步机制可以确保services.exe不会被执行两次,对我们的使用场景而言也是完美的逻辑,因为我们不希望干扰服务控制管理器(services.exe是SCM使用的镜像文件)。

现在,为了澄清services.exe加载的DLL,我们可以使用Process Monitor,设置一些过滤器。

从输出中可知,services.exe会加载3个DLL(都不是Known DLLs),但仅靠这些信息并不够,我们还需要找到导入了那些函数。因此我们需要观察PE的导入表。

从这里可知,dpapi.dll只导入了一个函数:CryptResetMachineCredentials。因此,这就是需要劫持的最简单的DLL。我们只需要记住,我们需要导出该函数,否则我们构造的DLL不会被加载。

但就这么简单吗?并非如此。在安装各种Windows并进行测试后,我发现这种行为并不一致。在某些版本的Windows 10上,由于某些原因,dpapi.dll根本不会被加载。此外,在Windows 8.1上,services.exe导入的DLL也完全不同。最后,我决定将这些区别考虑在内,以便开发出的工具能够使用所有最新版本的Windows(包括Server版)。

DLL文件映射

在前文中,我们了解了如何诱导CSRSS服务在\KnownDlls中创建任意符号链接对象,但我故意忽略了一个重要的因素:符号链接的目标路径。

符号链接实际上可以指向对象管理器中任意类型的对象,但在我们这个场景中,我们需要模仿程序库作为Known DLL加载的行为,这意味着目标必须为Section对象,而不是DLL文件路径。

前面提到过,“Known DLLs”是存储在\KnownDlls对象目录中的Section对象,这也是DLL搜索顺序中的第一个位置。因此,如果程序加载名为FOO.dll的DLL,并且Section对象\KnownDlls\FOO.dll存在,那么加载器就会使用这个镜像,而不是再次映射文件。在我们的场景中,我们必须手动执行该步骤。“手动”这个词有点不合适,因为如果我们以“合法方式”进行操作,就不需要自己真正去映射文件。

Section对象可以调用NtCreateSection来创建,这个原生API函数需要一个AllocationAttributes参数,通常设置为SEC_COMMITSEC_IMAGE。当设置为SEC_IMAGE,表示我们想将之前打开的文件映射为可执行镜像文件。因此,它可以被正确且自动地映射到内存中。但这意味着我们必须嵌入一个DLL,将其写入磁盘,使用CreateFile打开,获得文件句柄,最终调用NtCreateSection。对于PoC而言,这已经足够,但我想更进一步,找到更优雅的解决方案。

另一种方法是在内存中执行所有操作。与众所周知的Process Hollowing技术类似,我们需要创建一个具备足够内存空间的Section对象,以存储我们的DLL镜像,然后解析NT头,识别PE中的每个section,正确进行映射,这正是加载器做的工作。这是非常乏味的一个过程,我不想走这么远。在研究过程中,我偶尔发现@_ForrestOrr写过关于“DLL Hollowing”的一篇有趣的文章。在文章PoC中,他使用了Transactional NTFS(也称为TxF),将已有DLL文件内容替换为自己的payload,而没有真正修改磁盘上的文件。这种技术唯一的要求是我们必须具备目标文件的写权限。

在这个场景中,我们假设自己已具备管理员权限,所以条件很完美。我们可以在System目录中打开一个DLL作为目标,将其内容替换为我们的payload DLL,最后在NtCreateSection API调用中,使用已打开的句柄以及SEC_IMAGE标志。但我们仍然需要目标文件的写权限,即便我们不需要修改文件本身。这是一个问题,因为系统文件的所有者为TrustedInstaller。由于我们具备管理员权限,那么可以提升至TrustedInstaller权限,然而这里有个更简单的解决方案。事实证明,C:\Windows\System32\中的某些(DLL)文件所有者为SYSTEM,因此我们只要在该目录中搜索合适的目标即可。我们还应当确保其大小足够大,可以替换成我们自己的payload。

使用SYSTEM身份

在利用过程中,我坚持一个原则:必须以除SYSTEM之外的任何用户身份来调用DefineDosDevice API函数,否则整个“技巧”将无法正常工作。但如果我们已经是SYSTEM并且不具备管理员账户那该怎么办?我们可以创建一个临时的管理员账户,但这非常不优雅,更好的办法是简单模拟已存在的用户。比如,我们可以模拟LOCAL SERVICE或者NETWORK SERVICE,这两个用户都有自己的DOS设备目录。

假设我们具备debugimpersonate权限,我们可以枚举当前进程,找到以LOCAL SERVICE权限运行的进程,复制主令牌,临时模拟该用户,就这么简单。

无论我们以SYSTEM或者管理员来执行利用代码,这两种情况下我们都需要在两个身份之间来回移动。

 

0x05 总结

在本文中,我们了解了如何通过管理员,利用貌似无害的API函数,配合某些非常巧妙的技巧,最终将任意代码注入具有最高级别的PPL中。我参考ProcDump,在一款新的工具(PPLdump)中实现了这种技术。假设我们已经具备管理员或者SYSTEM权限,就可以转储任何PPL的内存,包括启用LSA保护时的LSASS。

这个“漏洞”最早于2018年公布,并且现在还没修复。如果想了解背后原因,我们可以参考微软漏洞赏金项目中的Windows Security Servicing Criteria,会发现即使在非管理员权限下实现PPL绕过也不是一个待解决的问题。

通过在一个独立工具中实现该技术,我学习了很多与Windows内部原理有关的知识,以前并没有机会去了解。我也在本文中涉及了部分内容。

 

0x06 参考资料

  • @tiraniddo – Windows Exploitation Tricks: Exploiting Arbitrary Object Directory Creation for Local Elevation of Privilege – link
  • @_ForrestOrr – Masking Malicious Memory Artifacts – Part I: Phantom DLL Hollowing – link
本文翻译自blog.scrt.ch 原文链接。如若转载请注明出处。
分享到:微信
+11赞
收藏
興趣使然的小胃
分享到:微信

发表评论

内容需知
合作单位
  • 安全客
  • 安全客
Copyright © 北京奇虎科技有限公司 三六零数字安全科技集团有限公司 安全客 All Rights Reserved 京ICP备08010314号-66