原文:https://googleprojectzero.blogspot.com/2018/08/windows-exploitation-tricks-exploiting.html
本文是Windows利用技巧系列的第二篇文章,在本文中,我们将为读者详细介绍如何利用Issue 1550漏洞,通过CSRSS特权进程来创建任意对象目录。我们之所以再次详细剖析特定漏洞的利用技巧,是为了帮助读者更好地认识Windows操作系统的复杂性,并向微软提供有关非内存损坏利用技术的信息,从而帮助和督促他们为这些漏洞提供相应的缓解措施。
漏洞概述
对象管理器目录与普通文件目录是相互独立的,换句话说,它们会使用一组单独的系统调用(如NtCreateDirectoryObject而不是NtCreateFile)来创建和操作目录。虽然它们本身并非文件目录,但是,对象管理器目录仍然很容易受到文件系统上发现的各种类型的安全问题的滋扰,其中包括提权型创建和符号链接植入攻击。
通过利用Issue 1550漏洞,攻击者不仅可以作为SYSTEM用户运行代码,同时,还能在处于用户控制下的位置内创建目录。该漏洞的根源,在于Desktop Bridge应用程序进程的创建过程。具体来说,是因为负责创建新应用程序进程的AppInfo服务,会调用一个未公开的API,即CreateAppContainerToken来执行一些内部管理工作。不幸的是,这个API会在用户的AppContainerNamedObjects对象目录下创建对象目录,以便为重定向BaseNamedObjects和RPC端点提供相应的支持。
由于这个API并非以用户的身份进行调用的(通常情况下,它是在CreateProcess中进行调用的,这样的话,就问题不大了),所以,这些对象目录实际上是以系统服务的身份(即SYSTEM)来创建的。同时,由于用户可以将任意对象写入其AppContainerNamedObjects目录,因此,他们就能够删除对象管理器符号链接,并几乎可以将目录创建重定向到对象管理器命名空间中的任何位置。另外,目录是使用显式安全描述符创建的,而该描述符会赋予用户完全访问权限——这一点对于该漏洞的利用来说,是非常重要的。
不过,该漏洞的一个利用难点是,如果没有在AppContainerNamedObjects下创建对象目录(比如,由于我们已对其位置进行了重定向),那么,完成令牌创建以及捕获目录句柄工作的底层NtCreateLowBoxToken系统调用将无法正常运行。这样的话,该该目录虽然会被创建,但几乎立刻又被删除掉了。之所以出现这种情况,实际上是由于我之前报告的一个问题所致,因为它改变了系统调用的行为。尽管面临这个问题,但是本文介绍的这个漏洞仍然是可以利用的,方法是在相关目录被删除之前打开一个创建目录的句柄,并且在实践中,只要您的系统具有多个处理器(所有现代系统基本上都是如此),这种方法几乎可以稳操胜券。打开句柄后,只要我们的漏洞利用代码需要,该目录就会一直存在。
实际上,我发送给MSRC的原始PoC的功能就到此为止了,该PoC所做的事情,无非就是创建了一个任意对象目录。读者可以在问题跟踪器中找到该PoC,它附加在原始漏洞报告中。接下来,让我们深入了解如何利用该漏洞从普通用户帐户华丽转身为具有特权的SYSTEM帐户。
漏洞利用
要想利用该漏洞,关键问题是找到这样一个位置——我们能够在其中创建一个对象目录,并可以利用该目录来提升我们的权限。事实证明,这个问题要比我们想象的更难。尽管几乎所有的Windows应用程序都会使用对象目录,例如BaseNamedObjects,但应用程序所用的通常是现有的目录,而这些目录都是无法通过该漏洞进行修改的。
我们发现,一个有可能被滥用的对象目录是KnownDlls(我曾经在本系列的前一篇文章中简单提过它)。该对象目录包含了许多具有名称的映像节区(image section)对象,并且都是采取NAME.DLL形式进行命名。当应用程序调用LoadLibrary加载SYSTEM32目录内的DLL时,加载程序首先会检查映像节区是否已经存在于KnownDlls对象目录中了,如果该节区已经存在的话,则将直接加载而不是创建新的节区对象。
严格来说,KnownDlls只允许管理员对其进行写操作(我们后面将会看到,实际上没有这么严格),因为如果您可以删除该目录中的任何节区对象的话,则可以强制系统服务加载已命名的DLL,例如,利用我在上一篇文章中介绍的Diagnostics Hub服务就可以达到这个目的,同时,它还能够映射节区,而非磁盘上的文件。但是,虽然该漏洞可以用来添加一个新子目录(这对于漏洞利用来说没有什么帮助),但是,却无法用来修改KnownDlls对象目录。那么,我们是否可以通该漏洞来滥用其他函数,从而间接定位KnownDlls呢?
每当我对某一产品的特定方面进行研究时,我总会记下值得注意或出乎意料的行为。例如,我在研究Windows符号链接时,一个行为就引起了我的注意。Win32 API提供了一个名为DefineDosDevice的函数,其的目是允许用户定义新的DOS驱动器号。该API需要三个参数,分别是一组标志,要创建的驱动器前缀(例如X:)和映射该驱动器的目标设备。实际上,该API的主要用途与CMD SUBST命令非常类似。
在现代版本的Windows系统上,该API会在用户自己的DOS设备对象目录中创建一个对象管理器符号链接,我们知道,这是一个普通的低权限用户帐户可以写入的位置。但是,如果您看一下DefineDosDevice的实现代码的话,您就会发现,这并不是在调用者的进程中实现的。相反,其实现代码在当前会话的CSRSS服务中调用了一个RPC方法,准确来说,就是BASESRV.DLL中的BaseSrvDefineDosDevice方法。这里调用特权服务的主要原因,是这样能够允许用户创建永久符号链接,当符号链接对象的所有句柄都关闭时,该链接也不会被删除。通常情况下,要想创建永久命名的内核对象的话,需要具有SeCreatePermanentPrivilege权限,但是,普通用户并没有该权限,而CSRSS却拥有该权限,因此,通过调用该服务,我们自然就可以创建永久符号链接了。
创建永久符号链接的能力固然值得我们关注,但是,如果我们只能在用户的DOS设备目录中创建驱动器号的话,那么这种能力也没有太大的用途。不过,我还注意到一个事实:该实现代码并没有对lpDeviceName参数是否为驱动器号进行相应的验证。例如,您可以将名称指定为“GLOBALROOT\RPC Control\ABC”,这样的话,它实际上会在用户的DosDevices目录之外创建一个符号链接,就这里来说,其路径为“\RPC Control\ABC”。之所以出现这种情况,是因为实现代码会将DosDevice前缀“\??”添加到设备名称,并将其传递给NtCreateSymbolicLink。内核将根据这个完整路径,找到GLOBALROOT——它实际上是一个用于返回根目录的特殊符号链接——并根据该路径创建任意对象。目前,由于我还不清楚这种行为是否是故意为之,所以,将来我会进一步研究该CSRSS的实现代码,下面是CSRSS实现的缩减版本。
NTSTATUS BaseSrvDefineDosDevice(DWORD dwFlags,
LPCWSTR lpDeviceName,
LPCWSTR lpTargetPath) {
WCHAR device_name[];
snwprintf_s(device_name, L"\\??\\%s", lpDeviceName);
UNICODE_STRING device_name_ustr;
OBJECT_ATTRIBUTES objattr;
RtlInitUnicodeString(&device_name_ustr, device_name);
InitializeObjectAttributes(&objattr, &device_name_ustr,
OBJ_CASE_INSENSITIVE);
BOOLEAN enable_impersonation = TRUE;
CsrImpersonateClient();
HANDLE handle;
NTSTATUS status = NtOpenSymbolicLinkObject(&handle, DELETE, &objattr);①
CsrRevertToSelf();
if (NT_SUCCESS(status)) {
BOOLEAN is_global = FALSE;
// Check if we opened a global symbolic link.
IsGlobalSymbolicLink(handle, &is_global); ②
if (is_global) {
enable_impersonation = FALSE; ③
snwprintf_s(device_name, L"\\GLOBAL??\\%s", lpDeviceName);
RtlInitUnicodeString(&device_name_ustr, device_name);
}
// Delete the existing symbolic link.
NtMakeTemporaryObject(handle);
NtClose(handle);
}
if (enable_impersonation) { ④
CsrRevertToSelf();
}
// Create the symbolic link.
UNICODE_STRING target_name_ustr;
RtlInitUnicodeString(&target_name_ustr, lpTargetPath);
status = NtCreateSymbolicLinkObject(&handle, MAXIMUM_ALLOWED,
objattr, target_name_ustr); ⑤
if (enable_impersonation) { ⑥
CsrRevertToSelf();
}
if (NT_SUCCESS(status)) {
status = NtMakePermanentObject(handle); ⑦
NtClose(handle);
}
return status;
}
如上所示,代码所做的第一件事就是构建设备名路径,然后尝试打开符号链接对象以便执行DELETE操作①。这是因为API支持重新定义现有的符号链接,因此,必须首先尝试删除原来的链接。如果我们使用相应链接并不位于其中的默认路径的话,将看到代码会将身份切换为调用者(在这种情况下为低权限用户)④,然后创建符号链接对象⑤,重新切换回原来的身份⑥,并在返回操作状态之前实现对象的永久化⑦。现在终于明白我们为什么可以创建任意符号链接了吧,因为所有代码都是在传递的设备名称前加上了“\??”。由于代码在执行所有重要操作时,都会将身份切换为调用者,因此,我们只能在用户具有写权限的位置创建链接。
更值得关注的是中间的条件,即是否为DELETE操作打开了目标符号链接,这是调用NtMakeTemporaryObject所必需的。打开的句柄将传递给另一个函数②,即IsGlobalSymbolicLink,并根据该函数的结果设置禁用身份切换的标志,并使用全局DOS设备位置\GLOBAL??作为前缀来重建设备名称③。那么,IsGlobalSymbolicLink到底是做什么的呢?别急,先来看看下列代码。
void IsGlobalSymbolicLink(HANDLE handle, BOOLEAN* is_global) {
BYTE buffer[0x1000];
NtQueryObject(handle, ObjectNameInformation, buffer, sizeof(buffer));
UNICODE_STRING prefix;
RtlInitUnicodeString(&prefix, L"\\GLOBAL??\\");
// Check if object name starts with \GLOBAL??
*is_global = RtlPrefixUnicodeString(&prefix, (PUNICODE_STRING)buffer);
}
上述代码首先会检查打开的对象的名称是否以\GLOBAL??\开头。如果是的话,就将is_global标志设为TRUE。这样的话,就会导致启用身份切换的标志被清空,同时,设备名称也将被重写。这就意味着,如果调用者具有对全局DOS设备目录内的符号链接的DELETE访问权限的话,则会在不进行身份切换的情况下重新创建符号链接,也就是说,将以SYSTEM用户身份来创建该链接。这本身并没有值得特别关注的地方,因为默认情况下,只有administrator用户才有权打开执行DELETE操作的全局符号链接。但是,如果我们可以在全局DOS设备目录下创建一个可由低权限用户写入的子目录的话,情况又会如何呢?可以打开该目录中的任何符号链接来执行DELETE操作,因为低权限用户可以随意指定访问权限,该代码可以将链接标记为全局链接,即使实际情况并非如此,同时,还能够禁用身份切换,并以SYSTEM 身份来重建链接。您猜怎么着,我们获得了一个允许我们在全局DOS设备目录下创建任意对象目录的漏洞。
同样,除非用于重写路径,否则,这个漏洞没有太大的利用价值。我们可以活用路径“\??\ABC”不同于“\GLOBAL??\ABC”这一事实,设法以SYSTEM身份在对象管理器命名空间中创建任意符号链接。但是,这对我们有什么帮助呢?如果您编写了一个指向KnownDlls的符号链接,那么,当DLL加载程序打开一个请求的节区时,内核将会使用该链接。因此,即使我们无法在KnownDlls中直接创建新的节区对象,我们仍然可以创建一个符号链接,让该链接指向该目录之外的低权限用户可以创建节区对象的位置。这样,我们就可以利用这种劫持方法,将任意DLL加载到特权进程内的内存空间中,从而达到提权的目的。
综上所述,我们可以通过下列步骤来利用该漏洞:
实际上,针对DefineDosDevice API的滥用还有另一种用途,那就是绕过Protected Process Light(PPL)保护。PPL进程仍然在使用KnownDlls,因此,如果您可以向该目录中添加内容的话,就可以将代码注入该受保护进程中。为了防御这种攻击,Windows使用进程信任标签来标记KnownDlls目录,该进程信任标签将阻止除最高级别PPL进程以外的所有进程对其进行写入,如下所示。
那么,我们的漏洞利用的是如何得逞的呢? 实际上,CSRSS是作为最高级别的PPL运行的,因此它具有KnownDlls目录的写权限。一旦身份切换被废除,该进程的身份就会一直被沿用,也就是说,一直拥有全部的访问权限。
如果你想测试这个漏洞利用的话,可以从这里下载新的PoC代码。
结束语
您可能想知道我是否MSRC报告了DefineDosDevice的这种行为?我没有,主要是因为它本身并不是一个漏洞。即使能够从Administrator权限提升到PPL权限,MSRC也不会认为是一个值得兴师动众的事情(具体参见这里)。当然,Windows开发人员可能会选择在将来修改该行为,如果它不会导致兼容性的重大倒退的话。这个功能自早期版本的Windows开始就已存在,至少可以追溯到Windows XP,因此,可能有些东西会依赖于它。我希望通过详细描述这个漏洞,给MS提供尽可能多的信息,以帮他们在将来克服这种漏洞利用技术。
我确实向MSRC报告了这个漏洞,并且,该漏洞已经在2018年6月的补丁中得到修复。那么,Microsoft是如何修复该漏洞的呢?开发人员添加了一个新的API,CreateAppContainerTokenForUser,它在创建新的AppContainer令牌期间,会进行相应的身份切换。通过在令牌创建期间进行身份切换,代码可确保仅使用用户的权限来创建所有对象。由于它是一个新的API,必须修改现有代码才能使用它,因此,您仍有机会在易受攻击的模式中找到使用旧CreateAppContainerToken的代码。
无论利用哪种平台上的漏洞,有时都需要深入了解不同组件的交互方式。在这个例子中,虽然最初的漏洞显然是一个安全问题,但尚不清楚如何进行充分利用。在逆向工程中遇到的有趣行为总是值得记录下来的,因为即使某些东西本身不是安全漏洞,但在利用另一个漏洞时,却可能帮上大忙。