When to Use / When Not to Use
- Use: when data/traffic needs to be decrypted but the key is unknown (static first: hardcoding → resources → import table; dynamic: memory dump)
- Use: to confirm whether the sample has a hardcoded key/password (configuration extraction)
- Use: the key is derived at runtime (PBKDF/HKDF) and the derivation process needs to be recovered
- Don't use: the algorithm itself hasn't been confirmed yet (do [[re-crypto-id]] first)
- Don't use: the key is delivered via external configuration/server (there is no local key to extract—honestly tell the user, see Pitfall 2)
- Don't use: only doing static format analysis ([[re-triage]] / [[re-format-pe]])
Tool Preparation
Verify all tools before use. Static search can skip the sandbox; memory dump/dynamic steps follow [[re-memdump]] default dump-first + [[re-analyze/platform-tips]] highest principle (run samples in a sandbox).
strings — quick scan for printable strings (all platforms)
- Linux:
apt install binutils/dnf install binutils/pacman -S binutils(most come with it) - macOS: bundled /usr/bin/strings
- Windows/WSL: Linux version inside WSL; on native Windows use Ghidra or
strings.exe(Sysinternals) - Verify:
strings --version(the GNU version has --version)
ghidra / rizin — decompilation and cross-reference search (for installation see [[re-ghidra]] / [[re-radare2]])
- Searching cross-references of constants/strings is the key to finding "who uses this key"
- Verify:
ghidra(GUI) orrz-ghidraplugins available;rizin -v
Dump artifacts — raw material for memory search ([[re-memdump]] default dump)
- Follow [[re-memdump]] Step 1 and use
gcore -o out <pid>to dump; the dump timing is determined by plaintext/code materialization (near the OEP for a single-layer packer is usually suitable; OEP is not a universal criterion, see [[re-analyze/platform-tips]] key experience) - Verify:
file outconfirm it is an ELF core,eu-stack -e outcan run
python3 — entropy-block/pattern scanning script
- For installation and verification see [[re-proto-rev]] tool preparation (python3)
Operating Steps
Execute in order, record the result of each step. Strategy order:static before dynamic(see Pitfall 3); record the artifact of each step (key/password + source evidence: offset, function name, dump path) for use by [[re-crypto-decrypt]].
-
Static: strings / cross-references to find hardcoding:
strings -n 6 sample.bin | grep -iE 'key|secret|pass|token|crypt|iv' | head -50 strings -el sample.bin | grep -iE 'key|secret|pass' | head -20 # UTF-16LE(Windows 常见) strings -n 8 sample.bin | head -100 # 全量扫描人工过一遍- For suspicious strings (fixed strings that look like keys), use the decompiler to check cross-references: [[re-ghidra]] / [[re-ida]] right-click Find References—see which function reads it and how it participates in computation (goes directly into encryption parameters → it's a key; participates in table lookup/comparison → it's a password or salt)
- Search constants in the decompiler (
Search > Memory/:> /v 0x...): 32-byte hex strings, repeated random data segments - Hex side: after using the [[re-crypto-id]] Step 1 script to find constant tables such as the AES S-box, the memory data near the table is often key material
-
Memory: after dumping, search for key patterns (16/32-byte entropy blocks, printable password strings):
gcore -o out <pid> # 默认转储([[re-memdump]]),按 materialization 定时机data = open('out','rb').read() import collections, math # 16/32 字节高熵块(AES-128/256 密钥候选) def ent(blk): c = collections.Counter(blk); n = len(blk) return -sum((v/n)*math.log2(v/n) for v in c.values()) for base in range(0, len(data)-32, 32): blk = data[base:base+32] if ent(blk) > 7.0 and 16 <= len(set(blk)) <= 24: print(f"0x{base:x}: 32B 高熵块") # 口令/可打印串 import re for m in re.finditer(rb'[ -~]{8,64}', data): s = m.group() if any(k in s.lower() for k in (b'key', b'pass', b'secret', b'pwd')): print(f"0x{m.start():x}: {s}")- Too many high-entropy block hits (the whole heap is one) → combine with [[re-memdump]]'s maps/offsets to narrow down to near the encryption context, or first use the Step 4 import table to locate the function and then retrieve the parameters
- The key may be fragmented or XOR-obfuscated on the heap/stack (see Pitfall 1)—once found, first verify it in the decryption script ([[re-crypto-decrypt]] step 4)
-
Resource files (.rsrc / embedded blob):
- PE: use
llvm-objdump -s -j .rsrc sample.exeor Ghidra to view the resource section;7z x sample.execan extract embedded icon/version/custom resources - ELF: find the embedded blob in
.rodata(objdump -s -j .rodata); combine with [[re-firmware]] experience—keys in firmware are often in configuration files/default certificates - The embedded blob may be serialized configuration (a JSON/INI-encoded key field); parse it as text first, then dig as binary; if the blob is encrypted (high entropy) → go back to [[re-crypto-id]], there may be an additional layer of decryption outside
- PE: use
-
*Import table clues (Crypt near functions)**:
objdump -p sample.exe | grep Crypt/ Ghidra Imports window to findCryptEncrypt/CryptDecrypt/BCrypt*/RSA*/EVP_*(OpenSSL)- Once found, decompile that function: the key parameter (handle/KEYEXCHANGE structure) usually comes from "a fixed value set somewhere earlier"—trace the data flow upward from the function: constant assignments, global variable initialization,
CryptSetKeyParamthepbKeyDataparameter of - Setting breakpoints to observe the parameters is more direct ([[re-gdb]] / [[re-x64dbg]]): after
b CryptSetKeyParamlook at the memory pointed to bypbKeyData—but note the highest principle of [[re-analyze/platform-tips]]: run inside a sandbox
-
Recovering the key derivation function (PBKDF):
- Recognize the
PBKDF2/scrypt/bcrypt/EVP_BytesToKeycall in the decompilation → key = KDF(password, salt, iteration count), extract parameter by parameter: password (hardcoded string or user input), salt (fixed bytes or context), iteration count (constant) - Write a recovery script (
pip install cryptography/hashlibhas built-in PBKDF2):import hashlib key = hashlib.pbkdf2_hmac('sha256', b'passphrase', b'<salt>', 100000, dklen=32) print(key.hex()) - Verification: the derived key matches the high-entropy block in memory from step 2 (indicating the key after the KDF ran is right there), or directly use [[re-crypto-decrypt]] to try decrypting known ciphertext
- Dynamic supplement: use Frida to hook the KDF function and observe the returned buffer (inside the sandbox, [[re-sandbox]]), and compare with the static recovery result
- Recognize the
Cross-domain combination
- [[re-protocol]]: step 3 (key) of this gateway workflow—the middle segment of the encrypted communication decryption chain (crypto-id → crypto-keys → crypto-decrypt)
- [[re-malware]]: C2 configuration extraction (hardcoded C2 key/password)—the key stage of re-malware step 4; malware sample keys are often buried in the configuration ([[re-behavior]] behavior confirmation + extraction by this skill)
- [[re-firmware]]: mining hardcoded passwords/keys inside firmware—re-firmware step 3 (rootfs configuration/default certificates)
- [[re-memdump]]: the default dump is the raw material for the memory search in this skill ([[re-analyze/platform-tips]] direct-read vs dump decision table)
- [[re-crypto-decrypt]]: downstream—the extracted key is handed to the decryption script for verification and use
- [[re-anti-analysis]]: unpack the in-pack key first (determine the dump point by materialization, see [[re-memdump]] dump timing)
- [[re-ioc]]: the extracted hardcoded keys/passwords can serve as YARA signatures and IOCs
Common pitfalls and traps
- Key stored in fragments/XOR-obfuscated: Symptom — the extracted single-block "key" does not decrypt the plaintext, or no complete key can be found in strings/memory; Cause — the sample splits the key into multiple segments (shards) or stores it XORed with a constant and reassembles it at runtime; Countermeasure — decompile to find the reassembly logic: key bytes come from multiple offsets/multiple XORs (see the "check hardcoding first" flow in pitfall 3; when confirming hardcoding, examine the operations at the reference sites); after recovering candidates, try them one by one in [[re-crypto-decrypt]]
- A truly random key ≠ statically extractable: Symptom — static/memory searches turn up nothing, yet the user still wants the key; Cause — the key is generated at startup
RAND_bytesor delivered by a server, and simply is not in the sample (no solution short of white-box attacks); Countermeasure — report honestly: no locally extractable key exists; switch to key-derivation interception (hookRAND_bytes/KDF input), algorithm-side attacks (if the decryption result can be verified against known plaintext), or go back to [[re-malware]] to see whether the key is delivered by C2 - Check for hardcoding before going dynamic: Symptom — the sample hardcodes the key, yet you first spend ages running the sandbox + Frida; Cause — the static-first order was not followed; Countermeasure — step 1, strings/cross-references, is a 30-second check; a static hit means skipping dynamic entirely; go dynamic only when static yields nothing and runtime material (KDF input, reassembly logic) is needed (see the static-first approach and top principle in [[re-analyze/platform-tips]])
- Dumping too early fails to capture the runtime key: Symptom — memory searches find no high-entropy block at all, or the ones found do not match; Cause — dumping before the packer has decrypted/before key initialization (you get the packer's initial state, see pitfall 2 of [[re-memdump]]); Countermeasure — confirm the plaintext/key has materialized (for a classic single-layer packer, after the OEP and after an encryption call has executed; for multi-stage/on-demand decryption, judge by "write→execute transfer + payload header/import recovery") before gcore; if necessary, trigger a breakpoint in the encryption function before dumping (with [[re-gdb]])
- Memory key candidate pattern misses AES-192: Symptom — high-entropy blocks are searched only at 16/32 bytes, so 24-byte keys are missed and decryption does not match; Cause — 16/32 bytes for AES-128/256 is a common assumption, but an AES-192 key is exactly 24 bytes (12 rounds); Countermeasure — make the high-entropy block search window cover all three sizes 16/24/32 (use the 10/12/14 rounds determination from [[re-crypto-id]] to pin down parameters), and once a candidate hits, verify it by trial decryption in [[re-crypto-decrypt]]
- Ransomware-style samples use-and-destroy keys, so a post-hoc dump is necessarily empty: Symptom — the sample is confirmed to have executed encryption, but no key material can be found in any memory dump; Cause — ransomware zeroes (wipes) the key immediately after encryption completes; the key exists in memory only briefly during the encryption phase (NotPetya/BadRabbit/Phobos experiments can plot a key existence timeline); Countermeasure — take multiple snapshot dumps during the encryption phase (while the business logic is running) and compare them to build a timeline, or use [[re-gdb]] to break before the encryption function returns and grab the arguments; this is more reliable than a single post-hoc dump
- The key plaintext appears only at a specific instant of execution, so a dump is bound to miss it: Symptom — the dump search finds nothing, but dynamic tracing shows the key appearing; Cause — for some samples the master key exists in memory as plaintext only in an extremely short window (e.g. the browser's v20_master_key appears only at the instant of decryption; VoidStealer relies on a hardware breakpoint to catch exactly this); Countermeasure — first locate the key's point of use (encryption function/derivation point), set a hardware breakpoint on it ([[re-gdb]]/[[re-x64dbg]], not relying on a breakpoint instruction) to intercept the plaintext in real time; this is more efficient than "dump first, then search for a needle in a haystack"