--- name: string-decryption description: > Extract and decrypt obfuscated strings from malware samples including XOR-encoded, RC4-encrypted, AES-CBC, base64-encoded, and stack-constructed strings. Use when static analysis reveals encrypted or obfuscated strings hiding C2 addresses, API names, or configuration data. Covers automated extraction with FLOSS, IDAPython/Ghidra scripting, .NET string decryption, dynamic extraction via debugger breakpoints, and building custom decryptors from reverse-engineered algorithms. Supports both Linux and Windows analysis environments. --- # String Decryption Identify, extract, and decrypt obfuscated strings embedded in malware samples to reveal hidden indicators of compromise, C2 infrastructure, API calls, and configuration data. ## Prerequisites - **Linux**: `python3`, `strings`, `file`, `xxd`, `radare2` (optional) - **Windows**: Python 3, x64dbg or WinDbg, IDA Pro or Ghidra - **Python packages**: `pefile`, `yara-python`, `capstone`, `unicorn`, `dnfile` (for .NET) - **Tools**: [FLOSS](https://github.com/mandiant/flare-floss) (FireEye Labs Obfuscated String Solver) - **Optional**: IDA Pro with IDAPython, Ghidra with Ghidrathon, dnSpy / ilspy for .NET Install Python dependencies: ```bash pip install pefile yara-python capstone unicorn dnfile cryptography ``` Install FLOSS: ```bash # Download latest release from GitHub wget https://github.com/mandiant/flare-floss/releases/latest/download/floss-linux.zip unzip floss-linux.zip -d /opt/floss chmod +x /opt/floss/floss export PATH=$PATH:/opt/floss ``` ## Step-by-Step Instructions ### Step 1: Identify Encrypted and Obfuscated Strings Determine whether the sample contains encrypted or obfuscated strings and characterize the obfuscation method. **Entropy analysis to find high-entropy regions (likely encrypted data):** ```bash python3 scripts/string_decryptor.py --mode detect --binary sample.exe --output detection_report.json ``` This script performs: - Section-by-section entropy analysis (entropy > 7.0 in data sections suggests encryption) - Scanning for XOR patterns (loops with XOR instructions operating on memory) - Detection of base64-encoded blobs (regex matching for base64 character sets) - Identification of stack string construction patterns - Locating high-entropy byte sequences within code sections **Manual entropy scanning per section:** ```bash python3 -c " import pefile, math pe = pefile.PE('sample.exe') for section in pe.sections: data = section.get_data() if not data: continue freq = [0]*256 for b in data: freq[b] += 1 ent = -sum((f/len(data))*math.log2(f/len(data)) for f in freq if f > 0) print(f'{section.Name.decode().strip(chr(0)):10s} entropy={ent:.4f} size={len(data):>8d} ' f'{"[HIGH - likely encrypted]" if ent > 7.0 else ""}' f'{"[MODERATE - possible encoding]" if 6.0 < ent <= 7.0 else ""}') " ``` **Check for common obfuscation indicators:** ```bash # Very few readable strings compared to binary size suggests obfuscation strings -n 6 sample.exe | wc -l file sample.exe ls -la sample.exe # Search for base64-encoded blobs strings -n 20 sample.exe | grep -E '^[A-Za-z0-9+/]{20,}={0,2}$' # Search for hex-encoded strings strings -n 20 sample.exe | grep -E '^[0-9a-fA-F]{20,}$' # Look for XOR key candidates in the binary strings sample.exe | grep -iE "(decrypt|encode|decode|xor|cipher|crypt|key|rc4|aes)" ``` ### Step 2: Automated Extraction with FLOSS FLOSS automatically extracts obfuscated strings using static analysis, stack string recovery, and emulation-based decoding. **Run FLOSS on the sample:** ```bash # Full analysis (static + stack strings + decoded strings) floss sample.exe -o floss_output.json --json # Extract only decoded (emulated) strings floss sample.exe --only decoded # Extract only stack strings floss sample.exe --only stack # Increase analysis timeout for complex samples floss sample.exe --timeout 600 # Verbose output for troubleshooting floss sample.exe -v ``` **Parse FLOSS output for actionable strings:** ```bash # Extract URLs and domains from FLOSS output cat floss_output.json | python3 -c " import json, sys, re data = json.load(sys.stdin) for category in ['decoded_strings', 'stack_strings', 'static_strings']: strings = data.get(category, []) for s in strings: text = s if isinstance(s, str) else s.get('string', '') if re.search(r'https?://|[a-zA-Z0-9.-]+\.(com|net|org|ru|cn|tk|xyz|top|cc)', text): print(f'[{category}] {text}') " ``` ### Step 3: Decrypt Common Encryption Methods Apply targeted decryption based on the identified encryption scheme. **Single-byte XOR decryption:** ```bash # If the XOR key is known (e.g., 0x5A found through analysis) python3 scripts/string_decryptor.py --mode decrypt --method xor-single --key 0x5A \ --input encrypted_blob.bin --output decrypted.bin --extract-strings # Brute-force single-byte XOR (tries all 256 keys, ranks by printability) python3 scripts/string_decryptor.py --mode decrypt --method xor-brute \ --input encrypted_blob.bin --output brute_results.json ``` **Multi-byte / rolling XOR decryption:** ```bash # Multi-byte repeating key XOR python3 scripts/string_decryptor.py --mode decrypt --method xor-multi \ --key "secretkey" --input encrypted.bin # Rolling XOR (key evolves with each byte) python3 scripts/string_decryptor.py --mode decrypt --method xor-rolling \ --key 0x41 --input encrypted.bin ``` **RC4 decryption (common in RATs like Gh0st, PlugX, Cobalt Strike):** ```bash python3 scripts/string_decryptor.py --mode decrypt --method rc4 \ --key "malwarekey123" --input encrypted.bin --extract-strings ``` **AES-CBC with hardcoded keys (common in modern malware):** ```bash python3 scripts/string_decryptor.py --mode decrypt --method aes-cbc \ --key 0102030405060708090a0b0c0d0e0f10 \ --iv 00000000000000000000000000000000 \ --input encrypted.bin --output decrypted.bin ``` **Stack string reconstruction:** Stack strings are built character-by-character on the stack to avoid appearing in the strings table. ```bash # FLOSS is the best tool for stack strings floss sample.exe --only stack # Manual identification in disassembly: look for patterns like: # mov [rbp-0x20], 0x68 ; 'h' # mov [rbp-0x1f], 0x74 ; 't' # mov [rbp-0x1e], 0x74 ; 't' # mov [rbp-0x1d], 0x70 ; 'p' ``` ### Step 4: IDAPython and Ghidra Scripts for Batch Decryption When you identify a decryption routine in the binary, write scripts to decrypt all strings at once. **IDAPython script template for batch XOR decryption:** ```python # Run in IDA Pro: File -> Script File import idautils import idc import ida_bytes def decrypt_xor_strings(xor_key, encrypted_refs): """Decrypt all XOR-encrypted strings referenced in the binary.""" for ea in encrypted_refs: encrypted = ida_bytes.get_bytes(ea, 256) if not encrypted: continue # Decrypt until null terminator decrypted = [] for b in encrypted: dec = b ^ xor_key if dec == 0: break decrypted.append(dec) result = bytes(decrypted).decode('utf-8', errors='replace') # Add comment at the reference address idc.set_cmt(ea, f'Decrypted: "{result}"', 0) print(f"0x{ea:08X}: {result}") # Find all cross-references to the decryption function decrypt_func = idc.get_name_ea_simple("decrypt_string") if decrypt_func != idc.BADADDR: xrefs = [ref.frm for ref in idautils.XrefsTo(decrypt_func)] decrypt_xor_strings(0x5A, xrefs) ``` **Ghidra script template (Python via Ghidrathon):** ```python # Run in Ghidra: Script Manager -> Run from ghidra.program.model.symbol import RefType def find_decrypt_calls(func_name): """Find all calls to the decryption function.""" fm = currentProgram.getFunctionManager() funcs = fm.getFunctions(True) for func in funcs: if func.getName() == func_name: refs = getReferencesTo(func.getEntryPoint()) return [ref.getFromAddress() for ref in refs if ref.getReferenceType() == RefType.UNCONDITIONAL_CALL] return [] def read_bytes_at(addr, length): """Read bytes from the program at a given address.""" mem = currentProgram.getMemory() buf = bytearray(length) mem.getBytes(addr, buf) return bytes(buf) # Example: decrypt all strings with XOR key 0x5A call_sites = find_decrypt_calls("FUN_00401234") for site in call_sites: print(f"Decryption call at: {site}") ``` ### Step 5: .NET String Decryption .NET malware frequently uses obfuscators like ConfuserEx, SmartAssembly, or custom string encryption. **ConfuserEx string decryption:** ```bash # Use de4dot for automated .NET deobfuscation de4dot sample.exe -o deobfuscated.exe # Verify deobfuscation strings deobfuscated.exe | wc -l # Compare with original strings sample.exe | wc -l ``` **Using dnSpy for interactive .NET string decryption:** 1. Load the .NET assembly in dnSpy 2. Find the string decryption method (often called from a static constructor) 3. Set a breakpoint on the decryption method's return statement 4. Run with debugging - each break reveals a decrypted string 5. Use the Analyzer pane to find all callers of the decryption method **Custom .NET string decryptor with dnfile:** ```python import dnfile import base64 def extract_dotnet_user_strings(filepath): """Extract all user strings from .NET metadata.""" pe = dnfile.dnPE(filepath) if hasattr(pe, 'net') and hasattr(pe.net, 'user_strings'): strings = [] for entry in pe.net.user_strings: if entry.value and len(entry.value) > 3: strings.append(entry.value) return strings return [] # SmartAssembly-style decryption (common pattern) def decrypt_smartassembly(encrypted_bytes, key): """Decrypt SmartAssembly-style string encryption.""" result = bytearray() for i, b in enumerate(encrypted_bytes): result.append(b ^ key[i % len(key)]) return result.decode('utf-16-le', errors='replace') ``` ### Step 6: Dynamic String Extraction via Debugger Breakpoints Extract strings at runtime by setting breakpoints on decryption routine outputs. **x64dbg approach:** 1. Identify the decryption function address through static analysis 2. Set a breakpoint at the function's return instruction (`ret`) 3. Log the return value (typically a pointer to the decrypted string in EAX/RAX) 4. Use conditional logging: `log "Decrypted: {s:[$result]}"` at the breakpoint 5. Run the sample and collect all logged strings **GDB/Linux approach:** ```bash # Break on the decryption function return and dump the result gdb -batch -ex "file sample.elf" \ -ex "break *0x08048567" \ -ex "commands" \ -ex " x/s \$eax" \ -ex " continue" \ -ex "end" \ -ex "run" 2>&1 | grep "0x" ``` **Frida-based dynamic string extraction:** ```python import frida import sys js_code = """ Interceptor.attach(ptr("0x00401234"), { onLeave: function(retval) { try { var str = retval.readUtf8String(); if (str && str.length > 2) { send({type: "decrypted_string", value: str, address: retval.toString()}); } } catch(e) {} } }); """ def on_message(message, data): if message['type'] == 'send': payload = message['payload'] print(f"[+] {payload['address']}: {payload['value']}") pid = frida.spawn(["./sample.exe"]) session = frida.attach(pid) script = session.create_script(js_code) script.on('message', on_message) script.load() frida.resume(pid) sys.stdin.read() ``` ### Step 7: Build Custom Decryptors from Identified Algorithms When you reverse engineer a custom encryption algorithm, implement it for batch decryption. **Workflow for building a custom decryptor:** 1. **Identify the algorithm** in the disassembly/decompilation 2. **Extract key material** (hardcoded keys, IVs, S-boxes) 3. **Reimplement the algorithm** in Python 4. **Locate all encrypted blobs** in the binary (cross-references to the encrypt function) 5. **Batch decrypt** all strings and annotate the disassembly ```bash # Use the string decryptor framework with a custom algorithm python3 scripts/string_decryptor.py --mode batch --binary sample.exe \ --decrypt-func-addr 0x00401234 --key 0x5A \ --output all_decrypted_strings.json ``` **Identifying the algorithm from disassembly patterns:** | Pattern | Likely Algorithm | |---------|-----------------| | Single XOR in a loop with constant byte | Single-byte XOR | | XOR in a loop with array index modulo key length | Multi-byte XOR | | 256-byte array initialization + swap loop | RC4 | | S-box lookup `0x63, 0x7c, 0x77, 0x7b...` | AES | | Feistel network structure (split, round, swap) | DES/3DES | | Character-by-character `mov [rbp+offset], imm8` | Stack strings | | Base64 alphabet string in data section | Base64 encoding | | Bit rotation (`ROL`/`ROR`) in loop | Custom cipher | ## Output Format The string decryptor produces JSON output: ```json { "metadata": { "tool": "string_decryptor.py", "timestamp": "2025-01-15T10:30:00Z", "binary": "sample.exe", "sha256": "a1b2c3d4e5f6...", "mode": "detect" }, "detection": { "overall_entropy": 7.42, "high_entropy_sections": [ {"name": ".data", "entropy": 7.85, "size": 45056, "offset": "0x00005000"} ], "xor_patterns_found": 3, "base64_blobs_found": 2, "stack_string_candidates": 15, "suspected_encryption": "single-byte XOR with key rotation" }, "decrypted_strings": [ { "offset": "0x00405120", "method": "xor-single", "key": "0x5A", "encrypted_hex": "32393a2f2f...", "decrypted": "http://malware-c2.example.com/gate.php", "category": "c2_url", "confidence": "high" }, { "offset": "0x00405180", "method": "xor-single", "key": "0x5A", "encrypted_hex": "2b3924332e...", "decrypted": "cmd.exe /c whoami", "category": "command", "confidence": "high" }, { "offset": "0x004051C0", "method": "stack-string", "key": null, "encrypted_hex": null, "decrypted": "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run", "category": "registry_key", "confidence": "medium" } ], "statistics": { "total_encrypted_regions": 45, "successfully_decrypted": 42, "failed": 3, "unique_strings": 38, "categories": { "c2_url": 5, "command": 8, "registry_key": 3, "api_name": 15, "file_path": 7 } } } ``` ## Tips - Start with FLOSS for quick wins before investing in manual decryption - XOR with 0x00 is a no-op; skip key 0x00 when brute-forcing single-byte XOR - Many malware families reuse the same encryption across versions - check public reports first - Stack strings are commonly used for API names to evade import table analysis - For RC4, the key scheduling algorithm's 256-iteration swap loop is a reliable signature - When brute-forcing XOR, look for known plaintext (MZ header, http://, common DLL names) to identify the key - .NET obfuscators often store the decryption key in the assembly metadata or a resource stream - Use YARA rules to identify encrypted string patterns across multiple samples in a campaign - Document every decrypted string with its offset - this helps correlate with behavioral analysis - If dynamic extraction is needed, use a snapshot-capable VM to revert after each run