--- name: malware-deobfuscation description: > Defeat obfuscation, packing, and encryption used to hide malware functionality. Use when a sample is packed, encrypted, or obfuscated and you need to recover the original code for analysis. Covers automated and manual unpacking, string decryption, control flow deobfuscation, import table reconstruction, and multi-layer packing. Supports both offline (local tools) and online (API-enriched) modes on Linux and Windows. --- # Malware Deobfuscation Identify and defeat obfuscation, packing, and encryption techniques used by malware to hide its true functionality. Recover the original code, strings, and imports for further static and dynamic analysis. ## Prerequisites - **Linux**: `file`, `strings`, `upx`, `binwalk`, `python3`, `objdump`, `readelf` - **Windows**: `upx.exe`, Python 3, PE analysis tools (pestudio, CFF Explorer) - **Python packages**: `pefile`, `yara-python`, `capstone`, `unicorn` (optional for emulation) - **Optional**: IDA Pro / Ghidra / Binary Ninja for manual analysis ## Step-by-Step Instructions ### Step 1: Identify Packing and Obfuscation Type Determine whether the sample is packed, and if so, identify the packer. **Entropy analysis (high entropy = likely packed/encrypted):** ```bash python3 scripts/packer_detector.py --file sample.exe ``` This script performs: - Section-level entropy analysis (entropy > 7.0 suggests packing) - Packer signature matching against a database of known packers - PE header anomaly detection (small code section, large data sections) - Import table analysis (very few imports suggests packed binary) **Manual entropy check:** ```bash python3 -c " import math, sys data = open(sys.argv[1], 'rb').read() entropy = -sum((c/len(data)) * math.log2(c/len(data)) for c in [data.count(bytes([b])) for b in range(256)] if c > 0) print(f'Overall entropy: {entropy:.4f}') print('Likely packed' if entropy > 7.0 else 'Likely not packed') " sample.exe ``` **Check for known packer strings:** ```bash strings sample.exe | grep -iE "(UPX|ASPack|Themida|VMProtect|Enigma|PECompact|MPRESS|Armadillo|Obsidium)" ``` See `references/packer-identification.md` for a comprehensive guide to identifying packers by entropy patterns, section names, and signature bytes. ### Step 2: Attempt Automated Unpacking For known packers, try automated unpacking first. **UPX (most common):** ```bash bash scripts/upx_unpack.sh sample.exe unpacked_sample.exe ``` Or manually: ```bash # Linux upx -d sample.exe -o unpacked_sample.exe # Windows upx.exe -d sample.exe -o unpacked_sample.exe ``` **Verify unpacking succeeded:** ```bash file unpacked_sample.exe python3 scripts/packer_detector.py --file unpacked_sample.exe ``` A successful unpack should show: - Lower entropy (below 7.0) - More readable strings - Complete import table - Standard section names (.text, .data, .rdata) **Other automated unpackers:** | Packer | Tool | |--------|------| | UPX | `upx -d` | | ASPack | `AspackDie`, manual OEP finding | | PECompact | `PECompact unpacker` | | MPRESS | `upx -d` (sometimes works) | | .NET obfuscators | `de4dot`, `dnSpy` | ### Step 3: Manual Unpacking (When Automated Fails) If automated unpacking fails, perform manual unpacking: 1. **Find the Original Entry Point (OEP):** - Load in debugger (x64dbg, OllyDbg, or GDB) - Set breakpoint on common OEP patterns: - `VirtualAlloc` / `VirtualProtect` (memory allocation for unpacking) - `WriteProcessMemory` (self-modifying code) - Tail jump after unpacking stub - Run until the unpacking stub completes - The OEP is typically reached via a far jump after decompression 2. **Dump the unpacked process:** ``` # In x64dbg: use Scylla plugin # In OllyDbg: use OllyDumpEx # On Linux with GDB: gdb -p (gdb) dump binary memory dumped.bin 0x400000 0x500000 ``` 3. **Fix the import table** (see Step 7) ### Step 4: Decrypt Encrypted Strings Malware frequently encrypts strings to hide IOCs, C2 addresses, and API names. **Using the string decryptor framework:** ```bash # Single-byte XOR python3 scripts/string_decryptor.py --method xor-single --key 0x5A --input encrypted.bin # Multi-byte XOR python3 scripts/string_decryptor.py --method xor-multi --key "secret" --input encrypted.bin # Rolling XOR python3 scripts/string_decryptor.py --method xor-rolling --key 0x41 --input encrypted.bin # Base64 python3 scripts/string_decryptor.py --method base64 --input encoded_strings.txt # Custom alphabet Base64 python3 scripts/string_decryptor.py --method base64-custom --alphabet "ZYXWVUTSRQPONMLKJIHGFEDCBAzyxwvutsrqponmlkjihgfedcba9876543210+/" --input encoded.bin # RC4 python3 scripts/string_decryptor.py --method rc4 --key "malwarekey" --input encrypted.bin # AES-CBC python3 scripts/string_decryptor.py --method aes-cbc --key 0102030405060708090a0b0c0d0e0f10 --iv 00000000000000000000000000000000 --input encrypted.bin # Brute-force single-byte XOR python3 scripts/string_decryptor.py --method xor-brute --input encrypted.bin ``` See `references/common-encryption.md` for identifying which encryption scheme a sample uses and approaches for each. **Identifying encryption in disassembly:** - XOR loops with a constant or key array - Calls to `CryptDecrypt`, `BCryptDecrypt` (Windows CryptoAPI) - AES S-box constants (`0x63, 0x7c, 0x77, 0x7b...`) - RC4 key scheduling (256-byte array initialization loop) ### Step 5: Deobfuscate Control Flow Obfuscated malware may use control flow flattening, opaque predicates, and dead code insertion. **Control flow flattening:** - Look for a dispatcher loop with a state variable - Map each state to its corresponding basic block - Reconstruct the original control flow by tracing state transitions - In Ghidra/IDA: use graph view to identify the dispatcher pattern **Opaque predicates:** - Identify conditional jumps that always take the same path - Common patterns: `x * (x-1) % 2 == 0` (always true for integers) - Replace with unconditional jumps or NOPs - Use symbolic execution (angr) to prove predicates are constant **Dead code elimination:** - Identify code blocks that are never reached - Look for computations whose results are never used - Focus analysis on the reachable code paths **Using angr for automated deobfuscation (Python):** ```python import angr proj = angr.Project("obfuscated_sample", auto_load_libs=False) cfg = proj.analyses.CFGFast() # Analyze control flow graph for dispatcher patterns ``` See `references/deobfuscation-techniques.md` for detailed manual techniques. ### Step 6: Handle Multi-Layer Packing Some malware uses multiple layers of packing/encryption. **Approach:** 1. Unpack the outermost layer first 2. Analyze the result to determine if another layer exists 3. Repeat until you reach the final payload 4. Document each layer for the analysis report **Detection of remaining layers:** ```bash # After each unpack, re-check entropy and packer signatures python3 scripts/packer_detector.py --file partially_unpacked.exe ``` **Common multi-layer patterns:** - Crypter (outer) -> Packer (inner) -> Original binary - Custom stub -> UPX -> Original binary - .NET packer -> Native loader -> Shellcode -> Final payload - Script dropper -> Encoded PE -> Packed PE -> Original binary ### Step 7: Reconstruct Import Tables Packed binaries often have destroyed or minimal import tables. **Using Scylla (Windows, with x64dbg):** 1. Run the sample to OEP in x64dbg 2. Open Scylla plugin 3. Set OEP address 4. Click "IAT Autosearch" then "Get Imports" 5. Fix any unresolved imports manually 6. Click "Fix Dump" on the memory dump **Using ImpREC:** 1. Run the packed executable 2. Attach ImpREC to the process 3. Set the OEP value 4. Click "AutoSearch" for IAT 5. Click "Get Imports" 6. Fix the dump file **Manual import reconstruction on Linux:** ```bash # Examine the unpacked binary for import patterns objdump -d unpacked.bin | grep -E "call.*\[" | head -20 readelf -d unpacked.bin ``` ### Step 8: Validate Unpacked Binary Confirm the unpacking was successful: ```bash # Compare entropy before and after python3 scripts/packer_detector.py --file original_packed.exe python3 scripts/packer_detector.py --file unpacked.exe # Check strings are readable strings unpacked.exe | wc -l strings unpacked.exe | grep -iE "(http|\.dll|\.exe|cmd|powershell)" | head -20 # Verify PE structure (Windows PE) python3 -c " import pefile pe = pefile.PE('unpacked.exe') print(f'Entry point: 0x{pe.OPTIONAL_HEADER.AddressOfEntryPoint:08x}') print(f'Sections: {len(pe.sections)}') for s in pe.sections: print(f' {s.Name.decode().strip(chr(0)):8s} entropy={s.get_entropy():.2f} size={s.SizeOfRawData}') print(f'Imports: {len(pe.DIRECTORY_ENTRY_IMPORT)}') for entry in pe.DIRECTORY_ENTRY_IMPORT: print(f' {entry.dll.decode()}: {len(entry.imports)} functions') " # Verify the unpacked binary runs (in sandbox only!) # Compare dynamic behavior with the packed version ``` ## Output Format The packer detector produces JSON output: ```json { "file": "sample.exe", "overall_entropy": 7.82, "likely_packed": true, "packer_signatures": ["UPX"], "sections": [ {"name": "UPX0", "entropy": 0.0, "raw_size": 0}, {"name": "UPX1", "entropy": 7.95, "raw_size": 233472} ], "import_count": 5, "suspicious_indicators": [ "Very few imports (< 10)", "High overall entropy", "Known packer section names" ] } ``` ## Tips - Always work on copies of samples, never modify the original - Some packers detect tampering; use a VM snapshot you can revert - If UPX unpacking fails, the sample may have a modified UPX header - For .NET malware, try dnSpy or de4dot before manual approaches - Emulation-based unpacking (QEMU, Unicorn) can handle custom packers - Keep a log of each deobfuscation step for reproducibility - Multi-layer packing is common in targeted attacks; be patient and systematic