Malware type | Binding type | Est. coverage |
Windows Script Host | Always Late | 100% |
PowerShell COM | Always Late | 100% |
AutoIT | Always Late | 100% |
VBA Macros | Mostly Late | 95% |
VB6 Malware | Mixed | 65% |
.NET COM Interop | Mixed | 60% |
C++ Malware | Rarely Late (WMI) | 10% |
Modern script-based malware (e.g., VBScript, JScript, PowerShell) relies heavily on COM automation to perform malicious operations. Traditional dynamic analysis tools capture low-level API calls but miss the semantic meaning of high-level COM interactions. Consider this attack pattern:

Behavioral monitoring will detect process creation, but the analyst often loses critical context such as who launched the process. In this scenario WMI spawns new processes with wmic.exe or wmiprvse.exe as the parent.
DispatchLogger starts with API hooking at the COM instantiation boundary. Every COM object creation in Windows flows through a small set of API functions. By intercepting these functions and returning transparent proxies deep visibility can be achieved without modifying malware behavior.
The core API hooking targets are:
Initial implementation attempts hooked only CoCreateInstance, filtering for direct IDispatch requests. However, testing revealed that most VBScript CreateObject calls were not being intercepted.
To diagnose this a minimal ActiveX library was created with a MsgBox in Class_Initialize to freeze the process. The VBScript was launched, and a debugger attached to examine the call stack. The following code flow was revealed:

Disassembly of vbscript.dll!GetObjectFromProgID (see Figure 3) confirmed the pattern. VBScript's internal implementation requests IUnknown first, then queries for IDispatch afterward:

The key line is CreateInstance(NULL, IID_IUnknown, &ppunk). Here, VBScript explicitly requests IUnknown, not IDispatch. This occurs because VBScript needs to perform additional safety checks and interface validation before accessing the IDispatch interface.
If we only wrap objects when IDispatch is directly requested in CoCreateInstance, we miss the majority of script instantiations. The solution is to also hook CoGetClassObject and wrap the returned IClassFactory:

The ClassFactoryProxy intercepts CreateInstance calls and handles both cases:

This ensures coverage regardless of which interface the script engine initially requests.
The DispatchProxy class implements IDispatch by forwarding all calls to the wrapped object while logging parameters, return values, and method names. If the function call returns another object, we test for IDispatch and automatically wrap it.

The proxy is transparent, meaning it implements the same interface, maintains proper reference counting, and handles QueryInterface correctly. Malware cannot detect the proxy through standard COM mechanisms.
The key capability is automatic recursive wrapping. Every IDispatch object returned from a method call is automatically wrapped before being returned to the malware. This creates a fully instrumented object graph.

Object relationships are tracked:
GetObject("winmgmts:") triggers hook, returns wrapped WMI service object .ExecQuery() goes through proxy, logs call with SQL parameter .Terminate() on items logs through their respective proxies VBScript/JScript For Each constructs use IEnumVARIANT for iteration. We proxy this interface to wrap objects as they're enumerated:

VBScript's GetObject() function uses monikers for binding to objects. We hook CoGetObject and MkParseDisplayName, then wrap returned moniker objects to intercept BindToObject() calls:

This ensures coverage of WMI access and other moniker-based object retrieval.
While standard API hooks can be implemented on a function-by-function basis, COM proxies require implementing all functions of a given interface. The table below details the interfaces and function counts that had to be replicated for this technique to operate.
Interface | Total Methods | Logged | Hooked/Wrapped | Passthrough |
IDispatch | 7 | 4 | 1 | 2 |
IEnumVARIANT | 7 | 1 | 1 | 5 |
IClassFactory | 5 | 2 | 1 | 2 |
IMoniker | 26 | 1 | 1 | 24 |
During execution, a script may create dozens or even hundreds of distinct COM objects. For this reason, interface implementations must be class-based and maintain a one-to-one relationship between each proxy instance and the underlying COM object it represents.
While generating this volume of boilerplate code by hand would be daunting, AI-assisted code generation significantly reduced the effort required to implement the complex interface scaffolding.
The real trick with COM interface hooking is object discovery. The initial static API entry points are only the beginning of the mission. Each additional object encountered must be probed, wrapping them recursively to maintain logging.
Multiple threads may create COM objects simultaneously. Proxy tracking uses a critical section to serialize access to the global proxy map:

Proper COM lifetime management is critical. The proxy maintains separate reference counts and forwards QueryInterface calls appropriately:

When script code executes with DispatchLogger active, comprehensive logs are generated. Here are excerpts from an actual analysis session:
[CLSIDFromProgID] 'Scripting.FileSystemObject' -> {0D43FE01-F093-11CF-8940-00A0C9054228}
[CoGetClassObject] FileSystemObject ({0D43FE01-F093-11CF-8940-00A0C9054228}) Context=0x00000015
[CoGetClassObject] Got IClassFactory for FileSystemObject – WRAPPING!
[FACTORY] Created factory proxy for FileSystemObject
[FACTORY] CreateInstance: FileSystemObject requesting Iunknown
[FACTORY] CreateInstance SUCCESS: Object at 0x03AD42D8
[FACTORY] Object supports IDispatch – WRAPPING!
[PROXY] Created proxy #1 for FileSystemObject (Original: 0x03AD42D8)
[FACTORY] !!! Replaced object with proxy!
[PROXY #1] >>> Invoke: FileSystemObject.GetSpecialFolder (METHOD PROPGET) ArgCount=1 [PROXY #1] Arg[0]: 2 [PROXY #1] <<< Result: IDispatch:0x03AD6C14 (HRESULT=0x00000000) [PROXY] Created proxy #2 for FileSystemObject.GetSpecialFolder (Original: 0x03AD6C14) [PROXY #1] !!! Wrapped returned IDispatch as new proxy [PROXY #2] >>> Invoke: FileSystemObject.GetSpecialFolder.Path (METHOD PROPGET) ArgCount=0 [PROXY #2] <<< Result: "C:\Users\home\AppData\Local\Temp" (HRESULT=0x00000000)
[CLSIDFromProgID] 'WScript.Shell' -> {72C24DD5-D70A-438B-8A42-98424B88AFB8}
[CoGetClassObject] WScript.Shell ({72C24DD5-D70A-438B-8A42-98424B88AFB8}) Context=0x00000015
[FACTORY] CreateInstance: WScript.Shell requesting IUnknown
[PROXY] Created proxy #3 for WScript.Shell (Original: 0x03AD04B0)
[PROXY #3] >>> Invoke: WScript.Shell.ExpandEnvironmentStrings (METHOD PROPGET) ArgCount=1
[PROXY #3] Arg[0]: "%WINDIR%"
[PROXY #3] <<< Result: "C:\WINDOWS" (HRESULT=0x00000000)
[CLSIDFromProgID] 'Scripting.Dictionary' -> {EE09B103-97E0-11CF-978F-00A02463E06F}
[PROXY] Created proxy #4 for Scripting.Dictionary (Original: 0x03AD0570)
[PROXY #4] >>> Invoke: Scripting.Dictionary.Add (METHOD) ArgCount=2
[PROXY #4] Arg[0]: "test"
[PROXY #4] Arg[1]: "value"
[PROXY #4] <<< Result: (void) HRESULT=0x00000000
[PROXY #4] >>> Invoke: Scripting.Dictionary.Item (METHOD PROPGET) ArgCount=1
[PROXY #4] Arg[0]: "test"
[PROXY #4] <<< Result: "value" (HRESULT=0x00000000)
This output provides:

DispatchLogger is implemented as a dynamic-link library (DLL) that can be injected into target processes.
Once loaded, the DLL:
No modifications to the target script or runtime environment are required.
Approach | Coverage | Semantic visibility | Detection risk |
Static analysis | Encrypted/obfuscated scripts missed | No runtime behavior | N/A |
API monitoring | Low-level calls only | Missing high-level intent | Medium |
Memory forensics | Point-in-time snapshots | No call sequence context | Low |
Debugger tracing | Manual breakpoints required | Analyst-driven, labor-intensive | High |
DispatchLogger | Complete late bound automation layer | Full semantic context | None |
DispatchLogger provides advantages for:
Proxy overhead is minimal:
In testing with real malware samples, execution time differences were negligible.
Current implementation constraints:
The sample code included with this article is a general purpose tool and proof of concept. It has not been tested at scale and does not attempt to prevent logging escapes.
Typical analysis workflow:
The tool has been tested against:
The techniques presented in this article emerged from earlier experimentation with IDispatch while developing a JavaScript engine capable of exposing dynamic JavaScript objects as late-bound COM objects. That work required deep control over name resolution, property creation, and IDispatch::Invoke handling. This framework allowed JavaScript objects to be accessed and modified transparently from COM clients.
The experience gained from that effort directly informed the transparent proxying and recursive object wrapping techniques used in DispatchLogger.
DispatchLogger addresses a significant gap in script-based malware analysis by providing deep, semantic-level visibility into COM automation operations. Through transparent proxy interception at the COM instantiation boundary, recursive object wrapping, and comprehensive logging, analysts gain great insight into malware behavior without modifying samples or introducing detection vectors.
The implementation demonstrates that decades-old COM architecture, when properly instrumented, provides powerful analysis capabilities for modern threats. By understanding COM internals and applying transparent proxying patterns, previously opaque script behavior becomes highly observable.
DispatchLogger is being released open source under the Apache license and can be downloaded from the Cisco Talos GitHub page.