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.DLL
的BaseSrvDefineDosDevice
中。这个服务的特殊之处在于,它以PPL运行,保护级别为WinTcb
。
虽然这是我们攻击的必要条件,但并不是DefineDosDevice
最有趣的一个特点。更有趣的是,lpDeviceName
的值没有被正确过滤。这意味着我们不一定要提供驱动器号,如E:
。下面我们可以利用这一点,欺骗CSRSS
服务在任意位置(比如\KnownDlls
)创建任意符号链接。
发表评论
您还未登录,请先登录。
登录