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/parser.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""JavaScript parser wrapper around esprima2."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import esprima
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_ASYNC_MAP = {'isAsync': 'async', 'allowAwait': 'await'}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _fast_to_dict(obj):
|
|
12
|
+
"""Convert esprima AST objects to plain dicts, ~2x faster than toDict()."""
|
|
13
|
+
if isinstance(obj, (str, int, float, bool, type(None))):
|
|
14
|
+
return obj
|
|
15
|
+
if isinstance(obj, list):
|
|
16
|
+
return [_fast_to_dict(item) for item in obj]
|
|
17
|
+
if isinstance(obj, re.Pattern):
|
|
18
|
+
return {}
|
|
19
|
+
# Object with __dict__ (esprima node)
|
|
20
|
+
result_dict = obj if isinstance(obj, dict) else obj.__dict__
|
|
21
|
+
output = {}
|
|
22
|
+
for key, value in result_dict.items():
|
|
23
|
+
if key.startswith('_'):
|
|
24
|
+
continue
|
|
25
|
+
if key == 'optional' and value is False:
|
|
26
|
+
continue
|
|
27
|
+
key = _ASYNC_MAP.get(key, key)
|
|
28
|
+
output[key] = _fast_to_dict(value)
|
|
29
|
+
return output
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse(code):
|
|
33
|
+
"""Parse JavaScript code into an ESTree-compatible AST.
|
|
34
|
+
|
|
35
|
+
Returns a Program node (dict).
|
|
36
|
+
Raises SyntaxError on parse failure.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
return _fast_to_dict(esprima.parseScript(code))
|
|
40
|
+
except esprima.Error:
|
|
41
|
+
try:
|
|
42
|
+
return _fast_to_dict(esprima.parseModule(code))
|
|
43
|
+
except Exception as e:
|
|
44
|
+
raise SyntaxError(f'Failed to parse JavaScript: {e}') from e
|
|
45
|
+
except Exception as e:
|
|
46
|
+
raise SyntaxError(f'Failed to parse JavaScript: {e}') from e
|
pyjsclear/scope.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
"""Variable scope and binding analysis for ESTree ASTs."""
|
|
2
|
+
|
|
3
|
+
from .utils.ast_helpers import _CHILD_KEYS
|
|
4
|
+
from .utils.ast_helpers import get_child_keys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Binding:
|
|
8
|
+
"""Represents a variable binding in a scope."""
|
|
9
|
+
|
|
10
|
+
__slots__ = ('name', 'node', 'kind', 'scope', 'references', 'assignments')
|
|
11
|
+
|
|
12
|
+
def __init__(self, name, node, kind, scope):
|
|
13
|
+
self.name = name
|
|
14
|
+
self.node = node # The declaration node
|
|
15
|
+
self.kind = kind # 'var', 'let', 'const', 'function', 'param'
|
|
16
|
+
self.scope = scope
|
|
17
|
+
self.references = [] # List of (node, parent, key, index) where name is referenced
|
|
18
|
+
self.assignments = [] # List of assignment nodes
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def is_constant(self):
|
|
22
|
+
"""True if the binding is never reassigned after declaration."""
|
|
23
|
+
if self.kind == 'const':
|
|
24
|
+
return True
|
|
25
|
+
if self.kind == 'function':
|
|
26
|
+
return len(self.assignments) == 0
|
|
27
|
+
# var/let/param: constant if exactly one init and no reassignments
|
|
28
|
+
return len(self.assignments) == 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Scope:
|
|
32
|
+
"""Represents a lexical scope."""
|
|
33
|
+
|
|
34
|
+
__slots__ = ('parent', 'node', 'bindings', 'children', 'is_function')
|
|
35
|
+
|
|
36
|
+
def __init__(self, parent, node, is_function=False):
|
|
37
|
+
self.parent = parent
|
|
38
|
+
self.node = node
|
|
39
|
+
self.bindings = {} # name -> Binding
|
|
40
|
+
self.children = []
|
|
41
|
+
self.is_function = is_function
|
|
42
|
+
if parent:
|
|
43
|
+
parent.children.append(self)
|
|
44
|
+
|
|
45
|
+
def add_binding(self, name, node, kind):
|
|
46
|
+
binding = Binding(name, node, kind, self)
|
|
47
|
+
self.bindings[name] = binding
|
|
48
|
+
return binding
|
|
49
|
+
|
|
50
|
+
def get_binding(self, name):
|
|
51
|
+
"""Look up a binding, walking up the scope chain."""
|
|
52
|
+
if name in self.bindings:
|
|
53
|
+
return self.bindings[name]
|
|
54
|
+
if self.parent:
|
|
55
|
+
return self.parent.get_binding(name)
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
def get_own_binding(self, name):
|
|
59
|
+
return self.bindings.get(name)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _nearest_function_scope(scope):
|
|
63
|
+
"""Walk up to the nearest function (or root) scope."""
|
|
64
|
+
while scope and not scope.is_function:
|
|
65
|
+
scope = scope.parent
|
|
66
|
+
return scope
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _is_non_reference_identifier(parent, parent_key):
|
|
70
|
+
"""Return True if this Identifier usage is not a variable reference."""
|
|
71
|
+
if not parent:
|
|
72
|
+
return False
|
|
73
|
+
parent_type = parent.get('type')
|
|
74
|
+
# Property names in member expressions (non-computed)
|
|
75
|
+
if parent_type == 'MemberExpression' and parent_key == 'property' and not parent.get('computed'):
|
|
76
|
+
return True
|
|
77
|
+
# Property keys in object literals (non-computed)
|
|
78
|
+
if parent_type == 'Property' and parent_key == 'key' and not parent.get('computed'):
|
|
79
|
+
return True
|
|
80
|
+
# Function/class names at declaration site
|
|
81
|
+
if parent_type in ('FunctionDeclaration', 'FunctionExpression', 'ClassDeclaration') and parent_key == 'id':
|
|
82
|
+
return True
|
|
83
|
+
# VariableDeclarator id
|
|
84
|
+
if parent_type == 'VariableDeclarator' and parent_key == 'id':
|
|
85
|
+
return True
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _recurse_into_children(node, child_keys_map, callback):
|
|
90
|
+
"""Walk child nodes, calling callback(child_node) for each dict with 'type'."""
|
|
91
|
+
node_type = node.get('type')
|
|
92
|
+
child_keys = child_keys_map.get(node_type)
|
|
93
|
+
if child_keys is None:
|
|
94
|
+
child_keys = get_child_keys(node)
|
|
95
|
+
for key in child_keys:
|
|
96
|
+
child = node.get(key)
|
|
97
|
+
if child is None:
|
|
98
|
+
continue
|
|
99
|
+
if isinstance(child, list):
|
|
100
|
+
for item in child:
|
|
101
|
+
if isinstance(item, dict) and 'type' in item:
|
|
102
|
+
callback(item)
|
|
103
|
+
elif isinstance(child, dict) and 'type' in child:
|
|
104
|
+
callback(child)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def build_scope_tree(ast):
|
|
108
|
+
"""Build a scope tree from an AST, collecting bindings and references.
|
|
109
|
+
|
|
110
|
+
Returns the root Scope and a dict mapping node id -> Scope.
|
|
111
|
+
"""
|
|
112
|
+
root_scope = Scope(None, ast, is_function=True)
|
|
113
|
+
# Maps id(node) -> scope for function/block scope nodes
|
|
114
|
+
node_scope = {id(ast): root_scope}
|
|
115
|
+
# We need to collect all declarations first, then references
|
|
116
|
+
all_scopes = [root_scope]
|
|
117
|
+
|
|
118
|
+
def _get_scope_for(node, current_scope):
|
|
119
|
+
"""Get or create the scope for a node."""
|
|
120
|
+
node_id = id(node)
|
|
121
|
+
if node_id in node_scope:
|
|
122
|
+
return node_scope[node_id]
|
|
123
|
+
return current_scope
|
|
124
|
+
|
|
125
|
+
_child_keys_map = _CHILD_KEYS
|
|
126
|
+
|
|
127
|
+
def _collect_declarations(node, scope):
|
|
128
|
+
"""Walk the AST collecting variable declarations into scopes."""
|
|
129
|
+
if not isinstance(node, dict):
|
|
130
|
+
return
|
|
131
|
+
node_type = node.get('type')
|
|
132
|
+
if node_type is None:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
# Create new scope for functions
|
|
136
|
+
if node_type in (
|
|
137
|
+
'FunctionDeclaration',
|
|
138
|
+
'FunctionExpression',
|
|
139
|
+
'ArrowFunctionExpression',
|
|
140
|
+
):
|
|
141
|
+
new_scope = Scope(scope, node, is_function=True)
|
|
142
|
+
node_scope[id(node)] = new_scope
|
|
143
|
+
all_scopes.append(new_scope)
|
|
144
|
+
|
|
145
|
+
# Function name goes in outer scope (for declarations) or inner (for expressions)
|
|
146
|
+
if node_type == 'FunctionDeclaration' and node.get('id'):
|
|
147
|
+
scope.add_binding(node['id']['name'], node, 'function')
|
|
148
|
+
elif node_type == 'FunctionExpression' and node.get('id'):
|
|
149
|
+
new_scope.add_binding(node['id']['name'], node, 'function')
|
|
150
|
+
|
|
151
|
+
# Params go in function scope
|
|
152
|
+
for param in node.get('params', []):
|
|
153
|
+
if param.get('type') == 'Identifier':
|
|
154
|
+
new_scope.add_binding(param['name'], param, 'param')
|
|
155
|
+
elif param.get('type') == 'AssignmentPattern' and param.get('left', {}).get('type') == 'Identifier':
|
|
156
|
+
new_scope.add_binding(param['left']['name'], param, 'param')
|
|
157
|
+
|
|
158
|
+
# Body - use the new scope
|
|
159
|
+
body = node.get('body')
|
|
160
|
+
if not body:
|
|
161
|
+
return
|
|
162
|
+
if isinstance(body, dict) and body.get('type') == 'BlockStatement':
|
|
163
|
+
node_scope[id(body)] = new_scope
|
|
164
|
+
for statement in body.get('body', []):
|
|
165
|
+
_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
|
+
|
|
203
|
+
# Recurse into children
|
|
204
|
+
_recurse_into_children(node, _child_keys_map, lambda child_node: _collect_declarations(child_node, scope))
|
|
205
|
+
|
|
206
|
+
def _collect_pattern_names(pattern, scope, kind, declaration):
|
|
207
|
+
"""Collect binding names from destructuring patterns."""
|
|
208
|
+
if not isinstance(pattern, dict):
|
|
209
|
+
return
|
|
210
|
+
match pattern.get('type', ''):
|
|
211
|
+
case 'ArrayPattern':
|
|
212
|
+
for element in pattern.get('elements', []):
|
|
213
|
+
if not element:
|
|
214
|
+
continue
|
|
215
|
+
if element.get('type') == 'Identifier':
|
|
216
|
+
scope.add_binding(element['name'], declaration, kind)
|
|
217
|
+
else:
|
|
218
|
+
_collect_pattern_names(element, scope, kind, declaration)
|
|
219
|
+
case 'ObjectPattern':
|
|
220
|
+
for property_node in pattern.get('properties', []):
|
|
221
|
+
value_node = property_node.get('value', property_node.get('argument'))
|
|
222
|
+
if not value_node:
|
|
223
|
+
continue
|
|
224
|
+
if value_node.get('type') == 'Identifier':
|
|
225
|
+
scope.add_binding(value_node['name'], declaration, kind)
|
|
226
|
+
else:
|
|
227
|
+
_collect_pattern_names(value_node, scope, kind, declaration)
|
|
228
|
+
case 'RestElement':
|
|
229
|
+
argument_node = pattern.get('argument')
|
|
230
|
+
if argument_node and argument_node.get('type') == 'Identifier':
|
|
231
|
+
scope.add_binding(argument_node['name'], declaration, kind)
|
|
232
|
+
case 'AssignmentPattern':
|
|
233
|
+
left = pattern.get('left')
|
|
234
|
+
if left and left.get('type') == 'Identifier':
|
|
235
|
+
scope.add_binding(left['name'], declaration, kind)
|
|
236
|
+
|
|
237
|
+
_collect_declarations(ast, root_scope)
|
|
238
|
+
|
|
239
|
+
# Second pass: collect references and assignments
|
|
240
|
+
def _collect_references(node, scope, parent=None, parent_key=None, parent_index=None):
|
|
241
|
+
if not isinstance(node, dict):
|
|
242
|
+
return
|
|
243
|
+
node_type = node.get('type')
|
|
244
|
+
if node_type is None:
|
|
245
|
+
return
|
|
246
|
+
|
|
247
|
+
# Look up scope for this node
|
|
248
|
+
scope = _get_scope_for(node, scope)
|
|
249
|
+
|
|
250
|
+
if node_type == 'Identifier':
|
|
251
|
+
name = node.get('name', '')
|
|
252
|
+
if _is_non_reference_identifier(parent, parent_key):
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
binding = scope.get_binding(name)
|
|
256
|
+
if not binding:
|
|
257
|
+
return
|
|
258
|
+
binding.references.append((node, parent, parent_key, parent_index))
|
|
259
|
+
if parent and parent.get('type') == 'AssignmentExpression' and parent_key == 'left':
|
|
260
|
+
binding.assignments.append(parent)
|
|
261
|
+
elif parent and parent.get('type') == 'UpdateExpression':
|
|
262
|
+
binding.assignments.append(parent)
|
|
263
|
+
return
|
|
264
|
+
|
|
265
|
+
# Recurse — can't use _recurse_into_children here because we need
|
|
266
|
+
# per-child (key, index) args for reference tracking
|
|
267
|
+
child_keys = _child_keys_map.get(node_type)
|
|
268
|
+
if child_keys is None:
|
|
269
|
+
child_keys = get_child_keys(node)
|
|
270
|
+
for key in child_keys:
|
|
271
|
+
child = node.get(key)
|
|
272
|
+
if child is None:
|
|
273
|
+
continue
|
|
274
|
+
if isinstance(child, list):
|
|
275
|
+
for i, item in enumerate(child):
|
|
276
|
+
if isinstance(item, dict) and 'type' in item:
|
|
277
|
+
_collect_references(item, scope, node, key, i)
|
|
278
|
+
elif isinstance(child, dict) and 'type' in child:
|
|
279
|
+
_collect_references(child, scope, node, key, None)
|
|
280
|
+
|
|
281
|
+
_collect_references(ast, root_scope)
|
|
282
|
+
|
|
283
|
+
return root_scope, node_scope
|
|
File without changes
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""AAEncode decoder.
|
|
2
|
+
|
|
3
|
+
AAEncode encodes JavaScript using Japanese-style emoticons.
|
|
4
|
+
This decoder reverses the encoding by replacing emoticon patterns
|
|
5
|
+
with their numeric values, then converting octal/hex to characters.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# The 16 AAEncode symbol table entries (indices 0-15)
|
|
12
|
+
_AA_SYMBOLS = [
|
|
13
|
+
'(c^_^o)',
|
|
14
|
+
'(\uff9f\u0398\uff9f)',
|
|
15
|
+
'((o^_^o) - (\uff9f\u0398\uff9f))',
|
|
16
|
+
'(o^_^o)',
|
|
17
|
+
'(\uff9f\uff70\uff9f)',
|
|
18
|
+
'((\uff9f\uff70\uff9f) + (\uff9f\u0398\uff9f))',
|
|
19
|
+
'((o^_^o) +(o^_^o))',
|
|
20
|
+
'((\uff9f\uff70\uff9f) + (o^_^o))',
|
|
21
|
+
'((\uff9f\uff70\uff9f) + (\uff9f\uff70\uff9f))',
|
|
22
|
+
'((\uff9f\uff70\uff9f) + (\uff9f\uff70\uff9f) + (\uff9f\u0398\uff9f))',
|
|
23
|
+
'(\uff9f\u0414\uff9f) .\uff9f\u03c9\uff9f\uff89',
|
|
24
|
+
'(\uff9f\u0414\uff9f) .\uff9f\u0398\uff9f\uff89',
|
|
25
|
+
"(\uff9f\u0414\uff9f) ['c']",
|
|
26
|
+
'(\uff9f\u0414\uff9f) .\uff9f\uff70\uff9f\uff89',
|
|
27
|
+
'(\uff9f\u0414\uff9f) .\uff9f\u0414\uff9f\uff89',
|
|
28
|
+
'(\uff9f\u0414\uff9f) [\uff9f\u0398\uff9f]',
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
# Detection pattern: AAEncoded code contains these characteristic markers
|
|
32
|
+
_AA_DETECT_RE = re.compile(r'\(\uff9f\u0414\uff9f\)\s*\[\uff9f\u03b5\uff9f\]') # (゚Д゚)[゚ε゚]
|
|
33
|
+
|
|
34
|
+
# Unicode marker for hex characters (code points > 127)
|
|
35
|
+
_UNICODE_MARKER = '(o\uff9f\uff70\uff9fo)+ '
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def is_aa_encoded(code):
|
|
39
|
+
"""Check if code is AAEncoded."""
|
|
40
|
+
return bool(_AA_DETECT_RE.search(code))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def aa_decode(code):
|
|
44
|
+
"""Decode AAEncoded JavaScript. Returns decoded string or None on failure."""
|
|
45
|
+
if not is_aa_encoded(code):
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
try:
|
|
49
|
+
text = code
|
|
50
|
+
# Replace each symbol with its numeric value
|
|
51
|
+
for i, symbol in enumerate(_AA_SYMBOLS):
|
|
52
|
+
search = symbol + '+ '
|
|
53
|
+
replacement = str(i) if i <= 7 else format(i, 'x')
|
|
54
|
+
text = text.replace(search, replacement)
|
|
55
|
+
|
|
56
|
+
# Remove the trailing execution wrapper
|
|
57
|
+
text = text.replace("(\uff9f\u0414\uff9f)[\uff9fo\uff9f]) (\uff9f\u0398\uff9f)) ('_');", '')
|
|
58
|
+
text = text.replace(
|
|
59
|
+
"(\uff9f\u0414\uff9f)[\uff9fo\uff9f])(\uff9f\u0398\uff9f))((\uff9f\u0398\uff9f)+(\uff9f\u0414\uff9f)[\uff9f\u03b5\uff9f]+((\uff9f\uff70\uff9f)+(\uff9f\u0398\uff9f))+(\uff9f\u0398\uff9f)+(\uff9f\u0414\uff9f)[\uff9fo\uff9f]);",
|
|
60
|
+
'',
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Split on the escape marker
|
|
64
|
+
parts = text.split('(\uff9f\u0414\uff9f)[\uff9f\u03b5\uff9f]+')
|
|
65
|
+
|
|
66
|
+
result = ''
|
|
67
|
+
for part in parts[1:]: # Skip the preamble
|
|
68
|
+
part = part.strip()
|
|
69
|
+
if not part:
|
|
70
|
+
continue
|
|
71
|
+
if part.startswith(_UNICODE_MARKER):
|
|
72
|
+
# Unicode character: parse as hex
|
|
73
|
+
hex_str = part[len(_UNICODE_MARKER) :].strip().rstrip('+').strip()
|
|
74
|
+
result += chr(int(hex_str, 16))
|
|
75
|
+
else:
|
|
76
|
+
# ASCII character: parse as octal
|
|
77
|
+
octal_str = part.strip().rstrip('+').strip()
|
|
78
|
+
if octal_str:
|
|
79
|
+
result += chr(int(octal_str, 8))
|
|
80
|
+
|
|
81
|
+
return result if result else None
|
|
82
|
+
except (ValueError, IndexError):
|
|
83
|
+
return None
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Remove anti-tamper, self-defending, and debug protection patterns.
|
|
2
|
+
|
|
3
|
+
Detects common obfuscator.io patterns:
|
|
4
|
+
- Self-defending functions (check if code was modified)
|
|
5
|
+
- Debug protection (infinite debugger loops)
|
|
6
|
+
- Console output disabling
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
from ..generator import generate
|
|
12
|
+
from ..traverser import REMOVE
|
|
13
|
+
from ..traverser import traverse
|
|
14
|
+
from .base import Transform
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class AntiTamperRemover(Transform):
|
|
18
|
+
"""Remove self-defending, debug protection, and console-disabling code."""
|
|
19
|
+
|
|
20
|
+
rebuild_scope = True
|
|
21
|
+
|
|
22
|
+
# Patterns to match in generated code for suspicious IIFEs
|
|
23
|
+
_SELF_DEFENDING_PATTERNS = [
|
|
24
|
+
re.compile(r'constructor\s*\(\s*\)\s*\.\s*constructor\s*\('),
|
|
25
|
+
re.compile(r'toString\s*\(\s*\)\s*\.\s*search'),
|
|
26
|
+
re.compile(r'prototype\s*\.\s*toString'),
|
|
27
|
+
re.compile(r'__proto__'),
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
_DEBUG_PATTERNS = [
|
|
31
|
+
re.compile(r'\bdebugger\b'),
|
|
32
|
+
re.compile(r'setInterval\s*\('),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_CONSOLE_PATTERNS = [
|
|
36
|
+
re.compile(r'console\s*\[\s*[\'"](?:log|warn|error|info|debug|trace|exception|table)'),
|
|
37
|
+
re.compile(r'console\s*\.\s*(?:log|warn|error|info|debug|trace|exception|table)\s*='),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def _extract_iife_call(expr):
|
|
42
|
+
"""Extract a CallExpression from an IIFE pattern."""
|
|
43
|
+
if expr.get('type') == 'CallExpression':
|
|
44
|
+
return expr
|
|
45
|
+
if expr.get('type') == 'UnaryExpression' and expr.get('argument', {}).get('type') == 'CallExpression':
|
|
46
|
+
return expr.get('argument')
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
def _matches_anti_tamper_pattern(self, src):
|
|
50
|
+
"""Check if source matches any anti-tamper pattern."""
|
|
51
|
+
for pattern in self._SELF_DEFENDING_PATTERNS:
|
|
52
|
+
if pattern.search(src):
|
|
53
|
+
return True
|
|
54
|
+
if any(p.search(src) for p in self._DEBUG_PATTERNS):
|
|
55
|
+
if re.search(r'\bdebugger\b', src) and (re.search(r'\bwhile\b|\bfor\b|\bsetInterval\b', src)):
|
|
56
|
+
return True
|
|
57
|
+
for pattern in self._CONSOLE_PATTERNS:
|
|
58
|
+
if pattern.search(src):
|
|
59
|
+
return True
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
def execute(self):
|
|
63
|
+
nodes_to_remove = []
|
|
64
|
+
|
|
65
|
+
def enter(node, parent, key, index):
|
|
66
|
+
if node.get('type') != 'ExpressionStatement':
|
|
67
|
+
return
|
|
68
|
+
expr = node.get('expression')
|
|
69
|
+
if not expr:
|
|
70
|
+
return
|
|
71
|
+
|
|
72
|
+
call = self._extract_iife_call(expr)
|
|
73
|
+
if not call:
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
callee = call.get('callee')
|
|
77
|
+
if not callee:
|
|
78
|
+
return
|
|
79
|
+
if callee.get('type') not in (
|
|
80
|
+
'FunctionExpression',
|
|
81
|
+
'ArrowFunctionExpression',
|
|
82
|
+
):
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
source_code = generate(callee)
|
|
87
|
+
except Exception:
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
if self._matches_anti_tamper_pattern(source_code):
|
|
91
|
+
nodes_to_remove.append(node)
|
|
92
|
+
|
|
93
|
+
traverse(self.ast, {'enter': enter})
|
|
94
|
+
|
|
95
|
+
# Remove flagged nodes
|
|
96
|
+
if nodes_to_remove:
|
|
97
|
+
remove_set = set(id(n) for n in nodes_to_remove)
|
|
98
|
+
|
|
99
|
+
def remover(node, parent, key, index):
|
|
100
|
+
if id(node) in remove_set:
|
|
101
|
+
self.set_changed()
|
|
102
|
+
return REMOVE
|
|
103
|
+
|
|
104
|
+
traverse(self.ast, {'enter': remover})
|
|
105
|
+
|
|
106
|
+
return self.has_changed()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Base transform class."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Transform:
|
|
5
|
+
"""Base class for all AST transforms."""
|
|
6
|
+
|
|
7
|
+
# Subclasses can set this to True to trigger scope rebuild after execution
|
|
8
|
+
rebuild_scope = False
|
|
9
|
+
|
|
10
|
+
def __init__(self, ast, scope_tree=None, node_scope=None):
|
|
11
|
+
self.ast = ast
|
|
12
|
+
self.scope_tree = scope_tree
|
|
13
|
+
self.node_scope = node_scope
|
|
14
|
+
self._changed = False
|
|
15
|
+
|
|
16
|
+
def execute(self):
|
|
17
|
+
"""Execute the transform. Returns True if the AST was modified."""
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
def set_changed(self):
|
|
21
|
+
self._changed = True
|
|
22
|
+
|
|
23
|
+
def has_changed(self):
|
|
24
|
+
return self._changed
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""Constant propagation — replace references to constant variables with their literal values."""
|
|
2
|
+
|
|
3
|
+
from ..scope import build_scope_tree
|
|
4
|
+
from ..traverser import REMOVE
|
|
5
|
+
from ..traverser import SKIP
|
|
6
|
+
from ..traverser import traverse
|
|
7
|
+
from ..utils.ast_helpers import deep_copy
|
|
8
|
+
from ..utils.ast_helpers import is_literal
|
|
9
|
+
from .base import Transform
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _should_skip_reference(ref_parent, ref_key):
|
|
13
|
+
"""Return True if this reference should not be replaced with its literal value."""
|
|
14
|
+
if not ref_parent:
|
|
15
|
+
return True
|
|
16
|
+
match ref_parent.get('type'):
|
|
17
|
+
case 'AssignmentExpression' if ref_key == 'left':
|
|
18
|
+
return True
|
|
19
|
+
case 'UpdateExpression':
|
|
20
|
+
return True
|
|
21
|
+
case 'VariableDeclarator' if ref_key == 'id':
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ConstantProp(Transform):
|
|
27
|
+
"""Find `const x = <literal>` and replace all references with the literal."""
|
|
28
|
+
|
|
29
|
+
rebuild_scope = True
|
|
30
|
+
|
|
31
|
+
def execute(self):
|
|
32
|
+
scope_tree, node_scope = build_scope_tree(self.ast)
|
|
33
|
+
|
|
34
|
+
replacements = dict(self._iter_constant_bindings(scope_tree))
|
|
35
|
+
if not replacements:
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
bindings_replaced = self._replace_references(replacements)
|
|
39
|
+
self._remove_fully_propagated(replacements, bindings_replaced)
|
|
40
|
+
return self.has_changed()
|
|
41
|
+
|
|
42
|
+
def _iter_constant_bindings(self, scope):
|
|
43
|
+
"""Yield (binding_id, (binding, literal)) for constant bindings with literal values."""
|
|
44
|
+
for name, binding in scope.bindings.items():
|
|
45
|
+
if not binding.is_constant:
|
|
46
|
+
continue
|
|
47
|
+
node = binding.node
|
|
48
|
+
if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
|
|
49
|
+
continue
|
|
50
|
+
init_val = node.get('init')
|
|
51
|
+
if not init_val or not is_literal(init_val):
|
|
52
|
+
continue
|
|
53
|
+
yield id(binding), (binding, init_val)
|
|
54
|
+
|
|
55
|
+
for child in scope.children:
|
|
56
|
+
yield from self._iter_constant_bindings(child)
|
|
57
|
+
|
|
58
|
+
def _replace_references(self, replacements):
|
|
59
|
+
"""Replace all qualifying references with their literal values."""
|
|
60
|
+
bindings_replaced = set()
|
|
61
|
+
for bind_id, (binding, literal) in replacements.items():
|
|
62
|
+
for ref_node, ref_parent, ref_key, ref_index in binding.references:
|
|
63
|
+
if _should_skip_reference(ref_parent, ref_key):
|
|
64
|
+
continue
|
|
65
|
+
new_node = deep_copy(literal)
|
|
66
|
+
if ref_index is not None:
|
|
67
|
+
ref_parent[ref_key][ref_index] = new_node
|
|
68
|
+
else:
|
|
69
|
+
ref_parent[ref_key] = new_node
|
|
70
|
+
self.set_changed()
|
|
71
|
+
bindings_replaced.add(bind_id)
|
|
72
|
+
return bindings_replaced
|
|
73
|
+
|
|
74
|
+
def _remove_fully_propagated(self, replacements, bindings_replaced):
|
|
75
|
+
"""Remove declarations whose bindings were fully propagated."""
|
|
76
|
+
for bind_id in bindings_replaced:
|
|
77
|
+
binding = replacements[bind_id][0]
|
|
78
|
+
if binding.assignments:
|
|
79
|
+
continue
|
|
80
|
+
decl_node = binding.node
|
|
81
|
+
if not isinstance(decl_node, dict):
|
|
82
|
+
continue
|
|
83
|
+
if decl_node.get('type') != 'VariableDeclarator':
|
|
84
|
+
continue
|
|
85
|
+
self._remove_declarator(decl_node)
|
|
86
|
+
|
|
87
|
+
def _remove_declarator(self, declarator_node):
|
|
88
|
+
"""Remove a VariableDeclarator from its parent VariableDeclaration."""
|
|
89
|
+
|
|
90
|
+
def enter(node, parent, key, index):
|
|
91
|
+
if node.get('type') != 'VariableDeclaration':
|
|
92
|
+
return
|
|
93
|
+
decls = node.get('declarations', [])
|
|
94
|
+
for i, declaration in enumerate(decls):
|
|
95
|
+
if declaration is not declarator_node:
|
|
96
|
+
continue
|
|
97
|
+
decls.pop(i)
|
|
98
|
+
self.set_changed()
|
|
99
|
+
if not decls:
|
|
100
|
+
return REMOVE
|
|
101
|
+
return SKIP
|
|
102
|
+
|
|
103
|
+
traverse(self.ast, {'enter': enter})
|