Introduction
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.
- Each Class Identifier in
HKEY_CLASSES_ROOT\CLSID\that contains a subkey calledOLEScriptcan be used with Windows Script Hosting. - The Component Categories Manager can enumerate CLSID for category identifiers
CATID_ActiveScriptorCATID_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.