pyjsclear 0.1.1__tar.gz → 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyjsclear-0.1.3/NOTICE +28 -0
- pyjsclear-0.1.3/PKG-INFO +124 -0
- pyjsclear-0.1.3/README.md +98 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/THIRD_PARTY_LICENSES.md +27 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/__init__.py +2 -2
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/deobfuscator.py +115 -20
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/generator.py +6 -1
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/scope.py +107 -81
- pyjsclear-0.1.3/pyjsclear/transforms/aa_decode.py +150 -0
- pyjsclear-0.1.3/pyjsclear/transforms/class_static_resolver.py +206 -0
- pyjsclear-0.1.3/pyjsclear/transforms/class_string_decoder.py +332 -0
- pyjsclear-0.1.3/pyjsclear/transforms/cleanup.py +272 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_branch.py +18 -0
- pyjsclear-0.1.3/pyjsclear/transforms/dead_class_props.py +251 -0
- pyjsclear-0.1.3/pyjsclear/transforms/dead_expressions.py +29 -0
- pyjsclear-0.1.3/pyjsclear/transforms/dead_object_props.py +150 -0
- pyjsclear-0.1.3/pyjsclear/transforms/else_if_flatten.py +32 -0
- pyjsclear-0.1.3/pyjsclear/transforms/enum_resolver.py +175 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/eval_unpack.py +3 -52
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/expression_simplifier.py +106 -0
- pyjsclear-0.1.3/pyjsclear/transforms/global_alias.py +117 -0
- pyjsclear-0.1.3/pyjsclear/transforms/hex_numerics.py +30 -0
- pyjsclear-0.1.3/pyjsclear/transforms/jj_decode.py +718 -0
- pyjsclear-0.1.3/pyjsclear/transforms/jsfuck_decode.py +593 -0
- pyjsclear-0.1.3/pyjsclear/transforms/member_chain_resolver.py +171 -0
- pyjsclear-0.1.3/pyjsclear/transforms/noop_calls.py +77 -0
- pyjsclear-0.1.3/pyjsclear/transforms/nullish_coalescing.py +98 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/object_simplifier.py +7 -5
- pyjsclear-0.1.3/pyjsclear/transforms/optional_chaining.py +152 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/property_simplifier.py +29 -4
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/proxy_functions.py +44 -2
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/reassignment.py +5 -0
- pyjsclear-0.1.3/pyjsclear/transforms/require_inliner.py +81 -0
- pyjsclear-0.1.3/pyjsclear/transforms/single_use_vars.py +155 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/string_revealer.py +2 -0
- pyjsclear-0.1.3/pyjsclear/transforms/unreachable_code.py +37 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/unused_vars.py +3 -2
- pyjsclear-0.1.3/pyjsclear/transforms/variable_renamer.py +511 -0
- pyjsclear-0.1.3/pyjsclear/transforms/xor_string_decode.py +228 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/utils/ast_helpers.py +72 -2
- pyjsclear-0.1.3/pyjsclear.egg-info/PKG-INFO +124 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/SOURCES.txt +19 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/tests/test_regression.py +288 -13
- pyjsclear-0.1.1/NOTICE +0 -19
- pyjsclear-0.1.1/PKG-INFO +0 -168
- pyjsclear-0.1.1/README.md +0 -142
- pyjsclear-0.1.1/pyjsclear/transforms/aa_decode.py +0 -83
- pyjsclear-0.1.1/pyjsclear/transforms/jj_decode.py +0 -91
- pyjsclear-0.1.1/pyjsclear/transforms/jsfuck_decode.py +0 -93
- pyjsclear-0.1.1/pyjsclear.egg-info/PKG-INFO +0 -168
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/LICENSE +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/MANIFEST.in +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/__main__.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/parser.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/__init__.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/anti_tamper.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/base.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/constant_prop.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/control_flow.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/hex_escapes.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/logical_to_if.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/object_packer.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/transforms/sequence_splitter.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/traverser.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/utils/__init__.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear/utils/string_decoders.py +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/dependency_links.txt +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/entry_points.txt +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/requires.txt +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyjsclear.egg-info/top_level.txt +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/pyproject.toml +0 -0
- {pyjsclear-0.1.1 → pyjsclear-0.1.3}/setup.cfg +0 -0
pyjsclear-0.1.3/NOTICE
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
PyJSClear
|
|
2
|
+
Copyright 2026 Intezer Labs
|
|
3
|
+
|
|
4
|
+
This product is a derivative work based on the following projects:
|
|
5
|
+
|
|
6
|
+
1. obfuscator-io-deobfuscator (v1.0.6)
|
|
7
|
+
Copyright Ben (ben-sb)
|
|
8
|
+
Licensed under the Apache License, Version 2.0
|
|
9
|
+
https://github.com/ben-sb/obfuscator-io-deobfuscator
|
|
10
|
+
|
|
11
|
+
2. javascript-deobfuscator
|
|
12
|
+
Copyright Ben (ben-sb)
|
|
13
|
+
Licensed under the Apache License, Version 2.0
|
|
14
|
+
https://github.com/ben-sb/javascript-deobfuscator
|
|
15
|
+
|
|
16
|
+
3. webcrack (v2.14.1)
|
|
17
|
+
Copyright (c) 2023 j4k0xb
|
|
18
|
+
Licensed under the MIT License
|
|
19
|
+
https://github.com/j4k0xb/webcrack
|
|
20
|
+
|
|
21
|
+
This Python library re-implements the deobfuscation algorithms and transform
|
|
22
|
+
logic from the above projects in pure Python. No source code was directly
|
|
23
|
+
copied; the implementations were written from scratch following the same
|
|
24
|
+
algorithmic approaches.
|
|
25
|
+
|
|
26
|
+
Test dataset: obfuscated JavaScript samples from the JSIMPLIFIER dataset
|
|
27
|
+
(https://zenodo.org/records/17531662) are included in tests/resources/ for
|
|
28
|
+
evaluation purposes only. They are not part of the distributed package.
|
pyjsclear-0.1.3/PKG-INFO
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyjsclear
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Pure Python JavaScript deobfuscator
|
|
5
|
+
Author-email: Intezer Labs <info@intezer.com>
|
|
6
|
+
License-Expression: Apache-2.0
|
|
7
|
+
Project-URL: Homepage, https://github.com/intezer/PyJSClear
|
|
8
|
+
Project-URL: Repository, https://github.com/intezer/PyJSClear
|
|
9
|
+
Project-URL: Issues, https://github.com/intezer/PyJSClear/issues
|
|
10
|
+
Keywords: javascript,deobfuscator,deobfuscation,security,malware-analysis,ast
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Information Technology
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Security
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.11
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
License-File: NOTICE
|
|
24
|
+
Requires-Dist: esprima2>=5.0.1
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img src="https://raw.githubusercontent.com/intezer/PyJSClear/main/PyJSClear.png" alt="PyJSClear" width="200">
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
# PyJSClear
|
|
32
|
+
|
|
33
|
+
Pure Python JavaScript deobfuscator.
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install pyjsclear
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For development:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git clone https://github.com/intezer/PyJSClear.git
|
|
45
|
+
cd PyJSClear
|
|
46
|
+
pip install -e .
|
|
47
|
+
pip install pytest
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
### Python API
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from pyjsclear import deobfuscate, deobfuscate_file
|
|
56
|
+
|
|
57
|
+
# From a string
|
|
58
|
+
cleaned = deobfuscate(obfuscated_code)
|
|
59
|
+
|
|
60
|
+
# From a file
|
|
61
|
+
deobfuscate_file("input.js", "output.js")
|
|
62
|
+
|
|
63
|
+
# Or get the result as a string
|
|
64
|
+
cleaned = deobfuscate_file("input.js")
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Command line
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# File to stdout
|
|
71
|
+
pyjsclear input.js
|
|
72
|
+
|
|
73
|
+
# File to file
|
|
74
|
+
pyjsclear input.js -o output.js
|
|
75
|
+
|
|
76
|
+
# Stdin to stdout
|
|
77
|
+
cat input.js | pyjsclear -
|
|
78
|
+
|
|
79
|
+
# With custom iteration limit
|
|
80
|
+
pyjsclear input.js --max-iterations 20
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## What it does
|
|
84
|
+
|
|
85
|
+
PyJSClear applies transforms in a multi-pass loop until the code
|
|
86
|
+
stabilizes (default limit: 50 iterations). A final one-shot pass renames
|
|
87
|
+
variables and converts var/let to const.
|
|
88
|
+
|
|
89
|
+
**Capabilities:**
|
|
90
|
+
- Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
|
|
91
|
+
- String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
|
|
92
|
+
- Constant propagation & reassignment elimination
|
|
93
|
+
- Dead code / dead branch / unreachable code removal
|
|
94
|
+
- Control-flow unflattening (switch-dispatch recovery)
|
|
95
|
+
- Proxy function & proxy object inlining
|
|
96
|
+
- Expression simplification & modern syntax recovery (?., ??)
|
|
97
|
+
- Anti-tamper / anti-debug removal
|
|
98
|
+
- Variable renaming (_0x… → readable names)
|
|
99
|
+
|
|
100
|
+
Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
|
|
101
|
+
that skips expensive transforms.
|
|
102
|
+
|
|
103
|
+
## Limitations
|
|
104
|
+
|
|
105
|
+
- **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
|
|
106
|
+
- **Large files get reduced treatment.** Files >500 KB or ASTs >50 K nodes skip expensive transforms; files >2 MB use a minimal lite mode.
|
|
107
|
+
- **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting; the deobfuscator catches this and returns the best partial result.
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
112
|
+
|
|
113
|
+
This project is a derivative work based on
|
|
114
|
+
[obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
|
|
115
|
+
(Apache 2.0),
|
|
116
|
+
[javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
|
|
117
|
+
(Apache 2.0), and
|
|
118
|
+
[webcrack](https://github.com/j4k0xb/webcrack) (MIT).
|
|
119
|
+
See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
|
|
120
|
+
[NOTICE](NOTICE) for full attribution.
|
|
121
|
+
|
|
122
|
+
Test samples include obfuscated JavaScript from the
|
|
123
|
+
[JSIMPLIFIER dataset](https://zenodo.org/records/17531662) (GPL-3.0),
|
|
124
|
+
used solely for evaluation purposes.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/intezer/PyJSClear/main/PyJSClear.png" alt="PyJSClear" width="200">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# PyJSClear
|
|
6
|
+
|
|
7
|
+
Pure Python JavaScript deobfuscator.
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install pyjsclear
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For development:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
git clone https://github.com/intezer/PyJSClear.git
|
|
19
|
+
cd PyJSClear
|
|
20
|
+
pip install -e .
|
|
21
|
+
pip install pytest
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
### Python API
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from pyjsclear import deobfuscate, deobfuscate_file
|
|
30
|
+
|
|
31
|
+
# From a string
|
|
32
|
+
cleaned = deobfuscate(obfuscated_code)
|
|
33
|
+
|
|
34
|
+
# From a file
|
|
35
|
+
deobfuscate_file("input.js", "output.js")
|
|
36
|
+
|
|
37
|
+
# Or get the result as a string
|
|
38
|
+
cleaned = deobfuscate_file("input.js")
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### Command line
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# File to stdout
|
|
45
|
+
pyjsclear input.js
|
|
46
|
+
|
|
47
|
+
# File to file
|
|
48
|
+
pyjsclear input.js -o output.js
|
|
49
|
+
|
|
50
|
+
# Stdin to stdout
|
|
51
|
+
cat input.js | pyjsclear -
|
|
52
|
+
|
|
53
|
+
# With custom iteration limit
|
|
54
|
+
pyjsclear input.js --max-iterations 20
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## What it does
|
|
58
|
+
|
|
59
|
+
PyJSClear applies transforms in a multi-pass loop until the code
|
|
60
|
+
stabilizes (default limit: 50 iterations). A final one-shot pass renames
|
|
61
|
+
variables and converts var/let to const.
|
|
62
|
+
|
|
63
|
+
**Capabilities:**
|
|
64
|
+
- Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
|
|
65
|
+
- String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
|
|
66
|
+
- Constant propagation & reassignment elimination
|
|
67
|
+
- Dead code / dead branch / unreachable code removal
|
|
68
|
+
- Control-flow unflattening (switch-dispatch recovery)
|
|
69
|
+
- Proxy function & proxy object inlining
|
|
70
|
+
- Expression simplification & modern syntax recovery (?., ??)
|
|
71
|
+
- Anti-tamper / anti-debug removal
|
|
72
|
+
- Variable renaming (_0x… → readable names)
|
|
73
|
+
|
|
74
|
+
Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
|
|
75
|
+
that skips expensive transforms.
|
|
76
|
+
|
|
77
|
+
## Limitations
|
|
78
|
+
|
|
79
|
+
- **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
|
|
80
|
+
- **Large files get reduced treatment.** Files >500 KB or ASTs >50 K nodes skip expensive transforms; files >2 MB use a minimal lite mode.
|
|
81
|
+
- **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting; the deobfuscator catches this and returns the best partial result.
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
86
|
+
|
|
87
|
+
This project is a derivative work based on
|
|
88
|
+
[obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
|
|
89
|
+
(Apache 2.0),
|
|
90
|
+
[javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
|
|
91
|
+
(Apache 2.0), and
|
|
92
|
+
[webcrack](https://github.com/j4k0xb/webcrack) (MIT).
|
|
93
|
+
See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
|
|
94
|
+
[NOTICE](NOTICE) for full attribution.
|
|
95
|
+
|
|
96
|
+
Test samples include obfuscated JavaScript from the
|
|
97
|
+
[JSIMPLIFIER dataset](https://zenodo.org/records/17531662) (GPL-3.0),
|
|
98
|
+
used solely for evaluation purposes.
|
|
@@ -237,3 +237,30 @@ https://github.com/ben-sb/javascript-deobfuscator/blob/master/LICENSE).
|
|
|
237
237
|
**Features derived from this project:** hex escape decoding (`--he`),
|
|
238
238
|
static array unpacking (`--su`), property access transformation (`--tp`).
|
|
239
239
|
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## webcrack
|
|
243
|
+
|
|
244
|
+
- **Version:** 2.14.1
|
|
245
|
+
- **Author:** j4k0xb
|
|
246
|
+
- **Repository:** https://github.com/j4k0xb/webcrack
|
|
247
|
+
- **License:** MIT
|
|
248
|
+
|
|
249
|
+
See [licenses/LICENSE-webcrack](licenses/LICENSE-webcrack) for the full license text.
|
|
250
|
+
|
|
251
|
+
**Features derived from this project:** general deobfuscation transform
|
|
252
|
+
patterns and architecture reference.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## Test dataset
|
|
257
|
+
|
|
258
|
+
- **JSIMPLIFIER dataset**
|
|
259
|
+
- **Author:** Dongchao Zhou
|
|
260
|
+
- **Source:** https://zenodo.org/records/17531662
|
|
261
|
+
- **License:** GPL-3.0
|
|
262
|
+
|
|
263
|
+
Obfuscated JavaScript samples from this dataset are included in
|
|
264
|
+
`tests/resources/` for evaluation purposes only. They are not part of the
|
|
265
|
+
distributed package and no code from JSIMPLIFIER is incorporated into
|
|
266
|
+
PyJSClear.
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
Combines functionality from obfuscator-io-deobfuscator (13 AST transforms)
|
|
4
4
|
and javascript-deobfuscator (3 surface-cleanup modules) into a single
|
|
5
|
-
Python package
|
|
5
|
+
Python package.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from .deobfuscator import Deobfuscator
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
__version__ = '0.1.
|
|
11
|
+
__version__ = '0.1.3'
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def deobfuscate(code, max_iterations=50):
|
|
@@ -2,31 +2,54 @@
|
|
|
2
2
|
|
|
3
3
|
from .generator import generate
|
|
4
4
|
from .parser import parse
|
|
5
|
-
from .transforms.aa_decode import aa_decode
|
|
6
|
-
from .transforms.aa_decode import is_aa_encoded
|
|
7
5
|
from .transforms.anti_tamper import AntiTamperRemover
|
|
6
|
+
from .transforms.class_static_resolver import ClassStaticResolver
|
|
7
|
+
from .transforms.class_string_decoder import ClassStringDecoder
|
|
8
|
+
from .transforms.cleanup import EmptyIfRemover
|
|
9
|
+
from .transforms.cleanup import LetToConst
|
|
10
|
+
from .transforms.cleanup import OptionalCatchBinding
|
|
11
|
+
from .transforms.cleanup import ReturnUndefinedCleanup
|
|
12
|
+
from .transforms.cleanup import TrailingReturnRemover
|
|
13
|
+
from .transforms.cleanup import VarToConst
|
|
8
14
|
from .transforms.constant_prop import ConstantProp
|
|
9
15
|
from .transforms.control_flow import ControlFlowRecoverer
|
|
10
16
|
from .transforms.dead_branch import DeadBranchRemover
|
|
17
|
+
from .transforms.dead_class_props import DeadClassPropRemover
|
|
18
|
+
from .transforms.dead_expressions import DeadExpressionRemover
|
|
19
|
+
from .transforms.dead_object_props import DeadObjectPropRemover
|
|
20
|
+
from .transforms.else_if_flatten import ElseIfFlattener
|
|
21
|
+
from .transforms.enum_resolver import EnumResolver
|
|
11
22
|
from .transforms.eval_unpack import eval_unpack
|
|
12
23
|
from .transforms.eval_unpack import is_eval_packed
|
|
13
24
|
from .transforms.expression_simplifier import ExpressionSimplifier
|
|
25
|
+
from .transforms.global_alias import GlobalAliasInliner
|
|
14
26
|
from .transforms.hex_escapes import HexEscapes
|
|
15
27
|
from .transforms.hex_escapes import decode_hex_escapes_source
|
|
28
|
+
from .transforms.hex_numerics import HexNumerics
|
|
29
|
+
from .transforms.aa_decode import aa_decode
|
|
30
|
+
from .transforms.aa_decode import is_aa_encoded
|
|
16
31
|
from .transforms.jj_decode import is_jj_encoded
|
|
17
32
|
from .transforms.jj_decode import jj_decode
|
|
18
|
-
from .transforms.jj_decode import jj_decode_via_eval
|
|
19
33
|
from .transforms.jsfuck_decode import is_jsfuck
|
|
20
34
|
from .transforms.jsfuck_decode import jsfuck_decode
|
|
21
35
|
from .transforms.logical_to_if import LogicalToIf
|
|
36
|
+
from .transforms.member_chain_resolver import MemberChainResolver
|
|
37
|
+
from .transforms.noop_calls import NoopCallRemover
|
|
38
|
+
from .transforms.nullish_coalescing import NullishCoalescing
|
|
22
39
|
from .transforms.object_packer import ObjectPacker
|
|
23
40
|
from .transforms.object_simplifier import ObjectSimplifier
|
|
41
|
+
from .transforms.optional_chaining import OptionalChaining
|
|
24
42
|
from .transforms.property_simplifier import PropertySimplifier
|
|
25
43
|
from .transforms.proxy_functions import ProxyFunctionInliner
|
|
26
44
|
from .transforms.reassignment import ReassignmentRemover
|
|
45
|
+
from .transforms.require_inliner import RequireInliner
|
|
27
46
|
from .transforms.sequence_splitter import SequenceSplitter
|
|
47
|
+
from .transforms.single_use_vars import SingleUseVarInliner
|
|
28
48
|
from .transforms.string_revealer import StringRevealer
|
|
49
|
+
from .transforms.unreachable_code import UnreachableCodeRemover
|
|
29
50
|
from .transforms.unused_vars import UnusedVariableRemover
|
|
51
|
+
from .transforms.variable_renamer import VariableRenamer
|
|
52
|
+
from .transforms.xor_string_decode import XorStringDecoder
|
|
30
53
|
from .traverser import simple_traverse
|
|
31
54
|
|
|
32
55
|
|
|
@@ -36,15 +59,36 @@ from .traverser import simple_traverse
|
|
|
36
59
|
TRANSFORM_CLASSES = [
|
|
37
60
|
StringRevealer,
|
|
38
61
|
HexEscapes,
|
|
62
|
+
HexNumerics,
|
|
63
|
+
ClassStringDecoder,
|
|
64
|
+
XorStringDecoder,
|
|
65
|
+
MemberChainResolver,
|
|
66
|
+
DeadClassPropRemover,
|
|
67
|
+
ClassStaticResolver,
|
|
68
|
+
EnumResolver,
|
|
69
|
+
RequireInliner,
|
|
70
|
+
GlobalAliasInliner,
|
|
39
71
|
UnusedVariableRemover,
|
|
40
72
|
ConstantProp,
|
|
41
73
|
ReassignmentRemover,
|
|
74
|
+
SingleUseVarInliner,
|
|
42
75
|
DeadBranchRemover,
|
|
76
|
+
UnreachableCodeRemover,
|
|
77
|
+
NoopCallRemover,
|
|
78
|
+
EmptyIfRemover,
|
|
79
|
+
DeadObjectPropRemover,
|
|
43
80
|
ObjectPacker,
|
|
44
81
|
ProxyFunctionInliner,
|
|
45
82
|
SequenceSplitter,
|
|
83
|
+
DeadExpressionRemover,
|
|
46
84
|
ExpressionSimplifier,
|
|
85
|
+
NullishCoalescing,
|
|
86
|
+
OptionalChaining,
|
|
47
87
|
LogicalToIf,
|
|
88
|
+
ElseIfFlattener,
|
|
89
|
+
OptionalCatchBinding,
|
|
90
|
+
ReturnUndefinedCleanup,
|
|
91
|
+
TrailingReturnRemover,
|
|
48
92
|
ControlFlowRecoverer,
|
|
49
93
|
PropertySimplifier,
|
|
50
94
|
AntiTamperRemover,
|
|
@@ -87,7 +131,7 @@ class Deobfuscator:
|
|
|
87
131
|
Returns decoded code if an encoding/packing was detected and decoded,
|
|
88
132
|
or None to continue with the normal AST pipeline.
|
|
89
133
|
"""
|
|
90
|
-
#
|
|
134
|
+
# JSFuck check (must be first — these are whole-file encodings)
|
|
91
135
|
if is_jsfuck(code):
|
|
92
136
|
decoded = jsfuck_decode(code)
|
|
93
137
|
if decoded:
|
|
@@ -101,7 +145,7 @@ class Deobfuscator:
|
|
|
101
145
|
|
|
102
146
|
# JJEncode check
|
|
103
147
|
if is_jj_encoded(code):
|
|
104
|
-
decoded = jj_decode(code)
|
|
148
|
+
decoded = jj_decode(code)
|
|
105
149
|
if decoded:
|
|
106
150
|
return decoded
|
|
107
151
|
|
|
@@ -113,6 +157,9 @@ class Deobfuscator:
|
|
|
113
157
|
|
|
114
158
|
return None
|
|
115
159
|
|
|
160
|
+
# Maximum number of outer re-parse cycles (generate → re-parse → re-transform)
|
|
161
|
+
_MAX_OUTER_CYCLES = 5
|
|
162
|
+
|
|
116
163
|
def execute(self):
|
|
117
164
|
"""Run all transforms and return cleaned source."""
|
|
118
165
|
code = self.original_code
|
|
@@ -134,25 +181,79 @@ class Deobfuscator:
|
|
|
134
181
|
return decoded
|
|
135
182
|
return self.original_code
|
|
136
183
|
|
|
137
|
-
#
|
|
138
|
-
|
|
184
|
+
# Outer loop: run AST transforms until generate→re-parse converges.
|
|
185
|
+
# Post-passes (VariableRenamer, VarToConst, LetToConst) only run on
|
|
186
|
+
# the final cycle to avoid interfering with subsequent transform rounds.
|
|
187
|
+
previous_code = code
|
|
188
|
+
last_changed_ast = None
|
|
189
|
+
try:
|
|
190
|
+
for _cycle in range(self._MAX_OUTER_CYCLES):
|
|
191
|
+
changed = self._run_ast_transforms(
|
|
192
|
+
ast,
|
|
193
|
+
code_size=len(previous_code),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if not changed:
|
|
197
|
+
break
|
|
198
|
+
|
|
199
|
+
last_changed_ast = ast
|
|
200
|
+
|
|
201
|
+
try:
|
|
202
|
+
generated = generate(ast)
|
|
203
|
+
except Exception:
|
|
204
|
+
break
|
|
205
|
+
|
|
206
|
+
if generated == previous_code:
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
previous_code = generated
|
|
210
|
+
|
|
211
|
+
# Re-parse for the next cycle
|
|
212
|
+
try:
|
|
213
|
+
ast = parse(generated)
|
|
214
|
+
except SyntaxError:
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
# Run post-passes on the final AST (always — they're cheap and handle
|
|
218
|
+
# cosmetic transforms like var→const even when no main transforms fired)
|
|
219
|
+
any_post_changed = False
|
|
220
|
+
for post_transform in [VariableRenamer, VarToConst, LetToConst]:
|
|
221
|
+
try:
|
|
222
|
+
if post_transform(ast).execute():
|
|
223
|
+
any_post_changed = True
|
|
224
|
+
except Exception:
|
|
225
|
+
pass
|
|
226
|
+
|
|
227
|
+
if last_changed_ast is None and not any_post_changed:
|
|
228
|
+
return self.original_code
|
|
229
|
+
|
|
230
|
+
try:
|
|
231
|
+
return generate(ast)
|
|
232
|
+
except Exception:
|
|
233
|
+
return previous_code
|
|
234
|
+
except RecursionError:
|
|
235
|
+
# Safety net: esprima's parser is purely recursive with no depth
|
|
236
|
+
# limit, so deeply nested JS hits Python's recursion limit during
|
|
237
|
+
# parsing or re-parsing. Our AST walkers are cheaper per level
|
|
238
|
+
# but also recursive. Return best result so far.
|
|
239
|
+
return previous_code
|
|
240
|
+
|
|
241
|
+
def _run_ast_transforms(self, ast, code_size=0):
|
|
242
|
+
"""Run all AST transform passes. Returns True if any transform changed the AST."""
|
|
243
|
+
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
244
|
+
|
|
139
245
|
lite_mode = code_size > _MAX_CODE_SIZE
|
|
140
246
|
max_iterations = self.max_iterations
|
|
141
247
|
if code_size > _LARGE_FILE_SIZE:
|
|
142
248
|
max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
|
|
143
249
|
|
|
144
|
-
# Check node count for expensive transform gating
|
|
145
|
-
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
146
|
-
|
|
147
250
|
# For very large ASTs, further reduce iterations
|
|
148
251
|
if node_count > 100_000:
|
|
149
252
|
max_iterations = min(max_iterations, 3)
|
|
150
253
|
|
|
151
254
|
# Build transform list based on mode
|
|
152
255
|
transform_classes = TRANSFORM_CLASSES
|
|
153
|
-
if lite_mode:
|
|
154
|
-
transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
|
|
155
|
-
elif node_count > _NODE_COUNT_LIMIT:
|
|
256
|
+
if lite_mode or node_count > _NODE_COUNT_LIMIT:
|
|
156
257
|
transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
|
|
157
258
|
|
|
158
259
|
# Track which transforms are no longer productive
|
|
@@ -182,10 +283,4 @@ class Deobfuscator:
|
|
|
182
283
|
if not modified:
|
|
183
284
|
break
|
|
184
285
|
|
|
185
|
-
|
|
186
|
-
return self.original_code
|
|
187
|
-
|
|
188
|
-
try:
|
|
189
|
-
return generate(ast)
|
|
190
|
-
except Exception:
|
|
191
|
-
return self.original_code
|
|
286
|
+
return any_transform_changed
|
|
@@ -363,9 +363,12 @@ def _gen_member(node, indent):
|
|
|
363
363
|
object_code = f'({object_code})'
|
|
364
364
|
|
|
365
365
|
property_code = generate(node['property'], indent)
|
|
366
|
+
dot = '?.' if node.get('optional') else '.'
|
|
366
367
|
if computed:
|
|
368
|
+
if node.get('optional'):
|
|
369
|
+
return f'{object_code}?.[{property_code}]'
|
|
367
370
|
return f'{object_code}[{property_code}]'
|
|
368
|
-
return f'{object_code}
|
|
371
|
+
return f'{object_code}{dot}{property_code}'
|
|
369
372
|
|
|
370
373
|
|
|
371
374
|
def _gen_call(node, indent):
|
|
@@ -374,6 +377,8 @@ def _gen_call(node, indent):
|
|
|
374
377
|
if callee_type in ('FunctionExpression', 'ArrowFunctionExpression', 'SequenceExpression'):
|
|
375
378
|
callee = f'({callee})'
|
|
376
379
|
args = ', '.join(generate(a, indent) for a in node.get('arguments', []))
|
|
380
|
+
if node.get('optional'):
|
|
381
|
+
return f'{callee}?.({args})'
|
|
377
382
|
return f'{callee}({args})'
|
|
378
383
|
|
|
379
384
|
|