Windows漏洞分析:利用IDispatch访问受困COM对象

本文深入分析了Windows系统中一类特殊的漏洞类型——通过IDispatch接口访问受困COM对象的技术细节。文章探讨了DCOM和.NET Remoting等面向对象远程技术中的安全风险,详细介绍了如何利用类型库和IDispatch接口在特权进程中执行代码的技术原理,并提供了实际案例和PowerShell代码示例。

Windows Bug Class: Accessing Trapped COM Objects with IDispatch

Posted by James Forshaw, Google Project Zero

面向对象的远程技术(如DCOM和.NET Remoting)使得开发能够跨越进程和安全边界的面向对象服务接口变得非常容易。这是因为它们被设计为支持广泛的对象,不仅包括服务中实现的对象,还包括任何其他兼容远程处理的对象。例如,如果您想要在客户端-服务器边界上公开XML文档,可以使用现有的COM或.NET库并将该对象返回给客户端。默认情况下,当对象被返回时,它通过引用进行封送,导致对象停留在进程外服务器中。

这种灵活性有许多缺点,其中之一就是本文的主题——受困对象漏洞类。并非所有可以远程处理的对象都 necessarily 安全。例如,前面提到的XML库(包括COM和.NET)支持在XSLT文档的上下文中执行任意脚本代码。如果XML文档对象可以通过边界访问,那么客户端可以在服务器进程的上下文中执行代码,这可能导致权限提升或远程代码执行。

有多种场景可能引入此漏洞类。最常见的情况是不安全对象被无意中共享。CVE-2019-0555就是一个例子。这个漏洞的引入是因为在开发Windows Runtime库时需要XML文档对象。开发人员决定在现有的XML DOM Document v6 COM对象中添加一些代码,以暴露运行时特定的接口。由于这些运行时接口不支持XSLT脚本功能,假设在跨权限边界暴露是安全的。不幸的是,恶意客户端可以查询仍然可访问的旧IXMLDOMDocument接口,并使用它运行XSLT脚本并逃逸沙箱。

另一种情况是存在异步封送基元。这意味着对象可以通过值和引用进行封送,平台选择引用作为默认机制。例如,FileInfo和DirectoryInfo .NET类都是可序列化的,因此可以通过值封送到.NET远程服务。但它们也继承自MarshalByRefObject类,这意味着它们可以通过引用进行封送。攻击者可以利用这一点,向服务器发送对象的序列化形式,当反序列化时,将在服务器的进程中创建对象的新实例。如果攻击者可以读回创建的对象,运行时将通过引用将其封送回攻击者,使对象受困于服务器进程中。最后,攻击者可以调用对象上的方法,例如创建新文件,这些方法将以服务器的权限执行。这种攻击在我的ExploitRemotingService工具中实现。

我将提到的最后一种情况与本文最相关,即滥用远程技术用于查找和实例化对象的内置机制来创建意外对象。例如,在COM中,如果您可以找到调用CoCreateInstance API的代码路径,使用任意CLSID,并将该对象传递回客户端,那么您可以使用它在服务器的上下文中运行任意代码。CVE-2017-0211就是这种形式的一个例子,这是一个跨安全边界暴露结构化存储对象的漏洞。存储对象支持IPropertyBag接口,该接口可用于在服务器的上下文中创建任意COM对象并将其返回给客户端。可以通过在服务器中创建XML DOM Document对象,通过引用封送回客户端,然后使用XSLT脚本功能在服务器的上下文中运行任意代码来提升权限。

IDispatch的作用是什么?

IDispatch接口是OLE Automation功能的一部分,这是COM的原始用例之一。它允许COM客户端在运行时晚期绑定到服务器,以便可以从VBA和JScript等脚本语言使用该对象。该接口完全支持跨进程和权限边界,尽管它更常用于进程内组件,如ActiveX。

为了便于在运行时调用COM对象,服务器必须向客户端暴露一些类型信息,以便客户端知道如何打包参数通过接口的Invoke方法发送。类型信息存储在开发人员定义的磁盘上的类型库文件中,客户端可以使用IDispatch接口的GetTypeInfo方法查询该库。由于类型库接口的COM实现是通过引用封送的,返回的ITypeInfo接口受困于服务器中,任何对其调用的方法都将在服务器的上下文中执行。

ITypeInfo接口暴露了两个有趣的方法,客户端可以调用:Invoke和CreateInstance。事实证明,Invoke对于我们的目的不是那么有用,因为它不支持远程处理,只有在当前进程中加载了类型库时才能调用。然而,CreateInstance被实现为可远程处理的,这将通过调用CoCreateInstance从CLSID实例化COM对象。关键的是,创建的对象将在服务器的进程中,而不是客户端。

但是,如果您查看链接的API文档,没有可以传递给CreateInstance的CLSID参数,那么类型库接口如何知道要创建什么对象?ITypeInfo接口表示类型库中可以存在的任何类型。GetTypeInfo返回的类型仅包含客户端要调用的接口的信息,因此调用CreateInstance只会返回错误。然而,类型库也可以存储"CoClass"类型的信息。这些类型定义了要创建的对象的CLSID,因此调用CreateInstance将会成功。

我们如何从接口类型信息对象转到表示类的对象?ITypeInfo接口为我们提供了GetContainingTypeLib方法,该方法返回对包含的ITypeLib接口的引用。然后可以使用该接口枚举类型库中所有支持的类。如果远程暴露,一个或多个类可能不安全。让我们使用我的OleView.NET PowerShell模块进行一个工作示例,首先我们想找到一些同时支持IDispatch的目标COM服务。这将为我们提供权限提升的潜在途径。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
PS> $cls = Get-ComClass -Service
PS> $cls | % { Get-ComInterface -Class $_ | Out-Null }
PS> $cls | ? { $true -in $_.Interfaces.InterfaceEntry.IsDispatch } | 
        Select Name, Clsid
Name                                                  Clsid
----                                                  -----
WaaSRemediation                                       72566e27-1abb-4eb3-b4f0-eb431cb1cb32
Search Gathering Manager                              9e175b68-f52a-11d8-b9a5-505054503030
Search Gatherer Notification                          9e175b6d-f52a-11d8-b9a5-505054503030
AutomaticUpdates                                      bfe18e9c-6d87-4450-b37c-e02f0b373803
Microsoft.SyncShare.SyncShareFactory Class            da1c0281-456b-4f14-a46d-8ed2e21a866f

Get-ComClass的-Service开关返回在本地服务中实现的类。然后我们查询所有支持的接口,我们不需要此命令的输出,因为查询的接口存储在Interfaces属性中。最后我们选择出任何暴露IDispatch的COM类,得到5个候选。接下来,我们将选择第一个类WaasRemediation并检查其类型库以寻找有趣的类。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
PS> $obj = New-ComObject -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32
PS> $lib = Import-ComTypeLib -Object $obj
PS> Get-ComObjRef $lib.Instance | Select ProcessId, ProcessName
ProcessId ProcessName
--------- -----------
    27020 svchost.exe

PS> $parsed = $lib.Parse()
PS> $parsed
Name               Version TypeLibId
----               -------- ---------
WaaSRemediationLib 1.0      3ff1aab8-f3d8-11d4-825d-00104b3646c0

PS> $parsed.Classes | Select Name, Uuid
Name                          Uuid
----                          ----
WaaSRemediationAgent          72566e27-1abb-4eb3-b4f0-eb431cb1cb32
WaaSProtectedSettingsProvider 9ea82395-e31b-41ca-8df7-ec1cee7194df

脚本创建COM对象,然后使用Import-ComTypeLib命令获取类型库接口。我们可以通过使用Get-ComObjRef封送它然后提取进程信息来检查类型库接口是否真的在进程外运行,显示它运行在svchost.exe的一个实例中,这是共享服务可执行文件。通过接口检查类型库很痛苦,为了更容易显示支持的类,我们可以使用Parse方法将库解析为更易于使用的对象模型。然后我们可以转储库的信息,包括其类的列表。

不幸的是,对于这个COM对象,类型库支持的唯一类已经注册在服务中运行,因此我们一无所获。我们需要的是一个仅在本地进程中注册运行但由类型库暴露的类。这是可能的,因为类型库可以由本地进程内组件和进程外服务共享。

我检查了其他4个COM类(其中一个注册不正确,未被相应服务暴露),没有找到有用的类来尝试利用。您可能决定在此时放弃,但事实证明有一些可访问的类,它们只是被隐藏了。这是因为类型库可以引用其他类型库,可以使用相同的接口集进行检查。让我们看一下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
PS> $parsed.ReferencedTypeLibs
Name   Version TypeLibId
----   ------- ---------
stdole 2.0     00020430-0000-0000-c000-000000000046

PS> $parsed.ReferencedTypeLibs[0].Parse().Classes | Select Name, Uuid
Name       Uuid
----       ----
StdFont    0be35203-8f91-11ce-9de3-00aa004bb851
StdPicture 0be35204-8f91-11ce-9de3-00aa004bb851

PS> $cls = Get-ComClass -Clsid 0be35203-8f91-11ce-9de3-00aa004bb851
PS> $cls.Servers

           Key Value
           --- -----
InProcServer32 C:\Windows\System32\oleaut32.dll

在示例中,我们可以使用ReferencedTypeLibs属性显示解析库时遇到哪些类型库。我们可以看到stdole的单个条目,这基本上总是会被导入。如果您幸运的话,可能还有其他导入的库可以检查。我们可以解析stdole库以检查其类列表。类型库导出了两个类,如果我们检查StdFont的服务器,可以看到它仅指定在进程中创建,我们现在有了一个目标类来寻找漏洞。要获取stdole类型库的进程外接口,我们需要找到一个引用它的类型。引用的原因是通用接口(如IUnknown和IDispatch)在库中定义,因此我们需要查询我们可以直接访问的接口的基本类型。让我们尝试在COM服务中创建对象。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PS> $iid = $parsed.Interfaces[0].Uuid
PS> $ti = $lib.GetTypeInfoOfGuid($iid)
PS> $href = $ti.GetRefTypeOfImplType(0)
PS> $base = $ti.GetRefTypeInfo($href)
PS> $stdole = $base.GetContainingTypeLib()
PS> $stdole.Parse()
Name   Version TypeLibId
----   ------- ---------
stdole 2.0     00020430-0000-0000-c000-000000000046

PS> $ti = $stdole.GetTypeInfoOfGuid("0be35203-8f91-11ce-9de3-00aa004bb851")
PS> $font = $ti.CreateInstance()
PS> Get-ComObjRef $font | Select ProcessId, ProcessName
ProcessId ProcessName
--------- -----------
    27020 svchost.exe

PS>  Get-ComInterface -Object $Obj
Name                 IID                                  HasProxy   HasTypeLib
----                 ---                                  --------   ----------
...
IFont                bef6e002-a874-101a-8bba-00aa00300cab True       False
IFontDisp            bef6e003-a874-101a-8bba-00aa00300cab True       True

我们通过GetRefTypeOfImplType和GetRefTypeInfo的组合查询现有接口的基本类型,然后使用GetContainingTypeLib获取引用的类型库接口。我们可以解析库以确信我们已经获得了stdole库。接下来,我们获取StdFont类的类型信息并调用CreateInstance。我们可以检查对象的进程以确保它是在进程外创建的,结果显示它受困于服务进程中。作为最终检查,我们可以查询对象的接口以证明它是一个字体对象。

现在我们只需要找到一种方法来利用这两个类中的一个,第一个问题是只能访问StdFont对象。StdPicture对象进行检查以防止其在进程外使用。我在字体对象中找不到有用的可利用行为,但我没有花太多时间寻找。当然,如果其他人想在该类中寻找合适的漏洞,请自便。

因此,这项研究陷入了死胡同,至少就系统服务而言是这样。可能有一些COM服务器可以从沙箱访问,但对从AppContainer可访问的服务器进行的初步分析没有显示任何明显的候选。然而,在进一步思考之后,我意识到这可能作为一种注入技术注入到以相同权限级别运行的进程中很有用。例如,我们可以劫持StdFont的COM注册,使用TreatAs注册表键指向任何其他类。这个其他类将是一些可利用的东西,例如将JScript引擎加载到目标进程并运行脚本。

尽管如此,注入技术通常不是我在这个博客上讨论的内容,那更像是恶意软件的领域。然而,有一种情况可能具有有趣的安全影响。如果我们能使用这个注入到Windows受保护进程中呢?在命运的奇怪转折中,我们刚刚检查的WaaSRemediationAgent类可能就是我们的门票:

1
2
3
PS> $cls = Get-ComClass -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32
PS> $cls.AppIDEntry.ServiceProtectionLevel
WindowsLight

当我们检查托管服务的保护级别时,它被配置为在PPL-Windows级别运行!让我们看看是否能从这项研究中挽救一些价值。

受保护进程注入

我之前已经博客(并演示)过关于注入Windows受保护进程的主题。我建议重新阅读那篇博客文章以更好地了解以前的注入攻击。然而,一个关键点是Microsoft不认为PPL是安全边界,因此他们通常不会在安全公告中及时修复任何漏洞,但他们可能会选择在新版本的Windows中修复它。

想法很简单,我们将重定向StdFont类注册以指向另一个类,以便当我们通过类型库创建它时,它将在受保护进程中运行。选择使用StdFont应该更通用,因为如果WaaSRemediationAgent被移除,我们可以转而使用不同的COM服务器。我们只需要一个合适的类,它能让我们获得任意代码执行,并且也能在受保护进程中工作。

不幸的是,这立即排除了任何像JScript这样的脚本引擎。如果您重新阅读我上一篇博客文章,代码完整性模块明确阻止常见的脚本引擎在受保护进程中加载。相反,我需要一个可以在进程外访问并且可以加载到受保护进程中的类。我意识到一个选项是加载一个已注册的.NET COM类。我曾经博客讨论过.NET DCOM是如何可被利用的,不应该被使用,但在这种情况下,我们想要的就是它的漏洞性。

那篇博客文章讨论了利用序列化基元,然而有一个更简单的攻击,我通过使用DCOM上的System.Type类进行了利用。通过访问Type对象,您可以执行任意反射并调用任何您喜欢的方法,包括从字节数组加载程序集,这将绕过签名检查并完全控制受保护进程。

Microsoft修复了这种行为,但他们留下了一个配置值AllowDCOMReflection,允许您再次打开它。由于我们没有提升权限,并且我们必须以管理员身份运行才能更改COM类注册信息,我们可以在注册表中启用DCOM反射,通过将AllowDCOMReflection的DWORD值1写入HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft.NETFramework键,然后再将.NET框架加载到受保护进程中。

需要采取以下步骤来实现注入:

  1. 在注册表中启用DCOM反射。
  2. 添加TreatAs键以将StdFont重定向到System.Object COM类。
  3. 创建WaaSRemediationAgent对象。
  4. 使用类型库获取StdFont类类型信息。
  5. 使用CreateInstance方法创建StdFont对象,这将真正加载.NET框架并返回System.Object类的实例。
  6. 使用.NET反射调用System.Reflection.Assembly::Load方法并传入字节数组。
  7. 在加载的程序集中创建一个对象以强制代码执行。
  8. 清理所有注册表更改。

您需要在非.NET语言中执行这些步骤,否则序列化机制将启动并在调用进程中重新创建反射对象。我用C++编写了我的PoC,但您可能也可以从Python等语言中完成。我不会提供PoC,但代码与我为CVE-2014-0257编写的漏洞利用非常相似,这将为您提供一个如何在C++中使用DCOM反射的示例。还要注意,.NET COM对象的默认值是使用v2框架运行它们,该框架默认不再安装。为了避免与v4一起使用而搞乱,我只是从Windows组件安装程序安装了v2。

我的PoC在Windows 10上第一次就工作了,但不幸的是,当我在Windows 11 24H2上运行它时失败了。我可以创建.NET对象,但调用对象上的任何方法都失败,错误为TYPE_E_CANTLOADLIBRARY。我本可以在这里停止,已经证明了我的观点,但我想知道在Windows 11上是什么失败了。让我们最后深入探讨一下,看看我们是否能做些什么让它在最新版本的Windows上工作。

Windows 11的问题

我能够证明问题与受保护进程有关,如果我更改服务注册以无保护运行,那么PoC就工作了。因此,在专门在受保护进程中运行时,必须有某些东西阻止了库的加载。这似乎一般不影响类型库,stdole的加载工作良好,所以是特定于.NET的。

在用Process Monitor检查PoC的行为后,很明显mscorlib.tlb库正在被加载以在服务器中实现存根类。由于某种原因,它未能加载,这阻止了存根的创建,进而导致任何调用失败。此时,我知道发生了什么。在上一篇博客文章中,我讨论了通过修改NGEN COM进程用于创建接口存根的类型库来引入类型混淆,从而攻击它。这使我能够覆盖KnownDlls句柄并强制将任意DLL加载到内存中。我从Clément Labro和其他人的工作中知道,围绕KnownDlls的大多数攻击现在都被阻止了,但我怀疑类型库类型混淆技巧也有某种修复。

深入挖掘oleaut32.dll,我找到了有问题的修复,VerifyTrust方法如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
NTSTATUS VerifyTrust(LoadInfo *load_info) {
  PS_PROTECTION protection;
  BOOL is_protected;
  
  CheckProtectedProcessForHardening(&is_protected, &protection);
  if (!is_protected)
    return SUCCESS;
  ULONG flags;
  BYTE level;
  HANDLE handle = load_info->Handle;
  NTSTATUS status = NtGetCachedSigningLevel(handle, &flags, &level, 
                                            NULL, NULL, NULL);
  if (FAILED(status) || 
     (flags & 0x182) == 0 || 
     FAILED(NtCompareSigningLevels(level, 12))) {
    status = NtSetCachedSigningLevel(0x804, 12, &handle, 1, handle);
  }
  return status;
}

该方法在加载类型库期间被调用。它使用缓存的签名级别(同样是我在上一篇博客文章中提到的)来验证文件是否具有12的签名级别,这对应于Windows签名级别。如果它没有适当的缓存签名级别,代码将尝试使用NtSetCachedSigningLevel来设置它。如果失败,它假定文件无法在受保护进程中加载并返回错误,导致类型库加载失败。注意,类似的修复阻止了滥用Running Object Table引用进程外类型库,但这与本次讨论无关。

根据Get-AuthenticodeSignature的输出,mscorlib.tlb文件已签名,尽管是目录签名。签名证书是Microsoft Windows Production PCA 2011,这与.NET Runtime DLL的证书完全相同,因此没有理由它不会获得Windows签名级别。让我们尝试使用我的NtObjectManager PowerShell模块手动设置缓存签名级别,看看是否能得到任何见解:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
PS> $path = "C:\windows\Microsoft.NET\Framework64\v4.0.30319\mscorlib.tlb"
PS> Set-NtCachedSigningLevel $path -Flags 0x804 -SigningLevel 12 -Win32Path
Exception calling "SetCachedSigningLevel" with "4" argument(s): "(0xC000007B) - {Bad Image}
%hs is either not designed to run on Windows or it contains an error. Try installing the program again using the
original installation media or contact your system administrator or the software vendor for support. Error status 0x"

PS> Format-HexDump $path -Length 64 -ShowAll
          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  - 0123456789ABCDEF
-----------------------------------------------------------------------------
00000000: 4D 53 46 54 02 00 01 00 00 00 00 00 09 04 00 00  - MSFT............
00000010: 00 00 00 00 43 00 00 00 02 00 04 00 00 00 00 00  - ....C...........
00000020: 25 06 00 00 00 00 00 00 00 00 00 00 00 00 00 00  - %...............
00000030: 2E 0D 00 00 33 FA 00 00 F8 08 01 00 FF FF FF FF  - ....3...........

设置签名级别给我们STATUS_INVALID_IMAGE_FORMAT错误。查看类型库文件的前64字节显示它是一个原始类型库,而不是打包在PE文件中。这在Windows上相当不常见,即使文件具有TLB扩展名,类型库通常仍然作为资源打包到PE文件中。我想我们运气不好,除非我们可以在文件上设置缓存签名级别,否则它将被阻止加载到受保护进程中,而我们需要它加载以支持存根类通过DCOM调用.NET接口。

顺便说一句,奇怪的是我有一个Windows 11的VM,其中类型库的非DLL形式确实可以设置缓存签名级别。我必须以某种方式更改了VM的配置以支持此功能,但我不知道那是什么,我决定不进一步深入研究它。

我们可以尝试找到类型库文件的先前版本,该版本既有效签名,又打包在PE文件中,但是,我宁愿不这样做。当然,几乎肯定有另一个我们可以加载的COM对象而不是.NET,可能会给我们任意代码执行,但我已经下定决心采用这种方法。最终解决方案比我预期的更简单,出于某种原因,类型库文件的32位版本(即在Framework中而不是Framework64中)打包在DLL中,我们可以在其上设置缓存签名级别。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
PS> $path = "C:\windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.tlb"
PS> Format-HexDump $path -Length 64 -ShowAll
          00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F  - 0123456789ABCDEF
-----------------------------------------------------------------------------
00000000: 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00  - MZ..............
00000010: B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00  - ........@.......
00000020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  - ................
00000030: 00 00 00 00 00 00 00 00 00 00 00 00 B8 00 00 00  - ................

PS> Set-NtCachedSigningLevel $path -Flags 0x804 -SigningLevel 12 -Win32Path
PS> Get-NtCachedSigningLevel $path -Win32Path
Flags               : TrustedSignature
SigningLevel        : Windows
Thumbprint          : B9590CE5B1B3F377EAA6F455574C977919BB785F12A444BEB2...
ThumbprintBytes     : {185, 89, 12, 229...}
ThumbprintAlgorithm : Sha256

因此,要在Windows 11 24H2上利用,我们可以将类型库注册路径从64位版本交换到32位版本并重新运行漏洞利用。VerifyTrust函数将自动为我们设置缓存签名级别,因此我们不需要做任何事来使其工作。尽管技术上它是类型库的不同版本,但对于我们的用例来说没有任何区别,存根生成器代码也不关心。

结论

我在这篇博客文章中讨论了Windows上一类有趣的漏洞类型,尽管它适用于任何类似的面向对象的远程跨进程或远程协议。它展示了如何通过利用OLE Automation的特性,特别是IDispatch接口和类型库,使COM对象受困于更高特权的进程中。

虽然我未能演示权限提升,但我展示了如何使用WaaSRemediationAgent类暴露的IDispatch接口将代码注入到PPL-Windows进程中。虽然这不是可能的最高保护级别,但它允许访问大多数受保护运行的进程,包括LSASS。我们看到Microsoft已经做了一些工作来尝试缓解现有的攻击,例如类型库类型混淆,但在我们的情况下,这种缓解不应该阻止加载,因为我们不需要更改类型库本身。虽然攻击需要管理员权限,但通用技术不需要。如果您能找到暴露IDispatch的合适COM服务器,您可以修改本地用户的COM和.NET注册以作为普通用户进行攻击注入到PPL中。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计