Abusing IDispatch for Trapped COM Object Access & Injecting into PPL Processes
Last updated
Was this helpful?
Last updated
Was this helpful?
In this post, I explore an interesting bug class identified by James Forshaw from Google Project Zero that relates to the IDispatch interface in COM servers. His research highlights a vulnerability in how certain COM servers, particularly those implementing the IDispatch interface, allow the creation of arbitrary objects within the process. Notably, every Object-Oriented Programming (OOP) COM server implementing IDispatch
exposes the ability to create objects like STDFONT, which was never intended to be used safely across process boundaries. This opens the door to potential exploitation, especially when interacting with cross-process COM remoting.
Forshaw’s work demonstrated the security risks in these implementations but did not provide a complete Proof of Concept (PoC). Drawing inspiration from his research and driven by my passion for COM object exploitation, I decided to take on the challenge and develop a functional PoC in C++. This blog expands on Forshaw’s findings, providing a working PoC that shows how the misuse of this COM feature can be leveraged to inject unsigned code into a Protected Process Light (PPL) process with the protection PsProtectedSignerWindows-Light.
This PoC demonstrates how the technique can bypass Protected Process Light (PPL) protection, highlighting the significant real-world implications of this vulnerability. It provides a powerful means of accessing critical protected processes, such as LSASS with LSA protection or a protected AV/EDR.
This section dissects the core mechanism of our exploit: leveraging C++/mscorlib interoperability to hijack COM activation and force the execution of arbitrary .NET code under the guise of trusted process.
At its core, this exploit leverages the Windows Update Medic Service’s WaaSRemediationAgent COM server, — a privileged component running as svchost.exe within a PPL process protected by PsProtectedSignerWindows-Light — to load and execute am unsigned .NET payload.
By manipulating registry keys to enable DCOM reflection and redirect COM activation, we trick the system into treating a legacy COM class (StdFont
) as a .NET System.Object
, effectively bridging the native and managed worlds.
By using mscorlib (the .NET runtime library) to reflectively load and execute an in-memory .NET assembly while masquerading as a benign COM operation. This bypasses PPL restrictions because the CLR (Common Language Runtime), once activated within a privileged process, inherently trusts code loaded via mscorlib's reflection APIs.
In the following breakdown, we’ll explore:
COM-to-.NET Redirection: How registry manipulation forces COM to activate .NET objects.
mscorlib as a Bridge: Using `System.Object` and System.Reflection
to load malicious assemblies.
In-Memory Execution: Avoiding disk writes by directly invoking .NET methods from C++.
PPL Bypass: Why the CLR’s trust in mscorlib allows unverified code to run in protected processes.
The registry key DCOM Reflection is enabled to allow COM objects to reflectively call into the managed code. This step allows COM to be aware of the .NET objects that exist and interact with them.
By enabling this, you configure COM to be able to dynamically locate and invoke managed code through reflection.
The registry key onlyUseLatestCLR is set, ensuring that the latest version of the .NET runtime (CLR) is used for managed code execution. This step was particularly important during the testing phase, as issues were encountered when trying to run the code with .NET v4. Initially, the exploit relied on .NET v2, which was still present on the system, but .NET v4 introduced some compatibility challenges.
As James Forshaw pointed out in his blog, .NET COM objects default to running under v2 of the framework. However, starting with Windows 10, .NET v2 is not installed by default, which caused issues for running the exploit in a modern environment. To avoid these issues, Forshaw installed .NET v2 manually via the Windows Components Installer. For testing with .NET v4, however, setting the registry key to OnlyUseLatestCLR ensured that the system would always use the latest CLR (v4), avoiding the need to manually install an older version of .NET.
The TreatAs registry key is used to redirect a legacy COM class (e.g., StdFont) to a .NET object (System.Object). This manipulation makes the system treat a traditional COM object as a .NET object, allowing the .NET object to be invoked within the context of COM.However, before the registry changes can take effect, it’s necessary to impersonate TrustedInstaller to be able to set specifically TreatAs key
With these registry manipulations, COM calls are redirected to .NET objects, bridging the gap between the native COM environment and the managed .NET environment.
After registry manipulation, the exploit proceeds by activating the COM object WaaSRemediationAgent
and using reflection to invoke methods within the .NET runtime. This transition from COM to .NET is at the heart of this exploit.
The CoCreateInstance
function is called to create the WaaSRemediationAgent COM object. Thanks to the registry manipulation, this COM activation leads to the creation of a .NET object instead.
In my PoC exploit, the core method of injecting a .NET payload into a PPL-protected process (like svchost.exe running the WaaSRemediationAgent) hinges on the IDispatch interface exposed by the COM class. This interface, part of COM Automation, enables dynamic method invocation on COM objects. By leveraging IDispatch, the attack is able to bridge the gap between the native COM world and the managed .NET world, allowing me to inject .NET code into a process with PsProtectedSignerWindows-Light protection, such as WaaSRemediationAgent.
When I trigger the activation of the WaaSRemediationAgent COM class, the IDispatch interface is automatically exposed, allowing me to invoke .NET methods dynamically.
The ITypeInfo interface is used to retrieve type information for the COM object. This metadata is needed to use reflection to invoke .NET methods.
Why 0?: The first parameter (0) specifies the interface index. Index 0 typically refers to the default interface (IDispatch).
Navigating to Base Interface
Gets a reference (HREFTYPE) to the first implemented interface (index 0).
Many COM objects implement IDispatch
as their base interface, which we'll exploit later.
Converts the HREFTYPE
reference into a usable ITypeInfo pointer. This pBaseTypeInfo
now describes the base interface (e.g., IDispatch) of WaaSRemediationAgent.
Finds which type library ("DLL for COM metadata") contains the base interface.
pStdoleTypeLib
will typically point to stdole32.tlb, the system type library containing standard COM definitions like StdFont.
Retrieve type information for CLSID_StdFont (normally a legacy font COM class).
Earlier registry modifications via SetTreatAs redirect this CLSID to a .NET class (CLSID_DotNetObject).
CreateInstance activates a .NET System.Object instance through COM, despite targeting CLSID_StdFont. This works due to the TreatAs registry redirection to CLSID_DotNetObject.
The __uuidof(mscorlib::_Object) GUID maps to System.Object in .NET, allowing direct interaction with managed objects.
The _TypePtr interface (from System.Type) enables introspection of .NET types. The code navigates to System.Type itself to prepare for assembly loading.
At this stage, CreateInstance method to dynamically create a .NET object. The key part here is the interaction with mscorlib, which is the core .NET assembly. Specifically, you're creating an object corresponding to System.Type, which is the foundation for Reflection in .NET.
By creating this object, I am setting up the environment to load and execute .NET code from memory, rather than from a file on disk.
The real payload is loaded from a file into memory using reflection. Here's how the assembly is read from the disk and converted into a byte array, which can then be dynamically loaded:
The assembly is converted into a byte array (variant_t), which is then passed to System.Reflection.Assembly.Load
. This function allows you to dynamically load the .NET assembly from the byte array, which is an effective way to load unsigned code without touching the disk.
GetStaticMethod: Retrieves Assembly.Load via reflection using its name and parameter count.
ExecuteMethod: Invokes Load with the byte array, loading the .NET assembly into the process.
While executing the exploit and using the System.Reflection.Assembly.LoadFile instead of System.Reflection.Assembly.Load method to load the .NET assembly into memory, I encountered the following error:
This corresponds to the HRESULT error code 0x80131604, which indicates that an uncaught exception was thrown during the method invocation via Reflection. This error can be interpreted as a failure in executing the .NET method through Reflection.
Once the assembly is loaded into memory, the final step is to execute the payload. Your exploit looks for the Main method in the injected assembly and invokes it via reflection:
At this point in the exploit, the malicious .NET payload is executed within the context of the svchost process. Since svchost runs with the PsProtectedSignerWindows-Light protection, this step grants the malicious code elevated access within the Windows environment. Specifically, it allows the code to interact with protected processes under the Windows signer type, which are typically off-limits for unprivileged or unsigned code.
By successfully injecting unsigned .NET code into a Protected Process Light (PPL) with a Windows signer, we effectively gain the ability to access highly secured processes, such as LSASS, or even bypass protections implemented by AV/EDR systems.
In the exploit's scenario, the svchost process runs with the Windows signer type (0x51), and the LSASS process, which is a critical security process, runs with the Lsa signer type (0x41). Despite LSASS having a higher protection level, the Windows signer type still has sufficient permissions to access the LSASS process because Windows is a higher-level signer than Lsa.
Now, we can proceed to dump the memory of the LSASS process using the elevated privileges granted by the svchost exploit. This can be done by accessing the LSASS process' memory region and reading or dumping its content:
The PoC exploit expects two arguments:
DLL Path (): This should be the full file path to a .NET DLL that you want to load.
Static Class Name (): This is the name of a static class within the DLL that contains a public static void Main() method.
Additionally, according to a blog by Elastic Security Labs, Microsoft defends Protected Process Light (PPL) by enforcing SEC_IMAGE
checks when creating image sections. This ensures that the digital signature of any file used to create an image section is validated, and only signed code can be loaded into PPL processes. However, .NET Reflection, which allocates assemblies into memory directly, bypasses this mechanism because it doesn't create an image section or require file-backed validation. This is why Reflection-based loading can bypass SEC_IMAGE integrity checks and potentially load malicious code into a PPL process without triggering these defences.
Finally, the bypass occurs because .NET Reflection (specifically via Assembly.Load(byte[])
) does not create an image section. Instead, it directly allocates memory for the assembly , bypassing the SEC_IMAGE
integrity checks. These checks are typically enforced when an image section is created, as they validate the digital signature of the file backing the section (via NtCreateSection
with SEC_IMAGE
). Since the assembly is loaded directly into memory rather than being backed by a file, there is no file-backed section to validate, allowing the payload to bypass the code integrity checks enforced by SEC_IMAGE for PPL processes.
In this analysis, we walk through how to detect, trace, and analyse a malicious assembly loaded into a .NET process, using various Windows debugging tools like WinDbg and CLR Debugging Extensions. The following steps highlight how we can identify suspicious activity, locate that .NET unsigned code in memory, and investigate the underlying behaviour.
!dumpdomain command shows several AppDomains that are currently loaded on the PPL svchost process.
DefaultDomain:
The DefaultDomain (address: 000001d900c57010) is where user-code and potentially the .NET unsigned code is loaded. In this domain, besides the standard mscorlib.dll assembly, there is also our targeted:
assembly: Assembly: debug, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null
Module Name: debug (loaded at 00007ff80dce4ad8).
This assembly is unusual, with its 0.0.0.0 version and no public key token, which is a red flag for malware or injected code.
The .NET unsigned code was identified through the System.Reflection.Assembly.Load
method, which is commonly used to load assemblies from byte arrays. Here's part of the debug stack trace showing the reflection-based loading:
From this stack trace, we can see that the System.Reflection.Assembly.Load
function is being invoked, which is a common technique for dynamically loading assemblies from raw byte arrays.
By examining the memory regions where the malicious assembly is loaded, we can see that:
The memory region has the PAGE_READWRITE protection, meaning it is writable, which is suspicious for code sections.
The size of the memory region (136 KB) aligns with the size of a small executable or DLL.
This type of memory allocation is common in fileless attacks, where malicious code does not touch the disk but instead resides entirely in memory.
The MZ header found within the memory indicates that it is a PE file that may contain executable instructions. The presence of this PE header further indicates that an executable payload is active in memory.
The memory region details of the loaded malicious assembly are:
The Base Address is 0x00000214bec00000, and the region size is 136 KB, indicating that a PE file is located in this memory region.
The presence of PAGE_READWRITE protection indicates that the memory is writable, which is a typical sign of malicious code being injected or loaded into memory.
Here is part of the debug log showing the object dump for the byte array:
As observed, the byte array contains an MZ header, which is a signature for PE files (commonly used in DLL or EXE files). This confirms that the loaded assembly is an executable.
In order to observe the CLR stack trace during the execution of the injected .NET code, I used the sxe ld clrjit debugger command to enable debugging of the Just-In-Time (JIT) compiler. This command is used within the Windows debugger (WinDbg) to set a breakpoint that triggers whenever the clrjit module (which is responsible for JIT compiling .NET code) is loaded.
This shows the invocation of the Main method in your injected payload. The PrestubMethodFrame shows that the CLR is preparing to invoke this method. This is the entry point of the injected .NET payload that will execute the malicious actions.
Reflection Invocations: A series of reflections (Invoke, UnsafeInvokeInternal, etc.) dynamically invoke methods, which could include COM-to-.NET redirection and other injected operations.
COM to CLR Invocation:
This represents a COM to CLR stub method. The L_STUB_COMtoCLR
method is used when you call a COM method that internally invokes .NET code. It acts as a bridge between COM and .NET, ensuring that the parameters are passed correctly between the two environments.
NativeVariant
: This refers to the data structure used for handling COM data types (such as VARIANT) in a way that can be understood by both COM and .NET. IntPtr: These are pointers used to pass memory addresses or references, likely pointing to COM objects or .NET objects.
This log shows how your injected payload interacts with COM objects and .NET reflection mechanisms.
This stack trace highlights how the exploit leverages reflection, COM redirection, and the CLR to execute code, potentially interacting with protected processes like LSASS or other system-level components.
By carefully analysing the stack traces, memory regions, and loaded assemblies in a WinDbg debugging session, we were able to trace the malicious assembly's behaviour and detect a potential fileless attack. This approach provides insight into how advanced attackers use reflection to execute payloads directly in memory.
In this Proof-of-Concept (PoC), I demonstrated how code can be injected into a Protected Process Light (PPL), utilizing reflection-based techniques and COM-to-.NET redirection to bypass signature checks and security measures typically enforced in Windows processes with PsProtectedSignerWindows-Light protection
. The detailed memory analysis, from CLR stack tracing to dissecting process behavior, provided valuable insights into how we can manipulate registry keys and leverage memory allocation techniques to sidestep the inherent protections.
A special mention must be made of James Forshaw, whose in-depth research and expertise in Windows internals have been invaluable in helping me navigate and understand the intricacies of this exploit. His work continues to be a source of learning and inspiration. Much of the techniques explored in this PoC are built upon principles found in his publications and blog posts.
The full C++ PoC code will be made available on my GitHub repository for those who wish to explore, learn, or further develop upon this concept.
Feel free to dive into the repository for a deeper look into how this exploit was constructed and the techniques that were applied. Your feedback and contributions are always welcome as we continue to explore and secure these complex systems.
https://learn.microsoft.com/en-us/windows/win32/api/oaidl/nf-oaidl-idispatch-gettypeinfo