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,403 @@
|
|
|
1
|
+
"""Evaluate static unary/binary expressions to literals."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
from ..traverser import traverse
|
|
6
|
+
from ..utils.ast_helpers import is_literal
|
|
7
|
+
from ..utils.ast_helpers import make_literal
|
|
8
|
+
from .base import Transform
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Sentinel to distinguish JS null (Literal value=None) from JS undefined
|
|
12
|
+
# (Identifier name='undefined'). Python None represents undefined.
|
|
13
|
+
_JS_NULL = object()
|
|
14
|
+
|
|
15
|
+
_RESOLVABLE_UNARY = {'-', '+', '!', '~', 'typeof', 'void'}
|
|
16
|
+
_RESOLVABLE_BINARY = {
|
|
17
|
+
'==',
|
|
18
|
+
'!=',
|
|
19
|
+
'===',
|
|
20
|
+
'!==',
|
|
21
|
+
'<',
|
|
22
|
+
'<=',
|
|
23
|
+
'>',
|
|
24
|
+
'>=',
|
|
25
|
+
'<<',
|
|
26
|
+
'>>',
|
|
27
|
+
'>>>',
|
|
28
|
+
'+',
|
|
29
|
+
'-',
|
|
30
|
+
'*',
|
|
31
|
+
'/',
|
|
32
|
+
'%',
|
|
33
|
+
'**',
|
|
34
|
+
'|',
|
|
35
|
+
'^',
|
|
36
|
+
'&',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ExpressionSimplifier(Transform):
|
|
41
|
+
"""Simplify constant unary/binary expressions to literals."""
|
|
42
|
+
|
|
43
|
+
def execute(self):
|
|
44
|
+
self._simplify_unary_binary()
|
|
45
|
+
self._simplify_conditionals()
|
|
46
|
+
self._simplify_awaits()
|
|
47
|
+
return self.has_changed()
|
|
48
|
+
|
|
49
|
+
def _simplify_unary_binary(self):
|
|
50
|
+
"""Fold constant unary and binary expressions."""
|
|
51
|
+
|
|
52
|
+
def enter(node, parent, key, index):
|
|
53
|
+
match node.get('type', ''):
|
|
54
|
+
case 'UnaryExpression':
|
|
55
|
+
result = self._simplify_unary(node)
|
|
56
|
+
case 'BinaryExpression':
|
|
57
|
+
result = self._simplify_binary(node)
|
|
58
|
+
case _:
|
|
59
|
+
return
|
|
60
|
+
if result is not None:
|
|
61
|
+
self.set_changed()
|
|
62
|
+
return result
|
|
63
|
+
|
|
64
|
+
traverse(self.ast, {'enter': enter})
|
|
65
|
+
|
|
66
|
+
def _simplify_conditionals(self):
|
|
67
|
+
"""Convert test ? false : true → !test."""
|
|
68
|
+
|
|
69
|
+
def enter(node, parent, key, index):
|
|
70
|
+
if node.get('type') != 'ConditionalExpression':
|
|
71
|
+
return
|
|
72
|
+
cons = node.get('consequent')
|
|
73
|
+
alt = node.get('alternate')
|
|
74
|
+
if is_literal(cons) and cons.get('value') is False and is_literal(alt) and alt.get('value') is True:
|
|
75
|
+
self.set_changed()
|
|
76
|
+
return {
|
|
77
|
+
'type': 'UnaryExpression',
|
|
78
|
+
'operator': '!',
|
|
79
|
+
'prefix': True,
|
|
80
|
+
'argument': node['test'],
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
traverse(self.ast, {'enter': enter})
|
|
84
|
+
|
|
85
|
+
def _simplify_awaits(self):
|
|
86
|
+
"""Simplify await (0x0, expr) → await expr."""
|
|
87
|
+
|
|
88
|
+
def enter(node, parent, key, index):
|
|
89
|
+
if node.get('type') != 'AwaitExpression':
|
|
90
|
+
return
|
|
91
|
+
arg = node.get('argument')
|
|
92
|
+
if not isinstance(arg, dict) or arg.get('type') != 'SequenceExpression':
|
|
93
|
+
return
|
|
94
|
+
exprs = arg.get('expressions', [])
|
|
95
|
+
if len(exprs) <= 1:
|
|
96
|
+
return
|
|
97
|
+
node['argument'] = exprs[-1]
|
|
98
|
+
self.set_changed()
|
|
99
|
+
|
|
100
|
+
traverse(self.ast, {'enter': enter})
|
|
101
|
+
|
|
102
|
+
def _simplify_unary(self, node):
|
|
103
|
+
operator = node.get('operator', '')
|
|
104
|
+
if operator not in _RESOLVABLE_UNARY:
|
|
105
|
+
return None
|
|
106
|
+
# Skip negative numeric literals (already in normal form)
|
|
107
|
+
if (
|
|
108
|
+
operator == '-'
|
|
109
|
+
and is_literal(node.get('argument'))
|
|
110
|
+
and isinstance(node['argument'].get('value'), (int, float))
|
|
111
|
+
):
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
argument = node.get('argument')
|
|
115
|
+
argument = self._simplify_expr(argument)
|
|
116
|
+
value, ok = self._get_resolvable_value(argument)
|
|
117
|
+
if not ok:
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
result = self._apply_unary(operator, value)
|
|
122
|
+
except Exception:
|
|
123
|
+
return None
|
|
124
|
+
return self._value_to_node(result)
|
|
125
|
+
|
|
126
|
+
def _simplify_binary(self, node):
|
|
127
|
+
operator = node.get('operator', '')
|
|
128
|
+
if operator not in _RESOLVABLE_BINARY:
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
left = self._simplify_expr(node.get('left'))
|
|
132
|
+
right = self._simplify_expr(node.get('right'))
|
|
133
|
+
|
|
134
|
+
left_value, left_resolved = self._get_resolvable_value(left)
|
|
135
|
+
right_value, right_resolved = self._get_resolvable_value(right)
|
|
136
|
+
|
|
137
|
+
if not (left_resolved and right_resolved):
|
|
138
|
+
# Convert x - (-y) to x + y
|
|
139
|
+
if operator == '-' and self._is_negative_numeric(right):
|
|
140
|
+
node['right'] = right['argument']
|
|
141
|
+
node['operator'] = '+'
|
|
142
|
+
return node
|
|
143
|
+
return None
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
result = self._apply_binary(operator, left_value, right_value)
|
|
147
|
+
except Exception:
|
|
148
|
+
return None
|
|
149
|
+
return self._value_to_node(result)
|
|
150
|
+
|
|
151
|
+
def _simplify_expr(self, node):
|
|
152
|
+
if not isinstance(node, dict):
|
|
153
|
+
return node
|
|
154
|
+
match node.get('type', ''):
|
|
155
|
+
case 'UnaryExpression':
|
|
156
|
+
result = self._simplify_unary(node)
|
|
157
|
+
return result if result is not None else node
|
|
158
|
+
case 'BinaryExpression':
|
|
159
|
+
result = self._simplify_binary(node)
|
|
160
|
+
return result if result is not None else node
|
|
161
|
+
return node
|
|
162
|
+
|
|
163
|
+
def _is_negative_numeric(self, node):
|
|
164
|
+
return (
|
|
165
|
+
isinstance(node, dict)
|
|
166
|
+
and node.get('type') == 'UnaryExpression'
|
|
167
|
+
and node.get('operator') == '-'
|
|
168
|
+
and is_literal(node.get('argument'))
|
|
169
|
+
and isinstance(node['argument'].get('value'), (int, float))
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def _get_resolvable_value(self, node):
|
|
173
|
+
if not isinstance(node, dict):
|
|
174
|
+
return None, False
|
|
175
|
+
match node.get('type', ''):
|
|
176
|
+
case 'Literal':
|
|
177
|
+
if node.get('regex'):
|
|
178
|
+
return None, False
|
|
179
|
+
value = node.get('value')
|
|
180
|
+
# Literal with value None is JS null, not undefined
|
|
181
|
+
return (_JS_NULL if value is None else value), True
|
|
182
|
+
case 'UnaryExpression' if node.get('operator') == '-':
|
|
183
|
+
arg = node.get('argument')
|
|
184
|
+
if is_literal(arg) and isinstance(arg.get('value'), (int, float)):
|
|
185
|
+
return -arg['value'], True
|
|
186
|
+
case 'Identifier' if node.get('name') == 'undefined':
|
|
187
|
+
return None, True
|
|
188
|
+
case 'ArrayExpression' if len(node.get('elements', [])) == 0:
|
|
189
|
+
return [], True
|
|
190
|
+
case 'ObjectExpression' if len(node.get('properties', [])) == 0:
|
|
191
|
+
return {}, True
|
|
192
|
+
return None, False
|
|
193
|
+
|
|
194
|
+
def _apply_unary(self, operator, value):
|
|
195
|
+
match operator:
|
|
196
|
+
case '-':
|
|
197
|
+
return -self._js_to_number(value)
|
|
198
|
+
case '+':
|
|
199
|
+
return self._js_to_number(value)
|
|
200
|
+
case '!':
|
|
201
|
+
return not self._js_truthy(value)
|
|
202
|
+
case '~':
|
|
203
|
+
n = self._js_to_number(value)
|
|
204
|
+
if isinstance(n, float) and math.isnan(n):
|
|
205
|
+
return -1 # ~NaN → -1
|
|
206
|
+
return ~int(n)
|
|
207
|
+
case 'typeof':
|
|
208
|
+
return self._js_typeof(value)
|
|
209
|
+
case 'void':
|
|
210
|
+
return None # JS undefined
|
|
211
|
+
|
|
212
|
+
def _apply_binary(self, operator, left, right):
|
|
213
|
+
match operator:
|
|
214
|
+
case '+':
|
|
215
|
+
if isinstance(left, str) or isinstance(right, str):
|
|
216
|
+
return self._js_to_string(left) + self._js_to_string(right)
|
|
217
|
+
return self._js_to_number(left) + self._js_to_number(right)
|
|
218
|
+
case '-':
|
|
219
|
+
return self._js_to_number(left) - self._js_to_number(right)
|
|
220
|
+
case '*':
|
|
221
|
+
return self._js_to_number(left) * self._js_to_number(right)
|
|
222
|
+
case '/':
|
|
223
|
+
result = self._js_to_number(right)
|
|
224
|
+
if result == 0:
|
|
225
|
+
raise ValueError('division by zero')
|
|
226
|
+
return self._js_to_number(left) / result
|
|
227
|
+
case '%':
|
|
228
|
+
result = self._js_to_number(right)
|
|
229
|
+
if result == 0:
|
|
230
|
+
raise ValueError('mod by zero')
|
|
231
|
+
return self._js_to_number(left) % result
|
|
232
|
+
case '**':
|
|
233
|
+
return self._js_to_number(left) ** self._js_to_number(right)
|
|
234
|
+
case '|':
|
|
235
|
+
return self._js_to_int32(left) | self._js_to_int32(right)
|
|
236
|
+
case '&':
|
|
237
|
+
return self._js_to_int32(left) & self._js_to_int32(right)
|
|
238
|
+
case '^':
|
|
239
|
+
return self._js_to_int32(left) ^ self._js_to_int32(right)
|
|
240
|
+
case '<<':
|
|
241
|
+
return self._js_to_int32(left) << (self._js_to_int32(right) & 31)
|
|
242
|
+
case '>>':
|
|
243
|
+
return self._js_to_int32(left) >> (self._js_to_int32(right) & 31)
|
|
244
|
+
case '>>>':
|
|
245
|
+
left_operand = self._js_to_int32(left) & 0xFFFFFFFF
|
|
246
|
+
result = self._js_to_int32(right) & 31
|
|
247
|
+
return left_operand >> result
|
|
248
|
+
case '==' | '!=':
|
|
249
|
+
eq = self._js_abstract_eq(left, right)
|
|
250
|
+
return eq if operator == '==' else not eq
|
|
251
|
+
case '===' | '!==':
|
|
252
|
+
eq = self._js_strict_eq(left, right)
|
|
253
|
+
return eq if operator == '===' else not eq
|
|
254
|
+
case '<':
|
|
255
|
+
return self._js_compare(left, right) < 0
|
|
256
|
+
case '<=':
|
|
257
|
+
return self._js_compare(left, right) <= 0
|
|
258
|
+
case '>':
|
|
259
|
+
return self._js_compare(left, right) > 0
|
|
260
|
+
case '>=':
|
|
261
|
+
return self._js_compare(left, right) >= 0
|
|
262
|
+
case _:
|
|
263
|
+
raise ValueError(f'Unknown operator: {operator}')
|
|
264
|
+
|
|
265
|
+
def _js_abstract_eq(self, left, right):
|
|
266
|
+
"""JS == (null and undefined are equal to each other only)."""
|
|
267
|
+
if (left is None or left is _JS_NULL) and (right is None or right is _JS_NULL):
|
|
268
|
+
return True
|
|
269
|
+
if left is _JS_NULL or right is _JS_NULL:
|
|
270
|
+
return False
|
|
271
|
+
return left == right
|
|
272
|
+
|
|
273
|
+
def _js_strict_eq(self, left, right):
|
|
274
|
+
"""JS === (null !== undefined)."""
|
|
275
|
+
if left is _JS_NULL:
|
|
276
|
+
return right is _JS_NULL
|
|
277
|
+
if right is _JS_NULL:
|
|
278
|
+
return False
|
|
279
|
+
return left == right and type(left) == type(right)
|
|
280
|
+
|
|
281
|
+
def _js_truthy(self, value):
|
|
282
|
+
if value is None or value is _JS_NULL:
|
|
283
|
+
return False
|
|
284
|
+
match value:
|
|
285
|
+
case bool():
|
|
286
|
+
return value
|
|
287
|
+
case int() | float():
|
|
288
|
+
return value != 0 and not (isinstance(value, float) and math.isnan(value))
|
|
289
|
+
case str():
|
|
290
|
+
return len(value) > 0
|
|
291
|
+
case list() | dict():
|
|
292
|
+
return True
|
|
293
|
+
case _:
|
|
294
|
+
return bool(value)
|
|
295
|
+
|
|
296
|
+
def _js_typeof(self, value):
|
|
297
|
+
if value is _JS_NULL:
|
|
298
|
+
return 'object' # typeof null === 'object' in JS
|
|
299
|
+
if value is None:
|
|
300
|
+
return 'undefined'
|
|
301
|
+
match value:
|
|
302
|
+
case bool():
|
|
303
|
+
return 'boolean'
|
|
304
|
+
case int() | float():
|
|
305
|
+
return 'number'
|
|
306
|
+
case str():
|
|
307
|
+
return 'string'
|
|
308
|
+
case list() | dict():
|
|
309
|
+
return 'object'
|
|
310
|
+
case _:
|
|
311
|
+
return 'undefined'
|
|
312
|
+
|
|
313
|
+
def _js_to_int32(self, value):
|
|
314
|
+
"""Coerce to 32-bit integer (for bitwise ops)."""
|
|
315
|
+
return int(self._js_to_number(value))
|
|
316
|
+
|
|
317
|
+
def _js_to_number(self, value):
|
|
318
|
+
if value is _JS_NULL:
|
|
319
|
+
return 0 # Number(null) → 0
|
|
320
|
+
if value is None:
|
|
321
|
+
return float('nan') # Number(undefined) → NaN
|
|
322
|
+
match value:
|
|
323
|
+
case bool():
|
|
324
|
+
return 1 if value else 0
|
|
325
|
+
case int() | float():
|
|
326
|
+
return value
|
|
327
|
+
case str():
|
|
328
|
+
try:
|
|
329
|
+
return (
|
|
330
|
+
int(value)
|
|
331
|
+
if value.isdigit() or (value.startswith('-') and value[1:].isdigit())
|
|
332
|
+
else float(value)
|
|
333
|
+
)
|
|
334
|
+
except (ValueError, IndexError):
|
|
335
|
+
return 0
|
|
336
|
+
case list():
|
|
337
|
+
return 0
|
|
338
|
+
case _:
|
|
339
|
+
return 0
|
|
340
|
+
|
|
341
|
+
def _js_to_string(self, value):
|
|
342
|
+
if value is _JS_NULL:
|
|
343
|
+
return 'null'
|
|
344
|
+
if value is None:
|
|
345
|
+
return 'undefined'
|
|
346
|
+
match value:
|
|
347
|
+
case bool():
|
|
348
|
+
return 'true' if value else 'false'
|
|
349
|
+
case int() | float():
|
|
350
|
+
return str(int(value)) if isinstance(value, float) and value == int(value) else str(value)
|
|
351
|
+
case str():
|
|
352
|
+
return value
|
|
353
|
+
case list():
|
|
354
|
+
return ''
|
|
355
|
+
case dict():
|
|
356
|
+
return '[object Object]'
|
|
357
|
+
case _:
|
|
358
|
+
return str(value)
|
|
359
|
+
|
|
360
|
+
def _js_compare(self, left, right):
|
|
361
|
+
# JS compares strings lexicographically, not numerically
|
|
362
|
+
if isinstance(left, str) and isinstance(right, str):
|
|
363
|
+
if left < right:
|
|
364
|
+
return -1
|
|
365
|
+
if left > right:
|
|
366
|
+
return 1
|
|
367
|
+
return 0
|
|
368
|
+
left_num = self._js_to_number(left)
|
|
369
|
+
right_num = self._js_to_number(right)
|
|
370
|
+
# NaN comparisons always return false in JS; returning NaN
|
|
371
|
+
# ensures < <= > >= all evaluate to False in the caller.
|
|
372
|
+
if isinstance(left_num, float) and math.isnan(left_num):
|
|
373
|
+
return float('nan')
|
|
374
|
+
if isinstance(right_num, float) and math.isnan(right_num):
|
|
375
|
+
return float('nan')
|
|
376
|
+
if left_num < right_num:
|
|
377
|
+
return -1
|
|
378
|
+
if left_num > right_num:
|
|
379
|
+
return 1
|
|
380
|
+
return 0
|
|
381
|
+
|
|
382
|
+
def _value_to_node(self, value):
|
|
383
|
+
if value is _JS_NULL:
|
|
384
|
+
return make_literal(None) # null literal
|
|
385
|
+
if value is None:
|
|
386
|
+
return {'type': 'Identifier', 'name': 'undefined'}
|
|
387
|
+
match value:
|
|
388
|
+
case bool():
|
|
389
|
+
return make_literal(value)
|
|
390
|
+
case int() | float():
|
|
391
|
+
if isinstance(value, float) and (value != value or math.isinf(value)):
|
|
392
|
+
return None
|
|
393
|
+
if value < 0:
|
|
394
|
+
return {
|
|
395
|
+
'type': 'UnaryExpression',
|
|
396
|
+
'operator': '-',
|
|
397
|
+
'prefix': True,
|
|
398
|
+
'argument': make_literal(abs(value)),
|
|
399
|
+
}
|
|
400
|
+
return make_literal(value)
|
|
401
|
+
case str():
|
|
402
|
+
return make_literal(value)
|
|
403
|
+
return None
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Decode \\xHH hex escape sequences in string literals."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from ..traverser import traverse
|
|
6
|
+
from .base import Transform
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class HexEscapes(Transform):
|
|
10
|
+
"""Pre-AST regex pass to decode hex escape sequences."""
|
|
11
|
+
|
|
12
|
+
def execute(self):
|
|
13
|
+
# This works on the raw source before/after AST
|
|
14
|
+
# But since we operate on AST, we decode hex in string literal raw values
|
|
15
|
+
|
|
16
|
+
def enter(node, parent, key, index):
|
|
17
|
+
if node.get('type') != 'Literal' or not isinstance(node.get('value'), str):
|
|
18
|
+
return
|
|
19
|
+
raw_string = node.get('raw', '')
|
|
20
|
+
if '\\x' not in raw_string and '\\u' not in raw_string:
|
|
21
|
+
return
|
|
22
|
+
# The value is already decoded by parser, just fix raw
|
|
23
|
+
value = node['value']
|
|
24
|
+
new_raw = (
|
|
25
|
+
'"'
|
|
26
|
+
+ value.replace('\\', '\\\\')
|
|
27
|
+
.replace('"', '\\"')
|
|
28
|
+
.replace('\n', '\\n')
|
|
29
|
+
.replace('\r', '\\r')
|
|
30
|
+
.replace('\t', '\\t')
|
|
31
|
+
+ '"'
|
|
32
|
+
)
|
|
33
|
+
if new_raw != raw_string:
|
|
34
|
+
node['raw'] = new_raw
|
|
35
|
+
self.set_changed()
|
|
36
|
+
|
|
37
|
+
traverse(self.ast, {'enter': enter})
|
|
38
|
+
return self.has_changed()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def decode_hex_escapes_source(code):
|
|
42
|
+
"""Decode hex escapes in source code string (pre-parse pass).
|
|
43
|
+
|
|
44
|
+
Only decodes hex escapes that produce printable characters (0x20-0x7e),
|
|
45
|
+
excluding the backslash (0x5c) and quote characters which would break
|
|
46
|
+
string literal syntax. Control characters (newlines, tabs, nulls etc.)
|
|
47
|
+
are left as \\xHH to avoid breaking the parser.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def replace_in_string(match_result):
|
|
51
|
+
quote = match_result.group(1)
|
|
52
|
+
content = match_result.group(2)
|
|
53
|
+
|
|
54
|
+
# Decode hex escapes, but skip backslash, both quote chars,
|
|
55
|
+
# and control chars. Quote chars are left for AST-level handling
|
|
56
|
+
# which normalizes to double quotes like Babel.
|
|
57
|
+
def replace_hex_in_context(hex_match):
|
|
58
|
+
value = int(hex_match.group(1), 16)
|
|
59
|
+
if 0x20 <= value <= 0x7E and value not in (0x22, 0x27, 0x5C):
|
|
60
|
+
return chr(value)
|
|
61
|
+
return hex_match.group(0)
|
|
62
|
+
|
|
63
|
+
decoded = re.sub(r'\\x([0-9a-fA-F]{2})', replace_hex_in_context, content)
|
|
64
|
+
return quote + decoded + quote
|
|
65
|
+
|
|
66
|
+
# Match string literals and decode hex escapes within them
|
|
67
|
+
result = re.sub(r"""(['"])((?:(?!\1|\\).|\\.)*?)\1""", replace_in_string, code)
|
|
68
|
+
return result
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""JJEncode decoder.
|
|
2
|
+
|
|
3
|
+
JJEncode encodes JavaScript using $ and _ variable manipulations.
|
|
4
|
+
This decoder uses Node.js subprocess since JJEncode requires
|
|
5
|
+
JavaScript execution to fully resolve the symbol table.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# Detection patterns for JJEncode
|
|
16
|
+
_JJENCODE_PATTERNS = [
|
|
17
|
+
re.compile(r'\$=~\[\];'),
|
|
18
|
+
re.compile(r'\$\$=\{___:\+\+\$'),
|
|
19
|
+
re.compile(r'\$\$\$=\(\$\[\$\]\+""\)\[\$\]'),
|
|
20
|
+
re.compile(r'[\$_]{3,}.*[\[\]]{2,}.*[\+!]{2,}'),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_jj_encoded(code):
|
|
25
|
+
"""Check if code is JJEncoded."""
|
|
26
|
+
first_line = code.split('\n', 1)[0]
|
|
27
|
+
return any(p.search(first_line) for p in _JJENCODE_PATTERNS)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def jj_decode(code):
|
|
31
|
+
"""Decode JJEncoded JavaScript using Node.js with Function interception."""
|
|
32
|
+
if not is_jj_encoded(code):
|
|
33
|
+
return None
|
|
34
|
+
return _run_with_function_intercept(code)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def jj_decode_via_eval(code):
|
|
38
|
+
"""Simpler fallback: just run the JJEncode in Node with Function intercepted."""
|
|
39
|
+
return _run_with_function_intercept(code)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _run_with_function_intercept(code):
|
|
43
|
+
"""Run JS code with Function constructor intercepted via Proxy."""
|
|
44
|
+
node = shutil.which('node')
|
|
45
|
+
if not node:
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
js_wrapper = (
|
|
49
|
+
'var _captured = [];\n'
|
|
50
|
+
'var _origFunction = Function;\n'
|
|
51
|
+
'var _handler = {\n'
|
|
52
|
+
' apply: function(target, thisArg, args) {\n'
|
|
53
|
+
' var body = args[args.length - 1];\n'
|
|
54
|
+
' if (typeof body === "string" && body.length > 0) _captured.push(body);\n'
|
|
55
|
+
' return target.apply(thisArg, args);\n'
|
|
56
|
+
' },\n'
|
|
57
|
+
' construct: function(target, args) {\n'
|
|
58
|
+
' var body = args[args.length - 1];\n'
|
|
59
|
+
' if (typeof body === "string" && body.length > 0) _captured.push(body);\n'
|
|
60
|
+
' return new target(...args);\n'
|
|
61
|
+
' }\n'
|
|
62
|
+
'};\n'
|
|
63
|
+
'var _proxyFn = new Proxy(_origFunction, _handler);\n'
|
|
64
|
+
'Function = _proxyFn;\n'
|
|
65
|
+
'Function.prototype.constructor = _proxyFn;\n'
|
|
66
|
+
'Object.getPrototypeOf(function(){}).constructor = _proxyFn;\n'
|
|
67
|
+
'try {\n' + code + '\n' + '} catch(e) {}\n'
|
|
68
|
+
'Function = _origFunction;\n'
|
|
69
|
+
'Function.prototype.constructor = _origFunction;\n'
|
|
70
|
+
'if (_captured.length > 0) {\n'
|
|
71
|
+
' console.log(_captured[_captured.length - 1]);\n'
|
|
72
|
+
'}\n'
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
fd, tmp_path = tempfile.mkstemp(suffix='.js')
|
|
77
|
+
try:
|
|
78
|
+
with os.fdopen(fd, 'w') as f:
|
|
79
|
+
f.write(js_wrapper)
|
|
80
|
+
result = subprocess.run(
|
|
81
|
+
[node, tmp_path],
|
|
82
|
+
capture_output=True,
|
|
83
|
+
text=True,
|
|
84
|
+
timeout=5,
|
|
85
|
+
)
|
|
86
|
+
output = result.stdout.strip()
|
|
87
|
+
return output if output else None
|
|
88
|
+
finally:
|
|
89
|
+
os.unlink(tmp_path)
|
|
90
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
|
|
91
|
+
return None
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""JSFUCK decoder.
|
|
2
|
+
|
|
3
|
+
JSFUCK encodes JavaScript using only []()!+ characters.
|
|
4
|
+
Decoding requires JavaScript execution, so we use Node.js subprocess.
|
|
5
|
+
If Node.js is unavailable, returns None (graceful degradation).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# JSFUCK uses only these characters (plus optional whitespace/semicolons)
|
|
16
|
+
_JSFUCK_RE = re.compile(r'^[\s\[\]\(\)!+;]+$')
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_jsfuck(code):
|
|
20
|
+
"""Check if code is JSFUCK-encoded.
|
|
21
|
+
|
|
22
|
+
JSFUCK code consists only of []()!+ characters (with optional whitespace/semicolons).
|
|
23
|
+
We also require minimum length to avoid false positives.
|
|
24
|
+
"""
|
|
25
|
+
stripped = code.strip()
|
|
26
|
+
if len(stripped) < 100:
|
|
27
|
+
return False
|
|
28
|
+
# Check if code starts with JSFUCK-style patterns
|
|
29
|
+
# Some JSFUCK variants have a preamble (like $ = String.fromCharCode(...))
|
|
30
|
+
# so we check if the majority of the code is JSFUCK chars
|
|
31
|
+
jsfuck_chars = set('[]()!+ \t\n\r;')
|
|
32
|
+
jsfuck_count = sum(1 for c in stripped if c in jsfuck_chars)
|
|
33
|
+
return jsfuck_count / len(stripped) > 0.9
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def jsfuck_decode(code):
|
|
37
|
+
"""Decode JSFUCK-encoded JavaScript using Node.js. Returns decoded string or None."""
|
|
38
|
+
node = shutil.which('node')
|
|
39
|
+
if not node:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
stripped = code.strip()
|
|
43
|
+
|
|
44
|
+
# Strategy: JSFUCK gets Function via []['flat']['constructor'] or similar
|
|
45
|
+
# prototype chain access. We intercept by patching Function.prototype.constructor
|
|
46
|
+
# and wrapping the code to capture what gets passed to Function().
|
|
47
|
+
js_wrapper = (
|
|
48
|
+
'var _captured = [];\n'
|
|
49
|
+
'var _origFunction = Function;\n'
|
|
50
|
+
# Patch the prototype chain so JSFUCK's []['constructor']['constructor'] is intercepted
|
|
51
|
+
'var _handler = {\n'
|
|
52
|
+
' apply: function(target, thisArg, args) {\n'
|
|
53
|
+
' var body = args[args.length - 1];\n'
|
|
54
|
+
' if (typeof body === "string" && body.length > 0) _captured.push(body);\n'
|
|
55
|
+
' return target.apply(thisArg, args);\n'
|
|
56
|
+
' },\n'
|
|
57
|
+
' construct: function(target, args) {\n'
|
|
58
|
+
' var body = args[args.length - 1];\n'
|
|
59
|
+
' if (typeof body === "string" && body.length > 0) _captured.push(body);\n'
|
|
60
|
+
' return new target(...args);\n'
|
|
61
|
+
' }\n'
|
|
62
|
+
'};\n'
|
|
63
|
+
'var _proxyFn = new Proxy(_origFunction, _handler);\n'
|
|
64
|
+
'Function = _proxyFn;\n'
|
|
65
|
+
'Function.prototype.constructor = _proxyFn;\n'
|
|
66
|
+
# Patch common prototype chains JSFUCK uses
|
|
67
|
+
'Object.getPrototypeOf(function(){}).constructor = _proxyFn;\n'
|
|
68
|
+
'try {\n' + stripped + '\n' + '} catch(e) {}\n'
|
|
69
|
+
'Function = _origFunction;\n'
|
|
70
|
+
'Function.prototype.constructor = _origFunction;\n'
|
|
71
|
+
'if (_captured.length > 0) {\n'
|
|
72
|
+
' console.log(_captured[_captured.length - 1]);\n'
|
|
73
|
+
'}\n'
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# Write to temp file to avoid shell arg length limits
|
|
78
|
+
fd, tmp_path = tempfile.mkstemp(suffix='.js')
|
|
79
|
+
try:
|
|
80
|
+
with os.fdopen(fd, 'w') as f:
|
|
81
|
+
f.write(js_wrapper)
|
|
82
|
+
result = subprocess.run(
|
|
83
|
+
[node, tmp_path],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
timeout=10,
|
|
87
|
+
)
|
|
88
|
+
output = result.stdout.strip()
|
|
89
|
+
return output if output else None
|
|
90
|
+
finally:
|
|
91
|
+
os.unlink(tmp_path)
|
|
92
|
+
except (subprocess.TimeoutExpired, subprocess.SubprocessError, OSError):
|
|
93
|
+
return None
|