
Static deobfuscation toolkit for compiled V8 JavaScript bytecode, focusing on JSCeal payloads. Provides pattern-driven filters, control-flow unflattening, string reconstruction, and optional LLM-assisted function renaming for analysis.
This tool is dedicated to statically deobfuscating compiled V8 JavaScript bytecode that was protected with javascript-obfuscator.
It operates on pseudocode produced by View8, rather than on the original JavaScript source. The project was developed and tested on JSCeal payloads.
The filters are pattern-driven and intended primarily as a research toolkit and reference implementation. The tool is not a general-purpose JavaScript deobfuscator, does not reconstruct the original source code, and does not produce runnable JavaScript. Its output remains View8 pseudocode intended for static inspection, searching, comparison, and function-tree export.
pickle. Loading a malicious or untrusted .pkl file can execute code. Only load serialized files that you generated locally with View8.requirements.txt;brotli command-line utility for the Linux batch unpacking workflow;Create an isolated Python environment:
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install --upgrade pip
python3 -m pip install -r requirements.txt
The OpenAI backend in deobf_ai.py additionally requires the OpenAI Python package:
python3 -m pip install openai
The Anthropic backend uses the HTTP API through requests. The Ollama backend expects a reachable Ollama server.
The original JSCeal app.jsc payload is Brotli-compressed. On Linux, it can be decompressed with the brotli utility:
brotli -d app.jsc -o app.decompressed.jsc
The batch workflow under scripts/ performs this step with scripts/unpack_all.sh.
On Windows, or when the brotli command-line utility is unavailable, the included Node.js helper can be used as a fallback. It only decompresses the input and does not execute it:
node Utils/decompress-jsc.js app.jsc
It writes:
app.jsc.decompressed.jsc
V8 code cache is version-specific. Use a disassembler built for the same V8 version as the payload.
The JSCeal samples used during development were based on V8 10.2.154.26. Default disassemblers from an unrelated V8 build will not work correctly.
The source tree contains the disassembler source and required V8 patches under:
Utils/disasm/v8dasm.cpp
Utils/disasm/patches/
A prebuilt Linux binary is distributed with the project release, while the source tree contains the source and patches needed to rebuild it. Detailed description is available on the project Wiki. After obtaining or building the matching v8dasm, run:
/path/to/v8dasm app.decompressed.jsc > app.jsc.disasm.txt
Feed the disassembled file to view8.py and produce both serialized output for further processing and human-readable pseudocode:
mkdir -p decompiled
python3 View8/view8.py \
--input_format disassembled \
--inp app.jsc.disasm.txt \
--normalize \
--out decompiled/app.dec.txt \
--export_format decompiled serialized
This produces:
decompiled/app.dec.txt
decompiled/app.dec.pkl
The --normalize option makes generated function identifiers reproducible across repeated disassembly and decompilation runs.
There are separate filters for the individual obfuscation layers. They can be applied together to the serialized View8 output with deobf_all.py:
mkdir -p deobfuscated
python3 deobf_all.py \
--inp decompiled/app.dec.pkl \
--out deobfuscated/app.deobf.txt \
--export_format decompiled serialized
The default string filter is variant 2, used by the majority of analyzed JSCeal payloads. To select the simpler string scheme explicitly, add:
--str_deobf 1
Typical outputs are:
deobfuscated/app.deobf.txt
deobfuscated/app.deobf.pkl
deobfuscated/app.deobf.txt.strings.txt
decompiled/app.dec.resolved_funcs.csv
The resolved-functions CSV is a sample-specific cache. When it is absent, the string pass recovers the required decoder configuration, writes the CSV, and continues deobfuscating strings in the same run. Later runs reuse the cache and are normally faster.
Do not reuse a resolved-functions CSV with a different decompiled payload.
After all structural deobfuscation filters have been applied, deobf_ai.py can propose names that describe function behavior. It supports Anthropic, OpenAI, and Ollama backends.
Pass the model explicitly so that runs remain reproducible.
export ANTHROPIC_API_KEY='...'
python3 deobf_ai.py \
--inp deobfuscated/app.deobf.pkl \
--out deobfuscated/app.renamed.txt \
--llm_backend anthropic \
--model '<model-id>' \
--export_format decompiled serialized
export OPENAI_API_KEY='...'
python3 deobf_ai.py \
--inp deobfuscated/app.deobf.pkl \
--out deobfuscated/app.renamed.txt \
--llm_backend openai \
--model '<model-id>' \
--export_format decompiled serialized
python3 deobf_ai.py \
--inp deobfuscated/app.deobf.pkl \
--out deobfuscated/app.renamed.txt \
--llm_backend ollama \
--model '<local-model>' \
--ollama_url http://localhost:11434 \
--export_format decompiled serialized
In the default mode, the renamer builds a direct-call tree starting from the entry function and renames only functions reached through calls. Add --greedy to include all visible function references, including callbacks and assigned handlers.
The generated two-column CSV acts as a cache and allows an interrupted run to continue. Select an existing cache explicitly with --csv:
python3 deobf_ai.py \
--inp deobfuscated/app.deobf.pkl \
--out deobfuscated/app.renamed.txt \
--csv deobfuscated/app.deobf.renamed_funcs.greedy.example-model.csv \
--llm_backend anthropic \
--model '<model-id>' \
--greedy \
--export_format decompiled serialized
In the normal mode, the CSV is treated as a potentially partial cache. Cached names are applied first, functions already covered by the cache are removed from the selected call or reference tree, and the LLM is invoked only for functions that remain unresolved. If the CSV completely covers that tree, no API key or LLM connection is required. If it covers only part of the tree, the selected backend is initialized and the newly generated mappings are appended to the same CSV.
Use the same tree mode that was used when the CSV was created. A CSV produced from a --greedy run normally needs --greedy again if the goal is to continue that run rather than only reuse the direct-call subset.
Use --apply-csv-only when the CSV already contains the labels you want to apply, including reviewed, edited, imported, or rebased mappings:
python3 deobf_ai.py \
--inp deobfuscated/app.deobf.pkl \
--out deobfuscated/app.renamed.txt \
--csv renamed_functions.normalized.csv \
--apply-csv-only \
--export_format decompiled serialized
This mode: