When to use / when not to use
- Use: obfuscation restoration for scripts and macros such as .ps1 / .docm / .xlsm / .js / .jse / .hta (encoding, string concatenation, execution chain)
- Use: phishing attachment macros, malicious downloader scripts ([[re-malware]] → [[re-managed]] path)
- Don't use: compiled artifacts (native goes to [[re-binary-core]], .NET goes to [[re-dotnet]], Java goes to [[re-java]])
- Don't use: scripts are fully readable (read the logic directly, no need for this skill)
- Note:Dynamic execution defaults to sandbox ([[re-analyze/platform-tips]] highest principle)——static decoding without execution can skip the sandbox; any "run script to get the next layer" step must go into [[re-sandbox]]
Tool preparation
See [[re-analyze/platform-tips]]——this skill is mainly static decoding, sandbox-free; steps in execution chain tracking that require a runtime (running scripts/opening macros) go into the sandbox per the highest principle.
python3 (main decoding workhorse)
- Linux:
apt install python3/dnf install python3/pacman -S python - macOS: built-in; Windows: official installer or
choco install python - Verify:
python3 --version
oletools (VBA macro extraction/decoding, optional but strongly recommended)
pip install oletools(pyparsing dependency installed automatically)- Verify:
olevba --help
xxd / sed (hex and text processing)
- Debian/Ubuntu:
apt install xxd(newer versions are a standalone package, older versions are invim-common);Fedora:dnf install vim-common(xxd has traditionally been provided by vim-common, F38+ also has a standalonexxdsubpackage); Arch:pacman -S xxd - macOS: built-in (bundled with vim)
- Windows: no built-in xxd——use WSL or PowerShell
Format-Hex - sed is built in on all platforms; verify:
xxd -v && sed --version
PowerShell (built into Windows) / pwsh (Linux/macOS)
- Windows: built-in
powershell.exe(or PowerShell 7pwsh) - Linux:
apt install powershell(Microsoft repo) orsnap install powershell;macOS:brew install --cask powershell - Verify:
pwsh --version
js-beautify (JS beautification, optional)
pip install jsbeautifier; verify:js-beautify --version
Procedure
Execute in order; archive each layer's restored result with a number (layer_01.ps1 → layer_02.ps1 …), and in execution chain tracking all "run script to get the next layer" steps are always inside the sandbox.
-
Identify script type and obfuscation style:
file sample.ps1 && head -c 512 sample.ps1 grep -oiE 'IEX|Invoke-Expression|-EncodedCommand|FromBase64String|eval\(|unescape|fromCharCode|Chr\(|StrReverse|Document_Open|AutoOpen' sample.ps1- PowerShell indicators:
-EncodedCommand/IEX/FromBase64String/[Convert]:: - VBA indicators:
Sub AutoOpen/Document_Open/Chr(/StrReverse/Evaluate/Shell - JS indicators:
eval(/unescape/fromCharCode/\xHHhex /document.write - Obfuscation style categories: pure encoding (base64/hex/char-code), string concatenation (
'co'+'mmand'), execution chain (outer IEX/eval nesting inner payload), character reversal/substitution
- PowerShell indicators:
-
Restore layer by layer (decode first, then follow the execution chain):
- Order: peel from the outermost layer——first decode the pure encoding layer, then follow the IEX/eval arguments down to the innermost payload
- After decoding each layer, return to step 1 and re-identify (the new layer may use a different obfuscation technique)
- Final goal: obtain readable "real logic" (URL, command, payload), archive the final layer
- Steps in the execution chain that "output the next layer after execution" → run inside the sandbox
-
PowerShell (-enc decoding, IEX chain):
-encdecoding (safe, no execution):pwsh -NoProfile -Command '$d=[Convert]::FromBase64String("BASE64"); [Text.Encoding]::Unicode.GetString($d)'- IEX chain (inside the sandbox, replace IEX with output to get the next layer):
sed 's/IEX(/Write-Host (/g' layer_01.ps1 > layer_02.ps1 - Structured extraction of string literals (AST, no execution):
$ast = [System.Management.Automation.Language.Parser]::ParseFile('layer_01.ps1', [ref]$null, [ref]$null) $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.StringConstantExpressionAst] }, $true) | % { $_.Value } - Common techniques:
-join/-splitcharacter concatenation,$env:XXXvariable substitution, string reversal
-
VBA (olevba macro extraction + restoration):
olevba -c sample.docm > macro.txt # 提取宏源码 olevba --decode -c sample.docm > macro_decoded.txt # 自动解常见字符串混淆(--decode 自 oletools 0.24/2015 起就有,无版本门槛)- Manual restoration:
Chr(65)concatenate and convert ASCII with python3;StrReverse(...)reverse;Evaluate("...")the execution string inside - Find auto-execution entry points:
AutoOpen/Document_Open/Workbook_Open/Auto_Open - Dynamic validation (sandbox required): open the document in Office/LibreOffice in a sandbox (enable macros), observe behavior and the next-layer output
- Manual restoration:
-
JS (deobfuscation/decoding):
eval(→ statically replace withconsole.log((orprint() to observe the payload- char-code decoding:
python3 -c "print(''.join(chr(int(c)) for c in '72,101,108,108,111'.split(',')))" \xHHhexadecimal:python3 -c "print(bytes.fromhex('68656c6c6f').decode())"(first strip the\xprefix)- base64 string:
python3 -c "import base64; print(base64.b64decode('...'))" - beautify:
js-beautify layer_01.js -o pretty.js - dynamic (sandbox):
node payload.jsor execute in a browser sandbox to get the next layer
Specific deobfuscation chain recipes
Following the obfuscation style classification from step 1, execute by recipe (the method is the core, tools are replaceable):
eval / execution function replacement: replace eval / IEX / Invoke-Expression / bash with an output statement (in JS eval = console.log; in PowerShell replace IEX(...) with printing), after running it prints the underlying code; decode one layer, look at one layer, then decode the next layer, archive each layer (layer_01.ps1 → layer_02.ps1 …)。
Common JS encoding functions (seeing them means they are decoding targets):unescape()(URL decoding %XX),String.fromCharCode()(character code array),atob()(Base64); hexadecimal \xHH / Unicode \uHHHH batch unescape decoding (python3 -c "import sys; print(sys.argv[1].encode().decode('unicode_escape').decode(errors='replace'))" '<串>');document.write replace with printing.
PowerShell -enc / IEX chain:-EncodedCommand after base64 decoding note UTF-16LE:
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($encoded))
Junk code detection(five identifying features):
- NOP sled
- push/pop appear in pairs, cancel each other out
- operation result is 0/identity (arithmetic junk)
- dead write: register written but never read before the next write
- unconditional jump to the next instruction
The real logic looks like junk, junk, junk, CALL target, junk, junk—extract all real call targets, ignore surrounding noise (can be implemented with a script filtering by call instructions + junk targets).
Hex encoded payload: after converting hex to bytes, first try common transformations: subtract 1 per byte, XOR single-byte key (try incrementally starting from 0x01).
Common decoding chain patterns:base64 decode → gzip decode → reverse → base64 decode; use file on each layer's output to determine the type and verify readability; if verification fails, the layer order or encoding determination is wrong, roll back one layer and retry.
Advanced obfuscation countermeasures (commercial obfuscators)
Techniques and countermeasures for JScrambler-type commercial obfuscators:
- Feature identification: bootstrap loader (self-executing entry), hidden string table (array + index references),
_0xvariable renaming patterns, encrypted function bodies (decrypted at runtime) - Control flow flattening (CFF) countermeasures: dispatcher identification (switch dispatch center) → state variable tracking → restore branches to sequential/conditional structures (method links to [[re-deobfuscate]])
- String encryption countermeasures: Hidden string table localization (decode function call sites) → runtime extraction (execute the decode function inside a sandbox to obtain values, see [[re-sandbox]]) or static restoration (script the decode loop)
- Dead code injection countermeasure: Unreferenced function filtering (based on call relationships; if there is no caller, it is a deletion candidate)
- Toolchain: js-beautify to prettify → choose a countermeasure path based on the technique; on the dynamic side, execute in a sandbox to obtain values (default sandbox principle)
Cross-domain joint
- [[re-managed]]: gateway workflow steps ② (decompile) ③ (deobfuscate) always invoke this skill
- [[re-malware]]: malicious script/phishing macro sample path (docm/ps1/js attachments → sandbox behavior + this skill restores the payload)
- [[re-sandbox]]: all dynamic execution (running scripts, enabling macros) is mandatory as a prerequisite, see [[re-analyze/platform-tips]] default sandbox principle
- Companion: [[re-triage]] (initial survey), [[re-ioc]] (extract C2 domains/URLs from scripts as IOCs), [[re-protocol]] (callback traffic downloaded by scripts)
Common pitfalls and traps
- Obfuscation nested layer upon layer requires patience to decode layer by layer: Symptom — after decoding one layer there is another, and it feels endless; Cause — multi-layer obfuscation stacking (encoding + concatenation + execution chain) is the norm; Countermeasure — decode layer by layer, archive and number each layer, rerun recognition after each decode, do not skip layers and do not expect to get it done in one step
- After olevba extraction it is still obfuscated: Symptom —
olevba -cThe extracted macro is still full ofChr(65) & Chr(66)concatenation; Cause — olevba is only responsible for extraction, not restoration; Countermeasure — use--decodeto automatically solve common techniques, manually restore the rest, and finally dynamically verify that the restoration result is correct - Malicious scripts must be run inside a sandbox: Symptom — locally directly
pwsh sample.ps1afterward the system is modified/a connection is made outbound; Cause — the script may be a downloader or persistence implant (platform-tipsa typical violation scenario of the default sandbox principle); Countermeasure — all dynamic execution goes into [[re-sandbox]] (network isolation + snapshot), static decoding can skip the sandbox - Blind regex modification easily breaks script structure: Symptom — after sed global replacement, syntax errors occur and the next layer cannot be decoded; Cause — the obfuscated string contains the replaced pattern, and string literals are accidentally damaged; Countermeasure — prefer AST/structured parsing (PowerShell AST, JS bracket matching), and after each replacement first perform a syntax check (
pwsh -NoProfile -Command "[scriptblock]::Create((Get-Content layer.ps1 -Raw))"/node --check layer.js) before proceeding to the next step - Locating signature functions in webpack bundles: Symptom — searching "sign" returns too many results; Cause — bundled and minified variable names; Countermeasure — search for characteristic strings (
sign=) or use the network panel initiator column to trace back the call stack that initiated the request (faster than searching the source code) - Local reproduction results are inconsistent: Symptom — the signature logic is correct but the server does not accept it; Cause — parameter ordering/timestamp precision is wrong; Countermeasure — check the source sort logic (by key alphabetical order + special character rules); for timestamps use
Math.floor(Date.now() / 1000)(second-level) - The key is in another chunk: Symptom — the key cannot be found in the signature function; Cause — the key is introduced from another chunk via require; Countermeasure — console.log the key variable at a breakpoint in the signature function (Source: reverse-skill field-journal, MIT)
- Restore transpiled artifacts before analysis: Symptom — in JS you see
var state=0; while(true){switch(state){…}}a state machine loop or_asyncToGenerator(function*(){…})Packaging, the logic is like a tangled mess; cause — generator/async is downgraded by transpilation chains such as Babel/Regenerator into state machines and wrappers, and source-level semantics are mechanically flattened out; countermeasure — after identifying the pattern of a "true constant condition while + single switch body", flatten it in case order into linear statements, and the artificial state variable disappears accordingly; for async, first scan the whole program for wrapped generator functions, then restore Yield in the function body to Await and inline the wrapper; first look at the helper function names to confirm the transpilation chain (Babel/Regenerator etc. each have characteristic fingerprints), and writing an inverse transform for a known toolchain is less effort than general restoration (Source: hermes-decomp (SymbioticSec), MIT) - Before patching, first calculate the change length clearly: symptom — the modified script/bundle cannot run or the structure table fails to parse; cause — a same-length change can be overwritten in place, while a variable-length change affects all subsequent offsets and structure sections, and if not corrected in coordination it will corrupt the file; countermeasure — same length -> overwrite in place, without touching the structure table; variable length -> rebuild the affected structure sections (such as the string table) and relocate subsequent sections, correcting all offsets in coordination; always reject patching data with shared storage/overlapping references; after patching, reparse the structure table once to perform a consistency check, and roll back if inconsistent — explicitly report an error for unsupported scenarios, and never stop work and corrupt the file (for general rules, see the corresponding pitfall entry in [[re-hybrid-app]]; for the specific Hermes layout, see that skill; source: hermes-decomp (SymbioticSec), MIT)