Chapter 21

Code Injection Detection

Code injection is the primary technique modern malware uses to hide inside legitimate processes. This chapter covers the main injection techniques — process hollowing, reflective DLL injection, shellcode injection — what each leaves in memory, and how to detect them with Volatility plugins and YARA rules.

Scenario

Cobalt Strike is running inside svchost.exe (PID 1384). The process looks legitimate from the outside — it's a real svchost.exe spawned by services.exe with a valid service path. But Cobalt Strike's beacon DLL was injected into it via reflective DLL injection: the beacon exists only in the process's memory space, not as a file on disk. From the attacker's perspective, the process hides among hundreds of legitimate svchost instances. From the forensic perspective, malfind shows a PAGE_EXECUTE_READWRITE region in PID 1384 at a virtual address not mapped to any on-disk DLL. The beacon config — including the C2 address, sleep time, and jitter — is in plaintext in that memory region after the attacker's decryption routine runs. This chapter shows you how to find it and extract it.

Code Injection Techniques and Memory Signatures

TechniqueHow it worksMemory signatureVolatility detection
Classic DLL injectionWriteProcessMemory + CreateRemoteThread — copies DLL path to target process and calls LoadLibraryNew DLL in process DLL list not present in legitimate process. DLL from unusual path.windows.dlllist — look for DLLs in temp/user dirs; path mismatch vs normal svchost.exe DLLs
Reflective DLL injectionFull DLL copied to target process memory with its own loader. No file on disk. DLL maps itself into memory.PE header in RWX anonymous memory (no backing file). malfind hits with MZ/PE header. Not in DLL list.windows.malfind — MZ header in private RWX page
Process hollowingLegitimate process created in suspended state, its code section unmapped, replaced with malware code, then resumedProcess has legitimate name/path but memory content doesn't match the file on disk. Differs from disk.windows.hollowfind or compare VAD entries vs on-disk PE headers
Shellcode injectionRaw shellcode (not a PE) written to target process memory and executed via CreateRemoteThread or APCsRWX anonymous memory with x86/x64 opcodes but no MZ header.windows.malfind — no PE header but executable private memory
Thread HijackingTarget thread suspended, its context modified to execute injected code, then resumedExisting thread with instruction pointer in anonymous memory regionwindows.threads + check if EIP/RIP points to mapped vs unmapped region

malfind Deep Dive Workflow

Bashmalfind-analysis.sh
RAM="/cases/CASE001/RAM.raw"
OUT="/cases/CASE001/memory"

# Run malfind, dump suspicious regions to files
vol -f $RAM windows.malfind --dump --output-dir $OUT/malfind-dumps/ \
  --output csv > $OUT/malfind.csv

# Review the malfind.csv — look for:
# PAGE_EXECUTE_READWRITE (Protection column) + PrivateMemory=True
# Process names that should have clean memory: svchost, lsass, explorer

# For each suspicious region dump (PID.ProcName.xxx.dmp):
# 1. Check for PE header (MZ signature at offset 0)
for dmp in $OUT/malfind-dumps/*.dmp; do
    head=$(xxd "$dmp" | head -1)
    echo "[$dmp]"
    echo "  Header bytes: $head"
    # MZ = 4d 5a = PE; \x90 = NOP sled (shellcode); E8/E9 = JMP/CALL (shellcode)
    echo ""
done

# 2. Run YARA rules against dumps
yara /opt/yara-rules/cobaltstrike.yar $OUT/malfind-dumps/
yara /opt/yara-rules/meterpreter.yar  $OUT/malfind-dumps/

# 3. Hash each dump for VT lookup
for dmp in $OUT/malfind-dumps/*.dmp; do
    sha256sum "$dmp"
done

Cobalt Strike Memory Signatures

Cobalt Strike is the most commonly encountered commercial C2 framework in incident response. Its beacon has characteristic memory patterns that survive obfuscation.

  Cobalt Strike Beacon Memory Indicators
  ═══════════════════════════════════════════════════════════════════

  Process indicators:
    ├── svchost.exe with unexpected network connections (beacon check-in)
    ├── Named pipe with pattern: \\.\pipe\MSSE-{random}-server  ← classic
    ├── Named pipe with pattern: \\.\pipe\postex_{random}       ← newer
    └── Suspicious RWX region in svchost.exe / spoolsv.exe

  Beacon config structure in memory (after decryption):
    ├── MagicMZ or xor-encoded PE header
    ├── Config structure with C2 hostname, port, sleep, jitter
    └── Public key for C2 communication

  YARA signature indicators:
    /x00/x00/x00/x00/x00/x00/x00/x00/x00/x00/x00/x00/x00/x00/x00/x00
    followed by 0x69 0x68 0x69 0x68 (CS config magic bytes — some versions)

  Extraction tool: 1768.py (SentinelOne) / cs-decrypt-metadata
    python3 1768.py memory_dump.bin
    → Outputs: C2 hostname, port, sleep interval, jitter, user-agent

  Named pipe enumeration:
    vol -f RAM.raw windows.handles --pid 1234 | grep -i pipe
    → Named pipes starting with MSSE, postex, or mojo patterns
Pythoncs-beacon-extract.py
"""
Extract Cobalt Strike beacon configuration from a memory dump.
Uses the 1768.py approach (based on Mark Baggett / SentinelOne research).
Simplified for demonstration — use 1768.py in production.
"""
import struct
import re
import sys

def find_cs_config(dump_path: str):
    with open(dump_path, "rb") as f:
        data = f.read()

    # Look for CS4 configuration XOR key patterns
    # CS beacons store config as XOR-encoded structure
    # Key indicator: 16 null bytes followed by config
    patterns = [
        rb'\x00{16}.{1,2048}(?:443|80|8080|8443).{1,100}(?:\.com|\.net|\.org)',
        rb'\xfc\x48\x89',  # CS shellcode prologue pattern
    ]

    for pattern in patterns:
        matches = list(re.finditer(pattern, data, re.DOTALL))
        for match in matches[:5]:  # limit output
            offset = match.start()
            print(f"Potential config at offset: 0x{offset:08x}")
            # Print surrounding bytes for manual inspection
            context = data[max(0, offset-16):offset+256]
            print(f"  Context hex: {context.hex()}")
            print()

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print(f"Usage: {sys.argv[0]} ")
        sys.exit(1)
    find_cs_config(sys.argv[1])

Process Hollowing Detection

Bashhollowfind.sh
RAM="/cases/CASE001/RAM.raw"

# Volatility 3 doesn't have a direct hollowfind equivalent to Vol2
# The manual approach: compare VAD (Virtual Address Descriptor) entries
# against what the on-disk PE file contains

# Step 1: Get process list with image paths
vol -f $RAM windows.pslist --output csv | grep -i "svchost\|explorer\|lsass" > /tmp/procs.csv

# Step 2: For suspicious processes, check their VAD entries
# A hollowed process has its main image (offset 0x1000) not matching the disk file
vol -f $RAM windows.vadinfo --pid 1384 > /tmp/pid1384-vad.txt

# Step 3: Dump the process's PE image from memory
vol -f $RAM windows.dumpfiles --pid 1384 --output-dir /tmp/dumps/

# Step 4: Compare hash of dumped PE vs hash of the real executable on disk
# If they differ, the process's code section was replaced (hollowed)
md5sum /tmp/dumps/pid.1384.svchost.exe.img
md5sum C:/Windows/System32/svchost.exe   # from disk image

# Different hashes = strong hollowing indicator
# Note: page differences alone are common (relocations) —
# the important difference is the CODE section, not data sections

Q & A

Q: malfind returns hundreds of hits, including many in browsers and .NET applications. How do you triage efficiently?

Triage by process priority, not by raw hit count: (1) First, eliminate expected false positives: browser processes (Chrome, Edge, Firefox) will always have JIT-compiled RWX pages — ignore them unless the process has network connections to known-malicious infrastructure. (2) Node.js, Java JVM, .NET CLR processes have RWX JIT pages by design — deprioritize unless other indicators are present. (3) Focus on processes that should never have dynamic code: svchost.exe, explorer.exe, lsass.exe, spoolsv.exe, taskhost.exe. RWX anonymous memory in these processes is almost always injection. (4) Cross-reference malfind hits with netscan — if a process has both a malfind hit AND an unexpected network connection, that combination is very high priority. (5) Check the dump size — a 200KB malfind dump in svchost is much more interesting than a 4KB shellcode stub. Larger injected regions often contain the full beacon DLL with recoverable configuration.