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.
Files changed (38) hide show
  1. pyjsclear/__init__.py +47 -0
  2. pyjsclear/__main__.py +37 -0
  3. pyjsclear/deobfuscator.py +191 -0
  4. pyjsclear/generator.py +772 -0
  5. pyjsclear/parser.py +46 -0
  6. pyjsclear/scope.py +283 -0
  7. pyjsclear/transforms/__init__.py +0 -0
  8. pyjsclear/transforms/aa_decode.py +83 -0
  9. pyjsclear/transforms/anti_tamper.py +106 -0
  10. pyjsclear/transforms/base.py +24 -0
  11. pyjsclear/transforms/constant_prop.py +103 -0
  12. pyjsclear/transforms/control_flow.py +330 -0
  13. pyjsclear/transforms/dead_branch.py +71 -0
  14. pyjsclear/transforms/eval_unpack.py +133 -0
  15. pyjsclear/transforms/expression_simplifier.py +403 -0
  16. pyjsclear/transforms/hex_escapes.py +68 -0
  17. pyjsclear/transforms/jj_decode.py +91 -0
  18. pyjsclear/transforms/jsfuck_decode.py +93 -0
  19. pyjsclear/transforms/logical_to_if.py +173 -0
  20. pyjsclear/transforms/object_packer.py +133 -0
  21. pyjsclear/transforms/object_simplifier.py +192 -0
  22. pyjsclear/transforms/property_simplifier.py +43 -0
  23. pyjsclear/transforms/proxy_functions.py +193 -0
  24. pyjsclear/transforms/reassignment.py +183 -0
  25. pyjsclear/transforms/sequence_splitter.py +215 -0
  26. pyjsclear/transforms/string_revealer.py +1259 -0
  27. pyjsclear/transforms/unused_vars.py +111 -0
  28. pyjsclear/traverser.py +195 -0
  29. pyjsclear/utils/__init__.py +0 -0
  30. pyjsclear/utils/ast_helpers.py +257 -0
  31. pyjsclear/utils/string_decoders.py +141 -0
  32. pyjsclear-0.1.0.dist-info/METADATA +168 -0
  33. pyjsclear-0.1.0.dist-info/RECORD +38 -0
  34. pyjsclear-0.1.0.dist-info/WHEEL +5 -0
  35. pyjsclear-0.1.0.dist-info/entry_points.txt +2 -0
  36. pyjsclear-0.1.0.dist-info/licenses/LICENSE +201 -0
  37. pyjsclear-0.1.0.dist-info/licenses/NOTICE +19 -0
  38. 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