When to Use / When Not to Use
- Use: need to recover ciphertext (data blob / traffic / config section) into plaintext
- Use: algorithm and key are known (or have been derived via [[re-crypto-id]] / [[re-crypto-keys]]), and batch decryption is needed
- Use: recover the decryption logic from a sample and rewrite it as a standalone reusable script
- Don't use: both algorithm and key are unknown (first [[re-crypto-id]] → [[re-crypto-keys]])
- Don't use: statically readable plaintext ([[re-triage]] low entropy, read directly)
- Don't use: want to run the original sample and observe output (that's the job of [[re-behavior]] / [[re-sandbox]]—the decryption script is for reproducing without the sample)
Tool Preparation
Verify all tools before use. This skill deals with dump/decompilation artifacts and ciphertext data; the sample-running part happens inside [[re-sandbox]] ([[re-analyze/platform-tips]] highest principle).
python3 + pycryptodome — the mainstay of decryption scripts
- Linux:
apt install python3 python3-pip/dnf install python3 python3-pip/pacman -S python python-pip - macOS:
brew install python - Windows: python.org installer (check Add to PATH); Linux version inside WSL
- Crypto library:
pip install pycryptodome(standard algorithms such as AES/DES/RSA/ChaCha) - Verify:
python3 -c "from Crypto.Cipher import AES; print('ok')";python3 --version
Target program dump/decompilation artifacts — the basis for recovering the algorithm
- Dump: [[re-memdump]] default dump (gcore) — locate the ciphertext input point and the decryption call site
- Decompilation: output of [[re-ghidra]] / [[re-ida]] / [[re-radare2]] (function decompilation view)
- Verify:
file outis an ELF core; the target function can be found in the decompiler (ghidra/rizincan start)
angr (optional) — symbolic execution to supplement logic that is hard to recover
- All platforms:
pip install angr——For Python version compatibility, refer to [[re-angr]]'s "Tool Preparation"(the version matrix is maintained in only one place there, to avoid drift across multiple locations); many dependencies, venv recommended:python3 -m venv venv && venv/bin/pip install angr - Verify:
venv/bin/python -c "import angr; print(angr.__version__)" - Use: when decompilation branch explosion/obfuscation is severe, use symbolic execution to solve the decryption function output (load target binary → set ciphertext input as symbolic → constraint solving)
Procedure
Execute in order, recording the results of each step. Prerequisite: the algorithm ([[re-crypto-id]]) and key ([[re-crypto-keys]]) are confirmed, or at least there is a candidate for one of them; archive the script and verification results (plaintext sample + sha256) for reference in the report.
-
Falsify "ciphertext": first locate the true ciphertext boundary(skipping this step is the most common way crypto analysis fails):
- A high-entropy section may be "pseudo-ciphertext"—container/compressed data streams mistaken for an encryption layer (such as the zip data body, or a compressed stream appended after an image). Compressed data entropy is also 8.0, indistinguishable from encryption
- First do an overall structural observation: file header/tail magic numbers, format marker distribution (JPEG per-segment size statistics), using a known tail structure as an anchor (the zip EOCD's
cd_offset/cd_sizeto infer the data start; the EOCD signature is known plaintext) - Determine format boundariesDo not use first/last bare marker searches: marker byte pairs can legitimately appear in the payload—PNG's
IENDstring can fall within tEXt chunk data, JPEG'sFF D9can fall inside length-delimited segments such as APP1 (taking the first hit as the end will truncate legitimate data). The correct approach is to parse according to the format specification: PNG, starting from the 8-byte signature, followsLength/Type/Data/CRCtraversing chunk by chunk, until the structure is valid and the zero-lengthIEND(verifying the CRC chunk by chunk if necessary); JPEG, starting fromSOI, is parsed by marker/segment, and after enteringSOSit is processed according toFF00stuffing andRSTnrules,and only theEOIat a syntactically valid position is the end. Raw byte searching can only be used to produce candidate positions; candidates must undergo structural validation before boundaries can be determined - For files of the same size, first compare the last 16 bytes/overall hash—a "header-swapped copy" (a second encapsulation of the same payload, differing only in the header) directly eliminates an entire branch
- Compression ratio is a signal: a layer that becomes significantly larger after decompression indicates the content has structure (compressible = encoding/plaintext), while a ratio close to 1:1 indicates the inner layer is already compressed or encrypted
- After confirming it is an isolated high-entropy blob, proceed to the ciphertext localization in step 1
-
Locate the decryption function (cross-reference the ciphertext input point):
- Starting from the ciphertext offset: use the high-entropy region offset from step 2 of [[re-crypto-id]] → in the decompiler, find the function that reads that offset/that global variable → follow the call chain to see who wrote it (the writer is often the decryption function)
- Starting from the API: the
Crypt*/EVP_*call site found in step 4 of [[re-crypto-keys]] is the candidate; observe whether the ciphertext pointer among the arguments points to the offset from step 2 - Dynamic assistance: [[re-gdb]] / [[re-x64dbg]] set a breakpoint at the candidate function (inside the sandbox), print the arguments/return value, and confirm it outputs readable plaintext
- No clear function found → the ciphertext may be processed by an inlined algorithm (with no call boundary); go back to [[re-crypto-id]] and locate it using data characteristics (at references to constant tables)
-
Reconstruct the algorithm by decompilation:
- Transcribe the decompiled view section by section into pseudocode, making clear: the algorithm (AES-CBC/custom XOR...), the key and IV sources (fixed value/derived/context), the mode and padding (where the CBC IV is, PKCS7 or zero padding)
- Custom algorithm: translate the bit operations one by one (XOR/shift/table lookup), and pay attention to endianness (same as pitfall 1 in [[re-proto-rev]]—try both little and big endian first for length/key fields)
- When reconstructing standard algorithms, pay attention to details: whether AES uses CBC or ECB, key length 16/24/32, whether the IV reuses the key (a common incorrect implementation, see pitfall 1)
- Loop/table-lookup logic that is unclear in the decompilation → use angr symbolic execution as a fallback (construct a solving script and treat the function as a black box to obtain the output)
-
Rewrite as a standalone script (python):
# decrypt.py —— 按反编译还原的算法重写 from Crypto.Cipher import AES import sys key = bytes.fromhex("...") # 来自 [[re-crypto-keys]] 步骤 1/2/5 iv = key[:16] # 样本实现: IV = key 前 16 字节 data = open(sys.argv[1], 'rb').read() pt = AES.new(key, AES.MODE_CBC, iv).decrypt(data) print(pt) # 或写文件 + 后续校验- Parameterize the script (key/IV/input file passed via arguments or configuration), write it correctly once and reuse it repeatedly—for batch traffic decryption
- Key point: the script logic must match the sample (padding handling, tail truncation); when inconsistent, go back and check the boundary conditions (see pitfall 1)
-
Verify with known plaintext:
- Sources of known plaintext: protocol header magic (such as
\xAA\x55), file headers (PKzip /\x89PNG), message fields (the fixed header in step 2 of [[re-proto-rev]]), or plaintext strings observed in the behavior in [[re-behavior]] - Verification method: a known plaintext fragment can be found in the decryption output → success; if not found → check in order: whether the key/IV are correct (try each [[re-crypto-keys]] candidate one by one), endianness, padding handling, and whether there is an outer layer of encryption (see Pitfall 3)
- When there is no known plaintext, use "readability" to verify: output printable rate >70% or passes
file -identify the format (PDF/zip/text) → treat as a success candidate
- Sources of known plaintext: protocol header magic (such as
-
Traffic scenario: decrypt the plaintext traffic stream:
- After confirming the traffic encryption algorithm and key according to [[re-crypto-id]] / [[re-crypto-keys]], extract the ciphertext payload from the pcap ([[re-netcap]] step 3 tshark export):
tshark -r c2.pcap -Y 'tcp.payload' -T fields -e data.data | sed 's/://g' | xxd -r -p > payloads.bin - Write a batch script: split by stream (one segment per TCP stream), call the decryption logic segment by segment (same script as step 3), and output plaintext stream files
- Verification: the protocol structure (session sequence number/command word) can be seen in the plaintext stream, then convert to [[re-proto-rev]] for state machine reconstruction
- Note session key changes (rederived on each handshake) → in the script, obtain the corresponding key for each session (the derivation restoration in [[re-crypto-keys]] step 5)
- After confirming the traffic encryption algorithm and key according to [[re-crypto-id]] / [[re-crypto-keys]], extract the ciphertext payload from the pcap ([[re-netcap]] step 3 tshark export):
Cross-domain combination
- [[re-protocol]]: step 4 of this gateway workflow (decryption) — the landing point of encrypted communication links (crypto-id → crypto-keys → crypto-decrypt → proto-rev)
- [[re-malware]]: C2 traffic decryption — step 4 of re-malware; use the decrypted plaintext (commands/configuration) for behavior judgment and IOCs ([[re-ioc]])
- [[re-firmware]]: firmware encryption layer/encrypted communication decryption — used with the encryption layer handling when [[re-fw-extract]] unpacking fails
- [[re-crypto-id]] / [[re-crypto-keys]]: upstream — the input sources for the algorithm and key
- [[re-memdump]]: location of ciphertext input points and decryption call sites (dump artifacts)
- [[re-anti-analysis]]: if decryption is inside a packer, unpack first (see Pitfall 2); [[re-gdb]] / [[re-x64dbg]] dynamically assist in confirming function behavior
- Convert the decrypted plaintext to [[re-proto-rev]] to reconstruct the state machine, or continue according to the [[re-firmware]] / [[re-malware]] workflow
Common pitfalls and traps
- The restoration script is inconsistent with the sample behavior → recheck boundary conditions (length/padding): Symptom — the plaintext decrypted by the script differs from the sample's own output (more/fewer bytes, garbled tail); cause — boundary conditions are not aligned: padding method (PKCS7/zero), whether the length field includes padding, whether the IV changes per packet, whether the ciphertext tail is truncated; countermeasure — check the padding and length handling code item by item in the decompilation, implement them explicitly in the script, then return to step 4 and verify with known plaintext
- Decryption is inside a packer → unpack first: Symptom — the decryption function cannot be found in a packed sample, or the function found is only the packer's decompression; cause — the ciphertext data/decryption logic is wrapped by the packer, and statically it appears as the packer's initial state (same as [[re-memdump]] Pitfall 2); countermeasure — first use [[re-anti-analysis]] to unpack (determine the dump timing according to plaintext/code materialization, and do not treat OEP as a universal criterion), then redo localization on the unpacked artifact
- Multi-round decryption chain: Symptom — after decrypting the first layer, it is still garbled/high-entropy (entropy 7.0+); cause — multi-layer encryption (XOR first then AES, or nested compression + encryption, see [[re-crypto-id]] Pitfall 3); countermeasure — verify each layer separately (after decrypting one layer, test entropy and readability once), and restore layer by layer; "entropy decrease means one layer of progress" can be used as a stopping condition
- Key/IV order used incorrectly results in garbled decryption: Symptom — known-plaintext verification fails, but the algorithm is determined to be AES; Cause — key/iv parameter order (whether the sample is key||iv concatenation or passed separately), key byte order, or IV reuse/fixed IV; Countermeasure — turn the candidates from [[re-crypto-keys]] (including big-endian/little-endian variants, common misimplementation variants such as IV=key prefix, etc.) into a parameter combination loop to brute-force decryption, and use readability/known-plaintext to determine a hit
- Restricted symbol set = encoding is not encryption (pseudo-encryption): Symptom — entropy 7.9+ but the byte value range is clearly restricted (e.g., only 66 symbols, uniform distribution, no periodicity), xortool/IC is completely flat for all key lengths; Cause — this is "encoding + single-byte XOR":single-byte XOR does not change the frequency distribution or the symbol set(it only shifts values), only multi-byte XOR scatters the symbols across all 256 values; Countermeasure — collect the complete set of symbol values, and perform XOR matching against known character sets (compare sets against 256 keys, millisecond-level, don't brute-force): Base64 64 characters +
\n+== 66 symbols, Ascii85 85, hex 16, etc.; after a hit, XOR to restore → decode; cross-validation: padding characters (e.g.,=appears only once at the end of the file after encryption), newline frequency, magic number after decoding - xortool/IC results all flat = a signal to change approach: Symptom — all key length candidates are ~10-20% and there is no prominent peak; Conclusion — statistically there is no periodic structure, multi-byte repeating-key XOR is basically ruled out, don't keep running with different parameters; Countermeasure — fall back to observing the data itself (value range/symbol set/entropy), usually it is an encoding layer or a one-time keystream (stream cipher)
- Pseudo-ciphertext confirmed but there is more than one layer → layered peeling discipline: after peeling each layer, use magic numbers/readability to verify the integrity of the product before moving to the next layer; keep and name all intermediate products (rollback possible); entropy does not decrease / still high entropy after decoding = there are more outer layers (e.g., Base64 outside XOR, and then compression)
- Pixel-level XOR (visual cryptography/image steganography): Symptom — two "random noise" images, the challenge hints at overlaying them; Cause — visual cryptography splits information into two shares, and XOR (or ADD) of the pixels makes it visible; Countermeasure —
ImageChops.logical_xoror numpya^bpixel-wise XOR the two images (all three RGB channels); the result is often "99.5% pure white background + extremely light gray text" (text 254 vs background 255) — neither direct viewing nor inversion makes it visible,use threshold enhancement:==255 → 黑、其余 → 白, the text appears immediately; for bitmap small text, further split it into a character grid (5x7) and read character by character, and verify repeated characters with a pairwise bitmap similarity matrix (e.g.,d562333drepeated patterns) - ADD saturated addition is a close relative of XOR: Symptom — when the XOR result has no content; Cause — the shares may have been generated by additive secret sharing (XOR produces noise, and only ADD/SUB yields an image); Countermeasure — similarly try
clip(a+b)saturated addition anda-bsubtraction, and compare multiple operation combinations - Heavy obfuscation in new versions → backtrack and locate in old versions: Symptom — after the core functions in the latest version are obfuscated, the size is abnormally huge, IDA analysis lags, and grinding through it manually is unrealistic; Cause — early versions of the same SDK did not have heavy obfuscation, and the core logic is clearer; Countermeasure — install historical version packages (Wandoujia and other channels) according to behavioral characteristics (e.g., "collecting is triggered as soon as the privacy agreement is agreed to") → click agree → capture packets to confirm the earliest version in which that parameter appears; choose a version with regular behavior as the starting point for analysis (easier debugging: both spawn/attach can reproduce it); the logic recovered from the old version in turn guides confirmation of which parameters the new version collects
- Segmented/layered encrypted reporting (different algorithm per segment): Symptom—the reported request fields captured all look like ciphertext on their own, but decrypting them uniformly with one algorithm yields only gibberish; Cause—the client-side encryption process is split into multiple segments:single-field encryption → segmented encryption → overall encryption, each segment uses a different algorithm/key (e.g., the first layer uses a random number as the key for byte-by-byte encryption, the second layer uses a fixed key, and the third layer concatenates and combines fields before encrypting again); moreover, the fields themselves may be risk features collected from the device (collected via svc instructions,
r_1_0type fields); Countermeasure—peel layer by layer following the discipline of "verify each layer separately" (entropy drop/readability means advance one layer; see the multi-round decryption chain pitfall); note that the "overall encryption" of the same-origin SDK may additionally wrap a VM-based algorithm (about 70 handles simulate basic instructions; when handles are not obfuscated, instruction semantics can be identified one by one); key sources fall into two categories: random (not reproducible via packet capture) and fixed (can be found statically); do fixed first, then random - Double AES + encrypted strings (key/iv also stored encrypted): Symptom—after confirming AES, direct decryption still fails, or the key/iv changes every session; Cause—a common implementation pattern: the first-layer key/iv is fixed (hardcoded in the so as encrypted strings, resolved at runtime via a decryption function), while the second-layer key/iv changes each time; between the two encryptions, a fixed prefix is concatenated (e.g.,
03000001) before entering the same function; and encryption/decryption may be the same function (a flag bit controls the direction); Countermeasure—hook the return point of the decryption function to export all decrypted strings in bulk (the encrypted string memcpy'd before JNI_OnLoad → global variable → decryption function), and search directly within them for the key/iv; to find the encryption point, use findcrypt to scan for characteristic constants (AES S-box, etc.) → xref to locate the function that checks the key/iv length (16); dynamically hook to print the data before and after two calls for comparison and verification