pyjsclear 0.1.2__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.2/pyjsclear.egg-info → pyjsclear-0.1.3}/PKG-INFO +14 -16
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/README.md +13 -15
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/THIRD_PARTY_LICENSES.md +27 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/__init__.py +1 -1
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/deobfuscator.py +84 -25
- pyjsclear-0.1.3/pyjsclear/transforms/aa_decode.py +150 -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.2 → pyjsclear-0.1.3/pyjsclear.egg-info}/PKG-INFO +14 -16
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/SOURCES.txt +2 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/tests/test_regression.py +1 -1
- pyjsclear-0.1.2/NOTICE +0 -19
- pyjsclear-0.1.2/pyjsclear/transforms/aa_decode.py +0 -83
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/LICENSE +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/MANIFEST.in +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/__main__.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/generator.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/parser.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/scope.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/__init__.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/anti_tamper.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/base.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/class_static_resolver.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/class_string_decoder.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/cleanup.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/constant_prop.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/control_flow.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_branch.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_class_props.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_expressions.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/dead_object_props.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/else_if_flatten.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/enum_resolver.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/eval_unpack.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/expression_simplifier.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/global_alias.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/hex_escapes.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/hex_numerics.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/logical_to_if.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/member_chain_resolver.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/noop_calls.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/nullish_coalescing.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/object_packer.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/object_simplifier.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/optional_chaining.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/property_simplifier.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/proxy_functions.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/reassignment.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/require_inliner.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/sequence_splitter.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/single_use_vars.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/string_revealer.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/unreachable_code.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/unused_vars.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/variable_renamer.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/transforms/xor_string_decode.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/traverser.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/utils/__init__.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/utils/ast_helpers.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear/utils/string_decoders.py +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/dependency_links.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/entry_points.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/requires.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyjsclear.egg-info/top_level.txt +0 -0
- {pyjsclear-0.1.2 → pyjsclear-0.1.3}/pyproject.toml +0 -0
- {pyjsclear-0.1.2 → 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyjsclear
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.3
|
|
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.
|
|
@@ -2,8 +2,6 @@
|
|
|
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
|
|
8
6
|
from .transforms.class_static_resolver import ClassStaticResolver
|
|
9
7
|
from .transforms.class_string_decoder import ClassStringDecoder
|
|
@@ -28,6 +26,12 @@ from .transforms.global_alias import GlobalAliasInliner
|
|
|
28
26
|
from .transforms.hex_escapes import HexEscapes
|
|
29
27
|
from .transforms.hex_escapes import decode_hex_escapes_source
|
|
30
28
|
from .transforms.hex_numerics import HexNumerics
|
|
29
|
+
from .transforms.aa_decode import aa_decode
|
|
30
|
+
from .transforms.aa_decode import is_aa_encoded
|
|
31
|
+
from .transforms.jj_decode import is_jj_encoded
|
|
32
|
+
from .transforms.jj_decode import jj_decode
|
|
33
|
+
from .transforms.jsfuck_decode import is_jsfuck
|
|
34
|
+
from .transforms.jsfuck_decode import jsfuck_decode
|
|
31
35
|
from .transforms.logical_to_if import LogicalToIf
|
|
32
36
|
from .transforms.member_chain_resolver import MemberChainResolver
|
|
33
37
|
from .transforms.noop_calls import NoopCallRemover
|
|
@@ -127,12 +131,24 @@ class Deobfuscator:
|
|
|
127
131
|
Returns decoded code if an encoding/packing was detected and decoded,
|
|
128
132
|
or None to continue with the normal AST pipeline.
|
|
129
133
|
"""
|
|
134
|
+
# JSFuck check (must be first — these are whole-file encodings)
|
|
135
|
+
if is_jsfuck(code):
|
|
136
|
+
decoded = jsfuck_decode(code)
|
|
137
|
+
if decoded:
|
|
138
|
+
return decoded
|
|
139
|
+
|
|
130
140
|
# AAEncode check
|
|
131
141
|
if is_aa_encoded(code):
|
|
132
142
|
decoded = aa_decode(code)
|
|
133
143
|
if decoded:
|
|
134
144
|
return decoded
|
|
135
145
|
|
|
146
|
+
# JJEncode check
|
|
147
|
+
if is_jj_encoded(code):
|
|
148
|
+
decoded = jj_decode(code)
|
|
149
|
+
if decoded:
|
|
150
|
+
return decoded
|
|
151
|
+
|
|
136
152
|
# Eval packer check
|
|
137
153
|
if is_eval_packed(code):
|
|
138
154
|
decoded = eval_unpack(code)
|
|
@@ -141,6 +157,9 @@ class Deobfuscator:
|
|
|
141
157
|
|
|
142
158
|
return None
|
|
143
159
|
|
|
160
|
+
# Maximum number of outer re-parse cycles (generate → re-parse → re-transform)
|
|
161
|
+
_MAX_OUTER_CYCLES = 5
|
|
162
|
+
|
|
144
163
|
def execute(self):
|
|
145
164
|
"""Run all transforms and return cleaned source."""
|
|
146
165
|
code = self.original_code
|
|
@@ -162,25 +181,79 @@ class Deobfuscator:
|
|
|
162
181
|
return decoded
|
|
163
182
|
return self.original_code
|
|
164
183
|
|
|
165
|
-
#
|
|
166
|
-
|
|
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
|
+
|
|
167
245
|
lite_mode = code_size > _MAX_CODE_SIZE
|
|
168
246
|
max_iterations = self.max_iterations
|
|
169
247
|
if code_size > _LARGE_FILE_SIZE:
|
|
170
248
|
max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
|
|
171
249
|
|
|
172
|
-
# Check node count for expensive transform gating
|
|
173
|
-
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
174
|
-
|
|
175
250
|
# For very large ASTs, further reduce iterations
|
|
176
251
|
if node_count > 100_000:
|
|
177
252
|
max_iterations = min(max_iterations, 3)
|
|
178
253
|
|
|
179
254
|
# Build transform list based on mode
|
|
180
255
|
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:
|
|
256
|
+
if lite_mode or node_count > _NODE_COUNT_LIMIT:
|
|
184
257
|
transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
|
|
185
258
|
|
|
186
259
|
# Track which transforms are no longer productive
|
|
@@ -210,18 +283,4 @@ class Deobfuscator:
|
|
|
210
283
|
if not modified:
|
|
211
284
|
break
|
|
212
285
|
|
|
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
|
|
286
|
+
return any_transform_changed
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Pure Python AAEncode decoder.
|
|
2
|
+
|
|
3
|
+
AAEncode (by Yosuke Hasegawa) encodes JavaScript into Japanese-style
|
|
4
|
+
emoticon characters using fullwidth/halfwidth katakana and special symbols.
|
|
5
|
+
Each source character is represented as an octal or hex escape built from
|
|
6
|
+
emoticon digit expressions, separated by a backslash-like marker.
|
|
7
|
+
|
|
8
|
+
This decoder performs iterative string replacements to recover the digit
|
|
9
|
+
sequences, then converts octal/hex values back to characters.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
|
|
14
|
+
# Characteristic pattern present in all AAEncoded output — the execution call.
|
|
15
|
+
_SIGNATURE = '\uff9f\u0414\uff9f)[\uff9f\u03b5\uff9f]'
|
|
16
|
+
|
|
17
|
+
# Separator between encoded characters (represents the escape character "\").
|
|
18
|
+
_SEPARATOR = '(\uff9f\u0414\uff9f)[\uff9f\u03b5\uff9f]+'
|
|
19
|
+
|
|
20
|
+
# Unicode hex marker — when present before a segment, the value is hex (\uXXXX).
|
|
21
|
+
# Note: real AAEncode uses U+FF70 (halfwidth katakana-hiragana prolonged sound mark ー),
|
|
22
|
+
# NOT U+30FC (fullwidth ー).
|
|
23
|
+
_UNICODE_MARKER = '(o\uff9f\uff70\uff9fo)'
|
|
24
|
+
|
|
25
|
+
# Sentinel used to track unicode marker positions after replacement.
|
|
26
|
+
_HEX_SENTINEL = '\x01'
|
|
27
|
+
|
|
28
|
+
# Replacement rules: longer/more specific patterns first to avoid partial matches.
|
|
29
|
+
# All patterns use U+FF70 (ー) to match real AAEncode output.
|
|
30
|
+
_REPLACEMENTS = [
|
|
31
|
+
('(o\uff9f\uff70\uff9fo)', _HEX_SENTINEL),
|
|
32
|
+
('((\uff9f\uff70\uff9f) + (\uff9f\uff70\uff9f) + (\uff9f\u0398\uff9f))', '5'),
|
|
33
|
+
('((\uff9f\uff70\uff9f) + (\uff9f\uff70\uff9f))', '4'),
|
|
34
|
+
('((\uff9f\uff70\uff9f) + (o^_^o))', '3'),
|
|
35
|
+
('((\uff9f\uff70\uff9f) + (\uff9f\u0398\uff9f))', '2'),
|
|
36
|
+
('((o^_^o) - (\uff9f\u0398\uff9f))', '2'),
|
|
37
|
+
('((o^_^o) + (o^_^o))', '6'),
|
|
38
|
+
('(\uff9f\uff70\uff9f)', '1'),
|
|
39
|
+
('(\uff9f\u0398\uff9f)', '1'),
|
|
40
|
+
('(c^_^o)', '0'),
|
|
41
|
+
('(o^_^o)', '3'),
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def is_aa_encoded(code):
|
|
46
|
+
"""Check if *code* looks like AAEncoded JavaScript.
|
|
47
|
+
|
|
48
|
+
Returns True when the characteristic execution pattern is found.
|
|
49
|
+
"""
|
|
50
|
+
if not isinstance(code, str):
|
|
51
|
+
return False
|
|
52
|
+
return _SIGNATURE in code
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def aa_decode(code):
|
|
56
|
+
"""Decode AAEncoded JavaScript.
|
|
57
|
+
|
|
58
|
+
Returns the decoded source string, or ``None`` on any failure.
|
|
59
|
+
All processing is iterative (no recursion).
|
|
60
|
+
"""
|
|
61
|
+
if not isinstance(code, str) or not is_aa_encoded(code):
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
return _decode_impl(code)
|
|
66
|
+
except Exception:
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
# Internal helpers
|
|
72
|
+
# ---------------------------------------------------------------------------
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _decode_impl(code):
|
|
76
|
+
"""Core decoding logic."""
|
|
77
|
+
# 1. Isolate the data section.
|
|
78
|
+
# AAEncode wraps data inside an execution pattern. The encoded payload
|
|
79
|
+
# is the series of segments joined by the separator, ending with a
|
|
80
|
+
# final execution call like (゚Д゚)['_'] or )('_');
|
|
81
|
+
# We look for the *first* separator occurrence and take everything from
|
|
82
|
+
# there up to the trailing execution wrapper.
|
|
83
|
+
|
|
84
|
+
# Find the data region: everything after the initial variable setup and
|
|
85
|
+
# before the trailing execution portion.
|
|
86
|
+
# The data starts at the first separator token.
|
|
87
|
+
sep_idx = code.find(_SEPARATOR)
|
|
88
|
+
if sep_idx == -1:
|
|
89
|
+
return None
|
|
90
|
+
|
|
91
|
+
# The trailing execution wrapper varies but typically looks like:
|
|
92
|
+
# (゚Д゚)['_'](゚Θ゚) or )('_');
|
|
93
|
+
# We strip from the last occurrence of (゚Д゚)['_'] onward.
|
|
94
|
+
tail_patterns = [
|
|
95
|
+
"(\uff9f\u0414\uff9f)['_']",
|
|
96
|
+
'(\uff9f\u0414\uff9f)["_"]',
|
|
97
|
+
]
|
|
98
|
+
data = code[sep_idx:]
|
|
99
|
+
for pat in tail_patterns:
|
|
100
|
+
tail_pos = data.rfind(pat)
|
|
101
|
+
if tail_pos != -1:
|
|
102
|
+
data = data[:tail_pos]
|
|
103
|
+
break
|
|
104
|
+
|
|
105
|
+
# 2. Apply emoticon-to-digit replacements.
|
|
106
|
+
for old, new in _REPLACEMENTS:
|
|
107
|
+
data = data.replace(old, new)
|
|
108
|
+
|
|
109
|
+
# 3. Split on the separator to get individual character segments.
|
|
110
|
+
segments = data.split(_SEPARATOR)
|
|
111
|
+
|
|
112
|
+
# The first element is the leading separator itself (empty or noise) — skip it.
|
|
113
|
+
# Actually, since we started data *at* the first separator, the split
|
|
114
|
+
# produces an empty first element. Handle gracefully.
|
|
115
|
+
|
|
116
|
+
result_chars = []
|
|
117
|
+
for segment in segments:
|
|
118
|
+
segment = segment.strip()
|
|
119
|
+
if not segment:
|
|
120
|
+
continue
|
|
121
|
+
|
|
122
|
+
# Determine hex vs octal mode.
|
|
123
|
+
is_hex = _HEX_SENTINEL in segment
|
|
124
|
+
|
|
125
|
+
# Remove hex sentinel and any remaining operator/whitespace noise.
|
|
126
|
+
cleaned = segment.replace(_HEX_SENTINEL, '')
|
|
127
|
+
cleaned = cleaned.replace('+', '').replace(' ', '').strip()
|
|
128
|
+
|
|
129
|
+
if not cleaned:
|
|
130
|
+
continue
|
|
131
|
+
|
|
132
|
+
# cleaned should now be a string of digit characters.
|
|
133
|
+
if not cleaned.isdigit() and not (is_hex and all(c in '0123456789abcdefABCDEF' for c in cleaned)):
|
|
134
|
+
# If we still have non-digit residue, try harder: keep only digits.
|
|
135
|
+
cleaned = re.sub(r'[^0-9a-fA-F]', '', cleaned)
|
|
136
|
+
if not cleaned:
|
|
137
|
+
continue
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
if is_hex:
|
|
141
|
+
result_chars.append(chr(int(cleaned, 16)))
|
|
142
|
+
else:
|
|
143
|
+
result_chars.append(chr(int(cleaned, 8)))
|
|
144
|
+
except (ValueError, OverflowError):
|
|
145
|
+
continue
|
|
146
|
+
|
|
147
|
+
if not result_chars:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
return ''.join(result_chars)
|