Shellcode: In-Memory Execution of JavaScript, VBScript, JScript and XSL

Introduction

UPDATE: After being available for twenty years, Dr.Dobb’s removed access to two articles linked from here. I’ve no idea why.

A DynaCall() Function for Win32 was published in the August 1998 edition of Dr.Dobbs Journal. The author, Ton Plooy, provided a function in C that allows an interpreted language such as VBScript to call external DLL functions via a registered COM object. An Automation Object for Dynamic DLL Calls published in November 1998 by Jeff Stong built upon this work to provide a more complete project which he called DynamicWrapper. In 2011, Blair Strang wrote a tool called vbsmem that used DynamicWrapper to execute shellcode from VBScript. DynamicWrapper was the source of inspiration for another tool called DynamicWrapperX that appeared in 2008 and it too was used to execute shellcode from VBScript by Casey Smith.

The May 2019 update of Defender Application Control included a number of new policies, one of which is “COM object registration”. Microsoft states the purpose of this policy is to enforce “a built-in allow list of COM object registrations to reduce the risk introduced from certain powerful COM objects.” Are they referring to DynamicWrapper? Possibly, but what about unregistered COM objects? Robert Freeman/IBM demonstrated in 2007 that unregistered COM objects may be useful for obfuscation purposes. His Virus Bulletin presentation Novel code obfuscation with COM doesn’t provide any proof-of-concept code, but does demonstrate the potential to misuse the IActiveScript interface for Dynamic DLL calls without COM registration.

Windows Script Host (WSH)

WSH is an automation technology available since Windows 95 that was popular among developers prior to the release of the .NET Framework in 2002. It was primarily used for generation of dynamic content like Active Server Pages (ASP) written in JScript or VBScript. As .NET superseded this technology, much of the wisdom developers acquired about Active Scripting up until 2002 slowly disappeared from the internet. One post that was recommended quite frequently on developer forums is the Active X FAQ by Mark Baker, which answers most questions developers have about the IActiveScript interface.

Enumerating Script Engines

Can be performed in at least two ways.

  1. Each Class Identifier in HKEY_CLASSES_ROOT\CLSID\ that contains a subkey called OLEScript can be used with Windows Script Hosting.
  2. The Component Categories Manager can enumerate CLSID for category identifiers CATID_ActiveScript or CATID_ActiveScriptParse.

Below is a snippet of code for displaying active script engines using the second approach. See full version here.

void DisplayScriptEngines(void) {
    ICatInformation *pci = NULL;
    IEnumCLSID      *pec = NULL;
    HRESULT         hr;
    CLSID           clsid;
    OLECHAR         *progID, *idStr, path[MAX_PATH], desc[MAX_PATH];
  
    // initialize COM
    CoInitialize(NULL);
    
    // obtain component category manager for this machine
    hr = CoCreateInstance(
      CLSID_StdComponentCategoriesMgr, 
      0, CLSCTX_SERVER, IID_ICatInformation, 
      (void**)&pci);
      
    if(hr == S_OK) {
      // obtain list of script engine parsers
      hr = pci->EnumClassesOfCategories(
        1, &CATID_ActiveScriptParse, 0, 0, &pec);
      
      if(hr == S_OK) {
        // print each CLSID and Program ID
        for(;;) {
          ZeroMemory(path, ARRAYSIZE(path));
          ZeroMemory(desc, ARRAYSIZE(desc));
          
          hr = pec->Next(1, &clsid, 0);
          if(hr != S_OK) {
            break;
          }
          ProgIDFromCLSID(clsid, &progID);
          StringFromCLSID(clsid, &idStr);
          GetProgIDInfo(idStr, path, desc);
          
          wprintf(L"\n*************************************\n");
          wprintf(L"Description : %s\n", desc);
          wprintf(L"CLSID       : %s\n", idStr);
          wprintf(L"Program ID  : %s\n", progID);
          wprintf(L"Path of DLL : %s\n", path);
          
          CoTaskMemFree(progID);
          CoTaskMemFree(idStr);
        }
        pec->Release();
      }
      pci->Release();
    }
}

The output of this code on a system with ActivePerl and ActivePython installed :

*************************************
Description : JScript Language
CLSID       : {16D51579-A30B-4C8B-A276-0FF4DC41E755}
Program ID  : JScript
Path of DLL : C:\Windows\System32\jscript9.dll

*************************************
Description : XML Script Engine
CLSID       : {989D1DC0-B162-11D1-B6EC-D27DDCF9A923}
Program ID  : XML
Path of DLL : C:\Windows\System32\msxml3.dll

*************************************
Description : VB Script Language
CLSID       : {B54F3741-5B07-11CF-A4B0-00AA004A55E8}
Program ID  : VBScript
Path of DLL : C:\Windows\System32\vbscript.dll

*************************************
Description : VBScript Language Encoding
CLSID       : {B54F3743-5B07-11CF-A4B0-00AA004A55E8}
Program ID  : VBScript.Encode
Path of DLL : C:\Windows\System32\vbscript.dll

*************************************
Description : JScript Compact Profile (ECMA 327)
CLSID       : {CC5BBEC3-DB4A-4BED-828D-08D78EE3E1ED}
Program ID  : JScript.Compact
Path of DLL : C:\Windows\System32\jscript.dll

*************************************
Description : Python ActiveX Scripting Engine
CLSID       : {DF630910-1C1D-11D0-AE36-8C0F5E000000}
Program ID  : Python.AXScript.2
Path of DLL : pythoncom36.dll

*************************************
Description : JScript Language
CLSID       : {F414C260-6AC0-11CF-B6D1-00AA00BBBB58}
Program ID  : JScript
Path of DLL : C:\Windows\System32\jscript.dll

*************************************
Description : JScript Language Encoding
CLSID       : {F414C262-6AC0-11CF-B6D1-00AA00BBBB58}
Program ID  : JScript.Encode
Path of DLL : C:\Windows\System32\jscript.dll

*************************************
Description : PerlScript Language
CLSID       : {F8D77580-0F09-11D0-AA61-3C284E000000}
Program ID  : PerlScript
Path of DLL : C:\Perl64\bin\PerlSE.dll

The PerlScript and Python scripting engines are provided by ActiveState. I would recommend using {16D51579-A30B-4C8B-A276-0FF4DC41E755} for JavaScript.

C Implementation of IActiveScript

During research into IActiveScript, I found COM in plain C, part 6 by Jeff Glatt to be helpful. The following code is the bare minimum required to execute VBS/JS files and does not support WSH objects. See here for the full source.

VOID run_script(PWCHAR lang, PCHAR script) {
    IActiveScriptParse     *parser;
    IActiveScript          *engine;
    MyIActiveScriptSite    mas;
    IActiveScriptSiteVtbl  vft;
    LPVOID                 cs;
    DWORD                  len;
    CLSID                  langId;
    HRESULT                hr;
    
    // 1. Initialize IActiveScript based on language
    CLSIDFromProgID(lang, &langId);
    CoInitializeEx(NULL, COINIT_MULTITHREADED);
    
    CoCreateInstance(
      &langId, 0, CLSCTX_INPROC_SERVER, 
      &IID_IActiveScript, (void **)&engine);
    
    // 2. Query engine for script parser and initialize
    engine->lpVtbl->QueryInterface(
        engine, &IID_IActiveScriptParse, 
        (void **)&parser);
        
    parser->lpVtbl->InitNew(parser);
    
    // 3. Initialize IActiveScriptSite interface
    vft.QueryInterface      = (LPVOID)QueryInterface;
    vft.AddRef              = (LPVOID)AddRef;
    vft.Release             = (LPVOID)Release;
    vft.GetLCID             = (LPVOID)GetLCID;
    vft.GetItemInfo         = (LPVOID)GetItemInfo;
    vft.GetDocVersionString = (LPVOID)GetDocVersionString;
    vft.OnScriptTerminate   = (LPVOID)OnScriptTerminate;
    vft.OnStateChange       = (LPVOID)OnStateChange;
    vft.OnScriptError       = (LPVOID)OnScriptError;
    vft.OnEnterScript       = (LPVOID)OnEnterScript;
    vft.OnLeaveScript       = (LPVOID)OnLeaveScript;
    
    mas.site.lpVtbl     = (IActiveScriptSiteVtbl*)&vft;
    mas.siteWnd.lpVtbl  = NULL;
    mas.m_cRef          = 0;
    
    engine->lpVtbl->SetScriptSite(
        engine, (IActiveScriptSite *)&mas);
        
    // 4. Convert script to unicode and execute
    len = MultiByteToWideChar(
      CP_ACP, 0, script, -1, NULL, 0);
    
    len *= sizeof(WCHAR);
    
    cs = malloc(len);
    
    len = MultiByteToWideChar(
      CP_ACP, 0, script, -1, cs, len);
    
    parser->lpVtbl->ParseScriptText(
         parser, cs, 0, 0, 0, 0, 0, 0, 0, 0);  
    
    engine->lpVtbl->SetScriptState(
         engine, SCRIPTSTATE_CONNECTED);
    
    // 5. cleanup
    parser->lpVtbl->Release(parser);
    engine->lpVtbl->Close(engine);
    engine->lpVtbl->Release(engine);
    free(cs);
}

x86 Assembly

Just for illustration, here’s something similar in x86 assembly with some limitations imposed: The script should not exceed 64KB, the UTF-16 conversion only works with ANSI(latin alphabet) characters, and the language (VBS or JS) must be predefined before assembling. When declaring a local variable on the stack that exceeds 4KB, compilers such as GCC and MSVC insert code to perform stack probing which allows the kernel to expand the amount of stack memory available to a thread. There are of course compiler/linker switches to increase the reserved size if you wanted to prevent stack probing, but they are rarely used in practice. Each thread on Windows initially has 16KB of stack available by default as you can see by subtracting the value of StackLimit from StackBase found in the Thread Environment Block (TEB).

0:004> !teb
TEB at 000000f4018bf000
    ExceptionList:        0000000000000000
    StackBase:            000000f401c00000
    StackLimit:           000000f401bfc000
    SubSystemTib:         0000000000000000
    FiberData:            0000000000001e00
    ArbitraryUserPointer: 0000000000000000
    Self:                 000000f4018bf000
    EnvironmentPointer:   0000000000000000
    ClientId:             0000000000001940 . 000000000000067c
    RpcHandle:            0000000000000000
    Tls Storage:          0000000000000000
    PEB Address:          000000f40185a000
    LastErrorValue:       0
    LastStatusValue:      0
    Count Owned Locks:    0
    HardErrorMode:        0
    
0:004> ? 000000f401c00000 - 000000f401bfc000 
Evaluate expression: 16384 = 00000000`00004000

The assembly code initially used VirtualAlloc to allocate enough space, but since this code is unlikely to be used for anything practical, the stack is used instead.

; In-Memory execution of VBScript/JScript using 392 bytes of x86 assembly
; Odzhan

      %include "ax.inc"
      
      %define VBS
      
      bits   32
      
      %ifndef BIN
        global run_scriptx
        global _run_scriptx
      %endif
      
run_scriptx:
_run_scriptx:
      pop    ecx             ; ecx = return address
      pop    eax             ; eax = script parameter
      push   ecx             ; save return address
      cdq                    ; edx = 0
      ; allocate 128KB of stack.
      push   32              ; ecx = 32
      pop    ecx
      mov    dh, 16          ; edx = 4096
      pushad                 ; save all registers
      xchg   eax, esi        ; esi = script
alloc_mem:
      sub    esp, edx        ; subtract size of page
      test   [esp], esp      ; stack probe
      loop   alloc_mem       ; continue for 32 pages
      mov    edi, esp        ; edi = memory
      xor    eax, eax
utf8_to_utf16:               ; YMMV. Prone to a stack overflow.
      cmp    byte[esi], al   ; ? [esi] == 0
      movsb                  ; [edi] = [esi], edi++, esi++
      stosb                  ; [edi] = 0, edi++
      jnz    utf8_to_utf16   ;
      stosd                  ; store 4 nulls at end      
      and    edi, -4         ; align by 4 bytes
      call   init_api        ; load address of invoke_api onto stack
      ; *******************************
      ; INPUT: eax contains hash of API
      ; Assumes DLL already loaded
      ; No support for resolving by ordinal or forward references
      ; *******************************
invoke_api:
      pushad
      push   TEB.ProcessEnvironmentBlock
      pop    ecx
      mov    eax, [fs:ecx]
      mov    eax, [eax+PEB.Ldr]
      mov    edi, [eax+PEB_LDR_DATA.InLoadOrderModuleList + LIST_ENTRY.Flink]
      jmp    get_dll
next_dll:    
      mov    edi, [edi+LDR_DATA_TABLE_ENTRY.InLoadOrderLinks + LIST_ENTRY.Flink]
get_dll:
      mov    ebx, [edi+LDR_DATA_TABLE_ENTRY.DllBase]
      mov    eax, [ebx+IMAGE_DOS_HEADER.e_lfanew]
      ; ecx = IMAGE_DATA_DIRECTORY[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress
      mov    ecx, [ebx+eax+IMAGE_NT_HEADERS.OptionalHeader + \
                           IMAGE_OPTIONAL_HEADER32.DataDirectory + \
                           IMAGE_DIRECTORY_ENTRY_EXPORT * IMAGE_DATA_DIRECTORY_size + \
                           IMAGE_DATA_DIRECTORY.VirtualAddress]
      jecxz  next_dll
      ; esi = offset IMAGE_EXPORT_DIRECTORY.NumberOfNames 
      lea    esi, [ebx+ecx+IMAGE_EXPORT_DIRECTORY.NumberOfNames]
      lodsd
      xchg   eax, ecx
      jecxz  next_dll        ; skip if no names
      ; ebp = IMAGE_EXPORT_DIRECTORY.AddressOfFunctions
      lodsd
      add    eax, ebx        ; ebp = RVA2VA(eax, ebx)
      xchg   eax, ebp        ;
      ; edx = IMAGE_EXPORT_DIRECTORY.AddressOfNames
      lodsd
      add    eax, ebx        ; edx = RVA2VA(eax, ebx)
      xchg   eax, edx        ;
      ; esi = IMAGE_EXPORT_DIRECTORY.AddressOfNameOrdinals      
      lodsd
      add    eax, ebx        ; esi = RVA2VA(eax, ebx)
      xchg   eax, esi
get_name:
      pushad
      mov    esi, [edx+ecx*4-4] ; esi = AddressOfNames[ecx-1]
      add    esi, ebx           ; esi = RVA2VA(esi, ebx)
      xor    eax, eax           ; eax = 0
      cdq                       ; h = 0
hash_name:    
      lodsb
      add    edx, eax
      ror    edx, 8
      dec    eax
      jns    hash_name
      cmp    edx, [esp + _eax + pushad_t_size]   ; hashes match?
      popad
      loopne get_name              ; --ecx && edx != hash
      jne    next_dll              ; get next DLL        
      movzx  eax, word [esi+ecx*2] ; eax = AddressOfNameOrdinals[ecx]
      add    ebx, [ebp+eax*4]      ; ecx = base + AddressOfFunctions[eax]
      mov    [esp+_eax], ebx
      popad                        ; restore all
      jmp    eax
_ds_section:
      ; ---------------------
      db     "ole32", 0, 0, 0
co_init:
      db     "CoInitializeEx", 0
co_init_len equ $-co_init
co_create:
      db     "CoCreateInstance", 0
co_create_len equ $-co_create
      ; IID_IActiveScript
      ; IID_IActiveScriptParse32 +1
      dd     0xbb1a2ae1
      dw     0xa4f9, 0x11cf
      db     0x8f, 0x20, 0x00, 0x80, 0x5f, 0x2c, 0xd0, 0x64
  %ifdef VBS
      ; CLSID_VBScript
      dd     0xB54F3741
      dw     0x5B07, 0x11cf
      db     0xA4, 0xB0, 0x00, 0xAA, 0x00, 0x4A, 0x55, 0xE8
  %else
      ; CLSID_JScript
      dd     0xF414C260
      dw     0x6AC0, 0x11CF
      db     0xB6, 0xD1, 0x00, 0xAA, 0x00, 0xBB, 0xBB, 0x58
  %endif
_QueryInterface:
      mov    eax, E_NOTIMPL     ; return E_NOTIMPL
      retn   3*4
_AddRef:
_Release:
      pop    eax                ; return S_OK
      push   eax
      push   eax
_GetLCID:
_GetItemInfo:
_GetDocVersionString:
      pop    eax                ; return S_OK
      push   eax
      push   eax
_OnScriptTerminate:
      xor    eax, eax           ; return S_OK
      retn   3*4
_OnStateChange:
_OnScriptError:
      jmp    _GetDocVersionString
_OnEnterScript:
_OnLeaveScript:
      jmp    _Release
init_api:
      pop    ebp
      lea    esi, [ebp + (_ds_section - invoke_api)] 
      
      ; LoadLibrary("ole32");
      push   esi                    ; "ole32", 0
      mov    eax, 0xFA183D4A        ; eax = hash("LoadLibraryA")
      call   ebp                    ; invoke_api(eax)
      xchg   ebx, eax               ; ebp = base of ole32
      lodsd                         ; skip "ole32"
      lodsd
      
      ; _CoInitializeEx = GetProcAddress(ole32, "CoInitializeEx");
      mov    eax, 0x4AAC90F7        ; eax = hash("GetProcAddress")
      push   eax                    ; save eax/hash
      push   esi                    ; esi = "CoInitializeEx"
      push   ebx                    ; base of ole32
      call   ebp                    ; invoke_api(eax)

      ; 1. _CoInitializeEx(NULL, COINIT_MULTITHREADED);
      cdq                           ; edx = 0
      push   edx                    ; COINIT_MULTITHREADED
      push   edx                    ; NULL
      call   eax                    ; CoInitializeEx
      
      add    esi, co_init_len       ; skip "CoInitializeEx", 0
      
      ; _CoCreateInstance = GetProcAddress(ole32, "CoCreateInstance");
      pop    eax                    ; eax = hash("GetProcAddress")
      push   esi                    ; "CoCreateInstance"
      push   ebx                    ; base of ole32
      call   ebp                    ; invoke_api

      add    esi, co_create_len     ; skip "CoCreateInstance", 0
      
      ; 2. _CoCreateInstance(
          ; &langId, 0, CLSCTX_INPROC_SERVER, 
          ; &IID_IActiveScript, (void **)&engine);
      push   edi                    ; &engine
      scasd                         ; skip engine
      mov    ebx, edi               ; ebx = &parser
      push   edi                    ; &IID_IActiveScript
      movsd
      movsd
      movsd
      movsd
      push   CLSCTX_INPROC_SERVER
      push   0                      ; 
      push   esi                    ; &CLSID_VBScript or &CLSID_JScript
      call   eax                    ; _CoCreateInstance
      
      ; 3. Query engine for script parser
      ; engine->lpVtbl->QueryInterface(
      ;  engine, &IID_IActiveScriptParse, 
      ;  (void **)&parser);
      push   edi                    ; &parser
      push   ebx                    ; &IID_IActiveScriptParse32
      inc    dword[ebx]             ; add 1 for IActiveScriptParse32
      mov    esi, [ebx-4]           ; esi = engine
      push   esi                    ; engine
      mov    eax, [esi]             ; eax = engine->lpVtbl
      call   dword[eax + IUnknownVtbl.QueryInterface]
      
      ; 4. Initialize parser    
      ; parser->lpVtbl->InitNew(parser);
      mov    ebx, [edi]             ; ebx = parser
      push   ebx                    ; parser
      mov    eax, [ebx]             ; eax = parser->lpVtbl
      call   dword[eax + IActiveScriptParse32Vtbl.InitNew]
      
      ; 5. Initialize IActiveScriptSite
      lea    eax, [ebp + (_QueryInterface - invoke_api)]
      push   edi                    ; save pointer to IActiveScriptSiteVtbl
      stosd                         ; vft.QueryInterface      = (LPVOID)QueryInterface;
      add    eax, _AddRef  - _QueryInterface
      stosd                         ; vft.AddRef              = (LPVOID)AddRef;
      stosd                         ; vft.Release             = (LPVOID)Release;
      add    eax, _GetLCID - _Release
      stosd                         ; vft.GetLCID             = (LPVOID)GetLCID;
      stosd                         ; vft.GetItemInfo         = (LPVOID)GetItemInfo;
      stosd                         ; vft.GetDocVersionString = (LPVOID)GetDocVersionString;
      add    eax, _OnScriptTerminate - _GetDocVersionString
      stosd                         ; vft.OnScriptTerminate   = (LPVOID)OnScriptTerminate;
      add    eax, _OnStateChange - _OnScriptTerminate
      stosd                         ; vft.OnStateChange       = (LPVOID)OnStateChange;
      stosd                         ; vft.OnScriptError       = (LPVOID)OnScriptError;
      inc    eax
      inc    eax
      stosd                         ; vft.OnEnterScript       = (LPVOID)OnEnterScript;
      stosd                         ; vft.OnLeaveScript       = (LPVOID)OnLeaveScript;
      pop    eax                    ; eax = &vft
      
      ; 6. Set script site 
      ; engine->lpVtbl->SetScriptSite(
      ;   engine, (IActiveScriptSite *)&mas);
      push    edi                   ; &IMyActiveScriptSite
      stosd                         ; IActiveScriptSite.lpVtbl = &vft
      xor     eax, eax
      stosd                         ; IActiveScriptSiteWindow.lpVtbl = NULL
      push    esi                   ; engine
      mov     eax, [esi]
      call    dword[eax + IActiveScriptVtbl.SetScriptSite]

      ; 7. Parse our script
      ; parser->lpVtbl->ParseScriptText(
      ;     parser, cs, 0, 0, 0, 0, 0, 0, 0, 0);
      mov    edx, esp
      push   8
      pop    ecx
init_parse:
      push   eax                    ; 0
      loop   init_parse
      push   edx                    ; script
      push   ebx                    ; parser
      mov    eax, [ebx]
      call   dword[eax + IActiveScriptParse32Vtbl.ParseScriptText]
      
      ; 8. Run script
      ; engine->lpVtbl->SetScriptState(
      ;     engine, SCRIPTSTATE_CONNECTED);
      push   SCRIPTSTATE_CONNECTED
      push   esi
      mov    eax, [esi]
      call   dword[eax + IActiveScriptVtbl.SetScriptState]
      
      ; 9. cleanup
      ; parser->lpVtbl->Release(parser);
      push   ebx
      mov    eax, [ebx]
      call   dword[eax + IUnknownVtbl.Release]
      
      ; engine->lpVtbl->Close(engine);
      push   esi                    ; engine
      push   esi                    ; engine
      lodsd                         ; eax = lpVtbl
      xchg   eax, edi
      call   dword[edi + IActiveScriptVtbl.Close]
      ; engine->lpVtbl->Release(engine);
      call   dword[edi + IUnknownVtbl.Release]
     
      inc    eax                    ; eax = 4096 * 32
      shl    eax, 17
      add    esp, eax
      popad
      ret
      

Windows Script Host Objects

Two named objects (WSH and WScript) are added to the script namespace by wscript.exe/cscript.exe that do not require instantiating at runtime. The ‘WScript’ object is used primarily for console I/O, accessing arguments and the path of script on disk. It can also be used to terminate a script via the Quit method or poll operations via the Sleep method. The IActiveScript interface only provides basic scripting functionality, so if we want our host to support those objects, or indeed any custom objects, they must be implemented manually. Consider the following code taken from ReVBShell that expects to run inside WSH.

  While True
    ' receive command from remote HTTP server
    ' other code omitted
    Select Case strCommand
      Case "KILL"
        SendStatusUpdate strRawCommand, "Goodbye!"
        WScript.Quit 0
    End Select
  Wend

When this was used for testing Donut shellcode, the script engine stopped running upon reaching the line “WScript.Quit 0” because it didn’t recognize the WScript object. “On Error Resume Next” was enabled, and so the script simply kept executing. Once the name of this object was added to the namespace via IActiveScript::AddNamedItem, a request for ITypeInfo and IUnknown interfaces was made via IActiveScriptSite::GetItemInfo. If we don’t provide an interface for the request, the parser calls IActiveScriptSite::OnScriptError with the message “Variable is undefined ‘WScript'” before terminating.

To enable support for ‘WScript’ requires a custom implementation of the WScript interface defined in type information found in wscript.exe/cscript.exe. First, add the name of the object to the scripting engine’s namespace using AddNamedItem. This makes any methods, properties and events part of this object visible to the script.

obj = SysAllocString(L"WScript");
engine->lpVtbl->AddNamedItem(engine, (LPCOLESTR)obj, SCRIPTITEM_ISVISIBLE);

Obtain the type information from wscript.exe or cscript.exe. IID_IHost is simply the class identifier retrieved from aforementioned EXE files. Below is a screenshot of OleWoo, but other TLB viewers may work just as well.

ITypeLib  lpTypeLib;
ITypeInfo lpTypeInfo;

LoadTypeLib(L"WScript.exe", &lpTypeLib);
lpTypeLib->lpVtbl->GetTypeInfoOfGuid(lpTypeLib, &IID_IHost, &lpTypeInfo);

Now, when the scripting engine first encounters the ‘WScript’ object and requests an IUnknown interface via IActiveScriptSite::GetItemInfo, Donut returns a pointer to a minimal implementation of the IHost interface.

After this, the IDispatch::Invoke method will be used to call the ‘Quit’ method requested by the script. At the moment, Donut only implements Quit and Sleep methods, but others can be supported if requested.

Extensible Stylesheet Language Transformations (XSLT)

XSL files can contain interpreted languages like JScript/VBScript. The following code found here is based on this example by TheWover.

void run_xml_script(const char *path) {
    IXMLDOMDocument *pDoc; 
    IXMLDOMNode     *pNode;
    HRESULT         hr;
    PWCHAR          xml_str;
    VARIANT_BOOL    loaded;
    BSTR            res;
    
    xml_str = read_script(path);
    
    if(xml_str == NULL) return;
    
    // 1. Initialize COM
    hr = CoInitialize(NULL);
    if(hr == S_OK) {
      // 2. Instantiate XMLDOMDocument object
      hr = CoCreateInstance(
        &CLSID_DOMDocument30, 
        NULL, CLSCTX_INPROC_SERVER,
        &IID_IXMLDOMDocument, 
        (void**)&pDoc);
        
      if(hr == S_OK) {
        // 3. load XML file
        hr = pDoc->lpVtbl->loadXML(pDoc, xml_str, &loaded);
        if(hr == S_OK) {
          // 4. create node interface
          hr = pDoc->lpVtbl->QueryInterface(
            pDoc, &IID_IXMLDOMNode, (void **)&pNode);
            
          if(hr == S_OK) {
            // 5. execute script
            hr = pDoc->lpVtbl->transformNode(pDoc, pNode, &res);
            pNode->lpVtbl->Release(pNode);
          }
        }
        pDoc->lpVtbl->Release(pDoc);
      }
      CoUninitialize();
    }
    free(xml_str);
}

PC-Relative Addressing in C

The linker makes an assumption about where a PE file will be loaded in memory. Most EXE files request an image base address of 0x00400000 for 32-bit or 0x0000000140000000 for 64-bit. If the PE loader can’t map at the requested address, it uses relocation information to fix position-dependent code and data. ARM has support for PC-relative addressing via the ADR, ADRP and LDR opcodes, but poor old x86 lacks a similar instruction. x64 does support RIP-relative addressing, but there’s no guarantee a compiler will use it even if we tell it to (-fPIC and -fPIE for GCC). Because we’re using C for the shellcode, we need to manually calculate the address of a function relative to where the shellcode resides in memory. We could apply relocations in the same way a PE loader does, but self-modifying code can trigger some anti-malware programs. Instead, the program counter (EIP on x86 or RIP on x64) is read using some assembly and this is used to calculate the virtual address of a function in-memory. The following code stub is placed at the end of the payload and returns the value of the program counter.

#if defined(_MSC_VER) 
  #if defined(_M_X64)

    #define PC_CODE_SIZE 9 // sub rsp, 40 / call get_pc

    static char *get_pc_stub(void) {
      return (char*)_ReturnAddress() - PC_CODE_SIZE;
    }
    
    static char *get_pc(void) {
      return get_pc_stub();
    }

  #elif defined(_M_IX86)
    __declspec(naked) static char *get_pc(void) {
      __asm {
          call   pc_addr
        pc_addr:
          pop    eax
          sub    eax, 5
          ret
      }
    }
  #endif  
#elif defined(__GNUC__) 
  #if defined(__x86_64__)
    static char *get_pc(void) {
        __asm__ (
        "call   pc_addr\n"
      "pc_addr:\n"
        "pop    %rax\n"
        "sub    $5, %rax\n"
        "ret");
    }
  #elif defined(__i386__)
    static char *get_pc(void) {
        __asm__ (
        "call   pc_addr\n"
      "pc_addr:\n"
        "popl   %eax\n"
        "subl   $5, %eax\n"
        "ret");
    }
  #endif
#endif

With this code, the linker will calculate the Relative Virtual Address (RVA) by subtracting the offset of our target function from the offset of the get_pc() function. Then at runtime, it will subtract the RVA from the program counter returned by get_pc() to obtain the Virtual Address of the target function. The position of get_pc() must be placed at the end of a payload, otherwise this would not work. The following macro (named after the ARM opcode ADR) is used to calculate the virtual address of a function in-memory.

#define ADR(type, addr) (type)(get_pc() - ((ULONG_PTR)&get_pc - (ULONG_PTR)addr))

To illustrate how it’s used, the following code from the payload shows how to initialize the IActiveScriptSite interface.

// initialize virtual function table
static VOID ActiveScript_New(PDONUT_INSTANCE inst, IActiveScriptSite *this) {
    MyIActiveScriptSite *mas = (MyIActiveScriptSite*)this;
    
    // Initialize IUnknown
    mas->site.lpVtbl->QueryInterface      = ADR(LPVOID, ActiveScript_QueryInterface);
    mas->site.lpVtbl->AddRef              = ADR(LPVOID, ActiveScript_AddRef);
    mas->site.lpVtbl->Release             = ADR(LPVOID, ActiveScript_Release);
    
    // Initialize IActiveScriptSite
    mas->site.lpVtbl->GetLCID             = ADR(LPVOID, ActiveScript_GetLCID);
    mas->site.lpVtbl->GetItemInfo         = ADR(LPVOID, ActiveScript_GetItemInfo);
    mas->site.lpVtbl->GetDocVersionString = ADR(LPVOID, ActiveScript_GetDocVersionString);
    mas->site.lpVtbl->OnScriptTerminate   = ADR(LPVOID, ActiveScript_OnScriptTerminate);
    mas->site.lpVtbl->OnStateChange       = ADR(LPVOID, ActiveScript_OnStateChange);
    mas->site.lpVtbl->OnScriptError       = ADR(LPVOID, ActiveScript_OnScriptError);
    mas->site.lpVtbl->OnEnterScript       = ADR(LPVOID, ActiveScript_OnEnterScript);
    mas->site.lpVtbl->OnLeaveScript       = ADR(LPVOID, ActiveScript_OnLeaveScript);
    
    mas->site.m_cRef                      = 0;
    mas->inst                             = inst;
}

Dynamic Calls to DLL Functions

After implementing support for some WScript methods, providing access to DLL functions directly from VBScript/JScript using a similar approach is much easier to understand. The initial problem is how to load type information directly from memory. One solution to this can be found in A lightweight approach for exposing C++ objects to a hosted Active Scripting engine. Confronted with the same problem, the author uses CreateDispTypeInfo and CreateStdDispatch to create the ITypeInfo and IDispatch interfaces necessary for interpreted languages to call C++ objects. The same approach can be used to call DLL functions and doesn’t require COM registration.

Summary

v0.9.2 of Donut will support in-memory execution of JScript/VBScript and XSL files. Dynamic calls to DLL functions without COM registration will be supported in a future release.

This entry was posted in assembly, programming, security, shellcode, windows and tagged , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s