pyjsclear 0.1.0__py3-none-any.whl
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/__init__.py +47 -0
- pyjsclear/__main__.py +37 -0
- pyjsclear/deobfuscator.py +191 -0
- pyjsclear/generator.py +772 -0
- pyjsclear/parser.py +46 -0
- pyjsclear/scope.py +283 -0
- pyjsclear/transforms/__init__.py +0 -0
- pyjsclear/transforms/aa_decode.py +83 -0
- pyjsclear/transforms/anti_tamper.py +106 -0
- pyjsclear/transforms/base.py +24 -0
- pyjsclear/transforms/constant_prop.py +103 -0
- pyjsclear/transforms/control_flow.py +330 -0
- pyjsclear/transforms/dead_branch.py +71 -0
- pyjsclear/transforms/eval_unpack.py +133 -0
- pyjsclear/transforms/expression_simplifier.py +403 -0
- pyjsclear/transforms/hex_escapes.py +68 -0
- pyjsclear/transforms/jj_decode.py +91 -0
- pyjsclear/transforms/jsfuck_decode.py +93 -0
- pyjsclear/transforms/logical_to_if.py +173 -0
- pyjsclear/transforms/object_packer.py +133 -0
- pyjsclear/transforms/object_simplifier.py +192 -0
- pyjsclear/transforms/property_simplifier.py +43 -0
- pyjsclear/transforms/proxy_functions.py +193 -0
- pyjsclear/transforms/reassignment.py +183 -0
- pyjsclear/transforms/sequence_splitter.py +215 -0
- pyjsclear/transforms/string_revealer.py +1259 -0
- pyjsclear/transforms/unused_vars.py +111 -0
- pyjsclear/traverser.py +195 -0
- pyjsclear/utils/__init__.py +0 -0
- pyjsclear/utils/ast_helpers.py +257 -0
- pyjsclear/utils/string_decoders.py +141 -0
- pyjsclear-0.1.0.dist-info/METADATA +168 -0
- pyjsclear-0.1.0.dist-info/RECORD +38 -0
- pyjsclear-0.1.0.dist-info/WHEEL +5 -0
- pyjsclear-0.1.0.dist-info/entry_points.txt +2 -0
- pyjsclear-0.1.0.dist-info/licenses/LICENSE +201 -0
- pyjsclear-0.1.0.dist-info/licenses/NOTICE +19 -0
- pyjsclear-0.1.0.dist-info/top_level.txt +1 -0
pyjsclear/__init__.py
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""Pure Python JavaScript deobfuscation library.
|
|
2
|
+
|
|
3
|
+
Combines functionality from obfuscator-io-deobfuscator (13 AST transforms)
|
|
4
|
+
and javascript-deobfuscator (3 surface-cleanup modules) into a single
|
|
5
|
+
Python package with no Node.js dependency.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .deobfuscator import Deobfuscator
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
__version__ = '0.1.0'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def deobfuscate(code, max_iterations=50):
|
|
15
|
+
"""Deobfuscate JavaScript code. Returns cleaned source.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
code: JavaScript source code string.
|
|
19
|
+
max_iterations: Maximum transform passes (default 50).
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Deobfuscated JavaScript source code.
|
|
23
|
+
"""
|
|
24
|
+
return Deobfuscator(code, max_iterations=max_iterations).execute()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def deobfuscate_file(input_path, output_path=None, max_iterations=50):
|
|
28
|
+
"""Deobfuscate a JavaScript file.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
input_path: Path to input JS file.
|
|
32
|
+
output_path: Path to write output (if None, returns string).
|
|
33
|
+
max_iterations: Maximum transform passes.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
True if content changed (when output_path given), or the deobfuscated string.
|
|
37
|
+
"""
|
|
38
|
+
with open(input_path, 'r', errors='replace') as f:
|
|
39
|
+
code = f.read()
|
|
40
|
+
|
|
41
|
+
result = deobfuscate(code, max_iterations=max_iterations)
|
|
42
|
+
|
|
43
|
+
if output_path:
|
|
44
|
+
with open(output_path, 'w') as f:
|
|
45
|
+
f.write(result)
|
|
46
|
+
return result != code
|
|
47
|
+
return result
|
pyjsclear/__main__.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""CLI entry point: python -m pyjsclear input.js [-o output.js]"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from . import deobfuscate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser(description='Deobfuscate JavaScript files.')
|
|
11
|
+
parser.add_argument('input', help='Input JS file (use - for stdin)')
|
|
12
|
+
parser.add_argument('-o', '--output', help='Output file (default: stdout)')
|
|
13
|
+
parser.add_argument(
|
|
14
|
+
'--max-iterations',
|
|
15
|
+
type=int,
|
|
16
|
+
default=50,
|
|
17
|
+
help='Maximum transform passes (default: 50)',
|
|
18
|
+
)
|
|
19
|
+
args = parser.parse_args()
|
|
20
|
+
|
|
21
|
+
if args.input == '-':
|
|
22
|
+
code = sys.stdin.read()
|
|
23
|
+
else:
|
|
24
|
+
with open(args.input, 'r', errors='replace') as input_file:
|
|
25
|
+
code = input_file.read()
|
|
26
|
+
|
|
27
|
+
result = deobfuscate(code, max_iterations=args.max_iterations)
|
|
28
|
+
|
|
29
|
+
if args.output:
|
|
30
|
+
with open(args.output, 'w') as output_file:
|
|
31
|
+
output_file.write(result)
|
|
32
|
+
else:
|
|
33
|
+
sys.stdout.write(result)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
if __name__ == '__main__':
|
|
37
|
+
main()
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Multi-pass deobfuscation orchestrator."""
|
|
2
|
+
|
|
3
|
+
from .generator import generate
|
|
4
|
+
from .parser import parse
|
|
5
|
+
from .transforms.aa_decode import aa_decode
|
|
6
|
+
from .transforms.aa_decode import is_aa_encoded
|
|
7
|
+
from .transforms.anti_tamper import AntiTamperRemover
|
|
8
|
+
from .transforms.constant_prop import ConstantProp
|
|
9
|
+
from .transforms.control_flow import ControlFlowRecoverer
|
|
10
|
+
from .transforms.dead_branch import DeadBranchRemover
|
|
11
|
+
from .transforms.eval_unpack import eval_unpack
|
|
12
|
+
from .transforms.eval_unpack import is_eval_packed
|
|
13
|
+
from .transforms.expression_simplifier import ExpressionSimplifier
|
|
14
|
+
from .transforms.hex_escapes import HexEscapes
|
|
15
|
+
from .transforms.hex_escapes import decode_hex_escapes_source
|
|
16
|
+
from .transforms.jj_decode import is_jj_encoded
|
|
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
|
|
21
|
+
from .transforms.logical_to_if import LogicalToIf
|
|
22
|
+
from .transforms.object_packer import ObjectPacker
|
|
23
|
+
from .transforms.object_simplifier import ObjectSimplifier
|
|
24
|
+
from .transforms.property_simplifier import PropertySimplifier
|
|
25
|
+
from .transforms.proxy_functions import ProxyFunctionInliner
|
|
26
|
+
from .transforms.reassignment import ReassignmentRemover
|
|
27
|
+
from .transforms.sequence_splitter import SequenceSplitter
|
|
28
|
+
from .transforms.string_revealer import StringRevealer
|
|
29
|
+
from .transforms.unused_vars import UnusedVariableRemover
|
|
30
|
+
from .traverser import simple_traverse
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# StringRevealer runs first to handle string arrays before other transforms
|
|
34
|
+
# modify the wrapper function structure.
|
|
35
|
+
# Remaining transforms follow obfuscator-io-deobfuscator order.
|
|
36
|
+
TRANSFORM_CLASSES = [
|
|
37
|
+
StringRevealer,
|
|
38
|
+
HexEscapes,
|
|
39
|
+
UnusedVariableRemover,
|
|
40
|
+
ConstantProp,
|
|
41
|
+
ReassignmentRemover,
|
|
42
|
+
DeadBranchRemover,
|
|
43
|
+
ObjectPacker,
|
|
44
|
+
ProxyFunctionInliner,
|
|
45
|
+
SequenceSplitter,
|
|
46
|
+
ExpressionSimplifier,
|
|
47
|
+
LogicalToIf,
|
|
48
|
+
ControlFlowRecoverer,
|
|
49
|
+
PropertySimplifier,
|
|
50
|
+
AntiTamperRemover,
|
|
51
|
+
ObjectSimplifier,
|
|
52
|
+
StringRevealer,
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
# Expensive transforms to skip in lite mode (large files)
|
|
56
|
+
_EXPENSIVE_TRANSFORMS = {ControlFlowRecoverer, ProxyFunctionInliner, ObjectPacker}
|
|
57
|
+
|
|
58
|
+
# Large file thresholds
|
|
59
|
+
_LARGE_FILE_SIZE = 500_000 # 500KB - reduce iterations
|
|
60
|
+
_MAX_CODE_SIZE = 2_000_000 # 2MB - use lite mode
|
|
61
|
+
_LITE_MAX_ITERATIONS = 10
|
|
62
|
+
_NODE_COUNT_LIMIT = 50_000 # Skip ControlFlowRecoverer above this
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _count_nodes(ast):
|
|
66
|
+
"""Count total AST nodes."""
|
|
67
|
+
count = 0
|
|
68
|
+
|
|
69
|
+
def cb(node, parent):
|
|
70
|
+
nonlocal count
|
|
71
|
+
count += 1
|
|
72
|
+
|
|
73
|
+
simple_traverse(ast, cb)
|
|
74
|
+
return count
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Deobfuscator:
|
|
78
|
+
"""Multi-pass JavaScript deobfuscator."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, code, max_iterations=50):
|
|
81
|
+
self.original_code = code
|
|
82
|
+
self.max_iterations = max_iterations
|
|
83
|
+
|
|
84
|
+
def _run_pre_passes(self, code):
|
|
85
|
+
"""Run encoding detection and eval unpacking pre-passes.
|
|
86
|
+
|
|
87
|
+
Returns decoded code if an encoding/packing was detected and decoded,
|
|
88
|
+
or None to continue with the normal AST pipeline.
|
|
89
|
+
"""
|
|
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
|
+
# AAEncode check
|
|
97
|
+
if is_aa_encoded(code):
|
|
98
|
+
decoded = aa_decode(code)
|
|
99
|
+
if decoded:
|
|
100
|
+
return decoded
|
|
101
|
+
|
|
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
|
+
# Eval packer check
|
|
109
|
+
if is_eval_packed(code):
|
|
110
|
+
decoded = eval_unpack(code)
|
|
111
|
+
if decoded:
|
|
112
|
+
return decoded
|
|
113
|
+
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
def execute(self):
|
|
117
|
+
"""Run all transforms and return cleaned source."""
|
|
118
|
+
code = self.original_code
|
|
119
|
+
|
|
120
|
+
# Pre-pass: encoding detection and eval unpacking
|
|
121
|
+
decoded = self._run_pre_passes(code)
|
|
122
|
+
if decoded:
|
|
123
|
+
# Feed decoded result back through the full pipeline for further cleanup
|
|
124
|
+
sub = Deobfuscator(decoded, max_iterations=self.max_iterations)
|
|
125
|
+
return sub.execute()
|
|
126
|
+
|
|
127
|
+
# Try to parse; if it fails, apply source-level hex decoding as fallback
|
|
128
|
+
try:
|
|
129
|
+
ast = parse(code)
|
|
130
|
+
except SyntaxError:
|
|
131
|
+
# Source-level hex decode for unparseable files (e.g. ES modules)
|
|
132
|
+
decoded = decode_hex_escapes_source(code)
|
|
133
|
+
if decoded != code:
|
|
134
|
+
return decoded
|
|
135
|
+
return self.original_code
|
|
136
|
+
|
|
137
|
+
# Determine optimization mode based on code size
|
|
138
|
+
code_size = len(code)
|
|
139
|
+
lite_mode = code_size > _MAX_CODE_SIZE
|
|
140
|
+
max_iterations = self.max_iterations
|
|
141
|
+
if code_size > _LARGE_FILE_SIZE:
|
|
142
|
+
max_iterations = min(max_iterations, _LITE_MAX_ITERATIONS)
|
|
143
|
+
|
|
144
|
+
# Check node count for expensive transform gating
|
|
145
|
+
node_count = _count_nodes(ast) if code_size > _LARGE_FILE_SIZE else 0
|
|
146
|
+
|
|
147
|
+
# For very large ASTs, further reduce iterations
|
|
148
|
+
if node_count > 100_000:
|
|
149
|
+
max_iterations = min(max_iterations, 3)
|
|
150
|
+
|
|
151
|
+
# Build transform list based on mode
|
|
152
|
+
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:
|
|
156
|
+
transform_classes = [t for t in TRANSFORM_CLASSES if t not in _EXPENSIVE_TRANSFORMS]
|
|
157
|
+
|
|
158
|
+
# Track which transforms are no longer productive
|
|
159
|
+
skip_transforms = set()
|
|
160
|
+
|
|
161
|
+
# Multi-pass transform loop
|
|
162
|
+
any_transform_changed = False
|
|
163
|
+
for i in range(max_iterations):
|
|
164
|
+
modified = False
|
|
165
|
+
for transform_class in transform_classes:
|
|
166
|
+
if transform_class in skip_transforms:
|
|
167
|
+
continue
|
|
168
|
+
try:
|
|
169
|
+
transform = transform_class(ast)
|
|
170
|
+
result = transform.execute()
|
|
171
|
+
except Exception:
|
|
172
|
+
continue
|
|
173
|
+
if result:
|
|
174
|
+
modified = True
|
|
175
|
+
any_transform_changed = True
|
|
176
|
+
else:
|
|
177
|
+
# If a transform didn't change anything after the first pass,
|
|
178
|
+
# skip it in subsequent iterations
|
|
179
|
+
if i > 0:
|
|
180
|
+
skip_transforms.add(transform_class)
|
|
181
|
+
|
|
182
|
+
if not modified:
|
|
183
|
+
break
|
|
184
|
+
|
|
185
|
+
if not any_transform_changed:
|
|
186
|
+
return self.original_code
|
|
187
|
+
|
|
188
|
+
try:
|
|
189
|
+
return generate(ast)
|
|
190
|
+
except Exception:
|
|
191
|
+
return self.original_code
|