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
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
"""Control flow flattening recovery.
|
|
2
|
+
|
|
3
|
+
Detects patterns like:
|
|
4
|
+
var _array = "1|0|3|2|4".split("|"), _index = 0;
|
|
5
|
+
while(true) { switch(_array[_index++]) { case "0": ...; continue; ... } break; }
|
|
6
|
+
|
|
7
|
+
And reconstructs the linear statement sequence.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ..utils.ast_helpers import get_child_keys
|
|
11
|
+
from ..utils.ast_helpers import is_identifier
|
|
12
|
+
from ..utils.ast_helpers import is_literal
|
|
13
|
+
from ..utils.ast_helpers import is_string_literal
|
|
14
|
+
from .base import Transform
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ControlFlowRecoverer(Transform):
|
|
18
|
+
"""Recover control flow from flattened switch/loop dispatchers."""
|
|
19
|
+
|
|
20
|
+
rebuild_scope = True
|
|
21
|
+
|
|
22
|
+
def execute(self):
|
|
23
|
+
self._recover_in_bodies(self.ast)
|
|
24
|
+
return self.has_changed()
|
|
25
|
+
|
|
26
|
+
def _recover_in_bodies(self, root):
|
|
27
|
+
"""Walk through the AST looking for bodies containing CFF patterns."""
|
|
28
|
+
stack = [root]
|
|
29
|
+
visited = set()
|
|
30
|
+
while stack:
|
|
31
|
+
node = stack.pop()
|
|
32
|
+
if not isinstance(node, dict) or 'type' not in node:
|
|
33
|
+
continue
|
|
34
|
+
|
|
35
|
+
node_id = id(node)
|
|
36
|
+
if node_id in visited:
|
|
37
|
+
continue
|
|
38
|
+
visited.add(node_id)
|
|
39
|
+
|
|
40
|
+
node_type = node.get('type', '')
|
|
41
|
+
|
|
42
|
+
# Check in body arrays
|
|
43
|
+
if node_type in ('Program', 'BlockStatement'):
|
|
44
|
+
self._try_recover_body(node, 'body', node.get('body', []))
|
|
45
|
+
|
|
46
|
+
# Queue children for processing
|
|
47
|
+
self._queue_children(node, stack)
|
|
48
|
+
|
|
49
|
+
@staticmethod
|
|
50
|
+
def _queue_children(node, stack):
|
|
51
|
+
"""Add all child nodes to the traversal stack."""
|
|
52
|
+
for key in get_child_keys(node):
|
|
53
|
+
child = node.get(key)
|
|
54
|
+
if child is None:
|
|
55
|
+
continue
|
|
56
|
+
if isinstance(child, list):
|
|
57
|
+
for item in child:
|
|
58
|
+
if isinstance(item, dict) and 'type' in item:
|
|
59
|
+
stack.append(item)
|
|
60
|
+
elif isinstance(child, dict) and 'type' in child:
|
|
61
|
+
stack.append(child)
|
|
62
|
+
|
|
63
|
+
def _try_recover_body(self, parent_node, body_key, body):
|
|
64
|
+
"""Try to find and recover CFF patterns in a body array."""
|
|
65
|
+
i = 0
|
|
66
|
+
while i < len(body):
|
|
67
|
+
statement = body[i]
|
|
68
|
+
if not isinstance(statement, dict):
|
|
69
|
+
i += 1
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
if self._try_recover_variable_pattern(body, i, statement):
|
|
73
|
+
continue
|
|
74
|
+
if self._try_recover_expression_pattern(body, i, statement):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
i += 1
|
|
78
|
+
|
|
79
|
+
def _try_recover_variable_pattern(self, body, i, stmt):
|
|
80
|
+
"""Try Pattern 1: VariableDeclaration with split + loop. Returns True if recovered."""
|
|
81
|
+
if stmt.get('type') != 'VariableDeclaration':
|
|
82
|
+
return False
|
|
83
|
+
state_info = self._find_state_array_in_decl(stmt)
|
|
84
|
+
if not state_info:
|
|
85
|
+
return False
|
|
86
|
+
states, state_var, counter_var = state_info
|
|
87
|
+
next_idx = i + 1
|
|
88
|
+
if next_idx >= len(body):
|
|
89
|
+
return False
|
|
90
|
+
recovered = self._try_recover_from_loop(body[next_idx], states, state_var, counter_var)
|
|
91
|
+
if recovered is None:
|
|
92
|
+
return False
|
|
93
|
+
body[i : next_idx + 1] = recovered
|
|
94
|
+
self.set_changed()
|
|
95
|
+
return True
|
|
96
|
+
|
|
97
|
+
def _try_recover_expression_pattern(self, body, i, stmt):
|
|
98
|
+
"""Try Pattern 2: ExpressionStatement with split assignment + loop."""
|
|
99
|
+
if stmt.get('type') != 'ExpressionStatement':
|
|
100
|
+
return False
|
|
101
|
+
expr = stmt.get('expression')
|
|
102
|
+
if not expr or expr.get('type') != 'AssignmentExpression':
|
|
103
|
+
return False
|
|
104
|
+
state_info = self._find_state_from_assignment(expr)
|
|
105
|
+
if not state_info:
|
|
106
|
+
return False
|
|
107
|
+
states, state_var = state_info
|
|
108
|
+
next_idx = i + 1
|
|
109
|
+
counter_var = None
|
|
110
|
+
if next_idx < len(body):
|
|
111
|
+
counter_variable = self._find_counter_init(body[next_idx])
|
|
112
|
+
if counter_variable is not None:
|
|
113
|
+
counter_var = counter_variable
|
|
114
|
+
next_idx += 1
|
|
115
|
+
if next_idx >= len(body):
|
|
116
|
+
return False
|
|
117
|
+
recovered = self._try_recover_from_loop(body[next_idx], states, state_var, counter_var or '_index')
|
|
118
|
+
if recovered is None:
|
|
119
|
+
return False
|
|
120
|
+
body[i : next_idx + 1] = recovered
|
|
121
|
+
self.set_changed()
|
|
122
|
+
return True
|
|
123
|
+
|
|
124
|
+
def _find_state_array_in_decl(self, decl):
|
|
125
|
+
"""Find "X".split("|") pattern in a VariableDeclaration."""
|
|
126
|
+
for declaration in decl.get('declarations', []):
|
|
127
|
+
initializer = declaration.get('init')
|
|
128
|
+
if not initializer or not self._is_split_call(initializer):
|
|
129
|
+
continue
|
|
130
|
+
states = self._extract_split_states(initializer)
|
|
131
|
+
if not states:
|
|
132
|
+
continue
|
|
133
|
+
if declaration.get('id', {}).get('type') != 'Identifier':
|
|
134
|
+
continue
|
|
135
|
+
state_var = declaration['id']['name']
|
|
136
|
+
counter_var = self._find_counter_in_declaration(decl, exclude=declaration)
|
|
137
|
+
return states, state_var, counter_var
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def _find_counter_in_declaration(self, decl, exclude):
|
|
141
|
+
"""Find a numeric-initialized counter variable in a declaration, skipping *exclude*."""
|
|
142
|
+
for declaration in decl.get('declarations', []):
|
|
143
|
+
if declaration is exclude:
|
|
144
|
+
continue
|
|
145
|
+
if declaration.get('id', {}).get('type') != 'Identifier':
|
|
146
|
+
continue
|
|
147
|
+
initializer = declaration.get('init')
|
|
148
|
+
if (
|
|
149
|
+
initializer
|
|
150
|
+
and initializer.get('type') == 'Literal'
|
|
151
|
+
and isinstance(initializer.get('value'), (int, float))
|
|
152
|
+
):
|
|
153
|
+
return declaration['id']['name']
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def _find_state_from_assignment(self, expr):
|
|
157
|
+
"""Find state array from assignment expression."""
|
|
158
|
+
if expr.get('type') != 'AssignmentExpression':
|
|
159
|
+
return None
|
|
160
|
+
if not is_identifier(expr.get('left')):
|
|
161
|
+
return None
|
|
162
|
+
right = expr.get('right')
|
|
163
|
+
if self._is_split_call(right):
|
|
164
|
+
states = self._extract_split_states(right)
|
|
165
|
+
if states:
|
|
166
|
+
return states, expr['left']['name']
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
def _find_counter_init(self, statement):
|
|
170
|
+
"""Find counter variable initialization."""
|
|
171
|
+
if not isinstance(statement, dict):
|
|
172
|
+
return None
|
|
173
|
+
match statement.get('type'):
|
|
174
|
+
case 'VariableDeclaration':
|
|
175
|
+
for declaration in statement.get('declarations', []):
|
|
176
|
+
if declaration.get('id', {}).get('type') == 'Identifier':
|
|
177
|
+
initializer = declaration.get('init')
|
|
178
|
+
if (
|
|
179
|
+
initializer
|
|
180
|
+
and initializer.get('type') == 'Literal'
|
|
181
|
+
and isinstance(initializer.get('value'), (int, float))
|
|
182
|
+
):
|
|
183
|
+
return declaration['id']['name']
|
|
184
|
+
case 'ExpressionStatement':
|
|
185
|
+
expr = statement.get('expression')
|
|
186
|
+
if (
|
|
187
|
+
expr
|
|
188
|
+
and expr.get('type') == 'AssignmentExpression'
|
|
189
|
+
and is_identifier(expr.get('left'))
|
|
190
|
+
and is_literal(expr.get('right'))
|
|
191
|
+
and isinstance(expr['right'].get('value'), (int, float))
|
|
192
|
+
):
|
|
193
|
+
return expr['left']['name']
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _is_split_call(self, node):
|
|
197
|
+
"""Check if node is "X".split("|")."""
|
|
198
|
+
if not isinstance(node, dict):
|
|
199
|
+
return False
|
|
200
|
+
if node.get('type') != 'CallExpression':
|
|
201
|
+
return False
|
|
202
|
+
callee = node.get('callee')
|
|
203
|
+
if not callee or callee.get('type') != 'MemberExpression':
|
|
204
|
+
return False
|
|
205
|
+
object_expression = callee.get('object')
|
|
206
|
+
property_expression = callee.get('property')
|
|
207
|
+
if not is_string_literal(object_expression):
|
|
208
|
+
return False
|
|
209
|
+
if not (is_identifier(property_expression) and property_expression.get('name') == 'split') and not (
|
|
210
|
+
is_string_literal(property_expression) and property_expression.get('value') == 'split'
|
|
211
|
+
):
|
|
212
|
+
return False
|
|
213
|
+
args = node.get('arguments', [])
|
|
214
|
+
if len(args) != 1 or not is_string_literal(args[0]):
|
|
215
|
+
return False
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
def _extract_split_states(self, node):
|
|
219
|
+
"""Extract states from "1|0|3|2|4".split("|")."""
|
|
220
|
+
callee = node['callee']
|
|
221
|
+
string = callee['object']['value']
|
|
222
|
+
separator = node['arguments'][0]['value']
|
|
223
|
+
return string.split(separator)
|
|
224
|
+
|
|
225
|
+
def _try_recover_from_loop(self, loop, states, state_var, counter_var):
|
|
226
|
+
"""Try to recover statements from a for/while loop with switch dispatcher."""
|
|
227
|
+
if not isinstance(loop, dict):
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
loop_type = loop.get('type', '')
|
|
231
|
+
switch_body = None
|
|
232
|
+
initial_value = 0
|
|
233
|
+
|
|
234
|
+
if loop_type == 'ForStatement':
|
|
235
|
+
# for(var _i = 0; ...) { switch(_array[_i++]) { ... } break; }
|
|
236
|
+
initial_value = self._extract_for_init_value(loop.get('init'))
|
|
237
|
+
switch_body = self._extract_switch_from_loop_body(loop.get('body'))
|
|
238
|
+
|
|
239
|
+
elif loop_type == 'WhileStatement':
|
|
240
|
+
test = loop.get('test')
|
|
241
|
+
if self._is_truthy(test):
|
|
242
|
+
switch_body = self._extract_switch_from_loop_body(loop.get('body'))
|
|
243
|
+
|
|
244
|
+
if switch_body is None:
|
|
245
|
+
return None
|
|
246
|
+
|
|
247
|
+
cases_map = self._build_case_map(switch_body.get('cases', []))
|
|
248
|
+
return self._reconstruct_statements(cases_map, states, initial_value)
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _extract_for_init_value(initializer):
|
|
252
|
+
"""Extract the initial counter value from a for-loop init clause."""
|
|
253
|
+
if not initializer:
|
|
254
|
+
return 0
|
|
255
|
+
if initializer.get('type') == 'VariableDeclaration':
|
|
256
|
+
for declaration in initializer.get('declarations', []):
|
|
257
|
+
if declaration.get('init') and declaration['init'].get('type') == 'Literal':
|
|
258
|
+
return int(declaration['init'].get('value', 0))
|
|
259
|
+
elif initializer.get('type') == 'AssignmentExpression' and is_literal(initializer.get('right')):
|
|
260
|
+
return int(initializer['right'].get('value', 0))
|
|
261
|
+
return 0
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def _build_case_map(cases):
|
|
265
|
+
"""Build map from case test value to (filtered statements, original statements)."""
|
|
266
|
+
cases_map = {}
|
|
267
|
+
for case in cases:
|
|
268
|
+
test = case.get('test')
|
|
269
|
+
if not (test and test.get('type') == 'Literal'):
|
|
270
|
+
continue
|
|
271
|
+
test_value = test['value']
|
|
272
|
+
if isinstance(test_value, float) and test_value == int(test_value):
|
|
273
|
+
key = str(int(test_value))
|
|
274
|
+
else:
|
|
275
|
+
key = str(test_value)
|
|
276
|
+
statements = [
|
|
277
|
+
statement
|
|
278
|
+
for statement in case.get('consequent', [])
|
|
279
|
+
if statement.get('type') not in ('ContinueStatement', 'BreakStatement')
|
|
280
|
+
]
|
|
281
|
+
cases_map[key] = (statements, case.get('consequent', []))
|
|
282
|
+
return cases_map
|
|
283
|
+
|
|
284
|
+
@staticmethod
|
|
285
|
+
def _reconstruct_statements(cases_map, states, initial_value):
|
|
286
|
+
"""Reconstruct linear statement sequence from case map and state order."""
|
|
287
|
+
recovered = []
|
|
288
|
+
for idx in range(initial_value, len(states)):
|
|
289
|
+
state = states[idx]
|
|
290
|
+
if state not in cases_map:
|
|
291
|
+
break
|
|
292
|
+
statements, original = cases_map[state]
|
|
293
|
+
recovered.extend(statements)
|
|
294
|
+
if original and original[-1].get('type') == 'ReturnStatement':
|
|
295
|
+
recovered.append(original[-1])
|
|
296
|
+
break
|
|
297
|
+
return recovered or None
|
|
298
|
+
|
|
299
|
+
def _extract_switch_from_loop_body(self, body):
|
|
300
|
+
"""Extract SwitchStatement from loop body."""
|
|
301
|
+
if not isinstance(body, dict):
|
|
302
|
+
return None
|
|
303
|
+
if body.get('type') == 'BlockStatement':
|
|
304
|
+
stmts = body.get('body', [])
|
|
305
|
+
for stmt in stmts:
|
|
306
|
+
if stmt.get('type') == 'SwitchStatement':
|
|
307
|
+
return stmt
|
|
308
|
+
elif body.get('type') == 'SwitchStatement':
|
|
309
|
+
return body
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
def _is_truthy(self, node):
|
|
313
|
+
"""Check if a test expression is always truthy."""
|
|
314
|
+
if not isinstance(node, dict):
|
|
315
|
+
return False
|
|
316
|
+
if node.get('type') == 'Literal':
|
|
317
|
+
return bool(node.get('value'))
|
|
318
|
+
# !0 = true, !![] = true
|
|
319
|
+
if node.get('type') == 'UnaryExpression' and node.get('operator') == '!':
|
|
320
|
+
arg = node.get('argument')
|
|
321
|
+
if arg and arg.get('type') == 'Literal' and arg.get('value') == 0:
|
|
322
|
+
return True
|
|
323
|
+
if arg and arg.get('type') == 'ArrayExpression':
|
|
324
|
+
return False # ![] = false, but !![] = true
|
|
325
|
+
if arg and arg.get('type') == 'UnaryExpression' and arg.get('operator') == '!':
|
|
326
|
+
# !!something
|
|
327
|
+
inner = arg.get('argument')
|
|
328
|
+
if inner and inner.get('type') == 'ArrayExpression':
|
|
329
|
+
return True
|
|
330
|
+
return False
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Remove unreachable if/ternary branches based on literal tests."""
|
|
2
|
+
|
|
3
|
+
from ..traverser import REMOVE
|
|
4
|
+
from ..traverser import traverse
|
|
5
|
+
from .base import Transform
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _is_truthy_literal(node):
|
|
9
|
+
"""Check if node is a literal that is truthy in JS. Returns None if unknown."""
|
|
10
|
+
if not isinstance(node, dict):
|
|
11
|
+
return None
|
|
12
|
+
match node.get('type', ''):
|
|
13
|
+
case 'Literal':
|
|
14
|
+
value = node.get('value')
|
|
15
|
+
if value is None:
|
|
16
|
+
return False # null is falsy
|
|
17
|
+
match value:
|
|
18
|
+
case bool():
|
|
19
|
+
return value
|
|
20
|
+
case int() | float():
|
|
21
|
+
return value != 0
|
|
22
|
+
case str():
|
|
23
|
+
return len(value) > 0
|
|
24
|
+
case _:
|
|
25
|
+
return True
|
|
26
|
+
case 'UnaryExpression' if node.get('operator') == '!':
|
|
27
|
+
inner = _is_truthy_literal(node.get('argument'))
|
|
28
|
+
if inner is not None:
|
|
29
|
+
return not inner
|
|
30
|
+
case 'ArrayExpression' if len(node.get('elements', [])) == 0:
|
|
31
|
+
return True # [] is truthy
|
|
32
|
+
case 'ObjectExpression' if len(node.get('properties', [])) == 0:
|
|
33
|
+
return True # {} is truthy
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _unwrap_block(node):
|
|
38
|
+
"""Unwrap a single-statement block to its contents."""
|
|
39
|
+
if isinstance(node, dict) and node.get('type') == 'BlockStatement':
|
|
40
|
+
body = node.get('body', [])
|
|
41
|
+
if len(body) == 1:
|
|
42
|
+
return body[0]
|
|
43
|
+
return node
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class DeadBranchRemover(Transform):
|
|
47
|
+
"""Remove dead branches from if statements and ternary expressions."""
|
|
48
|
+
|
|
49
|
+
def execute(self):
|
|
50
|
+
def enter(node, parent, key, index):
|
|
51
|
+
node_type = node.get('type', '')
|
|
52
|
+
|
|
53
|
+
if node_type == 'IfStatement':
|
|
54
|
+
truthy = _is_truthy_literal(node.get('test'))
|
|
55
|
+
if truthy is None:
|
|
56
|
+
return
|
|
57
|
+
self.set_changed()
|
|
58
|
+
if truthy:
|
|
59
|
+
return node.get('consequent')
|
|
60
|
+
alt = node.get('alternate')
|
|
61
|
+
return alt if alt else REMOVE
|
|
62
|
+
|
|
63
|
+
if node_type == 'ConditionalExpression':
|
|
64
|
+
truthy = _is_truthy_literal(node.get('test'))
|
|
65
|
+
if truthy is None:
|
|
66
|
+
return
|
|
67
|
+
self.set_changed()
|
|
68
|
+
return node.get('consequent' if truthy else 'alternate')
|
|
69
|
+
|
|
70
|
+
traverse(self.ast, {'enter': enter})
|
|
71
|
+
return self.has_changed()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Eval/packer unpacker.
|
|
2
|
+
|
|
3
|
+
Handles Dean Edwards packer (eval(function(p,a,c,k,e,d){...})) and
|
|
4
|
+
generic eval(...) wrappers by replacing eval with identity capture.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import re
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import tempfile
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Dean Edwards packer pattern
|
|
15
|
+
_PACKER_RE = re.compile(
|
|
16
|
+
r"""eval\(function\(p,a,c,k,e,[dr]\)\{"""
|
|
17
|
+
r""".*?return p\}"""
|
|
18
|
+
r"""\('(.*?)',\s*(\d+)\s*,\s*(\d+)\s*,\s*'(.*?)'\s*\.split\('\|'\)""",
|
|
19
|
+
re.DOTALL,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# Simpler packer pattern (single-quoted packed string)
|
|
23
|
+
_PACKER_RE2 = re.compile(
|
|
24
|
+
r"""eval\(function\s*\(p\s*,\s*a\s*,\s*c\s*,\s*k\s*,\s*e\s*,\s*[dr]\s*\)\s*\{"""
|
|
25
|
+
r"""[\s\S]*?return\s+p[\s\S]*?\}\s*\(\s*'((?:[^'\\]|\\.)*)'\s*,"""
|
|
26
|
+
r"""\s*(\d+)\s*,\s*(\d+)\s*,\s*'((?:[^'\\]|\\.)*)'\s*\.split\s*\(\s*'\|'\s*\)""",
|
|
27
|
+
re.DOTALL,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
# Generic eval(...) pattern
|
|
31
|
+
_EVAL_RE = re.compile(r'^eval\s*\(', re.MULTILINE)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_eval_packed(code):
|
|
35
|
+
"""Check if code uses eval packing."""
|
|
36
|
+
return bool(_PACKER_RE.search(code) or _PACKER_RE2.search(code) or _EVAL_RE.search(code.lstrip()))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _dean_edwards_unpack(packed, radix, count, keywords):
|
|
40
|
+
"""Pure Python implementation of Dean Edwards unpacker."""
|
|
41
|
+
|
|
42
|
+
# Build the replacement function
|
|
43
|
+
def base_encode(c):
|
|
44
|
+
prefix = '' if c < radix else base_encode(int(c / radix))
|
|
45
|
+
c = c % radix
|
|
46
|
+
if c > 35:
|
|
47
|
+
return prefix + chr(c + 29)
|
|
48
|
+
return prefix + ('0123456789abcdefghijklmnopqrstuvwxyz'[c] if c < 36 else chr(c + 29))
|
|
49
|
+
|
|
50
|
+
# Build dictionary
|
|
51
|
+
lookup = {}
|
|
52
|
+
while count > 0:
|
|
53
|
+
count -= 1
|
|
54
|
+
key = base_encode(count)
|
|
55
|
+
lookup[key] = keywords[count] if count < len(keywords) and keywords[count] else key
|
|
56
|
+
|
|
57
|
+
# Replace tokens in packed string
|
|
58
|
+
def replacer(match):
|
|
59
|
+
token = match.group(0)
|
|
60
|
+
return lookup.get(token, token)
|
|
61
|
+
|
|
62
|
+
return re.sub(r'\b\w+\b', replacer, packed)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def eval_unpack(code):
|
|
66
|
+
"""Unpack eval-packed JavaScript. Returns unpacked code or None."""
|
|
67
|
+
# Try Dean Edwards packer first (pure Python)
|
|
68
|
+
result = _try_dean_edwards(code)
|
|
69
|
+
if result:
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
# Try generic eval via Node.js
|
|
73
|
+
return _try_node_eval(code)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _try_dean_edwards(code):
|
|
77
|
+
"""Try to unpack Dean Edwards packer format."""
|
|
78
|
+
for pattern in [_PACKER_RE, _PACKER_RE2]:
|
|
79
|
+
m = pattern.search(code)
|
|
80
|
+
if m:
|
|
81
|
+
packed = m.group(1)
|
|
82
|
+
radix = int(m.group(2))
|
|
83
|
+
count = int(m.group(3))
|
|
84
|
+
keywords_str = m.group(4)
|
|
85
|
+
keywords = keywords_str.split('|')
|
|
86
|
+
|
|
87
|
+
# Unescape the packed string
|
|
88
|
+
packed = packed.replace("\\'", "'").replace('\\\\', '\\')
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
return _dean_edwards_unpack(packed, radix, count, keywords)
|
|
92
|
+
except Exception:
|
|
93
|
+
continue
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _try_node_eval(code):
|
|
98
|
+
"""Try to unpack generic eval() by intercepting via Node.js."""
|
|
99
|
+
stripped = code.strip()
|
|
100
|
+
if not _EVAL_RE.search(stripped):
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
node = shutil.which('node')
|
|
104
|
+
if not node:
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
# Replace eval with a capture function
|
|
108
|
+
js_wrapper = (
|
|
109
|
+
'var _captured = null;\n'
|
|
110
|
+
'var _origEval = eval;\n'
|
|
111
|
+
'eval = function(x) { _captured = x; return x; };\n'
|
|
112
|
+
'try {\n' + stripped + '\n' + '} catch(e) {}\n'
|
|
113
|
+
'eval = _origEval;\n'
|
|
114
|
+
'if (_captured && typeof _captured === "string") console.log(_captured);\n'
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
fd, tmp_path = tempfile.mkstemp(suffix='.js')
|
|
119
|
+
try:
|
|
120
|
+
with os.fdopen(fd, 'w') as f:
|
|
121
|
+
f.write(js_wrapper)
|
|
122
|
+
result = subprocess.run(
|
|
123
|
+
[node, tmp_path],
|
|
124
|
+
capture_output=True,
|
|
125
|
+
text=True,
|
|
126
|
+
timeout=5,
|
|
127
|
+
)
|
|
128
|
+
output = result.stdout.strip()
|
|
129
|
+
return output if output else None
|
|
130
|
+
finally:
|
|
131
|
+
os.unlink(tmp_path)
|
|
132
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
|
|
133
|
+
return None
|