
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:
--csv;--func.Rows whose original function identifier does not exist in the input are ignored. The command fails when the CSV contains no mappings applicable to the loaded file.
Use --func with the exact full function identifier to request a focused semantic analysis of one deobfuscated function:
python3 deobf_ai.py \
--inp deobfuscated/app.deobf.pkl \
--func func_example_0x100001234 \
--llm_backend anthropic \
--model '<model-id>'
The analysis includes a proposed name, behavior summary, inputs and return value, side effects, step-by-step logic, cleaned pseudocode, supporting evidence, and unresolved uncertainties. Supplying --csv adds cached semantic names as context for references inside the selected function without modifying the loaded corpus. Use --analysis-out analysis/function.md to save the report as Markdown. Fuzzy matches are printed only as suggestions; the requested function identifier must match exactly.
Use --help for options controlling temperature, batching, Anthropic thinking mode, token limits, and custom CSV paths.
LLM-generated names are navigation aids, not evidence. Always verify them against the deobfuscated body.
Deobfuscated JSCeal output is usually very large. Load the serialized output back into View8 and split it into smaller function trees.
At this stage, add --scope 0. Scope propagation has already been performed by the deobfuscator, and repeating it may propagate values incorrectly.
A tree based on declarer relationships:
python3 View8/view8.py \
--input_format serialized \
--inp deobfuscated/app.deobf.pkl \
--out trees/declarers \
--export_format decompiled \
--tree start \
--scope 0
A compact direct-call overview:
python3 View8/view8.py \
--input_format serialized \
--inp deobfuscated/app.deobf.pkl \
--out trees/calls \
--export_format decompiled \
--tree start \
--scope 0 \
--split_mode calls \
--inline_depth 1 \
--split_depth 5
A broader reference tree, including callbacks and assigned handlers:
python3 View8/view8.py \
--input_format serialized \
--inp deobfuscated/app.deobf.pkl \
--out trees/references \
--export_format decompiled \
--tree start \
--scope 0 \
--split_mode references \
--inline_depth 1 \
--split_depth 3
Different JSC files may use different string-obfuscation modes.
The simplest observed mode uses index shifting and is handled by deobf_str1.py. The most common JSCeal mode uses Base64, RC4, chunked strings, and transformed indexes; it is handled by deobf_str2.py.
The complete pipeline selects variant 2 by default. The filters can also be run independently for testing.
deobf_str2.pyUse --help to display all available modes and options:
python3 deobf_str2.py --help
A direct string-deobfuscation run can be started with:
python3 deobf_str2.py \
--inp decompiled/app.dec.pkl \
--out work/app.strings.txt \
--export_format decompiled serialized
During the run, the script identifies the string-decoder functions, loads any valid cached configurations, resolves missing ones, saves the resulting CSV, and decodes the strings. A second run is not required.
When deobf_str2.py is used directly, its default CSV name is resolved_funcs.csv. Select a sample-specific path with --csv or -c:
python3 deobf_str2.py \
--inp decompiled/app.dec.pkl \
--out work/app.strings.txt \
--csv decompiled/app.dec.resolved_funcs.csv \
--export_format decompiled serialized \
--verbosity 1
When chaining individual filters, preserve serialized output between stages so that later passes can continue operating on View8 objects.
deobf_all.py applies the following stages in order:
LLM-assisted function renaming is optional and is run separately after structural deobfuscation.
The repository includes a complete helper workflow under scripts/. All scripts are kept in one directory and source the same centralized configuration.
scripts/config.sh shared tool and workspace paths
scripts/copy_payloads.sh collect and MD5-name JSCeal app.jsc files
scripts/unpack_all.sh Brotli decompression
scripts/disasm_all.sh batch V8 disassembly
scripts/decompile_all.sh batch View8 decompilation
scripts/deobfuscate_all.sh batch deobfuscation with a combined log
scripts/run_unattended.sh detached deobfuscation and validation
scripts/collect_output.sh collect decoder caches and string listings
The supplied scripts/config.sh contains paths from an example environment:
JSC_DEOBF_ROOT="$HOME/jsc_deobfuscator"
V8DASM="$HOME/code/v8/v8dasm"
Edit that file once to configure the installation path, matching V8 disassembler, workspace directories, external commands, log paths, and harvest layout. The workspace defaults to the directory from which the helper script is launched.
Every value can also be overridden through an environment variable. JSC_HELPER_CONFIG can select a different configuration file.
A typical batch run is:
scripts/copy_payloads.sh
scripts/unpack_all.sh
scripts/disasm_all.sh
scripts/decompile_all.sh
scripts/deobfuscate_all.sh
scripts/collect_output.sh
The scripts preserve conventions used for the JSCeal corpus, including treating discovered app.jsc files as Brotli-compressed payloads and naming them by MD5. Review scripts/README.md before applying the workflow to unrelated samples.
For a long batch, scripts/run_unattended.sh launches deobfuscation with nohup, writes timestamped log, PID, and status files, and validates each generated output for unresolved references to cached string-decoder functions:
scripts/run_unattended.sh
Selected samples can be supplied explicitly:
scripts/run_unattended.sh \
decompiled/sample1.dec.pkl \
decompiled/sample2.dec.pkl
View8/ View8 decompiler and function-tree exporter
Utils/decompress-jsc.js Brotli decompression fallback for Windows
Utils/disasm/v8dasm.cpp V8 disassembler source
Utils/disasm/patches/ V8 patches required by the disassembler
Utils/check_unresolved_decoder_references.py
output validation helper
deobf_all.py complete default deobfuscation pipeline
deobf_str1.py simple string-index-shift filter
deobf_str2.py RC4/Base64 string filter with index recovery
deobf_scope2.py scope and dictionary propagation
deobf_unflattener.py control-flow unflattening
deobf_replace_ops.py proxy and operation-wrapper replacement
deobf_globals.py global propagation
deobf_inline_temporaries.py conservative final cleanup
deobf_ai.py optional LLM-assisted function renaming
scripts/ configurable batch and validation helpers
Every major pass can be executed separately for testing. Run the selected script with --help to see its complete interface:
python3 deobf_str1.py --help
python3 deobf_str2.py --help
python3 deobf_scope2.py --help
python3 deobf_unflattener.py --help
python3 deobf_replace_ops.py --help
python3 deobf_globals.py --help
python3 deobf_inline_temporaries.py --help
javascript-obfuscator output. New variants may require additional detectors or transformations.The pipeline has been regression-tested against the JSCeal corpus used in the accompanying research. Basic release checks include:
python3 -m compileall -q .
python3 deobf_all.py --help
python3 deobf_str2.py --help
python3 deobf_ai.py --help
python3 View8/view8.py --help
For each corpus sample, verify that the run:
.pkl and .txt output;The unattended helper script automates the final decoder-reference validation.
javascript-obfuscator.The JSC Deobfuscator source code authored for this project is licensed under
the GNU General Public License, version 2 or (at your option) any later version
(GPL-2.0-or-later). See LICENSE for the complete license text.
Copyright (C) 2026 Aleksandra "Hasherezade" Doniec @ Check Point Research.
The View8 submodule is a separate project. Third-party-derived disassembler
material under Utils/disasm/ retains its existing provenance and is not
relicensed by the copyright notice above.