导语:在这篇博客中,将主要探讨在Windows 10 1803上,如何向PPL进行代码注入。

概述

在Recon Montreal 2018上,我与Alex Ionescu共同发表了“已知DLL和其他代码完整性信任绕过的新方法”的演讲。我们描述了Microsoft Windows中的代码完整性机制,以及Microsoft是如何实现进程保护(Protected Processes,PP)的。在演讲中,我展示了如何绕过Protected Process Light(PPL)保护机制,有些绕过方法需要具有管理员权限,有些则不需要。

在这篇博客中,将主要探讨在Windows 10 1803上,如何向PPL进行代码注入。由于Microsoft表示存在于防御安全边界的唯一漏洞已经被修复,因此我可以更详细的讲解一下利用方法。

漏洞描述点击这里

Microsoft关于这一漏洞的公告

关于Windows受保护进程

Windows受保护进程(Windows Protected Process)模型可以追溯到Windows Vista,它是为了保护DRM进程而引入的。受保护进程模型受到严格限制,将其需要加载的DLL限制为随操作系统安装的代码集。此外,只有使用特定Microsoft证书进行签名,并将其嵌入到二进制文件中的可执行文件,才有权限启动保护。其中,有一项由内核强制执行的保护,使未受保护的进程无法打开受保护进程的句柄,因此没有足够权限来注入任意代码或读取内存。

在Windows 8.1中,引入了一种新的机制,称为Protected Process Light(PPL),该机制使保护变得更加通用。PPL放宽了可以受到保护的DLL范围,并为主要可执行文件引入了不同的签名要求。另外,还有一个重大变化,就是它加入了“签名级别”的概念,以此来区分不同类型的受保护进程。某个级别的PPL,对同级别或更低级别的任何进程拥有完全访问的权限,对更高级别的权限只有受限的访问权限。这一签名级别的概念,也同样扩展到旧的PP模式上,同一级的PP可以打开相同级别或更低级别的所有PP和PPL,但反过来就不行,PPL在任何级别的情况下都无法拥有对PP的完全访问权限。下图表示了其中的一些级别和相互关系:

1.png

由于引入了签名级别的概念,于是Microsoft向第三方开放了受保护进程。然而目前,第三方唯一可以创建的受保护进程是反恶意软件PPL。反恶意软件所具有的级别是特殊的,因为它允许第三方通过Early Launch Anti-Malware(ELAM,https://msdn.microsoft.com/en-us/library/windows/desktop/dn313124%28v=vs.85%29.aspx)来添加其他允许的签名密钥。此外,还有Microsoft的TruePlay,这是一个用于游戏的反作弊技术,但与本次分析无关,我们在此不做讨论。

在这里,我强烈推荐大家阅读Alex Ionescu的博客文章(分为三部分):第一部分第二部分第三部分

尽管文章是主要基于Windows 8.1进行讲解的,但其中的大多数概念在Windows 10中未发生重大变化。

我之前也写过一篇关于受保护进程的文章,主要讲解了Oracle如何在Windows环境下的VirtualBox虚拟化平台中实现。在文章中,我展示了如何使用多种不同的技术绕过进程保护。但是,我在当时的文章中没有提到过的是,我描述的第一种技术,将Jscript代码注入到进程中,也违背了Microsoft的PPL实现。我向Microsoft反映了这一问题,证明我可以向PPL中注入任意代码,但Microsoft决定不将其修复方式作为一个安全补丁发布。随后,Microsoft将下面的代码添加到内核代码完整性库CI.DLL中,从而修复了这一问题:

UNICODE_STRING g_BlockedDllsForPPL[] = {
 DECLARE_USTR("scrobj.dll"),
 DECLARE_USTR("scrrun.dll"),
 DECLARE_USTR("jscript.dll"),
 DECLARE_USTR("jscript9.dll"),
 DECLARE_USTR("vbscript.dll")
};
 
NTSTATUS CipMitigatePPLBypassThroughInterpreters(PEPROCESS Process,
                                                LPBYTE Image,
                                                SIZE_T ImageSize) {
 if (!PsIsProtectedProcess(Process))
   return STATUS_SUCCESS;
 
 UNICODE_STRING OriginalImageName;
 // Get the original filename from the image resources.
 SIPolicyGetOriginalFilenameAndVersionFromImageBase(
     Image, ImageSize, &OriginalImageName);
 for(int i = 0; i < _countof(g_BlockedDllsForPPL); ++i) {
   if (RtlEqualUnicodeString(g_BlockedDllsForPPL[i],
                             &OriginalImageName, TRUE)) {
     return STATUS_DYNAMIC_CODE_BLOCKED;
   }
 }
 return STATUS_SUCCESS;
}

这段修复代码会根据黑名单中的5个DLL,检查正在加载的映像资源段的原始文件名。黑名单包括例如JSCRIPT.DLL这类实现原始Jscript脚本引擎的DLL,以及例如SCROBJ.DLL这类实现scriptlet对象的DLL。如果内核检测到PP或者PPL正在加载其中一个DLL,会立即使用STATUS_DYNAMIC_CODE_BLOCKED拒绝映像加载。这样一来,我的漏洞利用方法就失效了。如果我们尝试修改其中一个DLL的资源段,那么该映像的签名随即无效,映像加载前进行的加密哈希值校验也将失败,因此无法加载映像。实际上,这种修复方式与Oracle修复VirtualBox中漏洞的方法相同,唯一的区别可能就在于这一次是在用户模式下实现的。

寻找新目标

在漏洞修复前,我们使用脚本代码进行注入,实际上是一种通用的技术,适用于加载COM对象的任何PPL。随着这一漏洞被修复,我决定再回顾一下有哪些可执行文件可以作为PPL加载,并分析其是否具有可以利用获取任意代码的明显漏洞。我选择了看起来似乎更容易找到问题的PPL开始研究。如果我们能够获得管理员权限,其实就有很多方法可以注入到PPL中,最简单的是通过加载内核驱动程序。但是,这种条件并不实际,因此我将目标限定为必须要从普通用户账户实现漏洞利用。此外,我们必须还要考虑签名级别的问题,普通用户能够获得的最高签名级别应该是Windows TCB级别,因此,这也将作为我们的另外一个限定条件。

第一步,需要识别作为受保护进程运行的可执行文件,这样我们就能具有最大的攻击面,从而发现漏洞。根据Alex的博客文章,为了作为PP或PPL加载,签名证书需要一个特殊的对象标识符(OID),该标识符位于证书的增强密钥用法(Enhanced Key Usage,EKU)扩展中。PP和PPL都各自有单独的OID。如下图所示,我们可以发现WERFAULTSECURE.EXE(可以作为PP或PPL运行)与CSRSS.EXE(只能作为PP运行)之间的区别。

2.png

我决定深入看看这些带有EKU OID嵌入式签名的可执行文件,并且我认为可以通过它来获得所有可执行文件的列表,从而查找到可以进行漏洞利用的行为。我为我的NtObjectManager PowerShell模块编写了Get-EmbeddedAuthenticodeSignature命令行脚本,用于提取此信息。

在这时,我意识到依赖证书签名的方法存在问题。有很多二进制文件都是允许作为PP或PPL运行的,但它们并没有在列表中出现。由于PP最初是为DRM涉及的,因此它并没有可执行文件来处理受保护的媒体路径,例如AUDIODG.EXE。另外,根据我之前对Device Guard和Windows 10S的研究,我了解.NET框架中必须有一个可执行文件,可以作为PPL运行,用于为NGEN生成的二进制文件添加缓存的签名级别信息(NGEN用于将.NET程序及转换为Native代码)。我决定执行动态分析,而不是静态分析,我们只需要启动每一个受保护的可执行文件,并查询其具有的保护级别。我编写了一个脚本,来测试单个可执行文件:

Import-Module NtObjectManager
 
function Test-ProtectedProcess {
   [CmdletBinding()]
   param(
       [Parameter(Mandatory, ValueFromPipelineByPropertyName)]
       [string]$FullName,
       [NtApiDotNet.PsProtectedType]$ProtectedType = 0,
       [NtApiDotNet.PsProtectedSigner]$ProtectedSigner = 0
       )
   BEGIN {
       $config = New-NtProcessConfig abc -ProcessFlags ProtectedProcess `
           -ThreadFlags Suspended -TerminateOnDispose `
           -ProtectedType $ProtectedType `
           -ProtectedSigner $ProtectedSigner
   }
 
   PROCESS {
       $path = Get-NtFilePath $FullName
       Write-Host $path
       try {
           Use-NtObject($p = New-NtProcess $path -Config $config) {
               $prot = $p.Process.Protection
               $props = @{
                   Path=$path;
                   Type=$prot.Type;
                   Signer=$prot.Signer;
                   Level=$prot.Level.ToString("X");
               }
               $obj = New-Object –TypeName PSObject –Prop $props
               Write-Output $obj
           }
       } catch {
       }
   }
}

在该脚本中,定义了一个函数Test-ProtectedProcess。该函数将获取可执行文件的路径,以特定的保护级别启动该可执行文件,并检查是否成功启动。如果ProtectedType和ProtectedSigner参数设置为0,那么内核会决定一个“最佳”的进程级别。这会导致出现一些问题。例如,SVCHOST.EXE将会被明确标记为PPL,并且是在PPL-Windows级别运行,但实际上,它也是一个经过签名的操作系统组件,其最高的级别应该是PP-Authenticode。另外一个有趣的问题是,使用本地进程创建API,可以将DLL作为主要可执行映像启动。在大量的系统DLL中,实际上都嵌入了Microsoft的签名,所以它们就都可以作为PP-Authenticode启动,即使这样的权限毫无作用。下面是作为PPL运行的二进制文件列表,以及它们的最大签名级别:

C:\windows\Microsoft.Net\Framework\v4.0.30319\mscorsvw.exe(CodeGen)
C:\windows\Microsoft.Net\Framework64\v4.0.30319\mscorsvw.exe(CodeGen)
C:\windows\system32\SecurityHealthService.exe(Windows)
C:\windows\system32\svchost.exe(Windows)
C:\windows\system32\xbgmsvc.exe(Windows)
C:\windows\system32\csrss.exe(Windows TCB)
C:\windows\system32\services.exe(Windows TCB)
C:\windows\system32\smss.exe(Windows TCB)
C:\windows\system32\werfaultsecure.exe(Windows TCB)
C:\windows\system32\wininit.exe(Windows TCB)

将任意代码注入NGEN

经过仔细查看作为PPL运行的可执行文件列表,我确定了要进行漏洞利用的目标,就是前面提到的.NET NGEN二进制文件MSCORSVW.EXE。之所以选择NGEN二进制文件,原因在于:

1、大多数其他二进制文件都是服务类,可能需要管理员权限才能正确启动;

2、二进制文件可能会加载复杂的功能(例如.NET框架)或者具有多个COM交互;

3、在一些特定情况下,它可能仍会产生Device Guard绕过问题,因为它作为PPL运行,被授予了缓存的签名级别,同时具有访问内核API的权限。即使我们无法在PPL中实现代码执行,这个二进制文件操作中的任何一个漏洞也可能会被利用。

但是,NGEN二进制文件也存在着它的问题。特别是,它不符合我自己制定的限制条件,也就是所能获得最高的级别是Windows TCB。但是,我听说Microsoft在修复之前漏洞的过程中,留下了一个后门,针对PPL作为调用进程的情况,在签名进程中保留了一个可写的句柄,如下所示:

NTSTATUS CiSetFileCache(HANDLE Handle, ...) {
 
 PFILE_OBJECT FileObject;
 ObReferenceObjectByHandle(Handle, &FileObject);
 
 if (FileObject->SharedWrite ||
    (FileObject->WriteAccess &&
     PsGetProcessProtection().Type != PROTECTED_LIGHT)) {
   return STATUS_SHARING_VIOLATION;
 }
 
 // Continue setting file cache.
}

如果我可以在NGEN二进制文件中获得代码执行,那么就可以重用这一后门,来缓存加载到任意PPL的任意文件。随后,我就可以劫持一个完整的PPL Windows TCB进程,从而实现我们的目标。

首先需要做的事,是确定如何使用MSCORSVW可执行文件。Microsoft没有在任何地方给出关于MSCORSVW的文档或说明,因此还需要我们自行研究。首先,这个二进制文件不应直接运行,而是应该在创建NGEN的二进制文件过程中由NGEN调用。因此,我们可以运行NGEN二进制文件,并使用Process Monitor等工具来捕获MSCORSVW进程使用的命令行。执行命令:

C:\> NGEN install c:\some\binary.dll

在命令行中,以下内容将被执行:

MSCORSVW -StartupEvent A -InterruptEvent B -NGENProcess C -Pipe D

其中,A、B、C和D都是NGEN确保在启动之前能继承到新进程的句柄。由于我们没有看到任何原始的NGEN命令行参数,所以推测它们可能是通过IPC机制传递。“Pipe”参数用于指示命名管道,以供IPC使用。通过深入研究MSCORSVW的代码,我们找到了NGenWorkerEmbedding方法,如下所示:

void NGenWorkerEmbedding(HANDLE hPipe) {
 CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
 CorSvcBindToWorkerClassFactory factory;
 
 // Marshal class factory.
 IStream* pStm;
 CreateStreamOnHGlobal(nullptr, TRUE, &pStm);
 CoMarshalInterface(pStm, &IID_IClassFactory, &factory,
                    MSHCTX_LOCAL, nullptr, MSHLFLAGS_NORMAL);
 
 // Read marshaled object and write to pipe.
 DWORD length;
 char* buffer = ReadEntireIStream(pStm, &length);
 WriteFile(hPipe, &length, sizeof(length));
 WriteFile(hPipe, buffer, length);
 CloseHandle(hPipe);
 
 // Set event to synchronize with parent.
 SetEvent(hStartupEvent);
 
 // Pump message loop to handle COM calls.
 MessageLoop();
 
 // ...
}

这段代码,其实不是我想要的。因为这部分代码并不是将命名管道用于整个通信通道,而是仅用于将封装的COM对象传回给调用进程。COM对象是一个类工厂实例(Class Factory Instance),通常会使用CoRegisterClassObject注册工厂,但是这会使得所有相同安全级别的进程都有权限访问它。因此,通过使用了封装,可以实现其私密性。由于我之前写过一篇文章,描述如何在.NET中实现COM对象,因此在这里,我对于使用COM的.NET相关进程非常感兴趣。接下来,我希望能确认这个COM对象是不是在.NET中实现的,可以通过查询其接口来确定。例如,我们在OleViewDotNet PowerShell模块中使用Get-ComInterface命令,结果如下所示。

3.png

我们的运气不太好,这个对象并没有在.NET中实现。在这里,只有一个接口ICorSvcBindToWorker,我们接下来深入了解该接口,看看是否有任何可以利用的地方。

突然,有一些东西引起了我的注意。在屏幕截图中,有一个HasTypeLib列,我们看到对于ICorSvcBindToWorker这列设置为True。HasTypeLib表明,这里接口代理代码的实现,并不是使用预定义的NDR字节流,而是从类型库中动态生成的。我之前滥用过这种自动生成代理的机制,并成功实现了到SYSTEM的全线提升,并将其报告为编号1112的问题。在这个问题中,我使用了系统运行对象表(ROT)中一些有趣的行为,来强制系统COM服务中产生类型混淆。尽管现在,Microsoft已经解决了到SYSTEM的权限提升漏洞,但并不能阻止我们使用类型混淆技巧,对作为PPL运行的相同权限级别的MSCORSVW进程进行漏洞利用,并获得任意代码执行。使用类型库的另一个优点是:普通代理只会作为DLL加载,这意味着它必须满足PPL签名级别要求。但类型库只是数据,它可以加载到PPL中,不会产生任何签名级别的错误。

那么,在这里怎么实现类型混淆呢?我们查看类型库中的ICorSvcBindToWorker接口:

interface ICorSvcBindToWorker : IUnknown {
   HRESULT BindToRuntimeWorker(
             [in] BSTR pRuntimeVersion,
             [in] unsigned long ParentProcessID,
             [in] BSTR pInterruptEventName,
             [in] ICorSvcLogger* pCorSvcLogger,
             [out] ICorSvcWorker** pCorSvcWorker);
};

单个BindToRuntimeWorker需要5个参数,4个负责入站,1个负责出站。当我们从不受信任的进程通过DCOM访问该方法时,系统会自动为其生成代理和存根(Stub)。该过程包括:将COM接口封装到缓冲区、将缓冲区内容发送到远程进程、在调用实际函数前对指针进行解封装。例如,我们可以想象一个更简单的函数,名称为DoSomething,它接受一个IUnknown指针。其封装过程如下所示:

4.png

方法调用的具体操作如下:

1、不受信任的进程在接口调用DoSomething,该接口实际上是指向DoSomethingProxy的指针,该指针是从传递IUnknown指针参数的类型库自动生成的。
2、DoSomethingProxy将IUnknown指针参数封装到缓冲区中,并通过RPC调用受保护进程中的Stub。
3、COM运行时调用DoSomethingStub方法来处理调用。该方法将从缓冲区解封装接口指针。需要注意的是,这里所说的指针并非第一步中的原始指针,它可能是一个回调到不受信任进程的新代理。
4、Stub调用服务器内部的实际实现方法,传递解封装的接口指针。
5、DoSomething使用接口指针,例如通过对象的VTable调用AddRef。

那么,我们如何进行漏洞利用?需要做的就是修改类型库,这样一来其传递的就是一个接口指针,而非其他内容。尽管类型库文件位于一个系统位置,我们无法修改,但我们可以在当前用户的注册表配置单元中替换相应的内容,或者使用与编号为1112漏洞相同的ROT技巧。举例来说,如果我们修改类型库,使其传递一个整数,而非接口指针,我们将得到如下内容:

5.jpg

现在的封装过程,变为如下操作:

1、不受信任的进程在接口上调用DoSomething,该接口实际上是指向DoSomethingProxy的指针,该指针是从传递任意整数参数的类型库自动生成的。
2、DoSomethingProxy将整数参数封装到缓冲区中,并通过RPC调用受保护进程中的Stub。
3、COM运行时调用DoSomethingStub方法来处理调用。此方法将从缓冲区解封装整数。
4、Stub调用服务器内部的实际工具方法,传递整数作为参数。但是DoSomething没有改变,它仍然是接受接口指针的相同方法。由于COM运行时此时没有更多类型信息,因此整数与接口指针的类型发生混淆。
5、DoSomething使用接口指针,例如通过对象的VTable调用AddRef。由于此指针完全受不受信任进程的控制,因此可能导致任意代码执行。
通过将参数类型由接口指针改为整数,就会引发类型混淆,这使得我们可以取消对任意指针的引用,从而导致任意代码执行。我们甚至可以通过向类型库添加以下结构,来简化攻击过程:

struct FakeObject {
   BSTR FakeVTable;
};

如果我们将指针传递给FakeObject而非接口指针,那么自动生成的代理将会封装结构以及BSTR,并在Stub的另一端重新创建。由于BSTR是一个计数字符串,所以它可以包含NULL,这样一来就会创建一个指向对象的指针,该对象包含指向任意字节数组的指针,而该数组可以作为VTable。如果将已知函数指针放在这一BSTR中,就可以轻松的重定向执行,无需猜测VTable缓冲区的位置。

为了充分利用这一方法,我们需要选择一个合适的方法进行调用,可能需要一个ROP链,并且可能还需要绕过CFG。目前看起来,这一过程并不简单,因此我们打算尝试采用不同的方法,即通过滥用KnownDlls来获得PPL二进制文件中运行的任意代码。

KnownDlls和受保护的进程

在我之前发表过的博客文章中,说明过一种权限提升的技术,可以通过在KnownDlls目录中添加一个项,从而将任意DLL加载到特权进程中,进而实现将任意对象目录的权限提升到SYSTEM。我发现,这也是管理员PPL代码注入的一种,因为PPL也会从系统的KnownDlls位置加载DLL。由于代码签名检查是在创建期间进行的,而不是在映射期间,所以只要能将特定项放入KnownDlls,就能将任意内容加载到PPL中,甚至是未签名的代码中。

现在还不能立即行动,因为要写入KnownDlls还需要管理员的权限。在此之前,我们首先研究一下过程中是如何加载已知DLL的,以及是如何实现滥用的。内部NTDLL加载工具(LDR)的函数代码如下,这一部分用于确定是否存在预先存在的Known DLL。

NTSTATUS LdrpFindKnownDll(PUNICODE_STRING DllName, HANDLE *SectionHandle) {
 // If KnownDll directory handle not open then return error.
 if (!LdrpKnownDllDirectoryHandle)
   return STATUS_DLL_NOT_FOUND;
 
 OBJECT_ATTRIBUTES ObjectAttributes;
 InitializeObjectAttributes(&ObjectAttributes,
   &DllName,
   OBJ_CASE_INSENSITIVE,
   LdrpKnownDllDirectoryHandle,
   nullptr);
 
 return NtOpenSection(SectionHandle,
                      SECTION_ALL_ACCESS,
                      &ObjectAttributes);
}

LdrpFindKnownDll函数调用NtOpenSection,以打开已知DLL的Named Section对象。这一过程中不会使用绝对路径,而是通过本地系统调用的功能,根据OBJECT_ATTRIBUTES结构中的对象名称来查找指定根目录。根目录记录在全局变量LdrpKnownDllDirectoryHandle中。通过这种方式实现调用,能够允许加载工具仅对文件名进行指定(例如EXAMPLE.DLL),而不必将绝对路径重新构建为相对于现有目录的查找。我们跟踪对LdrpKnownDllDirectoryHandle的引用,就可以发现它在LdrpInitializeProcess中进行初始化,如下所示:

NTSTATUS LdrpInitializeProcess() {
 // ...
 PPEB peb = // ...
 // If a full protected process don't use KnownDlls.
 if (peb->IsProtectedProcess && !peb->IsProtectedProcessLight) {
   LdrpKnownDllDirectoryHandle = nullptr;
 } else {
   OBJECT_ATTRIBUTES ObjectAttributes;
   UNICODE_STRING DirName;
   RtlInitUnicodeString(&DirName, L"\\KnownDlls");
   InitializeObjectAttributes(&ObjectAttributes,
                              &DirName,
                              OBJ_CASE_INSENSITIVE,
                              nullptr, nullptr);
   // Open KnownDlls directory.
   NtOpenDirectoryObject(&LdrpKnownDllDirectoryHandle,
                         DIRECTORY_QUERY | DIRECTORY_TRAVERSE,
                         &ObjectAttributes);
}

上述代码实现了对NtOpenDirectoryObject的调用,将绝对路径作为对象名称,传递给KnownDlls目录。打开的句柄将会存储在LdrpKnownDllDirectoryHandle全局变量中,供以后使用。值得注意的是,此代码还会检查PEB,以确定当前进程是否为完全受保护的进程。在完全受保护的进程模式下,禁用对加载已知DLL的支持,这也就是为什么即使具有管理员权限也只能攻破PPL而非PP的原因。

了解上述知识后,我们就可以不再尝试代码劫持,而是使用COM类型混淆技巧,将值写入任意内存位置,从而实现对数据的攻击。由于可以继承任意句柄到新的PPL进程中,所以我们可以使用Named Section来设定对象目录,然后使用类型混淆的方法,将LdrpKnownDllDirectoryHandle的值更改为继承句柄的值。如果我们使用一个从System32加载的已知名称DLL,LDR将检查伪目录来查找Named Section,并将无符号代码映射到内存中,甚至还会为我们调用DllMain。这个过程中,无需线程注入,无需ROP,也无需绕过CFG。

现在需要的,是一个合适的原语,用来写入任意值。不幸的是,尽管我找到了能产生任意写入的方法,但是却不能充分控制写入的内容。最终,我使用了以下接口和方法,该接口是在ICorSvcBindToWorker :: BindToRuntimeWorker返回的对象上实现的。

interface ICorSvcPooledWorker : IUnknown {
   HRESULT CanReuseProcess(
           [in] OptimizationScenario scenario,
           [in] ICorSvcLogger* pCorSvcLogger,
           [out] long* pCanContinue);
  };
};

在CanReuseProcess的实现过程中,pCanContinue的目标值总是初始化为0。因此,通过用[in] long替换类型库定义中的[out] long*,就可以将0写入我们指定的任何内存位置。通过使用到假KnownDlls目录的句柄,将其预先填充到新进程句柄表的低16位,就可以确定真实KnownDlls的别名,这些KnownDlls将在进程启动时打开。而针对假冒的这些,只需修改高16位句柄为0即可。如下图所示:

6.png

一旦我们使用0覆盖了前16位(写入是32位,但句柄实际是64位,并且在64位模式下运行,因此不会覆盖任何重要的内容),LdrpKnownDllDirectoryHandle就会指向假KnownDlls句柄中的一个。然后,可以通过向同一个方法发送自定义封装对象,来引起DLL加载,从而实现在PPL内部执行任意代码。

将PPL提升到Windows TCB签名级别

我们不能止步于此,对MSCORSVW的攻击,只能让我们在CodeGen级别获得PPL,而非Windows TCB。通过对Windows TCB级别的WERFAULTSECURE.EXE进行DLL劫持,我们应该能够获得Windows TCB签名级别的代码执行。该方法适用于Windows 10 1709以及更早版本,但对1803及以上版本无效。

在与Alex Ionescu进行讨论后,我决定使用一个简单的解析器,用于查看文件上的缓存签名数据。在NtObjectManager中,其作为Get-NtCachedSigningLevel命令公开。针对一个带有假签名的二进制文件和系统二进制文件运行此命令,我们发现该二进制文件的签名也同样被缓存,其区别如下:

7.png

对于伪造的签名文件,Flags为TrustedSignature (0x02)。但是对于系统二进制文件,由于PowerShell无法解码,因此只输出整数值66(十六进制为0x42)。0x40值是位于原始可信签名上的额外标志,如果没有这个标志,DLL似乎就不会被加载到PPL进程中。于是,我决定尝试将一个不含额外标志的有效签名DLL加载到PPL进程。通过Process Monitor的监控,我得到了答案:

8.PNG

Process Monitor的跟踪结果显示,首先内核从DLL中查询扩展属性(EA)。缓存的签名级别数据存储在文件的EA中,因此我们可以断定,这一过程就是在读取缓存的签名级别。实际上,这一过程还会检查许多内容,为简洁起见,我从截图中删去了一部分不需要关注的内容。在EA设置完成后,如果我再次检查文件的缓存签名级别,就可以发现它现在包含了额外标志。那么,我们需要关注的就是,缓存签名级别是如何自动设置的?通过对栈进行跟踪,我们可以发现它的工作原理:

9.png

查看栈跟踪的中间部分,能看到对CipSetFileCache的调用源自对NtCreateSection的调用。在需要时(例如PPL),内核将会自动缓存签名,这样一来后续映射过程中就不需要再重新检查签名。如果从一个具有写入权限的文件映射一个Image Section,那么就可以重复编号1332的漏洞利用过程,使用NtCreateSection替换对NtSetCachedSigningLevel的调用,从而可以伪造任何DLL。事实证明,在对1332进行修复后,会在写入检查之后发生设置文件缓存的调用,并且可以借助该调用绕过Device Guard。考虑到以上情况,我将这一漏洞进行上报,其编号为1597,CVE编号为CVE-2018-8449。与1332一样,PPL的后门仍然存在,因此即使修复程序解决了Device Guard绕过问题,仍然可以利用该漏洞实现从PPL-CodeGen到PPL-Windows TCB的提升。

总结

本文主要说明了如何在不需要管理员权限的情况下,将任意代码注入PPL。在普通用户权限下,能实现的操作其实并不多,无法对操作系统的某些重要部分进行修改。但是,如果成功将权限提升到管理员,然后注入PPL,那么就能得到更多的攻击点,例如CSRSS(通过对其进行攻击来获得内核代码执行)和Windows Defender。随着时间的推移,我相信PPL的大多数用例都会被虚拟安全模式(VSM)和隔离用户模式(IUM)的应用程序所取代,这些模式将具有更高的安全保障。

在我向Microsoft报告漏洞后,他们明确表示,不会修复仅影响PP和PPL的漏洞。因此,该漏洞并不会在当前版本的Windows中修复,但很可能会在下一个主要版本中实现修复。此外,Microsoft还明确列出了针对一些Windows中所使用的技术,是否会进行修复并向提交漏洞者支付赏金,我们明确看到,PPL不在此列。

10.png

因此,Microsoft仅修复了我报告的一个漏洞问题。但仔细考虑一下,只修复Device Guard实际上是不充分的,我们仍然可以通过注入PPL并设置缓存签名级别,从而绕过Device Guard。这样看来,Microsoft如此坚定的拒绝修复PPL问题是有些武断的。安全功能很少是独立存在的,都是通过彼此联系、环环相扣,来共同保障整个系统的安全。

后续,我还会发表一篇文章,讨论如何使用COM的另一个功能来实现完整PP-Windows TCB进程注入,敬请期待。

源链接

Hacking more

...