pyjsclear 0.1.0__tar.gz → 0.1.2__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.2/PKG-INFO +126 -0
- pyjsclear-0.1.2/README.md +100 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/__init__.py +2 -2
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/deobfuscator.py +53 -17
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/generator.py +6 -1
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/scope.py +107 -81
- pyjsclear-0.1.2/pyjsclear/transforms/class_static_resolver.py +206 -0
- pyjsclear-0.1.2/pyjsclear/transforms/class_string_decoder.py +332 -0
- pyjsclear-0.1.2/pyjsclear/transforms/cleanup.py +272 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/dead_branch.py +18 -0
- pyjsclear-0.1.2/pyjsclear/transforms/dead_class_props.py +251 -0
- pyjsclear-0.1.2/pyjsclear/transforms/dead_expressions.py +29 -0
- pyjsclear-0.1.2/pyjsclear/transforms/dead_object_props.py +150 -0
- pyjsclear-0.1.2/pyjsclear/transforms/else_if_flatten.py +32 -0
- pyjsclear-0.1.2/pyjsclear/transforms/enum_resolver.py +175 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/eval_unpack.py +3 -52
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/expression_simplifier.py +106 -0
- pyjsclear-0.1.2/pyjsclear/transforms/global_alias.py +117 -0
- pyjsclear-0.1.2/pyjsclear/transforms/hex_numerics.py +30 -0
- pyjsclear-0.1.2/pyjsclear/transforms/member_chain_resolver.py +171 -0
- pyjsclear-0.1.2/pyjsclear/transforms/noop_calls.py +77 -0
- pyjsclear-0.1.2/pyjsclear/transforms/nullish_coalescing.py +98 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/object_simplifier.py +7 -5
- pyjsclear-0.1.2/pyjsclear/transforms/optional_chaining.py +152 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/property_simplifier.py +29 -4
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/proxy_functions.py +44 -2
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/reassignment.py +5 -0
- pyjsclear-0.1.2/pyjsclear/transforms/require_inliner.py +81 -0
- pyjsclear-0.1.2/pyjsclear/transforms/single_use_vars.py +155 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/string_revealer.py +2 -0
- pyjsclear-0.1.2/pyjsclear/transforms/unreachable_code.py +37 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/unused_vars.py +3 -2
- pyjsclear-0.1.2/pyjsclear/transforms/variable_renamer.py +511 -0
- pyjsclear-0.1.2/pyjsclear/transforms/xor_string_decode.py +228 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/utils/ast_helpers.py +72 -2
- pyjsclear-0.1.2/pyjsclear.egg-info/PKG-INFO +126 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear.egg-info/SOURCES.txt +19 -3
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/tests/test_regression.py +288 -13
- pyjsclear-0.1.0/PKG-INFO +0 -168
- pyjsclear-0.1.0/README.md +0 -142
- pyjsclear-0.1.0/pyjsclear/transforms/jj_decode.py +0 -91
- pyjsclear-0.1.0/pyjsclear/transforms/jsfuck_decode.py +0 -93
- pyjsclear-0.1.0/pyjsclear.egg-info/PKG-INFO +0 -168
- pyjsclear-0.1.0/setup.py +0 -18
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/LICENSE +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/MANIFEST.in +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/NOTICE +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/THIRD_PARTY_LICENSES.md +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/__main__.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/parser.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/__init__.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/aa_decode.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/anti_tamper.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/base.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/constant_prop.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/control_flow.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/hex_escapes.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/logical_to_if.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/object_packer.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/transforms/sequence_splitter.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/traverser.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/utils/__init__.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear/utils/string_decoders.py +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear.egg-info/dependency_links.txt +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear.egg-info/entry_points.txt +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear.egg-info/requires.txt +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyjsclear.egg-info/top_level.txt +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/pyproject.toml +0 -0
- {pyjsclear-0.1.0 → pyjsclear-0.1.2}/setup.cfg +0 -0
pyjsclear-0.1.2/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyjsclear
|
|
3
|
+
Version: 0.1.2
|
|
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 ~40 transforms in a multi-pass loop until the code
|
|
86
|
+
stabilises (default limit: 50 iterations). A final one-shot pass renames
|
|
87
|
+
variables and converts var/let to const.
|
|
88
|
+
|
|
89
|
+
**Capabilities:**
|
|
90
|
+
- String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
|
|
91
|
+
- Constant propagation & reassignment elimination
|
|
92
|
+
- Dead code / dead branch / unreachable code removal
|
|
93
|
+
- Control-flow unflattening (switch-dispatch recovery)
|
|
94
|
+
- Proxy function & proxy object inlining
|
|
95
|
+
- Expression simplification & modern syntax recovery (?., ??)
|
|
96
|
+
- Anti-tamper / anti-debug removal
|
|
97
|
+
- Variable renaming (_0x… → readable names)
|
|
98
|
+
|
|
99
|
+
Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
|
|
100
|
+
that skips expensive transforms.
|
|
101
|
+
|
|
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
|
+
## Limitations
|
|
111
|
+
|
|
112
|
+
- **Optimised for obfuscator.io output.** Other obfuscation tools may only partially deobfuscate.
|
|
113
|
+
- **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
|
+
- **No minification reversal.** Minified-but-not-obfuscated code won't be reformatted or beautified.
|
|
115
|
+
- **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting.
|
|
116
|
+
|
|
117
|
+
## License
|
|
118
|
+
|
|
119
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
120
|
+
|
|
121
|
+
This project is a derivative work based on
|
|
122
|
+
[obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
|
|
123
|
+
(Apache 2.0) and
|
|
124
|
+
[javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
|
|
125
|
+
(Apache 2.0). See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
|
|
126
|
+
[NOTICE](NOTICE) for full attribution.
|
|
@@ -0,0 +1,100 @@
|
|
|
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 ~40 transforms in a multi-pass loop until the code
|
|
60
|
+
stabilises (default limit: 50 iterations). A final one-shot pass renames
|
|
61
|
+
variables and converts var/let to const.
|
|
62
|
+
|
|
63
|
+
**Capabilities:**
|
|
64
|
+
- String array decoding (obfuscator.io basic/base64/RC4, XOR, class-based)
|
|
65
|
+
- Constant propagation & reassignment elimination
|
|
66
|
+
- Dead code / dead branch / unreachable code removal
|
|
67
|
+
- Control-flow unflattening (switch-dispatch recovery)
|
|
68
|
+
- Proxy function & proxy object inlining
|
|
69
|
+
- Expression simplification & modern syntax recovery (?., ??)
|
|
70
|
+
- Anti-tamper / anti-debug removal
|
|
71
|
+
- Variable renaming (_0x… → readable names)
|
|
72
|
+
|
|
73
|
+
Large files (>500 KB / >50 K AST nodes) automatically use a lite mode
|
|
74
|
+
that skips expensive transforms.
|
|
75
|
+
|
|
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
|
+
## Limitations
|
|
85
|
+
|
|
86
|
+
- **Optimised for obfuscator.io output.** Other obfuscation tools may only partially deobfuscate.
|
|
87
|
+
- **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
|
+
- **No minification reversal.** Minified-but-not-obfuscated code won't be reformatted or beautified.
|
|
89
|
+
- **Recursive AST traversal** may hit Python's default recursion limit (~1 000 frames) on extremely deep nesting.
|
|
90
|
+
|
|
91
|
+
## License
|
|
92
|
+
|
|
93
|
+
Apache License 2.0 — see [LICENSE](LICENSE).
|
|
94
|
+
|
|
95
|
+
This project is a derivative work based on
|
|
96
|
+
[obfuscator-io-deobfuscator](https://github.com/ben-sb/obfuscator-io-deobfuscator)
|
|
97
|
+
(Apache 2.0) and
|
|
98
|
+
[javascript-deobfuscator](https://github.com/ben-sb/javascript-deobfuscator)
|
|
99
|
+
(Apache 2.0). See [THIRD_PARTY_LICENSES.md](THIRD_PARTY_LICENSES.md) and
|
|
100
|
+
[NOTICE](NOTICE) for full attribution.
|
|
@@ -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.2'
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
def deobfuscate(code, max_iterations=50):
|
|
@@ -5,28 +5,47 @@ from .parser import parse
|
|
|
5
5
|
from .transforms.aa_decode import aa_decode
|
|
6
6
|
from .transforms.aa_decode import is_aa_encoded
|
|
7
7
|
from .transforms.anti_tamper import AntiTamperRemover
|
|
8
|
+
from .transforms.class_static_resolver import ClassStaticResolver
|
|
9
|
+
from .transforms.class_string_decoder import ClassStringDecoder
|
|
10
|
+
from .transforms.cleanup import EmptyIfRemover
|
|
11
|
+
from .transforms.cleanup import LetToConst
|
|
12
|
+
from .transforms.cleanup import OptionalCatchBinding
|
|
13
|
+
from .transforms.cleanup import ReturnUndefinedCleanup
|
|
14
|
+
from .transforms.cleanup import TrailingReturnRemover
|
|
15
|
+
from .transforms.cleanup import VarToConst
|
|
8
16
|
from .transforms.constant_prop import ConstantProp
|
|
9
17
|
from .transforms.control_flow import ControlFlowRecoverer
|
|
10
18
|
from .transforms.dead_branch import DeadBranchRemover
|
|
19
|
+
from .transforms.dead_class_props import DeadClassPropRemover
|
|
20
|
+
from .transforms.dead_expressions import DeadExpressionRemover
|
|
21
|
+
from .transforms.dead_object_props import DeadObjectPropRemover
|
|
22
|
+
from .transforms.else_if_flatten import ElseIfFlattener
|
|
23
|
+
from .transforms.enum_resolver import EnumResolver
|
|
11
24
|
from .transforms.eval_unpack import eval_unpack
|
|
12
25
|
from .transforms.eval_unpack import is_eval_packed
|
|
13
26
|
from .transforms.expression_simplifier import ExpressionSimplifier
|
|
27
|
+
from .transforms.global_alias import GlobalAliasInliner
|
|
14
28
|
from .transforms.hex_escapes import HexEscapes
|
|
15
29
|
from .transforms.hex_escapes import decode_hex_escapes_source
|
|
16
|
-
from .transforms.
|
|
17
|
-
from .transforms.jj_decode import jj_decode
|
|
18
|
-
from .transforms.jj_decode import jj_decode_via_eval
|
|
19
|
-
from .transforms.jsfuck_decode import is_jsfuck
|
|
20
|
-
from .transforms.jsfuck_decode import jsfuck_decode
|
|
30
|
+
from .transforms.hex_numerics import HexNumerics
|
|
21
31
|
from .transforms.logical_to_if import LogicalToIf
|
|
32
|
+
from .transforms.member_chain_resolver import MemberChainResolver
|
|
33
|
+
from .transforms.noop_calls import NoopCallRemover
|
|
34
|
+
from .transforms.nullish_coalescing import NullishCoalescing
|
|
22
35
|
from .transforms.object_packer import ObjectPacker
|
|
23
36
|
from .transforms.object_simplifier import ObjectSimplifier
|
|
37
|
+
from .transforms.optional_chaining import OptionalChaining
|
|
24
38
|
from .transforms.property_simplifier import PropertySimplifier
|
|
25
39
|
from .transforms.proxy_functions import ProxyFunctionInliner
|
|
26
40
|
from .transforms.reassignment import ReassignmentRemover
|
|
41
|
+
from .transforms.require_inliner import RequireInliner
|
|
27
42
|
from .transforms.sequence_splitter import SequenceSplitter
|
|
43
|
+
from .transforms.single_use_vars import SingleUseVarInliner
|
|
28
44
|
from .transforms.string_revealer import StringRevealer
|
|
45
|
+
from .transforms.unreachable_code import UnreachableCodeRemover
|
|
29
46
|
from .transforms.unused_vars import UnusedVariableRemover
|
|
47
|
+
from .transforms.variable_renamer import VariableRenamer
|
|
48
|
+
from .transforms.xor_string_decode import XorStringDecoder
|
|
30
49
|
from .traverser import simple_traverse
|
|
31
50
|
|
|
32
51
|
|
|
@@ -36,15 +55,36 @@ from .traverser import simple_traverse
|
|
|
36
55
|
TRANSFORM_CLASSES = [
|
|
37
56
|
StringRevealer,
|
|
38
57
|
HexEscapes,
|
|
58
|
+
HexNumerics,
|
|
59
|
+
ClassStringDecoder,
|
|
60
|
+
XorStringDecoder,
|
|
61
|
+
MemberChainResolver,
|
|
62
|
+
DeadClassPropRemover,
|
|
63
|
+
ClassStaticResolver,
|
|
64
|
+
EnumResolver,
|
|
65
|
+
RequireInliner,
|
|
66
|
+
GlobalAliasInliner,
|
|
39
67
|
UnusedVariableRemover,
|
|
40
68
|
ConstantProp,
|
|
41
69
|
ReassignmentRemover,
|
|
70
|
+
SingleUseVarInliner,
|
|
42
71
|
DeadBranchRemover,
|
|
72
|
+
UnreachableCodeRemover,
|
|
73
|
+
NoopCallRemover,
|
|
74
|
+
EmptyIfRemover,
|
|
75
|
+
DeadObjectPropRemover,
|
|
43
76
|
ObjectPacker,
|
|
44
77
|
ProxyFunctionInliner,
|
|
45
78
|
SequenceSplitter,
|
|
79
|
+
DeadExpressionRemover,
|
|
46
80
|
ExpressionSimplifier,
|
|
81
|
+
NullishCoalescing,
|
|
82
|
+
OptionalChaining,
|
|
47
83
|
LogicalToIf,
|
|
84
|
+
ElseIfFlattener,
|
|
85
|
+
OptionalCatchBinding,
|
|
86
|
+
ReturnUndefinedCleanup,
|
|
87
|
+
TrailingReturnRemover,
|
|
48
88
|
ControlFlowRecoverer,
|
|
49
89
|
PropertySimplifier,
|
|
50
90
|
AntiTamperRemover,
|
|
@@ -87,24 +127,12 @@ class Deobfuscator:
|
|
|
87
127
|
Returns decoded code if an encoding/packing was detected and decoded,
|
|
88
128
|
or None to continue with the normal AST pipeline.
|
|
89
129
|
"""
|
|
90
|
-
# JSFUCK check (must be first — these are whole-file encodings)
|
|
91
|
-
if is_jsfuck(code):
|
|
92
|
-
decoded = jsfuck_decode(code)
|
|
93
|
-
if decoded:
|
|
94
|
-
return decoded
|
|
95
|
-
|
|
96
130
|
# AAEncode check
|
|
97
131
|
if is_aa_encoded(code):
|
|
98
132
|
decoded = aa_decode(code)
|
|
99
133
|
if decoded:
|
|
100
134
|
return decoded
|
|
101
135
|
|
|
102
|
-
# JJEncode check
|
|
103
|
-
if is_jj_encoded(code):
|
|
104
|
-
decoded = jj_decode(code) or jj_decode_via_eval(code)
|
|
105
|
-
if decoded:
|
|
106
|
-
return decoded
|
|
107
|
-
|
|
108
136
|
# Eval packer check
|
|
109
137
|
if is_eval_packed(code):
|
|
110
138
|
decoded = eval_unpack(code)
|
|
@@ -182,6 +210,14 @@ class Deobfuscator:
|
|
|
182
210
|
if not modified:
|
|
183
211
|
break
|
|
184
212
|
|
|
213
|
+
# Post-passes: cosmetic transforms that run once after convergence
|
|
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
|
+
|
|
185
221
|
if not any_transform_changed:
|
|
186
222
|
return self.original_code
|
|
187
223
|
|
|
@@ -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
|
|
|
@@ -70,19 +70,17 @@ def _is_non_reference_identifier(parent, parent_key):
|
|
|
70
70
|
"""Return True if this Identifier usage is not a variable reference."""
|
|
71
71
|
if not parent:
|
|
72
72
|
return False
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if parent_type == 'VariableDeclarator' and parent_key == 'id':
|
|
85
|
-
return True
|
|
73
|
+
match parent.get('type'):
|
|
74
|
+
case 'MemberExpression' if parent_key == 'property' and not parent.get('computed'):
|
|
75
|
+
return True
|
|
76
|
+
case 'Property' if parent_key == 'key' and not parent.get('computed'):
|
|
77
|
+
return True
|
|
78
|
+
case 'FunctionDeclaration' | 'FunctionExpression' | 'ClassDeclaration' | 'ClassExpression' if (
|
|
79
|
+
parent_key == 'id'
|
|
80
|
+
):
|
|
81
|
+
return True
|
|
82
|
+
case 'VariableDeclarator' if parent_key == 'id':
|
|
83
|
+
return True
|
|
86
84
|
return False
|
|
87
85
|
|
|
88
86
|
|
|
@@ -132,76 +130,104 @@ def build_scope_tree(ast):
|
|
|
132
130
|
if node_type is None:
|
|
133
131
|
return
|
|
134
132
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
133
|
+
match node_type:
|
|
134
|
+
case 'FunctionDeclaration' | 'FunctionExpression' | 'ArrowFunctionExpression':
|
|
135
|
+
new_scope = Scope(scope, node, is_function=True)
|
|
136
|
+
node_scope[id(node)] = new_scope
|
|
137
|
+
all_scopes.append(new_scope)
|
|
138
|
+
|
|
139
|
+
# Function name goes in outer scope (for declarations) or inner (for expressions)
|
|
140
|
+
if node_type == 'FunctionDeclaration' and node.get('id'):
|
|
141
|
+
scope.add_binding(node['id']['name'], node, 'function')
|
|
142
|
+
elif node_type == 'FunctionExpression' and node.get('id'):
|
|
143
|
+
new_scope.add_binding(node['id']['name'], node, 'function')
|
|
144
|
+
|
|
145
|
+
# Params go in function scope
|
|
146
|
+
for param in node.get('params', []):
|
|
147
|
+
match param.get('type'):
|
|
148
|
+
case 'Identifier':
|
|
149
|
+
new_scope.add_binding(param['name'], param, 'param')
|
|
150
|
+
case 'AssignmentPattern' if param.get('left', {}).get('type') == 'Identifier':
|
|
151
|
+
new_scope.add_binding(param['left']['name'], param, 'param')
|
|
152
|
+
case 'RestElement':
|
|
153
|
+
arg = param.get('argument')
|
|
154
|
+
if arg and arg.get('type') == 'Identifier':
|
|
155
|
+
new_scope.add_binding(arg['name'], param, 'param')
|
|
156
|
+
|
|
157
|
+
# Body - use the new scope
|
|
158
|
+
body = node.get('body')
|
|
159
|
+
if not body:
|
|
160
|
+
return
|
|
161
|
+
if isinstance(body, dict) and body.get('type') == 'BlockStatement':
|
|
162
|
+
node_scope[id(body)] = new_scope
|
|
163
|
+
for statement in body.get('body', []):
|
|
164
|
+
_collect_declarations(statement, new_scope)
|
|
165
|
+
else:
|
|
166
|
+
_collect_declarations(body, new_scope)
|
|
167
|
+
|
|
168
|
+
case 'ClassExpression' | 'ClassDeclaration':
|
|
169
|
+
class_id = node.get('id')
|
|
170
|
+
inner_scope = scope
|
|
171
|
+
if class_id and class_id.get('type') == 'Identifier':
|
|
172
|
+
name = class_id['name']
|
|
173
|
+
if node_type == 'ClassDeclaration':
|
|
174
|
+
scope.add_binding(name, node, 'function')
|
|
175
|
+
else:
|
|
176
|
+
inner_scope = Scope(scope, node)
|
|
177
|
+
node_scope[id(node)] = inner_scope
|
|
178
|
+
all_scopes.append(inner_scope)
|
|
179
|
+
inner_scope.add_binding(name, node, 'function')
|
|
180
|
+
body = node.get('body')
|
|
181
|
+
if body:
|
|
182
|
+
_collect_declarations(body, inner_scope)
|
|
183
|
+
superclass = node.get('superClass')
|
|
184
|
+
if superclass:
|
|
185
|
+
_collect_declarations(superclass, scope)
|
|
186
|
+
|
|
187
|
+
case 'VariableDeclaration':
|
|
188
|
+
kind = node.get('kind', 'var')
|
|
189
|
+
target_scope = (_nearest_function_scope(scope) or scope) if kind == 'var' else scope
|
|
190
|
+
for declaration in node.get('declarations', []):
|
|
191
|
+
declaration_id = declaration.get('id')
|
|
192
|
+
if declaration_id and declaration_id.get('type') == 'Identifier':
|
|
193
|
+
target_scope.add_binding(declaration_id['name'], declaration, kind)
|
|
194
|
+
_collect_pattern_names(declaration_id, target_scope, kind, declaration)
|
|
195
|
+
init = declaration.get('init')
|
|
196
|
+
if init:
|
|
197
|
+
_collect_declarations(init, scope)
|
|
198
|
+
|
|
199
|
+
case 'BlockStatement' if id(node) not in node_scope:
|
|
200
|
+
new_scope = Scope(scope, node)
|
|
201
|
+
node_scope[id(node)] = new_scope
|
|
202
|
+
all_scopes.append(new_scope)
|
|
203
|
+
for statement in node.get('body', []):
|
|
165
204
|
_collect_declarations(statement, new_scope)
|
|
166
|
-
else:
|
|
167
|
-
_collect_declarations(body, new_scope)
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
# Variable declarations
|
|
171
|
-
if node_type == 'VariableDeclaration':
|
|
172
|
-
kind = node.get('kind', 'var')
|
|
173
|
-
# var is function-scoped, let/const are block-scoped
|
|
174
|
-
target_scope = (_nearest_function_scope(scope) or scope) if kind == 'var' else scope
|
|
175
|
-
for declaration in node.get('declarations', []):
|
|
176
|
-
declaration_id = declaration.get('id')
|
|
177
|
-
if declaration_id and declaration_id.get('type') == 'Identifier':
|
|
178
|
-
target_scope.add_binding(declaration_id['name'], declaration, kind)
|
|
179
|
-
# Handle destructuring patterns
|
|
180
|
-
_collect_pattern_names(declaration_id, target_scope, kind, declaration)
|
|
181
|
-
return
|
|
182
|
-
|
|
183
|
-
# Block scopes (for, if, etc. with block statements)
|
|
184
|
-
if node_type == 'BlockStatement' and id(node) not in node_scope:
|
|
185
|
-
# Only create block scope if parent is not a function (handled above)
|
|
186
|
-
new_scope = Scope(scope, node)
|
|
187
|
-
node_scope[id(node)] = new_scope
|
|
188
|
-
all_scopes.append(new_scope)
|
|
189
|
-
for statement in node.get('body', []):
|
|
190
|
-
_collect_declarations(statement, new_scope)
|
|
191
|
-
return
|
|
192
|
-
|
|
193
|
-
if node_type == 'ForStatement':
|
|
194
|
-
new_scope = Scope(scope, node)
|
|
195
|
-
node_scope[id(node)] = new_scope
|
|
196
|
-
all_scopes.append(new_scope)
|
|
197
|
-
if node.get('init'):
|
|
198
|
-
_collect_declarations(node['init'], new_scope)
|
|
199
|
-
if node.get('body'):
|
|
200
|
-
_collect_declarations(node['body'], new_scope)
|
|
201
|
-
return
|
|
202
205
|
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
case 'ForStatement':
|
|
207
|
+
new_scope = Scope(scope, node)
|
|
208
|
+
node_scope[id(node)] = new_scope
|
|
209
|
+
all_scopes.append(new_scope)
|
|
210
|
+
if node.get('init'):
|
|
211
|
+
_collect_declarations(node['init'], new_scope)
|
|
212
|
+
if node.get('body'):
|
|
213
|
+
_collect_declarations(node['body'], new_scope)
|
|
214
|
+
|
|
215
|
+
case 'CatchClause':
|
|
216
|
+
catch_body = node.get('body')
|
|
217
|
+
if catch_body and catch_body.get('type') == 'BlockStatement':
|
|
218
|
+
catch_scope = Scope(scope, catch_body)
|
|
219
|
+
node_scope[id(catch_body)] = catch_scope
|
|
220
|
+
all_scopes.append(catch_scope)
|
|
221
|
+
param = node.get('param')
|
|
222
|
+
if param and param.get('type') == 'Identifier':
|
|
223
|
+
catch_scope.add_binding(param['name'], param, 'param')
|
|
224
|
+
for statement in catch_body.get('body', []):
|
|
225
|
+
_collect_declarations(statement, catch_scope)
|
|
226
|
+
|
|
227
|
+
case _:
|
|
228
|
+
_recurse_into_children(
|
|
229
|
+
node, _child_keys_map, lambda child_node: _collect_declarations(child_node, scope)
|
|
230
|
+
)
|
|
205
231
|
|
|
206
232
|
def _collect_pattern_names(pattern, scope, kind, declaration):
|
|
207
233
|
"""Collect binding names from destructuring patterns."""
|