Thursday, January 30, 2025

Windows Bug Class: Accessing Trapped COM Objects with IDispatch

Posted by James Forshaw, Google Project Zero

Object orientated remoting technologies such as DCOM and .NET Remoting make it very easy to develop an object-orientated interface to a service which can cross process and security boundaries. This is because they're designed to support a wide range of objects, not just those implemented in the service, but any other object compatible with being remoted. For example, if you wanted to expose an XML document across the client-server boundary, you could use a pre-existing COM or .NET library and return that object back to the client. By default when the object is returned it's marshaled by reference, which results in the object staying in the out-of-process server.

This flexibility has a number of downsides, one of which is the topic of this blog, the trapped object bug class. Not all objects which can be remoted are necessarily safe to do so. For example, the previously mentioned XML libraries, in both COM and .NET, support executing arbitrary script code in the context of an XSLT document. If an XML document object is made accessible over the boundary, then the client could execute code in the context of the server process, which can result in privilege escalation or remote-code execution.

There are a number of scenarios that can introduce this bug class. The most common is where an unsafe object is shared inadvertently. An example of this was CVE-2019-0555. This bug was introduced because when developing the Windows Runtime libraries an XML document object was needed. The developers decided to add some code to the existing XML DOM Document v6 COM object which exposed the runtime specific interfaces. As these runtime interfaces didn't support the XSLT scripting feature, the assumption was this was safe to expose across privilege boundaries. Unfortunately a malicious client could query for the old IXMLDOMDocument interface which was still accessible and use it to run an XSLT script and escape a sandbox.

Another scenario is where there exists an asynchronous marshaling primitive. This is where an object can be marshaled both by value and by reference and the platform chooses by reference as the default mechanism, For example the FileInfo and DirectoryInfo .NET classes are both serializable, so can be sent to a .NET remoting service marshaled by value. But they also derive from the MarshalByRefObject class, which means they can be marshaled by reference. An attacker can leverage this by sending to the server a serialized form of the object which when deserialized will create a new instance of the object in the server's process. If the attacker can read back the created object, the runtime will marshal it back to the attacker by reference, leaving the object trapped in the server process. Finally the attacker can call methods on the object, such as creating new files which will execute with the privileges of the server. This attack is implemented in my ExploitRemotingService tool.

The final scenario I'll mention as it has the most relevancy to this blog post is abusing the built in mechanisms the remoting technology uses to lookup and instantiate objects to create an unexpected object. For example, in COM if you can find a code path to call the CoCreateInstance API with an arbitrary CLSID and get that object passed back to the client then you can use it to run arbitrary code in the context of the server. An example of this form is CVE-2017-0211, which was a bug which exposed a Structured Storage object across a security boundary. The storage object supports the IPropertyBag interface which can be used to create an arbitrary COM object in the context of the server and get it returned to the client. This could be exploited by getting an XML DOM Document object created in the server, returned to the client marshaled by reference and then using the XSLT scripting feature to run arbitrary code in the context of the server to elevate privileges.

Where Does IDispatch Fits In?

The IDispatch interface is part of the OLE Automation feature, which was one of the original use cases for COM. It allows for late binding of a COM client to a server, so that the object can be consumed from scripting languages such as VBA and JScript. The interface is fully supported across process and privilege boundaries, although it's more commonly used for in-process components such as ActiveX.

To facilitate calling a COM object at runtime the server must expose some type information to the client so that it knows how to package up parameters to send via the interface's Invoke method. The type information is stored in a developer-defined Type Library file on disk, and the library can be queried by the client using the IDispatch interface's GetTypeInfo method. As the COM implementation of the type library interface is marshaled by reference, the returned ITypeInfo interface is trapped in the server and any methods called upon it will execute in the server's context.

The ITypeInfo interface exposes two interesting methods that can be called by a client, Invoke and CreateInstance. It turns out Invoke is not that useful for our purposes, as it's not supported for remoting, it can only be called if the type library is loaded in the current process. However, CreateInstance is implemented as remotable, this will instantiate a COM object from a CLSID by calling CoCreateInstance. Crucially the created object will be in the server's process, not the client.

However, if you look at the linked API documentation there is no CLSID parameter you can pass to CreateInstance, so how does the type library interface know what object to create? The ITypeInfo interface represents any type which can be present in a type library. The type returned by GetTypeInfo just contains information about the interface the client wants to call, therefore calling CreateInstance will just return an error. However, the type library can also store information of "CoClass" types. These types define the CLSID of the object to create, and so calling CreateInstance will succeed.

How can we go from the interface type information object, to one representing a class? The ITypeInfo interface provides us with the GetContainingTypeLib method which returns a reference to the containing ITypeLib interface. That can then be used to enumerate all supported classes in the type library. It's possible one or more of the classes are not safe if exposed remotely. Let's go through a worked example using my OleView.NET PowerShell module, first we want to find some target COM services which also support IDispatch. This will give us potential routes for privilege escalation.

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

The -Service switch for Get-ComClass returns classes which are implemented in local services. We then query for all the supported interfaces, we don't need the output from this command as the queried interfaces are stored in the Interfaces property. Finally we select out any COM class which exposes IDispatch resulting in 5 candidates. Next, we'll pick the first class, WaasRemediation and inspect its type library for interesting classes.

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

The script creates the COM object and then uses the Import-ComTypeLib command to get the type library interface. We can check that the type library interface is really running out of process by marshaling it with Get-ComObjRef then extracting the process information, showing it running in an instance of svchost.exe which is the shared service executable. Inspecting the type library through the interface is painful, to make it easier to display what classes are supported, we can parse the library into an easier to use object model with the Parse method. We can then dump information about the library, including a list of its classes.

Unfortunately for this COM object the only classes the type library supports are already registered to run in the service and so we've gained nothing. What we need is a class that is only registered to run in the local process, but is exposed by the type library. This is a possibility as a type library could be shared by both local in-process components and an out-of-process service.

I inspected the other 4 COM classes (one of which is incorrectly registered and isn't exposed by the corresponding service) and found no useful classes to try and exploit. You might decide to give up at this point, but it turns out there are some classes accessible, they're just hidden. This is because a type library can reference other type libraries, which can be inspected using the same set of interfaces. Let's take a look:

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

In the example we can use the ReferencedTypeLibs property to show what type libraries were encountered when the library was parsed. We can see a single entry for the stdole which is basically always going to be imported. If you're lucky, maybe there's other libraries that are imported that you can inspect. We can parse the stdole library to inspect its list of classes. There's two classes that are exported by the type library, if we inspect the servers for StdFont we can see that it is only specified to be creatable in process, we now have a target class to look for bugs. To get an out of process interface for the stdole type library we need to find a type which references it. The reason for the reference is that common interfaces such as IUnknown and IDispatch are defined in the library, so we need to query the base type of an interface we can directly access.  Let's try to create the object in the COM service.

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

We query the base type of an existing interface through a combination of GetRefTypeOfImplType and GetRefTypeInfo, then use GetContainingTypeLib to get the referenced type library interface. We can parse the library to be confident that we've got the stdole library. Next we get the type info for the StdFont class and call CreateInstance. We can inspect the object's process to ensure it was created out of process, the results shows its trapped in the service process. As a final check we can query for the object's interfaces to prove that it's a font object.

Now we just need to find a way of exploiting one of these two classes, the first problem is only the StdFont object can be accessed. The StdPicture object does a check to prevent it being used out of process. I couldn't find useful exploitable behavior in the font object, but I didn't spend too much time looking. Of course, if anyone else wants to look for a suitable bug in the class then go ahead.

This research was therefore at a dead end, at least as far as system services go. There might be some COM server accessible from a sandbox but an initial analysis of ones accessible from AppContainer didn't show any obvious candidates. However, after thinking a bit more about this I realized it could be useful as an injection technique into a process running at the same privilege level. For example, we could hijack the COM registration for StdFont, to point to any other class using the TreatAs registry key. This other class would be something exploitable, such as loading the JScript engine into the target process and running a script.

Still, injection techniques are not something I'd usually discuss on this blog, that's more in the realm of malware. However, there is a scenario where it might have interesting security implications. What if we could use this to inject into a Windows Protected Process? In a strange twist of fate, the WaaSRemediationAgent class we've just been inspecting might just be our ticket to ride:

PS> $cls = Get-ComClass -Clsid 72566e27-1abb-4eb3-b4f0-eb431cb1cb32

PS> $cls.AppIDEntry.ServiceProtectionLevel

WindowsLight

When we inspect the protection level for the hosting service it's configured to run at the PPL-Windows level! Let's see if we can salvage some value out of this research.

Protected Process Injection

I've blogged (and presented) on the topic of injecting into Windows Protected Processes before. I'd recommend re-reading that blog post to get a better background of previous injection attacks. However, one key point is that Microsoft does not consider PPL a security boundary and so they won't generally fix any bugs in a security bulletin in a timely manner, but they might choose to fix it in a new version of Windows.

The idea is simple, we'll redirect the StdFont class registration to point to another class so that when we create it via the type library it'll be running the protected process. Choosing to use StdFont should be more generic as we could move to using a different COM server if WaaSRemediationAgent is removed. We just need a suitable class which gets us arbitrary code execution which also works in a protected process.

Unfortunately this immediately rules out any of the scripting engines like JScript. If you've re-read my last blog post, the Code Integrity module explicitly blocks the common script engines from loading in a protected process. Instead, I need a class which is accessible out of process and can be loaded into a protected process. I realized one option is to load a registered .NET COM class. I've blogged about how .NET DCOM is exploitable, and shouldn't be used, but in this case we want the buggyness.

The blog post discussed exploiting serialization primitives, however there was a much simpler attack which I exploited by using the System.Type class over DCOM. With access to a Type object you could perform arbitrary reflection and call any method you liked, including loading an assembly from a byte array which would bypass the signature checking and give full control over the protected process.

Microsoft fixed this behavior, but they left a configuration value, AllowDCOMReflection, which allows you to turn it back on again. As we're not elevating privileges, and we have to be running as an administrator to change the COM class registration information, we can just enable DCOM reflection in the registry by writing the AllowDCOMReflection with the DWORD value of 1 to the HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework key before loading the .NET framework into the protected process.

The following steps need to be taken to achieve injection:

  1. Enable DCOM reflection in the registry.
  2. Add the TreatAs key to redirect StdFont to the System.Object COM class.
  3. Create the WaaSRemediationAgent object.
  4. Use the type library to get the StdFont class type info.
  5. Create a StdFont object using the CreateInstance method which will really load the .NET framework and return an instance of the System.Object class.
  6. Use .NET reflection to call the System.Reflection.Assembly::Load method with a byte array.
  7. Create an object in the loaded assembly to force code to execute.
  8. Cleanup all registry changes.

You'll need to do these steps in a non .NET language as otherwise the serialization mechanisms will kick in and recreate the reflection objects in the calling process. I wrote my PoC in C++, but you can probably do it from things like Python if you're so inclined. I'm not going to make the PoC available but the code is very similar to the exploit I wrote for CVE-2014-0257, that'll give you an example of how to use DCOM reflection in C++. Also note that the default for .NET COM objects is to run them using the v2 framework which is no longer installed by default. Rather than mess around with getting this working with v4 I just installed v2 from the Windows components installer.

My PoC worked first-time on Windows 10, but unfortunately when I ran it on Windows 11 24H2 it failed. I could create the .NET object, but calling any method on the object failed with the error TYPE_E_CANTLOADLIBRARY. I could have stopped here, having proven my point but I wanted to know what was failing on Windows 11. Lets finish up with diving into that, to see if we could do something to get it to work on the latest version of Windows.

The Problem with Windows 11

I was able to prove that the issue was related to protected processes, if I changed the service registration to run unprotected then the PoC worked. Therefore there must be something blocking the loading of the library when specifically running in a protected process. This didn't seem to impact type libraries generally, the loading of stdole worked just fine, so it was something specific to .NET.

After inspecting the behavior of the PoC with Process Monitor it was clear the mscorlib.tlb library was being loaded to implement the stub class in the server. For some reason it failed to load, which prevented the stub from being created, which in turn caused any call to fail. At this point I had an idea of what's happening. In the previous blog post I discussed attacking the NGEN COM process by modifying the type library it used to create the interface stub to introduce a type-confusion. This allowed me to overwrite the KnownDlls handle and force an arbitrary DLL to get loaded into memory. I knew from the work of Clément Labro and others that most of the attacks around KnownDlls are now blocked, but I suspected that there was also some sort of fix for the type library type-confusion trick.

Digging into oleaut32.dll I found the offending fix, the VerifyTrust method is shown below:

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;

}

This method is called during the loading of the type library. It's using the cached signing level, again something I mentioned in the previous blog post, to verify if the file has a signing level of 12, which corresponds to Windows signing level. If it doesn't have the appropriate cached signing level the code will try to use NtSetCachedSigningLevel to set it. If that fails it assumes the file can't be loaded in the protected process and returns the error, which results in the type library failing to load. Note, a similar fix blocks the abuse of the Running Object Table to reference an out-of-process type library, but that's not relevant to this discussion.

Based on the output from Get-AuthenticodeSignature the mscorlib.tlb file is signed, admittedly with a catalog signing. The signing certificate is Microsoft Windows Production PCA 2011 which is exactly the same certificate as the .NET Runtime DLL so there should be no reason it wouldn't get a Windows signing level. Let's try and set the cached signature level manually using my NtObjectManager PowerShell module to see if we get any insights:

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...........

Setting the signing level gives us the STATUS_INVALID_IMAGE_FORMAT error. Looking at the first 64 bytes of type library file shows that it's a raw type library rather than packaged in a PE file. This is fairly uncommon on Windows, even when a file has the extension TLB it's common for the type library to still be packed into a PE file as a resource. I guess we're out of luck, unless we can set a cached signing level on the file, it will be blocked from loading into the protected process and we need it to load to support the stub class to call the .NET interfaces over DCOM.

As an aside, oddly I have a VM of Windows 11 with the non-DLL form of the type library which does work to set a cached signing level. I must have changed the VM's configuration in some way to support this feature, but I've no idea what that is and I've decided not to dig further into it.

We could try and find a previous version of the type library file which is both validly signed, and is packaged in a PE file, however, I'd rather not do that. Of course there's almost certainly another COM object we could load rather than .NET which might give us arbitrary code execution but I'd set my heart on this approach. In the end the solution was simpler than I expected, for some reason the 32 bit version of the type library file (i.e. in Framework rather than Framework64) is packed in a DLL, and we can set a cached signing level on it.

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

Thus to exploit on Windows 11 24H2 we can swap the type library registration path from the 64 bit version to the 32 bit version and rerun the exploit. The VerifyTrust function will automatically set the cached signing level for us so we don't need to do anything to make it work. Even though it's technically a different version of the type library, it doesn't make any difference for our use case and the stub generator code doesn't care.

Conclusions

I discussed in this blog post an interesting type of bug class on Windows, although it is applicable to any similar object-orientated remoting cross process or remoting protocol. It shows how you can get a COM object trapped in a more privileged process by exploiting a feature of OLE Automation, specifically the IDispatch interface and type libraries.

While I wasn't able to demonstrate a privilege escalation, I showed how you can use the IDispatch interface exposed by the WaaSRemediationAgent class to inject code into a PPL-Windows process. While this isn't the highest possible protection level it allows access to the majority of processes running protected including LSASS. We saw that Microsoft has done some work to try and mitigate existing attacks such as type library type-confusions, but in our case this mitigation shouldn't have blocked the load as we didn't need to change the type library itself. While the attack required admin privilege, the general technique does not. You could modify the local user's registration for COM and .NET to do the attack as a normal user to inject into a PPL if you can find a suitable COM server exposing IDispatch.

No comments:

Post a Comment