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

 

0x00 前言

2018年,James Forshaw发表了一篇文章,其中简要提到了一种技巧,可以使用管理员权限将任意代码注入PPL中。然而,我觉得这篇文章并没有得到足够的重视,其中隐含着能够在用户态下绕过PPL(包括LSA Protection)的攻击方法。

之前在研究受保护进程(Protected Process)时,我偶然发现了一篇文章:Windows Exploitation Tricks: Exploiting Arbitrary Object Directory Creation for Local Elevation of Privilege,这是James Forshaw于2018年在Project Zero上撰写的文章,其中讨论了一种特殊的权限提升技巧,本意并不是用来绕过PPL。然而,我注意到文中有这样一段话:

实际上,对DefineDosDevice API的滥用还存在第二种场景,即利用管理员权限来绕过Protected Process Light(PPL)。

据我所知,到目前为止,已公开的用来绕过PPL的技术都涉及到驱动,以便在内核中执行任意代码(除了我在上一篇文章中提到的pypykatz)。在那篇文章中,James Forshaw顺便提到了用户态下的一种绕过技术,并且似乎没有在渗透测试社区中引起人们的注意。

本文的目标是进一步讨论这种技术的细节。首先我将回顾一下PPL进程的某些关键概念,解释PP(Protected Process)以及PPL(Protected Process Light)之间主要的一个区别。然后,我们可以来看一下如何通过管理员身份利用这个细微的区别。最后,我向大家介绍我开发的一款工具,可以利用这个漏洞,不使用任何内核代码,转储任何PPL的内存。

 

0x01 背景

我曾在之前的一篇文章中提到了PP(L)的核心概念,所以大家也可以不阅读这一部分,直接参考那篇文章。

PPL(S)

Windows Vista首先引入了PP模型,当时进程要么受保护,要么不受保护。从Windows 8.1开始,PPL模型扩展了这一概念,引入了保护级别,直接导致某些PP(L)会比其他进程受到更多的保护。这个概念中有个最基本的原则:不受保护的进程只能使用受限的访问标志集(如PROCESS_QUERY_LIMITED_INFORMATION)来打开受保护的进程,如果请求高级别访问权限,系统就会返回拒绝访问(Access is Denied)错误。

对于PP(L),情况要复杂一些。这些进程可以请求的访问权限取决于他们自身的保护级别。这种保护级别部分由文件数字证书中的特殊EKU字段所确定。当受保护进程被创建时,保护信息存放在EPROCESS内核结构中的某个特殊值中。这个值存储着保护级别(PP或者PPL)以及签名者类型(比如反恶意软件、LSA、WinTcb等)。签名者类型在PP(L)之间建立了一种层次结构,应用于PP(L)的基本规则如下:

  • 如果PP的签名者类型比PP或者PPL的更高或者持平,那么就可以使用完整访问权限打开后者。
  • 如果PPL的签名者类型比PPL更高或者持平,就可以使用完整访问权限打开后者。
  • PPL无法使用完整权限打开PP,不论前者使用何种签名者类型。

比如,当启用LSA保护后,lsass.exe就会以PPL方式来执行,我们可以使用Process Explorer观察到PsProtectedSignerLsa-Light保护级别。如果我们想访问该进程的内存,那么需要调用OpenProcess,指定PROCESS_VM_READ访问标志。如果调用方进程不受保护,那么这次调用就会立即失败,返回拒绝访问错误,不论用户使用的是什么权限。然而,如果调用方进程为PPL,具备更高的级别(比如WinTcb),那么相同的调用行为则会成功(当然用户还需要具备正确的权限)。因此,如果我们能创建这样一个进程,并且在里面执行任意代码,那么即使启用了LSA保护,我们也能访问LSASS。这里的问题是:我们能否不使用内核代码来实现这一目标?

PP vs PPL

PP(L)模型有效阻止了不受保护进程使用诸如OpenProcess之类的API,访问具备扩展访问权限的受保护进程。这样能阻止简单的内存访问,但还有另一个作用,可以阻止这些进程加载未签名的DLL。这一点也很好理解,否则整个安全模型将形同虚设,因为我们可以使用任何形式的DLL劫持技术,将任意代码注入自己的PPL进程中。这也解释了为什么当启用LSA保护时,我们需要特别注意第三方认证模型。

然而这个规则有一个例外,这也可能是PP和PPL之间最大的区别所在。如果我们了解Windows上的DLL搜索顺序,那么可知当进程被创建时,首先会遍历一个“Known DLLs”列表,然后继续处理应用程序目录、System目录等等。在这个搜索顺序中,“Known DLLs”是一个特殊步骤,用户无法控制,因此在DLL劫持中通常不会考虑在内。然而,对我们而言,这个步骤恰巧是PPL进程的“阿喀琉斯之踵”。

“Known DLLs”是Windows应用程序最常加载的DLL,因此,为了提高整体性能,这些DLL会被预加载到内存中(即被缓存)。如果想查看“Known DLLs”的完整列表,我们可以使用WinObj,在对象管理器中查看\KnownDlls目录的内容。

由于这些DLL已经在内存中,因此如果我们使用Process Monitor来检查典型Windows应用的文件操作,应该看不到这些DLL。然而对于受保护进程,情况略微有点不同,这里我们以SgrmBroker.exe来举个例子:

Process Explorer所示,SgrmBroker.exe为受保护进程(PP)。当进程启动时,被加载的首个DLL为kernel32.dll以及KernelBase.dll,这些都是“Known DLLs”。是的,对于PP,即便是“Known DLLs”也会从硬盘上加载,这意味着每个文件的数字签名都会被校验。然而,如果我们对PPL进行同样的测试,就无法在Process Monitor中看到这些DLL,这里PPL与普通的进程行为一样。

这个现象非常有趣,因为DLL的数字签名只有在文件被映射(即创建Section)时才会验证。这意味着如果我们可以在\KnownDlls目录中添加任意条目,就可以注入任意DLL,在PPL中执行未签名代码。

\KnownDlls中添加条目说起来容易做起来难,因为微软已经考虑到了这种攻击方式。James Forshaw曾在一篇文章中指出,\KnownDlls对象目录被标有特殊的进程信任标签(Process Trust Label),如下图所示:

可以想象得出,根据标签名,只有受保护进程的级别高于或者等于WinTcb(实际上是PPL的最高级别),才能请求该目录的写访问权限。但大家不要灰心,James Forshaw为我们找到了一种很好的技巧。

 

0x02 MS-DOS设备名

James Forshaw发现的技术需要使用到DefineDosDevice API,还涉及到一些不容易掌握的Windows内部原理。因此,我决定在分析这个方法前,先回顾一些基本概念。

DefineDosDevice

DefineDosDevice函数原型如下:

BOOL DefineDosDeviceW(
  DWORD   dwFlags,
  LPCWSTR lpDeviceName,
  LPCWSTR lpTargetPath
);

如函数名所示,DefineDosDevice的作用是定义MS-DOS设备名称。根据官方文档,MS-DOS设备名是对象管理器中的符号链接,格式为\DosDevices\DEVICE_NAME(比如\DosDevices\C:)。因此,该函数允许我们将一个实际的“设备”映射到“DOS设备”。我们插入外部驱动器或者USB设备时就会出现这种情况,设备会被自动分配一个驱动器号,比如E:,我们可以调用QueryDosDevice来查询对应的映射。

WCHAR path[MAX_PATH + 1];

if (QueryDosDevice(argv[1], path, MAX_PATH)) {
    wprintf(L"%ws -> %ws\n", argv[1], path);
}

如上图所示,目标设备为\Device\HarddiskVolume5,对应的MS-DOS设备名为E:。前面提到过,MS-DOS设备名格式为\DosDevices\DEVICE_NAME,因此这不会只是一个驱动器号。对于DefineDosDevice以及QueryDosDevice\DosDevices\部分会被隐掉,这些函数会在“设备名”前面自动加上\??\。因此,如果我们提供E:作为设备名,那么这些函数内部使用的会是NT路径\??\E:。但即便这样,\??\也不是\DosDevices\,这里WinObj可以帮我们解开谜团。在对象管理器的根目录下,我们可以看到\DosDevices是指向\??的一个符号链接。因此,\DosDevices\E: -> \??\E:,我们可以将这两者当成同一个。这个符号链接主要考虑到的是兼容问题,在老版本的Windows中,只有一个DOS设备目录。

本地DOS设备目录

\??\这个路径前缀本身有非常特殊的含义,代表用户的本地DOS设备目录,因此在对象管理器中,会根据用户上下文指向不同的位置。具体而言,\??指向的完整路径为\Sessions\0\DosDevices\00000000-XXXXXXXX,其中XXXXXXXX为用户的登录认证ID。然而这里有个例外,对于NT AUTHORITY\SYSTEM\??指向的是\GLOBAL??。这个概念非常重要,因此我将举两个例子来说明。第一个例子是我前面使用过的USB设备,第二个是通过资源管理器手动挂载的SMB共享。

对于USB设备,我们已知\??\E:是指向\Device\HarddiskVolume5的符号链接。由于该设备由SYSTEM挂载,这个链接应该存在于\GLOBAL??\中,我们可以使用WinObj来验证这一点。

一切正常,现在我们将一个“SMB共享”映射到本地磁盘,看会发生什么事情。

此时该设备以已登录的用户来挂载,因此\??应当映射到\Sessions\0\DosDevices\00000000-XXXXXXXX,然而XXXXXXXX的具体值为多少?为了找到这个值,我使用了Process Hacker,检查explorer.exe进程主访问令牌的高级属性。

认证ID为0x1abce,因此符号链接应该在\Sessions\0\DosDevices\00000000-0001abce中创建。我们可以使用WinObj来验证。

很好,该符号链接的确在该目录中创建。

选择DefineDosDevice

如前文分析,设备映射操作会在调用方的DOS设备目录下创建一个简单的符号链接,任何用户都可以执行该操作,因为这样只会影响用户自己的会话。但这里有个问题,由于低权限用户只能创建“临时的”内核对象,当句柄被关闭时,这些对象就会被移除。为了解决这个问题,对象必须被标记为“永久”,但这需要用户不具备的一种特殊权限(SeCreatePermanentPrivilege)。因此,该操作必须由具备该能力的特权服务来执行。

JF在博客中提到过,DefineDosDevice只是RPC方法调用的一个封装函数,该方法由CSRSS服务对外提供,具体实现位于BASESRV.DLLBaseSrvDefineDosDevice中。这个服务的特殊之处在于,它以PPL运行,保护级别为WinTcb

虽然这是我们攻击的必要条件,但并不是DefineDosDevice最有趣的一个特点。更有趣的是,lpDeviceName的值没有被正确过滤。这意味着我们不一定要提供驱动器号,如E:。下面我们可以利用这一点,欺骗CSRSS服务在任意位置(比如\KnownDlls)创建任意符号链接。

(完)