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,1259 @@
|
|
|
1
|
+
"""String array detection, rotation, and decoding for obfuscator.io patterns."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
from ..generator import generate
|
|
7
|
+
from ..scope import build_scope_tree
|
|
8
|
+
from ..traverser import REMOVE
|
|
9
|
+
from ..traverser import find_parent
|
|
10
|
+
from ..traverser import simple_traverse
|
|
11
|
+
from ..traverser import traverse
|
|
12
|
+
from ..utils.ast_helpers import is_identifier
|
|
13
|
+
from ..utils.ast_helpers import is_numeric_literal
|
|
14
|
+
from ..utils.ast_helpers import is_string_literal
|
|
15
|
+
from ..utils.ast_helpers import make_literal
|
|
16
|
+
from ..utils.string_decoders import Base64StringDecoder
|
|
17
|
+
from ..utils.string_decoders import BasicStringDecoder
|
|
18
|
+
from ..utils.string_decoders import DecoderType
|
|
19
|
+
from ..utils.string_decoders import Rc4StringDecoder
|
|
20
|
+
from .base import Transform
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_BASE_64_REGEX = re.compile(r"""['"]abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\+/=['"]""")
|
|
24
|
+
_RC4_REGEX = re.compile(r"""fromCharCode.{0,30}\^""")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _eval_numeric(node):
|
|
28
|
+
"""Evaluate an AST node to a numeric value if it's a constant expression."""
|
|
29
|
+
if not isinstance(node, dict):
|
|
30
|
+
return None
|
|
31
|
+
match node.get('type', ''):
|
|
32
|
+
case 'Literal':
|
|
33
|
+
val = node.get('value')
|
|
34
|
+
return val if isinstance(val, (int, float)) else None
|
|
35
|
+
case 'UnaryExpression':
|
|
36
|
+
arg = _eval_numeric(node.get('argument'))
|
|
37
|
+
if arg is None:
|
|
38
|
+
return None
|
|
39
|
+
match node.get('operator'):
|
|
40
|
+
case '-':
|
|
41
|
+
return -arg
|
|
42
|
+
case '+':
|
|
43
|
+
return +arg
|
|
44
|
+
return None
|
|
45
|
+
case 'BinaryExpression':
|
|
46
|
+
left = _eval_numeric(node.get('left'))
|
|
47
|
+
right = _eval_numeric(node.get('right'))
|
|
48
|
+
if left is None or right is None:
|
|
49
|
+
return None
|
|
50
|
+
return _apply_arith(node.get('operator'), left, right)
|
|
51
|
+
return None
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _js_parse_int(s):
|
|
55
|
+
"""Mimic JavaScript's parseInt: extract leading integer from string."""
|
|
56
|
+
if not isinstance(s, str):
|
|
57
|
+
return float('nan')
|
|
58
|
+
s = s.strip()
|
|
59
|
+
m = re.match(r'^[+-]?\d+', s)
|
|
60
|
+
if m:
|
|
61
|
+
return int(m.group())
|
|
62
|
+
return float('nan')
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _apply_arith(operator, left, right):
|
|
66
|
+
"""Apply a binary arithmetic operator."""
|
|
67
|
+
match operator:
|
|
68
|
+
case '+':
|
|
69
|
+
return left + right
|
|
70
|
+
case '-':
|
|
71
|
+
return left - right
|
|
72
|
+
case '*':
|
|
73
|
+
return left * right
|
|
74
|
+
case '/':
|
|
75
|
+
return left / right if right != 0 else None
|
|
76
|
+
case '%':
|
|
77
|
+
return left % right if right != 0 else None
|
|
78
|
+
case _:
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _collect_object_literals(ast):
|
|
83
|
+
"""Collect simple object literal assignments: var o = {a: 0x1b1, b: 'str'}.
|
|
84
|
+
|
|
85
|
+
Returns a dict mapping (object_name, property_name) -> value (int or str).
|
|
86
|
+
"""
|
|
87
|
+
result = {}
|
|
88
|
+
|
|
89
|
+
def visitor(node, parent):
|
|
90
|
+
if node.get('type') != 'VariableDeclarator':
|
|
91
|
+
return
|
|
92
|
+
name_node = node.get('id')
|
|
93
|
+
init = node.get('init')
|
|
94
|
+
if not is_identifier(name_node) or not init or init.get('type') != 'ObjectExpression':
|
|
95
|
+
return
|
|
96
|
+
object_name = name_node['name']
|
|
97
|
+
for prop in init.get('properties', []):
|
|
98
|
+
if prop.get('type') != 'Property':
|
|
99
|
+
continue
|
|
100
|
+
key = prop.get('key')
|
|
101
|
+
value = prop.get('value')
|
|
102
|
+
if not key or not value:
|
|
103
|
+
continue
|
|
104
|
+
if is_identifier(key):
|
|
105
|
+
property_name = key.get('name')
|
|
106
|
+
elif is_string_literal(key):
|
|
107
|
+
property_name = key.get('value')
|
|
108
|
+
else:
|
|
109
|
+
continue
|
|
110
|
+
numeric_value = _eval_numeric(value)
|
|
111
|
+
if numeric_value is not None:
|
|
112
|
+
result[(object_name, property_name)] = numeric_value
|
|
113
|
+
elif is_string_literal(value):
|
|
114
|
+
result[(object_name, property_name)] = value['value']
|
|
115
|
+
|
|
116
|
+
simple_traverse(ast, visitor)
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _resolve_arg_value(arg, object_literals):
|
|
121
|
+
"""Try to resolve a call argument to a numeric value.
|
|
122
|
+
|
|
123
|
+
Handles numeric literals, string hex literals, and member expressions
|
|
124
|
+
referencing known object literal properties.
|
|
125
|
+
"""
|
|
126
|
+
numeric_value = _eval_numeric(arg)
|
|
127
|
+
if numeric_value is not None:
|
|
128
|
+
return numeric_value
|
|
129
|
+
|
|
130
|
+
if is_string_literal(arg):
|
|
131
|
+
try:
|
|
132
|
+
string_value = arg['value']
|
|
133
|
+
return int(string_value, 16) if string_value.startswith('0x') else int(string_value)
|
|
134
|
+
except (ValueError, TypeError):
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
if arg.get('type') == 'MemberExpression' and not arg.get('computed'):
|
|
138
|
+
object_node = arg.get('object')
|
|
139
|
+
property_node = arg.get('property')
|
|
140
|
+
if is_identifier(object_node) and is_identifier(property_node):
|
|
141
|
+
looked_up = object_literals.get((object_node['name'], property_node['name']))
|
|
142
|
+
if isinstance(looked_up, (int, float)):
|
|
143
|
+
return looked_up
|
|
144
|
+
if isinstance(looked_up, str):
|
|
145
|
+
try:
|
|
146
|
+
return int(looked_up, 16) if looked_up.startswith('0x') else int(looked_up)
|
|
147
|
+
except (ValueError, TypeError):
|
|
148
|
+
pass
|
|
149
|
+
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _resolve_string_arg(arg, object_literals):
|
|
154
|
+
"""Try to resolve a call argument to a string value.
|
|
155
|
+
|
|
156
|
+
Handles string literals and member expressions referencing known object properties.
|
|
157
|
+
"""
|
|
158
|
+
if is_string_literal(arg):
|
|
159
|
+
return arg['value']
|
|
160
|
+
if arg.get('type') == 'MemberExpression' and not arg.get('computed'):
|
|
161
|
+
object_node = arg.get('object')
|
|
162
|
+
property_node = arg.get('property')
|
|
163
|
+
if is_identifier(object_node) and is_identifier(property_node):
|
|
164
|
+
looked_up = object_literals.get((object_node['name'], property_node['name']))
|
|
165
|
+
if isinstance(looked_up, str):
|
|
166
|
+
return looked_up
|
|
167
|
+
return None
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class WrapperInfo:
|
|
171
|
+
"""Info about a wrapper function that calls the decoder."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, name, param_index, wrapper_offset, func_node, key_param_index=None):
|
|
174
|
+
self.name = name
|
|
175
|
+
self.param_index = param_index
|
|
176
|
+
self.wrapper_offset = wrapper_offset
|
|
177
|
+
self.func_node = func_node
|
|
178
|
+
self.key_param_index = key_param_index
|
|
179
|
+
|
|
180
|
+
def get_effective_index(self, call_args):
|
|
181
|
+
"""Given call argument values, compute the effective decoder index."""
|
|
182
|
+
if self.param_index >= len(call_args):
|
|
183
|
+
return None
|
|
184
|
+
value = call_args[self.param_index]
|
|
185
|
+
if not isinstance(value, (int, float)):
|
|
186
|
+
return None
|
|
187
|
+
return int(value) + self.wrapper_offset
|
|
188
|
+
|
|
189
|
+
def get_key(self, call_args):
|
|
190
|
+
"""Get the RC4 key argument if applicable."""
|
|
191
|
+
if self.key_param_index is not None and self.key_param_index < len(call_args):
|
|
192
|
+
return call_args[self.key_param_index]
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class StringRevealer(Transform):
|
|
197
|
+
"""Decode obfuscated string arrays and replace wrapper calls with literals."""
|
|
198
|
+
|
|
199
|
+
rebuild_scope = True
|
|
200
|
+
_rotation_locals = {}
|
|
201
|
+
|
|
202
|
+
def execute(self):
|
|
203
|
+
scope_tree, node_scope = build_scope_tree(self.ast)
|
|
204
|
+
|
|
205
|
+
# Strategy 1: Direct string array declarations (var arr = ["a","b","c"])
|
|
206
|
+
self._process_direct_arrays(scope_tree)
|
|
207
|
+
|
|
208
|
+
# Strategy 2: Obfuscator.io function-wrapped string arrays
|
|
209
|
+
self._process_obfuscatorio_pattern()
|
|
210
|
+
|
|
211
|
+
# Strategy 2b: Var-based string array with rotation + decoder
|
|
212
|
+
self._process_var_array_pattern()
|
|
213
|
+
|
|
214
|
+
# Strategy 3: Simple static array unpacking (js-deob --su)
|
|
215
|
+
self._process_static_arrays()
|
|
216
|
+
|
|
217
|
+
return self.has_changed()
|
|
218
|
+
|
|
219
|
+
# ================================================================
|
|
220
|
+
# Strategy 2: Obfuscator.io pattern
|
|
221
|
+
# ================================================================
|
|
222
|
+
|
|
223
|
+
def _process_obfuscatorio_pattern(self):
|
|
224
|
+
"""Handle obfuscator.io: array func -> decoder func(s) -> wrapper funcs -> rotation."""
|
|
225
|
+
body = self.ast.get('body', [])
|
|
226
|
+
|
|
227
|
+
# Step 1: Find string array function
|
|
228
|
+
array_func_name, string_array, array_func_idx = self._find_string_array_function(body)
|
|
229
|
+
if array_func_name is None:
|
|
230
|
+
return
|
|
231
|
+
|
|
232
|
+
# Step 2: Find ALL decoder functions that call the array function
|
|
233
|
+
decoder_infos = self._find_all_decoder_functions(body, array_func_name)
|
|
234
|
+
if not decoder_infos:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
# Step 3: Create decoders, wrappers, and aliases for each decoder function
|
|
238
|
+
decoders = {} # decoder_name -> decoder instance
|
|
239
|
+
decoder_wrappers = {} # decoder_name -> {wrapper_name: WrapperInfo}
|
|
240
|
+
all_wrappers = {} # all wrappers combined
|
|
241
|
+
all_decoder_aliases = set()
|
|
242
|
+
decoder_indices = set()
|
|
243
|
+
for d_name, d_offset, d_idx, d_type in decoder_infos:
|
|
244
|
+
decoder = self._create_base_decoder(string_array, d_offset, d_type)
|
|
245
|
+
decoders[d_name] = decoder
|
|
246
|
+
decoder_indices.add(d_idx)
|
|
247
|
+
wrappers = self._find_all_wrappers(d_name)
|
|
248
|
+
decoder_wrappers[d_name] = wrappers
|
|
249
|
+
all_wrappers.update(wrappers)
|
|
250
|
+
all_decoder_aliases.update(self._find_decoder_aliases(d_name))
|
|
251
|
+
|
|
252
|
+
# Use the first decoder as the primary (for rotation — all share the same array)
|
|
253
|
+
primary_decoder = decoders[decoder_infos[0][0]]
|
|
254
|
+
|
|
255
|
+
# Build a combined alias-to-decoder map for rotation evaluation
|
|
256
|
+
alias_decoder_map = {}
|
|
257
|
+
for d_name, decoder in decoders.items():
|
|
258
|
+
alias_decoder_map[d_name] = decoder
|
|
259
|
+
for alias in self._find_decoder_aliases(d_name):
|
|
260
|
+
alias_decoder_map[alias] = decoder
|
|
261
|
+
|
|
262
|
+
# Step 5: Find and execute rotation
|
|
263
|
+
rotation_result = self._find_and_execute_rotation(
|
|
264
|
+
body,
|
|
265
|
+
array_func_name,
|
|
266
|
+
string_array,
|
|
267
|
+
primary_decoder,
|
|
268
|
+
all_wrappers,
|
|
269
|
+
all_decoder_aliases,
|
|
270
|
+
alias_decoder_map=alias_decoder_map,
|
|
271
|
+
all_decoders=decoders,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Update the AST array to reflect rotation so future passes
|
|
275
|
+
# re-extract the correct (rotated) array.
|
|
276
|
+
if rotation_result is not None:
|
|
277
|
+
self._update_ast_array(body[array_func_idx], string_array)
|
|
278
|
+
|
|
279
|
+
# Collect object literals for member expression resolution
|
|
280
|
+
obj_literals = _collect_object_literals(self.ast)
|
|
281
|
+
|
|
282
|
+
# Step 6-8: Replace calls and remove aliases for each decoder
|
|
283
|
+
for d_name, decoder in decoders.items():
|
|
284
|
+
aliases_for_decoder = self._find_decoder_aliases(d_name)
|
|
285
|
+
|
|
286
|
+
self._replace_all_wrapper_calls(decoder_wrappers[d_name], decoder, obj_literals)
|
|
287
|
+
self._replace_direct_decoder_calls(d_name, decoder, aliases_for_decoder, obj_literals)
|
|
288
|
+
self._remove_decoder_aliases(d_name, aliases_for_decoder)
|
|
289
|
+
|
|
290
|
+
# Step 9: Remove rotation IIFE, decoder and array functions
|
|
291
|
+
indices_to_remove = set()
|
|
292
|
+
if rotation_result is not None:
|
|
293
|
+
rotation_idx, rotation_call_expr = rotation_result
|
|
294
|
+
if rotation_call_expr is not None:
|
|
295
|
+
# Rotation was inside a SequenceExpression — remove only that sub-expression
|
|
296
|
+
seq_expr = body[rotation_idx]['expression']
|
|
297
|
+
expressions = seq_expr.get('expressions', [])
|
|
298
|
+
try:
|
|
299
|
+
expressions.remove(rotation_call_expr)
|
|
300
|
+
except ValueError:
|
|
301
|
+
pass
|
|
302
|
+
# If only one expression remains, unwrap the SequenceExpression
|
|
303
|
+
if len(expressions) == 1:
|
|
304
|
+
body[rotation_idx]['expression'] = expressions[0]
|
|
305
|
+
else:
|
|
306
|
+
indices_to_remove.add(rotation_idx)
|
|
307
|
+
# Only remove decoder/array funcs if we actually decoded strings
|
|
308
|
+
if self.has_changed():
|
|
309
|
+
indices_to_remove.update(decoder_indices)
|
|
310
|
+
indices_to_remove.add(array_func_idx)
|
|
311
|
+
self._remove_body_indices(body, *indices_to_remove)
|
|
312
|
+
|
|
313
|
+
def _find_string_array_function(self, body):
|
|
314
|
+
"""Find the string array function declaration.
|
|
315
|
+
|
|
316
|
+
Pattern: function X() { var a = ['s1','s2',...]; X = function(){return a;}; return X(); }
|
|
317
|
+
"""
|
|
318
|
+
for i, stmt in enumerate(body):
|
|
319
|
+
if stmt.get('type') != 'FunctionDeclaration':
|
|
320
|
+
continue
|
|
321
|
+
func_name = stmt.get('id', {}).get('name')
|
|
322
|
+
if not func_name:
|
|
323
|
+
continue
|
|
324
|
+
func_body = stmt.get('body', {}).get('body', [])
|
|
325
|
+
if len(func_body) < 2:
|
|
326
|
+
continue
|
|
327
|
+
|
|
328
|
+
string_array = self._extract_array_from_statement(func_body[0])
|
|
329
|
+
if string_array is not None and len(string_array) >= 2:
|
|
330
|
+
return func_name, string_array, i
|
|
331
|
+
|
|
332
|
+
return None, None, None
|
|
333
|
+
|
|
334
|
+
@staticmethod
|
|
335
|
+
def _string_array_from_expression(node):
|
|
336
|
+
"""Return list of string values if node is an ArrayExpression of all string literals."""
|
|
337
|
+
if not node or node.get('type') != 'ArrayExpression':
|
|
338
|
+
return None
|
|
339
|
+
elements = node.get('elements', [])
|
|
340
|
+
if not elements or not all(is_string_literal(e) for e in elements):
|
|
341
|
+
return None
|
|
342
|
+
return [e['value'] for e in elements]
|
|
343
|
+
|
|
344
|
+
def _extract_array_from_statement(self, stmt):
|
|
345
|
+
"""Extract string array from a variable declaration or assignment."""
|
|
346
|
+
if stmt.get('type') == 'VariableDeclaration':
|
|
347
|
+
for declaration in stmt.get('declarations', []):
|
|
348
|
+
result = self._string_array_from_expression(declaration.get('init'))
|
|
349
|
+
if result is not None:
|
|
350
|
+
return result
|
|
351
|
+
elif stmt.get('type') == 'ExpressionStatement':
|
|
352
|
+
expr = stmt.get('expression')
|
|
353
|
+
if expr and expr.get('type') == 'AssignmentExpression':
|
|
354
|
+
return self._string_array_from_expression(expr.get('right'))
|
|
355
|
+
return None
|
|
356
|
+
|
|
357
|
+
def _find_all_decoder_functions(self, body, array_func_name):
|
|
358
|
+
"""Find all decoder functions that call the array function.
|
|
359
|
+
|
|
360
|
+
Returns list of (func_name, offset, body_index, decoder_type) tuples.
|
|
361
|
+
"""
|
|
362
|
+
results = []
|
|
363
|
+
for i, stmt in enumerate(body):
|
|
364
|
+
if stmt.get('type') != 'FunctionDeclaration':
|
|
365
|
+
continue
|
|
366
|
+
func_name = stmt.get('id', {}).get('name')
|
|
367
|
+
if not func_name or func_name == array_func_name:
|
|
368
|
+
continue
|
|
369
|
+
|
|
370
|
+
if not self._function_calls(stmt, array_func_name):
|
|
371
|
+
continue
|
|
372
|
+
|
|
373
|
+
offset = self._extract_decoder_offset(stmt)
|
|
374
|
+
|
|
375
|
+
source = generate(stmt)
|
|
376
|
+
if _BASE_64_REGEX.search(source):
|
|
377
|
+
dtype = DecoderType.RC4 if _RC4_REGEX.search(source) else DecoderType.BASE_64
|
|
378
|
+
else:
|
|
379
|
+
dtype = DecoderType.BASIC
|
|
380
|
+
|
|
381
|
+
results.append((func_name, offset, i, dtype))
|
|
382
|
+
|
|
383
|
+
return results
|
|
384
|
+
|
|
385
|
+
def _function_calls(self, func_node, callee_name):
|
|
386
|
+
"""Check if a function body contains a call to callee_name."""
|
|
387
|
+
found = [False]
|
|
388
|
+
|
|
389
|
+
def visitor(node, parent):
|
|
390
|
+
if found[0]:
|
|
391
|
+
return
|
|
392
|
+
if (
|
|
393
|
+
node.get('type') == 'CallExpression'
|
|
394
|
+
and is_identifier(node.get('callee'))
|
|
395
|
+
and node['callee']['name'] == callee_name
|
|
396
|
+
):
|
|
397
|
+
found[0] = True
|
|
398
|
+
|
|
399
|
+
simple_traverse(func_node, visitor)
|
|
400
|
+
return found[0]
|
|
401
|
+
|
|
402
|
+
def _extract_decoder_offset(self, func_node):
|
|
403
|
+
"""Extract offset from decoder's inner param = param OP EXPR pattern."""
|
|
404
|
+
found_offset = [None]
|
|
405
|
+
|
|
406
|
+
def find_offset(node, parent):
|
|
407
|
+
if found_offset[0] is not None:
|
|
408
|
+
return
|
|
409
|
+
if node.get('type') != 'AssignmentExpression':
|
|
410
|
+
return
|
|
411
|
+
left = node.get('left')
|
|
412
|
+
right = node.get('right')
|
|
413
|
+
if not left or not right:
|
|
414
|
+
return
|
|
415
|
+
if not is_identifier(left):
|
|
416
|
+
return
|
|
417
|
+
if right.get('type') != 'BinaryExpression':
|
|
418
|
+
return
|
|
419
|
+
if not (is_identifier(right.get('left')) and right['left']['name'] == left['name']):
|
|
420
|
+
return
|
|
421
|
+
operator = right.get('operator')
|
|
422
|
+
if operator not in ('+', '-'):
|
|
423
|
+
return
|
|
424
|
+
right_value = _eval_numeric(right.get('right'))
|
|
425
|
+
if right_value is not None:
|
|
426
|
+
found_offset[0] = int(-right_value) if operator == '-' else int(right_value)
|
|
427
|
+
|
|
428
|
+
simple_traverse(func_node, find_offset)
|
|
429
|
+
return found_offset[0] if found_offset[0] is not None else 0
|
|
430
|
+
|
|
431
|
+
def _create_base_decoder(self, string_array, offset, dtype):
|
|
432
|
+
"""Create the appropriate decoder instance."""
|
|
433
|
+
match dtype:
|
|
434
|
+
case DecoderType.RC4:
|
|
435
|
+
return Rc4StringDecoder(string_array, offset)
|
|
436
|
+
case DecoderType.BASE_64:
|
|
437
|
+
return Base64StringDecoder(string_array, offset)
|
|
438
|
+
case _:
|
|
439
|
+
return BasicStringDecoder(string_array, offset)
|
|
440
|
+
|
|
441
|
+
def _find_all_wrappers(self, decoder_name):
|
|
442
|
+
"""Find all wrapper functions throughout the AST that call the decoder.
|
|
443
|
+
|
|
444
|
+
Pattern: function W(p0,..,pN) { return DECODER(p_i OP OFFSET, p_j); }
|
|
445
|
+
"""
|
|
446
|
+
wrappers = {}
|
|
447
|
+
|
|
448
|
+
def visitor(node, parent):
|
|
449
|
+
if node.get('type') == 'FunctionDeclaration':
|
|
450
|
+
info = self._analyze_wrapper(node, decoder_name)
|
|
451
|
+
if info:
|
|
452
|
+
wrappers[info.name] = info
|
|
453
|
+
elif node.get('type') == 'VariableDeclarator':
|
|
454
|
+
init = node.get('init')
|
|
455
|
+
name_node = node.get('id')
|
|
456
|
+
if (
|
|
457
|
+
init
|
|
458
|
+
and init.get('type') in ('FunctionExpression', 'ArrowFunctionExpression')
|
|
459
|
+
and is_identifier(name_node)
|
|
460
|
+
):
|
|
461
|
+
info = self._analyze_wrapper_expr(name_node['name'], init, decoder_name)
|
|
462
|
+
if info:
|
|
463
|
+
wrappers[info.name] = info
|
|
464
|
+
|
|
465
|
+
simple_traverse(self.ast, visitor)
|
|
466
|
+
return wrappers
|
|
467
|
+
|
|
468
|
+
def _analyze_wrapper(self, func_node, decoder_name):
|
|
469
|
+
"""Check if a FunctionDeclaration is a wrapper. Returns WrapperInfo or None."""
|
|
470
|
+
func_name = func_node.get('id', {}).get('name')
|
|
471
|
+
if not func_name:
|
|
472
|
+
return None
|
|
473
|
+
return self._analyze_wrapper_expr(func_name, func_node, decoder_name)
|
|
474
|
+
|
|
475
|
+
def _analyze_wrapper_expr(self, func_name, func_node, decoder_name):
|
|
476
|
+
"""Analyze a function node (declaration or expression) as a potential wrapper."""
|
|
477
|
+
func_body = func_node.get('body', {})
|
|
478
|
+
if func_body.get('type') == 'BlockStatement':
|
|
479
|
+
statements = func_body.get('body', [])
|
|
480
|
+
else:
|
|
481
|
+
return None
|
|
482
|
+
|
|
483
|
+
if len(statements) != 1:
|
|
484
|
+
return None
|
|
485
|
+
|
|
486
|
+
return_statement = statements[0]
|
|
487
|
+
if return_statement.get('type') != 'ReturnStatement':
|
|
488
|
+
return None
|
|
489
|
+
|
|
490
|
+
argument = return_statement.get('argument')
|
|
491
|
+
if not argument or argument.get('type') != 'CallExpression':
|
|
492
|
+
return None
|
|
493
|
+
|
|
494
|
+
callee = argument.get('callee')
|
|
495
|
+
if not (is_identifier(callee) and callee['name'] == decoder_name):
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
call_args = argument.get('arguments', [])
|
|
499
|
+
if not call_args:
|
|
500
|
+
return None
|
|
501
|
+
|
|
502
|
+
params = func_node.get('params', [])
|
|
503
|
+
param_names = [p['name'] for p in params if is_identifier(p)]
|
|
504
|
+
|
|
505
|
+
param_index, wrapper_offset = self._extract_wrapper_offset(call_args[0], param_names)
|
|
506
|
+
if param_index is None:
|
|
507
|
+
return None
|
|
508
|
+
|
|
509
|
+
key_param_index = None
|
|
510
|
+
if len(call_args) > 1:
|
|
511
|
+
second = call_args[1]
|
|
512
|
+
if is_identifier(second) and second['name'] in param_names:
|
|
513
|
+
key_param_index = param_names.index(second['name'])
|
|
514
|
+
|
|
515
|
+
return WrapperInfo(func_name, param_index, wrapper_offset, func_node, key_param_index)
|
|
516
|
+
|
|
517
|
+
def _extract_wrapper_offset(self, expr, param_names):
|
|
518
|
+
"""Extract (param_index, offset) from wrapper's first argument to decoder.
|
|
519
|
+
|
|
520
|
+
Handles: p_N, p_N + LIT, p_N - LIT, p_N - -LIT, p_N + -LIT
|
|
521
|
+
"""
|
|
522
|
+
if is_identifier(expr) and expr['name'] in param_names:
|
|
523
|
+
return param_names.index(expr['name']), 0
|
|
524
|
+
|
|
525
|
+
if expr.get('type') != 'BinaryExpression':
|
|
526
|
+
return None, None
|
|
527
|
+
operator = expr.get('operator')
|
|
528
|
+
if operator not in ('+', '-'):
|
|
529
|
+
return None, None
|
|
530
|
+
|
|
531
|
+
left = expr.get('left')
|
|
532
|
+
if not (is_identifier(left) and left['name'] in param_names):
|
|
533
|
+
return None, None
|
|
534
|
+
|
|
535
|
+
right_value = _eval_numeric(expr.get('right'))
|
|
536
|
+
if right_value is None:
|
|
537
|
+
return None, None
|
|
538
|
+
|
|
539
|
+
param_idx = param_names.index(left['name'])
|
|
540
|
+
offset = int(-right_value) if operator == '-' else int(right_value)
|
|
541
|
+
return param_idx, offset
|
|
542
|
+
|
|
543
|
+
def _remove_decoder_aliases(self, decoder_name, aliases):
|
|
544
|
+
"""Remove variable declarations that are aliases for the decoder.
|
|
545
|
+
|
|
546
|
+
Removes: const _0xABC = _0x22e6; and transitive: const _0xDEF = _0xABC;
|
|
547
|
+
"""
|
|
548
|
+
if not aliases:
|
|
549
|
+
return
|
|
550
|
+
# The set of names to remove includes the decoder and all aliases
|
|
551
|
+
removable_inits = aliases | {decoder_name}
|
|
552
|
+
|
|
553
|
+
def enter(node, parent, key, index):
|
|
554
|
+
if node.get('type') != 'VariableDeclaration':
|
|
555
|
+
return
|
|
556
|
+
decls = node.get('declarations', [])
|
|
557
|
+
i = 0
|
|
558
|
+
while i < len(decls):
|
|
559
|
+
declaration = decls[i]
|
|
560
|
+
name_node = declaration.get('id')
|
|
561
|
+
init = declaration.get('init')
|
|
562
|
+
if (
|
|
563
|
+
is_identifier(name_node)
|
|
564
|
+
and name_node['name'] in aliases
|
|
565
|
+
and init
|
|
566
|
+
and is_identifier(init)
|
|
567
|
+
and init['name'] in removable_inits
|
|
568
|
+
):
|
|
569
|
+
decls.pop(i)
|
|
570
|
+
self.set_changed()
|
|
571
|
+
else:
|
|
572
|
+
i += 1
|
|
573
|
+
if not decls:
|
|
574
|
+
return REMOVE
|
|
575
|
+
|
|
576
|
+
traverse(self.ast, {'enter': enter})
|
|
577
|
+
|
|
578
|
+
def _find_decoder_aliases(self, decoder_name):
|
|
579
|
+
"""Find all variable declarations that are aliases for the decoder.
|
|
580
|
+
|
|
581
|
+
Handles transitive aliases: const a = decoder; const b = a; const c = b;
|
|
582
|
+
Returns a set of all alias names.
|
|
583
|
+
"""
|
|
584
|
+
# First pass: collect all simple assignments (const x = y)
|
|
585
|
+
assignments = {} # name -> init_name
|
|
586
|
+
|
|
587
|
+
def visitor(node, parent):
|
|
588
|
+
if node.get('type') == 'VariableDeclarator':
|
|
589
|
+
init = node.get('init')
|
|
590
|
+
name_node = node.get('id')
|
|
591
|
+
if init and is_identifier(init) and is_identifier(name_node):
|
|
592
|
+
assignments[name_node['name']] = init['name']
|
|
593
|
+
|
|
594
|
+
simple_traverse(self.ast, visitor)
|
|
595
|
+
|
|
596
|
+
# Resolve transitively: follow chains back to decoder_name
|
|
597
|
+
aliases = set()
|
|
598
|
+
for name, init_name in assignments.items():
|
|
599
|
+
# Walk the chain: name -> init_name -> ... -> decoder_name?
|
|
600
|
+
seen = set()
|
|
601
|
+
current = init_name
|
|
602
|
+
while current and current not in seen:
|
|
603
|
+
if current == decoder_name:
|
|
604
|
+
aliases.add(name)
|
|
605
|
+
break
|
|
606
|
+
seen.add(current)
|
|
607
|
+
current = assignments.get(current)
|
|
608
|
+
|
|
609
|
+
return aliases
|
|
610
|
+
|
|
611
|
+
# ---- Rotation ----
|
|
612
|
+
|
|
613
|
+
def _find_and_execute_rotation(
|
|
614
|
+
self,
|
|
615
|
+
body,
|
|
616
|
+
array_func_name,
|
|
617
|
+
string_array,
|
|
618
|
+
decoder,
|
|
619
|
+
wrappers,
|
|
620
|
+
decoder_aliases=None,
|
|
621
|
+
alias_decoder_map=None,
|
|
622
|
+
all_decoders=None,
|
|
623
|
+
):
|
|
624
|
+
"""Find rotation IIFE and execute it.
|
|
625
|
+
|
|
626
|
+
Returns (body_index, rotation_call_expr_or_none) on success, or None.
|
|
627
|
+
When the rotation is inside a SequenceExpression, rotation_call_expr is
|
|
628
|
+
the specific sub-expression to remove (not the whole statement).
|
|
629
|
+
"""
|
|
630
|
+
for i, stmt in enumerate(body):
|
|
631
|
+
if stmt.get('type') != 'ExpressionStatement':
|
|
632
|
+
continue
|
|
633
|
+
expr = stmt.get('expression')
|
|
634
|
+
if not expr:
|
|
635
|
+
continue
|
|
636
|
+
|
|
637
|
+
if expr.get('type') == 'CallExpression':
|
|
638
|
+
if self._try_execute_rotation_call(
|
|
639
|
+
expr,
|
|
640
|
+
array_func_name,
|
|
641
|
+
string_array,
|
|
642
|
+
decoder,
|
|
643
|
+
wrappers,
|
|
644
|
+
decoder_aliases,
|
|
645
|
+
alias_decoder_map=alias_decoder_map,
|
|
646
|
+
all_decoders=all_decoders,
|
|
647
|
+
):
|
|
648
|
+
return (i, None)
|
|
649
|
+
|
|
650
|
+
elif expr.get('type') == 'SequenceExpression':
|
|
651
|
+
for sub in expr.get('expressions', []):
|
|
652
|
+
if sub.get('type') != 'CallExpression':
|
|
653
|
+
continue
|
|
654
|
+
if self._try_execute_rotation_call(
|
|
655
|
+
sub,
|
|
656
|
+
array_func_name,
|
|
657
|
+
string_array,
|
|
658
|
+
decoder,
|
|
659
|
+
wrappers,
|
|
660
|
+
decoder_aliases,
|
|
661
|
+
alias_decoder_map=alias_decoder_map,
|
|
662
|
+
all_decoders=all_decoders,
|
|
663
|
+
):
|
|
664
|
+
return (i, sub)
|
|
665
|
+
|
|
666
|
+
return None
|
|
667
|
+
|
|
668
|
+
def _try_execute_rotation_call(
|
|
669
|
+
self,
|
|
670
|
+
call_expr,
|
|
671
|
+
array_func_name,
|
|
672
|
+
string_array,
|
|
673
|
+
decoder,
|
|
674
|
+
wrappers,
|
|
675
|
+
decoder_aliases,
|
|
676
|
+
alias_decoder_map=None,
|
|
677
|
+
all_decoders=None,
|
|
678
|
+
):
|
|
679
|
+
"""Try to parse and execute a single rotation call expression. Returns True on success."""
|
|
680
|
+
callee = call_expr.get('callee')
|
|
681
|
+
args = call_expr.get('arguments', [])
|
|
682
|
+
|
|
683
|
+
if not callee or callee.get('type') != 'FunctionExpression':
|
|
684
|
+
return False
|
|
685
|
+
if len(args) != 2:
|
|
686
|
+
return False
|
|
687
|
+
if not (is_identifier(args[0]) and args[0]['name'] == array_func_name):
|
|
688
|
+
return False
|
|
689
|
+
|
|
690
|
+
stop_value = _eval_numeric(args[1])
|
|
691
|
+
if stop_value is None:
|
|
692
|
+
return False
|
|
693
|
+
stop_value = int(stop_value)
|
|
694
|
+
|
|
695
|
+
rotation_expr = self._extract_rotation_expression(callee)
|
|
696
|
+
if rotation_expr is None:
|
|
697
|
+
return False
|
|
698
|
+
|
|
699
|
+
# Collect local object literals from the rotation IIFE for argument resolution
|
|
700
|
+
self._rotation_locals = self._collect_rotation_locals(callee)
|
|
701
|
+
|
|
702
|
+
operation = self._parse_rotation_op(rotation_expr, wrappers, decoder_aliases)
|
|
703
|
+
if operation is None:
|
|
704
|
+
return False
|
|
705
|
+
|
|
706
|
+
self._execute_rotation(
|
|
707
|
+
string_array,
|
|
708
|
+
operation,
|
|
709
|
+
wrappers,
|
|
710
|
+
decoder,
|
|
711
|
+
stop_value,
|
|
712
|
+
alias_decoder_map=alias_decoder_map,
|
|
713
|
+
)
|
|
714
|
+
return True
|
|
715
|
+
|
|
716
|
+
@staticmethod
|
|
717
|
+
def _collect_rotation_locals(iife_func):
|
|
718
|
+
"""Collect local object literal assignments from the rotation IIFE.
|
|
719
|
+
|
|
720
|
+
Returns dict: var_name -> {prop_name: value}.
|
|
721
|
+
Handles: var J = {A: 0xb9, S: 0xa7, D: 'M8Y&'};
|
|
722
|
+
"""
|
|
723
|
+
result = {}
|
|
724
|
+
func_body = iife_func.get('body', {}).get('body', [])
|
|
725
|
+
for stmt in func_body:
|
|
726
|
+
if stmt.get('type') != 'VariableDeclaration':
|
|
727
|
+
continue
|
|
728
|
+
for decl in stmt.get('declarations', []):
|
|
729
|
+
name_node = decl.get('id')
|
|
730
|
+
init = decl.get('init')
|
|
731
|
+
if not is_identifier(name_node) or not init or init.get('type') != 'ObjectExpression':
|
|
732
|
+
continue
|
|
733
|
+
obj = {}
|
|
734
|
+
for prop in init.get('properties', []):
|
|
735
|
+
key = prop.get('key')
|
|
736
|
+
value = prop.get('value')
|
|
737
|
+
if not key or not value:
|
|
738
|
+
continue
|
|
739
|
+
if is_identifier(key):
|
|
740
|
+
k = key['name']
|
|
741
|
+
elif is_string_literal(key):
|
|
742
|
+
k = key['value']
|
|
743
|
+
else:
|
|
744
|
+
continue
|
|
745
|
+
num = _eval_numeric(value)
|
|
746
|
+
if num is not None:
|
|
747
|
+
obj[k] = int(num)
|
|
748
|
+
elif is_string_literal(value):
|
|
749
|
+
obj[k] = value['value']
|
|
750
|
+
if obj:
|
|
751
|
+
result[name_node['name']] = obj
|
|
752
|
+
return result
|
|
753
|
+
|
|
754
|
+
def _extract_rotation_expression(self, iife_func):
|
|
755
|
+
"""Extract the arithmetic expression from the try block in the rotation loop."""
|
|
756
|
+
func_body = iife_func.get('body', {}).get('body', [])
|
|
757
|
+
if not func_body:
|
|
758
|
+
return None
|
|
759
|
+
|
|
760
|
+
loop = None
|
|
761
|
+
for stmt in func_body:
|
|
762
|
+
if stmt.get('type') in ('WhileStatement', 'ForStatement'):
|
|
763
|
+
loop = stmt
|
|
764
|
+
|
|
765
|
+
if loop is None:
|
|
766
|
+
return None
|
|
767
|
+
|
|
768
|
+
loop_body = loop.get('body', {})
|
|
769
|
+
stmts = loop_body.get('body', []) if loop_body.get('type') == 'BlockStatement' else [loop_body]
|
|
770
|
+
|
|
771
|
+
for stmt in stmts:
|
|
772
|
+
if stmt.get('type') != 'TryStatement':
|
|
773
|
+
continue
|
|
774
|
+
block = stmt.get('block', {}).get('body', [])
|
|
775
|
+
if not block:
|
|
776
|
+
continue
|
|
777
|
+
result = self._expression_from_try_block(block[0])
|
|
778
|
+
if result is not None:
|
|
779
|
+
return result
|
|
780
|
+
return None
|
|
781
|
+
|
|
782
|
+
@staticmethod
|
|
783
|
+
def _expression_from_try_block(first_statement):
|
|
784
|
+
"""Extract the init/rhs expression from the first statement in a try block."""
|
|
785
|
+
if first_statement.get('type') == 'VariableDeclaration':
|
|
786
|
+
decls = first_statement.get('declarations', [])
|
|
787
|
+
return decls[0].get('init') if decls else None
|
|
788
|
+
if first_statement.get('type') == 'ExpressionStatement':
|
|
789
|
+
expr = first_statement.get('expression')
|
|
790
|
+
if expr and expr.get('type') == 'AssignmentExpression':
|
|
791
|
+
return expr.get('right')
|
|
792
|
+
return None
|
|
793
|
+
|
|
794
|
+
def _parse_rotation_op(self, expr, wrappers, decoder_aliases=None):
|
|
795
|
+
"""Parse a rotation expression into an operation tree."""
|
|
796
|
+
if not isinstance(expr, dict):
|
|
797
|
+
return None
|
|
798
|
+
aliases = decoder_aliases or set()
|
|
799
|
+
|
|
800
|
+
match expr.get('type', ''):
|
|
801
|
+
case 'Literal' if isinstance(expr.get('value'), (int, float)):
|
|
802
|
+
return {'op': 'literal', 'value': expr['value']}
|
|
803
|
+
|
|
804
|
+
case 'UnaryExpression' if expr.get('operator') == '-':
|
|
805
|
+
child = self._parse_rotation_op(expr.get('argument'), wrappers, decoder_aliases)
|
|
806
|
+
return {'op': 'negate', 'child': child} if child else None
|
|
807
|
+
|
|
808
|
+
case 'BinaryExpression' if expr.get('operator') in (
|
|
809
|
+
'+',
|
|
810
|
+
'-',
|
|
811
|
+
'*',
|
|
812
|
+
'/',
|
|
813
|
+
'%',
|
|
814
|
+
):
|
|
815
|
+
left = self._parse_rotation_op(expr.get('left'), wrappers, decoder_aliases)
|
|
816
|
+
right = self._parse_rotation_op(expr.get('right'), wrappers, decoder_aliases)
|
|
817
|
+
if left and right:
|
|
818
|
+
return {
|
|
819
|
+
'op': 'binary',
|
|
820
|
+
'operator': expr['operator'],
|
|
821
|
+
'left': left,
|
|
822
|
+
'right': right,
|
|
823
|
+
}
|
|
824
|
+
return None
|
|
825
|
+
|
|
826
|
+
case 'CallExpression':
|
|
827
|
+
return self._parse_parseInt_call(expr, wrappers, aliases)
|
|
828
|
+
|
|
829
|
+
return None
|
|
830
|
+
|
|
831
|
+
def _parse_parseInt_call(self, expr, wrappers, aliases):
|
|
832
|
+
"""Parse parseInt(wrapperOrDecoder(...)) into an operation node."""
|
|
833
|
+
callee = expr.get('callee')
|
|
834
|
+
args = expr.get('arguments', [])
|
|
835
|
+
if not (is_identifier(callee) and callee['name'] == 'parseInt' and len(args) == 1):
|
|
836
|
+
return None
|
|
837
|
+
inner = args[0]
|
|
838
|
+
if inner.get('type') != 'CallExpression':
|
|
839
|
+
return None
|
|
840
|
+
inner_callee = inner.get('callee')
|
|
841
|
+
if not is_identifier(inner_callee):
|
|
842
|
+
return None
|
|
843
|
+
cname = inner_callee['name']
|
|
844
|
+
arg_values = []
|
|
845
|
+
for a in inner.get('arguments', []):
|
|
846
|
+
resolved = self._resolve_rotation_arg(a)
|
|
847
|
+
if resolved is not None:
|
|
848
|
+
arg_values.append(resolved)
|
|
849
|
+
else:
|
|
850
|
+
return None
|
|
851
|
+
if cname in wrappers:
|
|
852
|
+
return {'op': 'call', 'wrapper_name': cname, 'args': arg_values}
|
|
853
|
+
if cname in aliases:
|
|
854
|
+
return {'op': 'direct_decoder_call', 'alias_name': cname, 'args': arg_values}
|
|
855
|
+
return None
|
|
856
|
+
|
|
857
|
+
def _resolve_rotation_arg(self, arg):
|
|
858
|
+
"""Resolve a rotation call argument to a numeric or string value.
|
|
859
|
+
|
|
860
|
+
Handles literals, string hex, and MemberExpression referencing local objects.
|
|
861
|
+
"""
|
|
862
|
+
val = _eval_numeric(arg)
|
|
863
|
+
if val is not None:
|
|
864
|
+
return int(val)
|
|
865
|
+
if is_string_literal(arg):
|
|
866
|
+
s = arg['value']
|
|
867
|
+
try:
|
|
868
|
+
return int(s, 16) if s.startswith('0x') else int(s)
|
|
869
|
+
except (ValueError, TypeError):
|
|
870
|
+
return s
|
|
871
|
+
# MemberExpression: J.A or J['A']
|
|
872
|
+
if arg.get('type') == 'MemberExpression':
|
|
873
|
+
obj = arg.get('object')
|
|
874
|
+
prop = arg.get('property')
|
|
875
|
+
if is_identifier(obj) and obj['name'] in self._rotation_locals:
|
|
876
|
+
local_obj = self._rotation_locals[obj['name']]
|
|
877
|
+
if not arg.get('computed') and is_identifier(prop):
|
|
878
|
+
return local_obj.get(prop['name'])
|
|
879
|
+
elif is_string_literal(prop):
|
|
880
|
+
return local_obj.get(prop['value'])
|
|
881
|
+
return None
|
|
882
|
+
|
|
883
|
+
def _decode_and_parse_int(self, decoder, idx, key=None):
|
|
884
|
+
"""Decode a string and parse it as an integer. Raises on failure."""
|
|
885
|
+
decoded = decoder.get_string(int(idx), key) if key is not None else decoder.get_string(int(idx))
|
|
886
|
+
if decoded is None:
|
|
887
|
+
raise ValueError('Decoder returned None')
|
|
888
|
+
result = _js_parse_int(decoded)
|
|
889
|
+
if math.isnan(result):
|
|
890
|
+
raise ValueError('NaN from parseInt')
|
|
891
|
+
return result
|
|
892
|
+
|
|
893
|
+
def _apply_rotation_op(self, operation, wrappers, decoder, alias_decoder_map=None):
|
|
894
|
+
"""Evaluate a parsed rotation operation tree."""
|
|
895
|
+
match operation['op']:
|
|
896
|
+
case 'literal':
|
|
897
|
+
return operation['value']
|
|
898
|
+
case 'negate':
|
|
899
|
+
return -self._apply_rotation_op(operation['child'], wrappers, decoder, alias_decoder_map)
|
|
900
|
+
case 'binary':
|
|
901
|
+
left = self._apply_rotation_op(operation['left'], wrappers, decoder, alias_decoder_map)
|
|
902
|
+
right = self._apply_rotation_op(operation['right'], wrappers, decoder, alias_decoder_map)
|
|
903
|
+
return _apply_arith(operation['operator'], left, right)
|
|
904
|
+
case 'call':
|
|
905
|
+
wrapper = wrappers[operation['wrapper_name']]
|
|
906
|
+
call_args = operation['args']
|
|
907
|
+
effective_idx = wrapper.get_effective_index(call_args)
|
|
908
|
+
if effective_idx is None:
|
|
909
|
+
raise ValueError('Invalid wrapper args')
|
|
910
|
+
return self._decode_and_parse_int(decoder, effective_idx, wrapper.get_key(call_args))
|
|
911
|
+
case 'direct_decoder_call':
|
|
912
|
+
call_args = operation['args']
|
|
913
|
+
if not call_args:
|
|
914
|
+
raise ValueError('No args for direct decoder call')
|
|
915
|
+
key = call_args[1] if len(call_args) > 1 else None
|
|
916
|
+
# Use alias-specific decoder if available
|
|
917
|
+
alias_name = operation.get('alias_name')
|
|
918
|
+
target_decoder = decoder
|
|
919
|
+
if alias_name and alias_decoder_map and alias_name in alias_decoder_map:
|
|
920
|
+
target_decoder = alias_decoder_map[alias_name]
|
|
921
|
+
return self._decode_and_parse_int(target_decoder, int(call_args[0]), key)
|
|
922
|
+
case _:
|
|
923
|
+
raise ValueError(f'Unknown op: {operation["op"]}')
|
|
924
|
+
|
|
925
|
+
def _execute_rotation(self, string_array, operation, wrappers, decoder, stop_value, alias_decoder_map=None):
|
|
926
|
+
"""Rotate array until the expression evaluates to stop_value."""
|
|
927
|
+
# Collect all decoders that need cache clearing on each rotation
|
|
928
|
+
all_decoders = set()
|
|
929
|
+
all_decoders.add(decoder)
|
|
930
|
+
if alias_decoder_map:
|
|
931
|
+
all_decoders.update(alias_decoder_map.values())
|
|
932
|
+
|
|
933
|
+
for _ in range(100001):
|
|
934
|
+
try:
|
|
935
|
+
value = self._apply_rotation_op(operation, wrappers, decoder, alias_decoder_map)
|
|
936
|
+
if int(value) == stop_value:
|
|
937
|
+
return True
|
|
938
|
+
string_array.append(string_array.pop(0))
|
|
939
|
+
except Exception:
|
|
940
|
+
string_array.append(string_array.pop(0))
|
|
941
|
+
# Clear decoder caches after rotation since array contents shifted
|
|
942
|
+
for d in all_decoders:
|
|
943
|
+
if hasattr(d, '_cache'):
|
|
944
|
+
d._cache.clear()
|
|
945
|
+
return False
|
|
946
|
+
|
|
947
|
+
# ---- Replacement ----
|
|
948
|
+
|
|
949
|
+
def _replace_all_wrapper_calls(self, wrappers, decoder, obj_literals=None):
|
|
950
|
+
"""Replace all calls to wrapper functions with decoded string literals."""
|
|
951
|
+
if not wrappers:
|
|
952
|
+
return True
|
|
953
|
+
all_replaced = [True]
|
|
954
|
+
_obj_literals = obj_literals or {}
|
|
955
|
+
|
|
956
|
+
def enter(node, parent, key, index):
|
|
957
|
+
if node.get('type') != 'CallExpression':
|
|
958
|
+
return
|
|
959
|
+
callee = node.get('callee')
|
|
960
|
+
if not is_identifier(callee):
|
|
961
|
+
return
|
|
962
|
+
if callee['name'] not in wrappers:
|
|
963
|
+
return
|
|
964
|
+
|
|
965
|
+
wrapper = wrappers[callee['name']]
|
|
966
|
+
call_args = node.get('arguments', [])
|
|
967
|
+
|
|
968
|
+
# Only need the active param (index) and optionally the key param
|
|
969
|
+
if wrapper.param_index >= len(call_args):
|
|
970
|
+
all_replaced[0] = False
|
|
971
|
+
return
|
|
972
|
+
|
|
973
|
+
index_value = _resolve_arg_value(call_args[wrapper.param_index], _obj_literals)
|
|
974
|
+
if index_value is None:
|
|
975
|
+
all_replaced[0] = False
|
|
976
|
+
return
|
|
977
|
+
|
|
978
|
+
effective_idx = int(index_value) + wrapper.wrapper_offset
|
|
979
|
+
|
|
980
|
+
key = None
|
|
981
|
+
if wrapper.key_param_index is not None and wrapper.key_param_index < len(call_args):
|
|
982
|
+
key = _resolve_string_arg(call_args[wrapper.key_param_index], _obj_literals)
|
|
983
|
+
|
|
984
|
+
try:
|
|
985
|
+
decoded = (
|
|
986
|
+
decoder.get_string(int(effective_idx), key)
|
|
987
|
+
if key is not None
|
|
988
|
+
else decoder.get_string(int(effective_idx))
|
|
989
|
+
)
|
|
990
|
+
if isinstance(decoded, str):
|
|
991
|
+
self.set_changed()
|
|
992
|
+
return make_literal(decoded)
|
|
993
|
+
all_replaced[0] = False
|
|
994
|
+
except Exception:
|
|
995
|
+
all_replaced[0] = False
|
|
996
|
+
|
|
997
|
+
traverse(self.ast, {'enter': enter})
|
|
998
|
+
return all_replaced[0]
|
|
999
|
+
|
|
1000
|
+
def _replace_direct_decoder_calls(self, decoder_name, decoder, decoder_aliases=None, obj_literals=None):
|
|
1001
|
+
"""Replace direct calls to the decoder function (and its aliases) with literals."""
|
|
1002
|
+
names = {decoder_name}
|
|
1003
|
+
if decoder_aliases:
|
|
1004
|
+
names.update(decoder_aliases)
|
|
1005
|
+
_obj_literals = obj_literals or {}
|
|
1006
|
+
|
|
1007
|
+
def enter(node, parent, key, index):
|
|
1008
|
+
if node.get('type') != 'CallExpression':
|
|
1009
|
+
return
|
|
1010
|
+
callee = node.get('callee')
|
|
1011
|
+
if not (is_identifier(callee) and callee['name'] in names):
|
|
1012
|
+
return
|
|
1013
|
+
args = node.get('arguments', [])
|
|
1014
|
+
if not args:
|
|
1015
|
+
return
|
|
1016
|
+
|
|
1017
|
+
first_val = _resolve_arg_value(args[0], _obj_literals)
|
|
1018
|
+
if first_val is None:
|
|
1019
|
+
return
|
|
1020
|
+
|
|
1021
|
+
key = None
|
|
1022
|
+
if len(args) > 1:
|
|
1023
|
+
key = _resolve_string_arg(args[1], _obj_literals)
|
|
1024
|
+
|
|
1025
|
+
try:
|
|
1026
|
+
decoded = (
|
|
1027
|
+
decoder.get_string(int(first_val), key) if key is not None else decoder.get_string(int(first_val))
|
|
1028
|
+
)
|
|
1029
|
+
if isinstance(decoded, str):
|
|
1030
|
+
self.set_changed()
|
|
1031
|
+
return make_literal(decoded)
|
|
1032
|
+
except Exception:
|
|
1033
|
+
pass
|
|
1034
|
+
|
|
1035
|
+
traverse(self.ast, {'enter': enter})
|
|
1036
|
+
|
|
1037
|
+
@staticmethod
|
|
1038
|
+
def _find_array_expression_in_statement(stmt):
|
|
1039
|
+
"""Find the first ArrayExpression node in a variable declaration or assignment."""
|
|
1040
|
+
if stmt.get('type') == 'VariableDeclaration':
|
|
1041
|
+
for declaration in stmt.get('declarations', []):
|
|
1042
|
+
init = declaration.get('init')
|
|
1043
|
+
if init and init.get('type') == 'ArrayExpression':
|
|
1044
|
+
return init
|
|
1045
|
+
elif stmt.get('type') == 'ExpressionStatement':
|
|
1046
|
+
expr = stmt.get('expression')
|
|
1047
|
+
if expr and expr.get('type') == 'AssignmentExpression':
|
|
1048
|
+
right = expr.get('right')
|
|
1049
|
+
if right and right.get('type') == 'ArrayExpression':
|
|
1050
|
+
return right
|
|
1051
|
+
return None
|
|
1052
|
+
|
|
1053
|
+
def _update_ast_array(self, func_node, rotated_array):
|
|
1054
|
+
"""Update the AST's array function to contain the rotated string array."""
|
|
1055
|
+
func_body = func_node.get('body', {}).get('body', [])
|
|
1056
|
+
if not func_body:
|
|
1057
|
+
return
|
|
1058
|
+
arr_expr = self._find_array_expression_in_statement(func_body[0])
|
|
1059
|
+
if arr_expr is not None:
|
|
1060
|
+
arr_expr['elements'] = [make_literal(s) for s in rotated_array]
|
|
1061
|
+
|
|
1062
|
+
def _remove_body_indices(self, body, *indices):
|
|
1063
|
+
"""Remove statements at given indices from body."""
|
|
1064
|
+
for idx in sorted(set(i for i in indices if i is not None), reverse=True):
|
|
1065
|
+
if 0 <= idx < len(body):
|
|
1066
|
+
body.pop(idx)
|
|
1067
|
+
self.set_changed()
|
|
1068
|
+
|
|
1069
|
+
# ================================================================
|
|
1070
|
+
# Strategy 2b: Var-based string array + rotation IIFE + decoder
|
|
1071
|
+
# Pattern:
|
|
1072
|
+
# var _0xARR = ['s1', 's2', ...];
|
|
1073
|
+
# (function(arr, count) { var f = function(n) { while(--n) arr.push(arr.shift()); }; f(++count); })(_0xARR, 0xNN);
|
|
1074
|
+
# var _0xDEC = function(a, b) { a = a - OFFSET; var x = _0xARR[a]; return x; };
|
|
1075
|
+
# ================================================================
|
|
1076
|
+
|
|
1077
|
+
def _process_var_array_pattern(self):
|
|
1078
|
+
"""Handle var-based string array with simple rotation and decoder."""
|
|
1079
|
+
body = self.ast.get('body', [])
|
|
1080
|
+
if len(body) < 3:
|
|
1081
|
+
return
|
|
1082
|
+
|
|
1083
|
+
# Step 1: Find var _0x... = [string array] at top level
|
|
1084
|
+
array_name, string_array, array_idx = self._find_var_string_array(body)
|
|
1085
|
+
if array_name is None:
|
|
1086
|
+
return
|
|
1087
|
+
|
|
1088
|
+
# Step 2: Find rotation IIFE that references the array var
|
|
1089
|
+
rotation_idx, rotation_count = self._find_simple_rotation(body, array_name)
|
|
1090
|
+
|
|
1091
|
+
# Step 3: Execute rotation if found
|
|
1092
|
+
if rotation_idx is not None and rotation_count is not None:
|
|
1093
|
+
for _ in range(rotation_count):
|
|
1094
|
+
string_array.append(string_array.pop(0))
|
|
1095
|
+
|
|
1096
|
+
# Step 4: Find decoder function (var _0x... = function(a,b) { a=a-OFFSET; ... _0xARR[a] ... })
|
|
1097
|
+
decoder_name, decoder_offset, decoder_idx = self._find_var_decoder(body, array_name)
|
|
1098
|
+
if decoder_name is None:
|
|
1099
|
+
return
|
|
1100
|
+
|
|
1101
|
+
# Step 5: Create decoder and find wrappers
|
|
1102
|
+
decoder = BasicStringDecoder(string_array, decoder_offset)
|
|
1103
|
+
wrappers = self._find_all_wrappers(decoder_name)
|
|
1104
|
+
decoder_aliases = self._find_decoder_aliases(decoder_name)
|
|
1105
|
+
object_literals = _collect_object_literals(self.ast)
|
|
1106
|
+
|
|
1107
|
+
# Step 6: Replace wrapper calls
|
|
1108
|
+
self._replace_all_wrapper_calls(wrappers, decoder, object_literals)
|
|
1109
|
+
|
|
1110
|
+
# Step 7: Replace direct decoder calls (including aliases)
|
|
1111
|
+
self._replace_direct_decoder_calls(decoder_name, decoder, decoder_aliases, object_literals)
|
|
1112
|
+
|
|
1113
|
+
# Step 8: Remove aliases
|
|
1114
|
+
self._remove_decoder_aliases(decoder_name, decoder_aliases)
|
|
1115
|
+
|
|
1116
|
+
# Step 9: Remove infrastructure (array, rotation, decoder)
|
|
1117
|
+
if self.has_changed():
|
|
1118
|
+
indices_to_remove = {array_idx, decoder_idx}
|
|
1119
|
+
if rotation_idx is not None:
|
|
1120
|
+
indices_to_remove.add(rotation_idx)
|
|
1121
|
+
self._remove_body_indices(body, *indices_to_remove)
|
|
1122
|
+
|
|
1123
|
+
def _find_var_string_array(self, body):
|
|
1124
|
+
"""Find var _0x... = ['s1', 's2', ...] at top of body."""
|
|
1125
|
+
for i, stmt in enumerate(body[:3]):
|
|
1126
|
+
if stmt.get('type') != 'VariableDeclaration':
|
|
1127
|
+
continue
|
|
1128
|
+
for declaration in stmt.get('declarations', []):
|
|
1129
|
+
name_node = declaration.get('id')
|
|
1130
|
+
if not is_identifier(name_node):
|
|
1131
|
+
continue
|
|
1132
|
+
init = declaration.get('init')
|
|
1133
|
+
if not init or init.get('type') != 'ArrayExpression':
|
|
1134
|
+
continue
|
|
1135
|
+
elements = init.get('elements', [])
|
|
1136
|
+
if len(elements) < 3:
|
|
1137
|
+
continue
|
|
1138
|
+
if not all(is_string_literal(e) for e in elements):
|
|
1139
|
+
continue
|
|
1140
|
+
return name_node['name'], [e['value'] for e in elements], i
|
|
1141
|
+
return None, None, None
|
|
1142
|
+
|
|
1143
|
+
def _find_simple_rotation(self, body, array_name):
|
|
1144
|
+
"""Find (function(arr, count) { ...push/shift... })(array, N) rotation IIFE."""
|
|
1145
|
+
for i, stmt in enumerate(body):
|
|
1146
|
+
if stmt.get('type') != 'ExpressionStatement':
|
|
1147
|
+
continue
|
|
1148
|
+
expr = stmt.get('expression')
|
|
1149
|
+
if not expr:
|
|
1150
|
+
continue
|
|
1151
|
+
|
|
1152
|
+
candidates = []
|
|
1153
|
+
if expr.get('type') == 'CallExpression':
|
|
1154
|
+
candidates.append(expr)
|
|
1155
|
+
elif expr.get('type') == 'SequenceExpression':
|
|
1156
|
+
candidates.extend(sub for sub in expr.get('expressions', []) if sub.get('type') == 'CallExpression')
|
|
1157
|
+
|
|
1158
|
+
for call_expr in candidates:
|
|
1159
|
+
callee = call_expr.get('callee')
|
|
1160
|
+
args = call_expr.get('arguments', [])
|
|
1161
|
+
if not callee or callee.get('type') != 'FunctionExpression':
|
|
1162
|
+
continue
|
|
1163
|
+
if len(args) != 2:
|
|
1164
|
+
continue
|
|
1165
|
+
if not (is_identifier(args[0]) and args[0]['name'] == array_name):
|
|
1166
|
+
continue
|
|
1167
|
+
|
|
1168
|
+
count_val = _eval_numeric(args[1])
|
|
1169
|
+
if count_val is None:
|
|
1170
|
+
continue
|
|
1171
|
+
|
|
1172
|
+
src = generate(callee)
|
|
1173
|
+
if 'push' in src and 'shift' in src:
|
|
1174
|
+
return i, int(count_val)
|
|
1175
|
+
|
|
1176
|
+
return None, None
|
|
1177
|
+
|
|
1178
|
+
def _find_var_decoder(self, body, array_name):
|
|
1179
|
+
"""Find var _0xDEC = function(a) { a = a - OFFSET; var x = ARR[a]; return x; }."""
|
|
1180
|
+
for i, stmt in enumerate(body):
|
|
1181
|
+
if stmt.get('type') != 'VariableDeclaration':
|
|
1182
|
+
continue
|
|
1183
|
+
for declaration in stmt.get('declarations', []):
|
|
1184
|
+
name_node = declaration.get('id')
|
|
1185
|
+
if not is_identifier(name_node):
|
|
1186
|
+
continue
|
|
1187
|
+
init = declaration.get('init')
|
|
1188
|
+
if not init or init.get('type') != 'FunctionExpression':
|
|
1189
|
+
continue
|
|
1190
|
+
source = generate(init)
|
|
1191
|
+
if array_name not in source:
|
|
1192
|
+
continue
|
|
1193
|
+
offset = self._extract_decoder_offset(init)
|
|
1194
|
+
return name_node['name'], offset, i
|
|
1195
|
+
return None, None, None
|
|
1196
|
+
|
|
1197
|
+
# ================================================================
|
|
1198
|
+
# Strategy 1: Direct string array declarations
|
|
1199
|
+
# ================================================================
|
|
1200
|
+
|
|
1201
|
+
def _try_replace_array_access(self, ref_parent, ref_key, string_array):
|
|
1202
|
+
"""Replace arr[N] member expression with the string literal if valid."""
|
|
1203
|
+
if not ref_parent or ref_parent.get('type') != 'MemberExpression':
|
|
1204
|
+
return
|
|
1205
|
+
if ref_key != 'object' or not ref_parent.get('computed'):
|
|
1206
|
+
return
|
|
1207
|
+
prop = ref_parent.get('property')
|
|
1208
|
+
if not is_numeric_literal(prop):
|
|
1209
|
+
return
|
|
1210
|
+
idx = int(prop['value'])
|
|
1211
|
+
if not (0 <= idx < len(string_array)):
|
|
1212
|
+
return
|
|
1213
|
+
self._replace_node_in_ast(ref_parent, make_literal(string_array[idx]))
|
|
1214
|
+
self.set_changed()
|
|
1215
|
+
|
|
1216
|
+
def _process_direct_arrays(self, scope_tree):
|
|
1217
|
+
"""Find direct array declarations and replace indexed accesses."""
|
|
1218
|
+
for name, binding in list(scope_tree.bindings.items()):
|
|
1219
|
+
node = binding.node
|
|
1220
|
+
if not isinstance(node, dict) or node.get('type') != 'VariableDeclarator':
|
|
1221
|
+
continue
|
|
1222
|
+
init = node.get('init')
|
|
1223
|
+
if not init or init.get('type') != 'ArrayExpression':
|
|
1224
|
+
continue
|
|
1225
|
+
elements = init.get('elements', [])
|
|
1226
|
+
if not elements or not all(is_string_literal(e) for e in elements):
|
|
1227
|
+
continue
|
|
1228
|
+
|
|
1229
|
+
string_array = [e['value'] for e in elements]
|
|
1230
|
+
for reference_node, reference_parent, reference_key, ref_index in binding.references[:]:
|
|
1231
|
+
self._try_replace_array_access(reference_parent, reference_key, string_array)
|
|
1232
|
+
for child in scope_tree.children:
|
|
1233
|
+
self._process_direct_arrays_in_scope(child, name, string_array)
|
|
1234
|
+
|
|
1235
|
+
def _process_direct_arrays_in_scope(self, scope, name, string_array):
|
|
1236
|
+
"""Process direct array accesses in child scopes."""
|
|
1237
|
+
binding = scope.get_binding(name)
|
|
1238
|
+
if not binding:
|
|
1239
|
+
return
|
|
1240
|
+
for reference_node, reference_parent, reference_key, ref_index in binding.references[:]:
|
|
1241
|
+
self._try_replace_array_access(reference_parent, reference_key, string_array)
|
|
1242
|
+
|
|
1243
|
+
def _replace_node_in_ast(self, target, replacement):
|
|
1244
|
+
"""Replace a node in the AST with a replacement."""
|
|
1245
|
+
result = find_parent(self.ast, target)
|
|
1246
|
+
if result:
|
|
1247
|
+
parent, key, index = result
|
|
1248
|
+
if index is not None:
|
|
1249
|
+
parent[key][index] = replacement
|
|
1250
|
+
else:
|
|
1251
|
+
parent[key] = replacement
|
|
1252
|
+
|
|
1253
|
+
# ================================================================
|
|
1254
|
+
# Strategy 3: Simple static array unpacking
|
|
1255
|
+
# ================================================================
|
|
1256
|
+
|
|
1257
|
+
def _process_static_arrays(self):
|
|
1258
|
+
"""No-op: static array unpacking is handled by _process_direct_arrays."""
|
|
1259
|
+
pass
|