pyjsclear 0.1.2__tar.gz → 0.1.4__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.4/NOTICE +28 -0
- {pyjsclear-0.1.2/pyjsclear.egg-info → pyjsclear-0.1.4}/PKG-INFO +14 -16
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/README.md +13 -15
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/THIRD_PARTY_LICENSES.md +27 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/__init__.py +7 -7
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/__main__.py +4 -3
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/deobfuscator.py +126 -36
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/generator.py +111 -110
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/parser.py +2 -2
- pyjsclear-0.1.4/pyjsclear/scope.py +505 -0
- pyjsclear-0.1.4/pyjsclear/transforms/aa_decode.py +150 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/anti_tamper.py +12 -15
- pyjsclear-0.1.4/pyjsclear/transforms/base.py +58 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/class_static_resolver.py +27 -21
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/class_string_decoder.py +64 -63
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/cleanup.py +54 -43
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/constant_prop.py +41 -32
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/control_flow.py +94 -97
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_branch.py +9 -9
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_class_props.py +55 -52
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_expressions.py +5 -5
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/dead_object_props.py +29 -29
- pyjsclear-0.1.4/pyjsclear/transforms/else_if_flatten.py +32 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/enum_resolver.py +15 -15
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/eval_unpack.py +28 -27
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/expression_simplifier.py +96 -78
- pyjsclear-0.1.4/pyjsclear/transforms/global_alias.py +128 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/hex_escapes.py +10 -13
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/hex_numerics.py +2 -2
- pyjsclear-0.1.4/pyjsclear/transforms/jj_decode.py +720 -0
- pyjsclear-0.1.4/pyjsclear/transforms/jsfuck_decode.py +610 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/logical_to_if.py +57 -57
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/member_chain_resolver.py +51 -73
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/noop_calls.py +22 -20
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/nullish_coalescing.py +39 -28
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/object_packer.py +11 -13
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/object_simplifier.py +44 -41
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/optional_chaining.py +44 -43
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/property_simplifier.py +4 -4
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/proxy_functions.py +38 -35
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/reassignment.py +26 -17
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/require_inliner.py +13 -17
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/sequence_splitter.py +59 -61
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/single_use_vars.py +30 -21
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/string_revealer.py +99 -88
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/unreachable_code.py +14 -13
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/unused_vars.py +22 -19
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/variable_renamer.py +108 -108
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/xor_string_decode.py +39 -39
- pyjsclear-0.1.4/pyjsclear/traverser.py +434 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/utils/ast_helpers.py +36 -36
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/utils/string_decoders.py +18 -19
- {pyjsclear-0.1.2 → pyjsclear-0.1.4/pyjsclear.egg-info}/PKG-INFO +14 -16
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear.egg-info/SOURCES.txt +2 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/tests/test_regression.py +26 -29
- pyjsclear-0.1.2/NOTICE +0 -19
- pyjsclear-0.1.2/pyjsclear/scope.py +0 -309
- pyjsclear-0.1.2/pyjsclear/transforms/aa_decode.py +0 -83
- pyjsclear-0.1.2/pyjsclear/transforms/base.py +0 -24
- pyjsclear-0.1.2/pyjsclear/transforms/else_if_flatten.py +0 -32
- pyjsclear-0.1.2/pyjsclear/transforms/global_alias.py +0 -117
- pyjsclear-0.1.2/pyjsclear/traverser.py +0 -195
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/LICENSE +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/MANIFEST.in +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/transforms/__init__.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear/utils/__init__.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear.egg-info/dependency_links.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear.egg-info/entry_points.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear.egg-info/requires.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyjsclear.egg-info/top_level.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/pyproject.toml +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.4}/setup.cfg +0 -0
pyjsclear-0.1.4/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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyjsclear
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.4
|
|
4
4
|
Summary: Pure Python JavaScript deobfuscator
|
|
5
5
|
Author-email: Intezer Labs <info@intezer.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -82,11 +82,12 @@ pyjsclear input.js --max-iterations 20
|
|
|
82
82
|
|
|
83
83
|
## What it does
|
|
84
84
|
|
|
85
|
-
PyJSClear applies
|
|
86
|
-
|
|
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
87
|
variables and converts var/let to const.
|
|
88
88
|
|
|
89
89
|
**Capabilities:**
|
|
90
|
+
- Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
|
|
90
91
|
- String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
|
|
91
92
|
- Constant propagation & reassignment elimination
|
|
92
93
|
- Dead code / dead branch / unreachable code removal
|
|
@@ -99,20 +100,11 @@ variables and converts var/let to const.
|
|
|
99
100
|
Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
|
|
100
101
|
that skips expensive transforms.
|
|
101
102
|
|
|
102
|
-
## Testing
|
|
103
|
-
|
|
104
|
-
```bash
|
|
105
|
-
pytest tests/ # all tests
|
|
106
|
-
pytest tests/test_regression.py # regression suite (62 tests across 25 samples)
|
|
107
|
-
pytest tests/ -n auto # parallel execution (requires pytest-xdist)
|
|
108
|
-
```
|
|
109
|
-
|
|
110
103
|
## Limitations
|
|
111
104
|
|
|
112
|
-
- **
|
|
105
|
+
- **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
|
|
113
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.
|
|
114
|
-
- **
|
|
115
|
-
- **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting.
|
|
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.
|
|
116
108
|
|
|
117
109
|
## License
|
|
118
110
|
|
|
@@ -120,7 +112,13 @@ Apache License 2.0 — see [LICENSE](LICENSE).
|
|
|
120
112
|
|
|
121
113
|
This project is a derivative work based on
|
|
122
114
|
[obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
|
|
123
|
-
(Apache 2.0)
|
|
115
|
+
(Apache 2.0),
|
|
124
116
|
[javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
|
|
125
|
-
(Apache 2.0)
|
|
117
|
+
(Apache 2.0), and
|
|
118
|
+
[webcrack](https://github.com/j4k0xb/webcrack) (MIT).
|
|
119
|
+
See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
|
|
126
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.
|
|
@@ -56,11 +56,12 @@ pyjsclear input.js --max-iterations 20
|
|
|
56
56
|
|
|
57
57
|
## What it does
|
|
58
58
|
|
|
59
|
-
PyJSClear applies
|
|
60
|
-
|
|
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
61
|
variables and converts var/let to const.
|
|
62
62
|
|
|
63
63
|
**Capabilities:**
|
|
64
|
+
- Whole-file encoding detection: JSFuck, JJEncode, AAEncode, eval-packing
|
|
64
65
|
- String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
|
|
65
66
|
- Constant propagation & reassignment elimination
|
|
66
67
|
- Dead code / dead branch / unreachable code removal
|
|
@@ -73,20 +74,11 @@ variables and converts var/let to const.
|
|
|
73
74
|
Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
|
|
74
75
|
that skips expensive transforms.
|
|
75
76
|
|
|
76
|
-
## Testing
|
|
77
|
-
|
|
78
|
-
```bash
|
|
79
|
-
pytest tests/ # all tests
|
|
80
|
-
pytest tests/test_regression.py # regression suite (62 tests across 25 samples)
|
|
81
|
-
pytest tests/ -n auto # parallel execution (requires pytest-xdist)
|
|
82
|
-
```
|
|
83
|
-
|
|
84
77
|
## Limitations
|
|
85
78
|
|
|
86
|
-
- **
|
|
79
|
+
- **Best results on obfuscator.io output.** JSFuck, JJEncode, AAEncode, and eval-packed code are fully decoded; other obfuscation tools may only partially deobfuscate.
|
|
87
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.
|
|
88
|
-
- **
|
|
89
|
-
- **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting.
|
|
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.
|
|
90
82
|
|
|
91
83
|
## License
|
|
92
84
|
|
|
@@ -94,7 +86,13 @@ Apache License 2.0 — see [LICENSE](LICENSE).
|
|
|
94
86
|
|
|
95
87
|
This project is a derivative work based on
|
|
96
88
|
[obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
|
|
97
|
-
(Apache 2.0)
|
|
89
|
+
(Apache 2.0),
|
|
98
90
|
[javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
|
|
99
|
-
(Apache 2.0)
|
|
91
|
+
(Apache 2.0), and
|
|
92
|
+
[webcrack](https://github.com/j4k0xb/webcrack) (MIT).
|
|
93
|
+
See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
|
|
100
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.
|
|
@@ -8,10 +8,10 @@ Python package.
|
|
|
8
8
|
from .deobfuscator import Deobfuscator
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
__version__ = '0.1.
|
|
11
|
+
__version__ = '0.1.4'
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
def deobfuscate(code, max_iterations=50):
|
|
14
|
+
def deobfuscate(code: str, max_iterations: int = 50) -> str:
|
|
15
15
|
"""Deobfuscate JavaScript code. Returns cleaned source.
|
|
16
16
|
|
|
17
17
|
Args:
|
|
@@ -24,7 +24,7 @@ def deobfuscate(code, max_iterations=50):
|
|
|
24
24
|
return Deobfuscator(code, max_iterations=max_iterations).execute()
|
|
25
25
|
|
|
26
26
|
|
|
27
|
-
def deobfuscate_file(input_path, output_path=None, max_iterations=50):
|
|
27
|
+
def deobfuscate_file(input_path: str, output_path: str | None = None, max_iterations: int = 50) -> str | bool:
|
|
28
28
|
"""Deobfuscate a JavaScript file.
|
|
29
29
|
|
|
30
30
|
Args:
|
|
@@ -35,13 +35,13 @@ def deobfuscate_file(input_path, output_path=None, max_iterations=50):
|
|
|
35
35
|
Returns:
|
|
36
36
|
True if content changed (when output_path given), or the deobfuscated string.
|
|
37
37
|
"""
|
|
38
|
-
with open(input_path, 'r', errors='replace') as
|
|
39
|
-
code =
|
|
38
|
+
with open(input_path, 'r', errors='replace') as input_file:
|
|
39
|
+
code = input_file.read()
|
|
40
40
|
|
|
41
41
|
result = deobfuscate(code, max_iterations=max_iterations)
|
|
42
42
|
|
|
43
43
|
if output_path:
|
|
44
|
-
with open(output_path, 'w') as
|
|
45
|
-
|
|
44
|
+
with open(output_path, 'w') as output_file:
|
|
45
|
+
output_file.write(result)
|
|
46
46
|
return result != code
|
|
47
47
|
return result
|
|
@@ -6,7 +6,7 @@ import sys
|
|
|
6
6
|
from . import deobfuscate
|
|
7
7
|
|
|
8
8
|
|
|
9
|
-
def main():
|
|
9
|
+
def main() -> None:
|
|
10
10
|
parser = argparse.ArgumentParser(description='Deobfuscate JavaScript files.')
|
|
11
11
|
parser.add_argument('input', help='Input JS file (use - for stdin)')
|
|
12
12
|
parser.add_argument('-o', '--output', help='Output file (default: stdout)')
|
|
@@ -29,8 +29,9 @@ def main():
|
|
|
29
29
|
if args.output:
|
|
30
30
|
with open(args.output, 'w') as output_file:
|
|
31
31
|
output_file.write(result)
|
|
32
|
-
|
|
33
|
-
|
|
32
|
+
return
|
|
33
|
+
|
|
34
|
+
sys.stdout.write(result)
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
if __name__ == '__main__':
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from .generator import generate
|
|
4
4
|
from .parser import parse
|
|
5
|
+
from .scope import build_scope_tree
|
|
5
6
|
from .transforms.aa_decode import aa_decode
|
|
6
7
|
from .transforms.aa_decode import is_aa_encoded
|
|
7
8
|
from .transforms.anti_tamper import AntiTamperRemover
|
|
@@ -28,6 +29,10 @@ from .transforms.global_alias import GlobalAliasInliner
|
|
|
28
29
|
from .transforms.hex_escapes import HexEscapes
|
|
29
30
|
from .transforms.hex_escapes import decode_hex_escapes_source
|
|
30
31
|
from .transforms.hex_numerics import HexNumerics
|
|
32
|
+
from .transforms.jj_decode import is_jj_encoded
|
|
33
|
+
from .transforms.jj_decode import jj_decode
|
|
34
|
+
from .transforms.jsfuck_decode import is_jsfuck
|
|
35
|
+
from .transforms.jsfuck_decode import jsfuck_decode
|
|
31
36
|
from .transforms.logical_to_if import LogicalToIf
|
|
32
37
|
from .transforms.member_chain_resolver import MemberChainResolver
|
|
33
38
|
from .transforms.noop_calls import NoopCallRemover
|
|
@@ -49,6 +54,22 @@ from .transforms.xor_string_decode import XorStringDecoder
|
|
|
49
54
|
from .traverser import simple_traverse
|
|
50
55
|
|
|
51
56
|
|
|
57
|
+
# Transforms that use build_scope_tree and benefit from cached scope
|
|
58
|
+
_SCOPE_TRANSFORMS = frozenset(
|
|
59
|
+
{
|
|
60
|
+
ConstantProp,
|
|
61
|
+
SingleUseVarInliner,
|
|
62
|
+
ReassignmentRemover,
|
|
63
|
+
ProxyFunctionInliner,
|
|
64
|
+
UnusedVariableRemover,
|
|
65
|
+
ObjectSimplifier,
|
|
66
|
+
StringRevealer,
|
|
67
|
+
VariableRenamer,
|
|
68
|
+
VarToConst,
|
|
69
|
+
LetToConst,
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
|
|
52
73
|
# StringRevealer runs first to handle string arrays before other transforms
|
|
53
74
|
# modify the wrapper function structure.
|
|
54
75
|
# Remaining transforms follow obfuscator-io-deobfuscator order.
|
|
@@ -102,37 +123,49 @@ _LITE_MAX_ITERATIONS = 10
|
|
|
102
123
|
_NODE_COUNT_LIMIT = 50_000 # Skip ControlFlowRecoverer above this
|
|
103
124
|
|
|
104
125
|
|
|
105
|
-
def _count_nodes(ast):
|
|
126
|
+
def _count_nodes(ast: dict) -> int:
|
|
106
127
|
"""Count total AST nodes."""
|
|
107
128
|
count = 0
|
|
108
129
|
|
|
109
|
-
def
|
|
130
|
+
def increment_count(node: dict, parent: dict | None) -> None:
|
|
110
131
|
nonlocal count
|
|
111
132
|
count += 1
|
|
112
133
|
|
|
113
|
-
simple_traverse(ast,
|
|
134
|
+
simple_traverse(ast, increment_count)
|
|
114
135
|
return count
|
|
115
136
|
|
|
116
137
|
|
|
117
138
|
class Deobfuscator:
|
|
118
139
|
"""Multi-pass JavaScript deobfuscator."""
|
|
119
140
|
|
|
120
|
-
def __init__(self, code, max_iterations=50):
|
|
141
|
+
def __init__(self, code: str, max_iterations: int = 50) -> None:
|
|
121
142
|
self.original_code = code
|
|
122
143
|
self.max_iterations = max_iterations
|
|
123
144
|
|
|
124
|
-
def _run_pre_passes(self, code):
|
|
145
|
+
def _run_pre_passes(self, code: str) -> str | None:
|
|
125
146
|
"""Run encoding detection and eval unpacking pre-passes.
|
|
126
147
|
|
|
127
148
|
Returns decoded code if an encoding/packing was detected and decoded,
|
|
128
149
|
or None to continue with the normal AST pipeline.
|
|
129
150
|
"""
|
|
151
|
+
# JSFuck check (must be first — these are whole-file encodings)
|
|
152
|
+
if is_jsfuck(code):
|
|
153
|
+
decoded = jsfuck_decode(code)
|
|
154
|
+
if decoded:
|
|
155
|
+
return decoded
|
|
156
|
+
|
|
130
157
|
# AAEncode check
|
|
131
158
|
if is_aa_encoded(code):
|
|
132
159
|
decoded = aa_decode(code)
|
|
133
160
|
if decoded:
|
|
134
161
|
return decoded
|
|
135
162
|
|
|
163
|
+
# JJEncode check
|
|
164
|
+
if is_jj_encoded(code):
|
|
165
|
+
decoded = jj_decode(code)
|
|
166
|
+
if decoded:
|
|
167
|
+
return decoded
|
|
168
|
+
|
|
136
169
|
# Eval packer check
|
|
137
170
|
if is_eval_packed(code):
|
|
138
171
|
decoded = eval_unpack(code)
|
|
@@ -141,7 +174,10 @@ class Deobfuscator:
|
|
|
141
174
|
|
|
142
175
|
return None
|
|
143
176
|
|
|
144
|
-
|
|
177
|
+
# Maximum number of outer re-parse cycles (generate → re-parse → re-transform)
|
|
178
|
+
_MAX_OUTER_CYCLES = 5
|
|
179
|
+
|
|
180
|
+
def execute(self) -> str:
|
|
145
181
|
"""Run all transforms and return cleaned source."""
|
|
146
182
|
code = self.original_code
|
|
147
183
|
|
|
@@ -162,66 +198,120 @@ class Deobfuscator:
|
|
|
162
198
|
return decoded
|
|
163
199
|
return self.original_code
|
|
164
200
|
|
|
165
|
-
#
|
|
166
|
-
|
|
201
|
+
# Outer loop: run AST transforms until generate→re-parse converges.
|
|
202
|
+
# Post-passes (VariableRenamer, VarToConst, LetToConst) only run on
|
|
203
|
+
# the final cycle to avoid interfering with subsequent transform rounds.
|
|
204
|
+
previous_code = code
|
|
205
|
+
last_changed_ast = None
|
|
206
|
+
try:
|
|
207
|
+
for _cycle in range(self._MAX_OUTER_CYCLES):
|
|
208
|
+
changed = self._run_ast_transforms(
|
|
209
|
+
ast,
|
|
210
|
+
code_size=len(previous_code),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
if not changed:
|
|
214
|
+
break
|
|
215
|
+
|
|
216
|
+
last_changed_ast = ast
|
|
217
|
+
|
|
218
|
+
try:
|
|
219
|
+
generated = generate(ast)
|
|
220
|
+
except Exception:
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
if generated == previous_code:
|
|
224
|
+
break
|
|
225
|
+
|
|
226
|
+
previous_code = generated
|
|
227
|
+
|
|
228
|
+
# Re-parse for the next cycle
|
|
229
|
+
try:
|
|
230
|
+
ast = parse(generated)
|
|
231
|
+
except SyntaxError:
|
|
232
|
+
break
|
|
233
|
+
|
|
234
|
+
# Run post-passes on the final AST (always — they're cheap and handle
|
|
235
|
+
# cosmetic transforms like var→const even when no main transforms fired)
|
|
236
|
+
any_post_changed = False
|
|
237
|
+
for post_transform in [VariableRenamer, VarToConst, LetToConst]:
|
|
238
|
+
try:
|
|
239
|
+
if post_transform(ast).execute():
|
|
240
|
+
any_post_changed = True
|
|
241
|
+
except Exception:
|
|
242
|
+
pass
|
|
243
|
+
|
|
244
|
+
if last_changed_ast is None and not any_post_changed:
|
|
245
|
+
return self.original_code
|
|
246
|
+
|
|
247
|
+
try:
|
|
248
|
+
return generate(ast)
|
|
249
|
+
except Exception:
|
|
250
|
+
return previous_code
|
|
251
|
+
except RecursionError:
|
|
252
|
+
# Safety net: esprima's parser is purely recursive with no depth
|
|
253
|
+
# limit, so deeply nested JS hits Python's recursion limit during
|
|
254
|
+
# parsing or re-parsing. Our AST walkers are cheaper per level
|
|
255
|
+
# but also recursive. Return best result so far.
|
|
256
|
+
return previous_code
|
|
257
|
+
|
|
258
|
+
def _run_ast_transforms(self, ast: dict, code_size: int = 0) -> bool:
|
|
259
|
+
"""Run all AST transform passes. Returns True if any transform changed the AST."""
|
|
260
|
+
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
261
|
+
|
|
167
262
|
lite_mode = code_size > _MAX_CODE_SIZE
|
|
168
263
|
max_iterations = self.max_iterations
|
|
169
264
|
if code_size > _LARGE_FILE_SIZE:
|
|
170
265
|
max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
|
|
171
266
|
|
|
172
|
-
# Check node count for expensive transform gating
|
|
173
|
-
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
174
|
-
|
|
175
267
|
# For very large ASTs, further reduce iterations
|
|
176
268
|
if node_count > 100_000:
|
|
177
269
|
max_iterations = min(max_iterations, 3)
|
|
178
270
|
|
|
179
271
|
# Build transform list based on mode
|
|
180
272
|
transform_classes = TRANSFORM_CLASSES
|
|
181
|
-
if lite_mode:
|
|
182
|
-
transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
|
|
183
|
-
elif node_count > _NODE_COUNT_LIMIT:
|
|
273
|
+
if lite_mode or node_count > _NODE_COUNT_LIMIT:
|
|
184
274
|
transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
|
|
185
275
|
|
|
186
276
|
# Track which transforms are no longer productive
|
|
187
277
|
skip_transforms = set()
|
|
188
278
|
|
|
279
|
+
# Cache scope tree across transforms — only rebuild when a transform
|
|
280
|
+
# that modifies bindings returns changed=True
|
|
281
|
+
scope_tree = None
|
|
282
|
+
node_scope = None
|
|
283
|
+
scope_dirty = True # Start dirty to build on first use
|
|
284
|
+
|
|
189
285
|
# Multi-pass transform loop
|
|
190
286
|
any_transform_changed = False
|
|
191
|
-
for
|
|
287
|
+
for iteration in range(max_iterations):
|
|
192
288
|
modified = False
|
|
193
289
|
for transform_class in transform_classes:
|
|
194
290
|
if transform_class in skip_transforms:
|
|
195
291
|
continue
|
|
196
292
|
try:
|
|
197
|
-
|
|
293
|
+
# Build scope tree lazily when needed by a scope-using transform
|
|
294
|
+
if transform_class in _SCOPE_TRANSFORMS and scope_dirty:
|
|
295
|
+
scope_tree, node_scope = build_scope_tree(ast)
|
|
296
|
+
scope_dirty = False
|
|
297
|
+
|
|
298
|
+
if transform_class in _SCOPE_TRANSFORMS:
|
|
299
|
+
transform = transform_class(ast, scope_tree=scope_tree, node_scope=node_scope)
|
|
300
|
+
else:
|
|
301
|
+
transform = transform_class(ast)
|
|
198
302
|
result = transform.execute()
|
|
199
303
|
except Exception:
|
|
200
304
|
continue
|
|
201
305
|
if result:
|
|
202
306
|
modified = True
|
|
203
307
|
any_transform_changed = True
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
308
|
+
# Any AST change invalidates the cached scope tree
|
|
309
|
+
scope_dirty = True
|
|
310
|
+
elif iteration > 0:
|
|
311
|
+
# Skip transforms that haven't changed anything after the first pass
|
|
312
|
+
skip_transforms.add(transform_class)
|
|
209
313
|
|
|
210
314
|
if not modified:
|
|
211
315
|
break
|
|
212
316
|
|
|
213
|
-
|
|
214
|
-
for post_transform in [VariableRenamer, VarToConst, LetToConst]:
|
|
215
|
-
try:
|
|
216
|
-
if post_transform(ast).execute():
|
|
217
|
-
any_transform_changed = True
|
|
218
|
-
except Exception:
|
|
219
|
-
pass
|
|
220
|
-
|
|
221
|
-
if not any_transform_changed:
|
|
222
|
-
return self.original_code
|
|
223
|
-
|
|
224
|
-
try:
|
|
225
|
-
return generate(ast)
|
|
226
|
-
except Exception:
|
|
227
|
-
return self.original_code
|
|
317
|
+
return any_transform_changed
|