js-confuser 2.0.0-alpha.5 → 2.0.1

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 (113) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +43 -43
  2. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -20
  3. package/.github/workflows/node.js.yml +28 -28
  4. package/.prettierrc +4 -4
  5. package/CHANGELOG.md +1015 -989
  6. package/CODE_OF_CONDUCT.md +131 -131
  7. package/CONTRIBUTING.md +52 -52
  8. package/LICENSE +21 -21
  9. package/Migration.md +72 -71
  10. package/README.md +86 -78
  11. package/dist/constants.js +43 -43
  12. package/dist/index.js +14 -23
  13. package/dist/obfuscator.js +31 -25
  14. package/dist/order.js +4 -4
  15. package/dist/presets.js +31 -31
  16. package/dist/templates/integrityTemplate.js +4 -4
  17. package/dist/templates/template.js +1 -2
  18. package/dist/transforms/astScrambler.js +1 -2
  19. package/dist/transforms/calculator.js +1 -2
  20. package/dist/transforms/controlFlowFlattening.js +93 -63
  21. package/dist/transforms/deadCode.js +1 -2
  22. package/dist/transforms/dispatcher.js +4 -5
  23. package/dist/transforms/extraction/duplicateLiteralsRemoval.js +1 -2
  24. package/dist/transforms/extraction/objectExtraction.js +1 -2
  25. package/dist/transforms/finalizer.js +1 -2
  26. package/dist/transforms/flatten.js +1 -2
  27. package/dist/transforms/identifier/globalConcealing.js +15 -2
  28. package/dist/transforms/identifier/movedDeclarations.js +8 -7
  29. package/dist/transforms/identifier/renameVariables.js +7 -7
  30. package/dist/transforms/lock/integrity.js +11 -10
  31. package/dist/transforms/lock/lock.js +2 -3
  32. package/dist/transforms/minify.js +11 -29
  33. package/dist/transforms/opaquePredicates.js +1 -2
  34. package/dist/transforms/pack.js +5 -2
  35. package/dist/transforms/plugin.js +18 -19
  36. package/dist/transforms/preparation.js +16 -16
  37. package/dist/transforms/renameLabels.js +1 -2
  38. package/dist/transforms/rgf.js +8 -9
  39. package/dist/transforms/shuffle.js +1 -2
  40. package/dist/transforms/string/encoding.js +1 -2
  41. package/dist/transforms/string/stringCompression.js +3 -4
  42. package/dist/transforms/string/stringConcealing.js +8 -3
  43. package/dist/transforms/string/stringEncoding.js +1 -2
  44. package/dist/transforms/variableMasking.js +1 -2
  45. package/dist/utils/NameGen.js +2 -2
  46. package/dist/utils/PredicateGen.js +1 -2
  47. package/dist/utils/ast-utils.js +87 -88
  48. package/dist/utils/function-utils.js +8 -8
  49. package/dist/utils/node.js +5 -6
  50. package/dist/utils/object-utils.js +4 -4
  51. package/dist/utils/random-utils.js +20 -20
  52. package/dist/utils/static-utils.js +1 -2
  53. package/dist/validateOptions.js +4 -7
  54. package/index.d.ts +17 -17
  55. package/package.json +61 -59
  56. package/src/constants.ts +168 -168
  57. package/src/index.ts +118 -118
  58. package/src/obfuscationResult.ts +49 -49
  59. package/src/obfuscator.ts +501 -497
  60. package/src/options.ts +407 -407
  61. package/src/order.ts +54 -54
  62. package/src/presets.ts +125 -125
  63. package/src/templates/bufferToStringTemplate.ts +57 -57
  64. package/src/templates/deadCodeTemplates.ts +1185 -1185
  65. package/src/templates/getGlobalTemplate.ts +76 -76
  66. package/src/templates/integrityTemplate.ts +64 -64
  67. package/src/templates/setFunctionLengthTemplate.ts +11 -11
  68. package/src/templates/stringCompressionTemplate.ts +20 -20
  69. package/src/templates/tamperProtectionTemplates.ts +120 -120
  70. package/src/templates/template.ts +224 -224
  71. package/src/transforms/astScrambler.ts +99 -99
  72. package/src/transforms/calculator.ts +99 -99
  73. package/src/transforms/controlFlowFlattening.ts +1716 -1664
  74. package/src/transforms/deadCode.ts +82 -82
  75. package/src/transforms/dispatcher.ts +450 -450
  76. package/src/transforms/extraction/duplicateLiteralsRemoval.ts +156 -158
  77. package/src/transforms/extraction/objectExtraction.ts +186 -186
  78. package/src/transforms/finalizer.ts +74 -74
  79. package/src/transforms/flatten.ts +421 -420
  80. package/src/transforms/identifier/globalConcealing.ts +315 -295
  81. package/src/transforms/identifier/movedDeclarations.ts +252 -251
  82. package/src/transforms/identifier/renameVariables.ts +328 -321
  83. package/src/transforms/lock/integrity.ts +117 -114
  84. package/src/transforms/lock/lock.ts +418 -425
  85. package/src/transforms/minify.ts +615 -629
  86. package/src/transforms/opaquePredicates.ts +100 -100
  87. package/src/transforms/pack.ts +239 -231
  88. package/src/transforms/plugin.ts +173 -173
  89. package/src/transforms/preparation.ts +349 -347
  90. package/src/transforms/renameLabels.ts +175 -175
  91. package/src/transforms/rgf.ts +322 -322
  92. package/src/transforms/shuffle.ts +82 -82
  93. package/src/transforms/string/encoding.ts +144 -144
  94. package/src/transforms/string/stringCompression.ts +128 -128
  95. package/src/transforms/string/stringConcealing.ts +312 -298
  96. package/src/transforms/string/stringEncoding.ts +80 -80
  97. package/src/transforms/string/stringSplitting.ts +77 -77
  98. package/src/transforms/variableMasking.ts +257 -257
  99. package/src/utils/IntGen.ts +33 -33
  100. package/src/utils/NameGen.ts +116 -116
  101. package/src/utils/PredicateGen.ts +61 -61
  102. package/src/utils/ast-utils.ts +663 -663
  103. package/src/utils/function-utils.ts +50 -50
  104. package/src/utils/gen-utils.ts +48 -48
  105. package/src/utils/node.ts +78 -78
  106. package/src/utils/object-utils.ts +21 -21
  107. package/src/utils/random-utils.ts +93 -93
  108. package/src/utils/static-utils.ts +66 -66
  109. package/src/validateOptions.ts +256 -259
  110. package/tsconfig.json +13 -14
  111. package/dist/probability.js +0 -1
  112. package/dist/transforms/functionOutlining.js +0 -230
  113. package/dist/utils/ControlObject.js +0 -125
@@ -1,1664 +1,1716 @@
1
- import traverse, { NodePath, Scope, Visitor } from "@babel/traverse";
2
- import { PluginArg, PluginObject } from "./plugin";
3
- import { Order } from "../order";
4
- import {
5
- ensureComputedExpression,
6
- getParentFunctionOrProgram,
7
- isDefiningIdentifier,
8
- isStrictMode,
9
- isVariableIdentifier,
10
- replaceDefiningIdentifierToMemberExpression,
11
- } from "../utils/ast-utils";
12
- import * as t from "@babel/types";
13
- import { numericLiteral, deepClone } from "../utils/node";
14
- import Template from "../templates/template";
15
- import {
16
- chance,
17
- choice,
18
- getRandomInteger,
19
- shuffle,
20
- } from "../utils/random-utils";
21
- import { IntGen } from "../utils/IntGen";
22
- import { ok } from "assert";
23
- import { NameGen } from "../utils/NameGen";
24
- import {
25
- NodeSymbol,
26
- UNSAFE,
27
- NO_RENAME,
28
- PREDICTABLE,
29
- variableFunctionName,
30
- WITH_STATEMENT,
31
- } from "../constants";
32
-
33
- // Function deemed unsafe for CFF
34
- const CFF_UNSAFE = Symbol("CFF_UNSAFE");
35
-
36
- /**
37
- * Breaks functions into DAGs (Directed Acyclic Graphs)
38
- *
39
- * - 1. Break functions into chunks
40
- * - 2. Shuffle chunks but remember their original position
41
- * - 3. Create a Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition.
42
- *
43
- * The Switch statement:
44
- *
45
- * - 1. The state variable controls which case will run next
46
- * - 2. At the end of each case, the state variable is updated to the next block of code.
47
- * - 3. The while loop continues until the the state variable is the end state.
48
- */
49
- export default ({ Plugin }: PluginArg): PluginObject => {
50
- const me = Plugin(Order.ControlFlowFlattening, {
51
- changeData: {
52
- functions: 0,
53
- blocks: 0,
54
- ifStatements: 0,
55
- deadCode: 0,
56
- variables: 0,
57
- },
58
- });
59
-
60
- // in Debug mode, the output is much easier to read
61
- const isDebug = false;
62
- const flattenIfStatements = true; // Converts IF-statements into equivalent 'goto style of code'
63
- const flattenFunctionDeclarations = true; // Converts Function Declarations into equivalent 'goto style of code'
64
- const addRelativeAssignments = true; // state += (NEW_STATE - CURRENT_STATE)
65
- const addDeadCode = true; // add fakes chunks of code
66
- const addFakeTests = true; // case 100: case 490: case 510: ...
67
- const addComplexTests = true; // case s != 49 && s - 10:
68
- const addPredicateTests = true; // case scope.A + 10: ...
69
- const mangleNumericalLiterals = true; // 50 => state + X
70
- const mangleBooleanLiterals = true; // true => state == X
71
- const addWithStatement = true; // Disabling not supported yet
72
-
73
- const cffPrefix = me.getPlaceholder();
74
-
75
- // Amount of blocks changed by Control Flow Flattening
76
- let cffCounter = 0;
77
-
78
- const functionsModified = new Set<t.Node>();
79
-
80
- function flagFunctionToAvoid(path: NodePath, reason: string) {
81
- var fnOrProgram = getParentFunctionOrProgram(path);
82
-
83
- fnOrProgram.node[CFF_UNSAFE] = reason;
84
- }
85
-
86
- return {
87
- post: () => {
88
- functionsModified.forEach((node) => {
89
- (node as NodeSymbol)[UNSAFE] = true;
90
- });
91
- },
92
-
93
- visitor: {
94
- // Unsafe detection
95
- ThisExpression(path) {
96
- flagFunctionToAvoid(path, "this");
97
- },
98
- VariableDeclaration(path) {
99
- if (path.node.declarations.length !== 1) {
100
- path.getAncestry().forEach((p) => {
101
- p.node[CFF_UNSAFE] = "multipleDeclarations";
102
- });
103
- }
104
- },
105
- Identifier(path) {
106
- if (
107
- path.node.name === variableFunctionName ||
108
- path.node.name === "arguments"
109
- ) {
110
- flagFunctionToAvoid(path, "arguments");
111
- }
112
- },
113
- "Super|MetaProperty|AwaitExpression|YieldExpression"(path) {
114
- flagFunctionToAvoid(path, "functionSpecific");
115
- },
116
-
117
- // Main CFF transformation
118
- "Program|Function": {
119
- exit(_path) {
120
- let programOrFunctionPath = _path as NodePath<t.Program | t.Function>;
121
-
122
- // Exclude loops
123
- if (
124
- programOrFunctionPath.find((p) => p.isForStatement() || p.isWhile())
125
- )
126
- return;
127
-
128
- // Exclude 'CFF_UNSAFE' functions
129
- if (programOrFunctionPath.node[CFF_UNSAFE]) return;
130
-
131
- let programPath = _path.isProgram() ? _path : null;
132
- let functionPath = _path.isFunction() ? _path : null;
133
-
134
- let blockPath: NodePath<t.Block>;
135
- if (programPath) {
136
- blockPath = programPath;
137
- } else {
138
- let fnBlockPath = functionPath.get("body");
139
- if (!fnBlockPath.isBlock()) return;
140
- blockPath = fnBlockPath;
141
- }
142
-
143
- // Don't apply to strict mode blocks
144
- const strictModeEnforcingBlock = programOrFunctionPath.find((path) =>
145
- isStrictMode(path as NodePath<t.Block>)
146
- );
147
- if (strictModeEnforcingBlock) return;
148
-
149
- // Must be at least 3 statements or more
150
- if (blockPath.node.body.length < 3) return;
151
-
152
- // Check user's threshold setting
153
- if (!me.computeProbabilityMap(me.options.controlFlowFlattening)) {
154
- return;
155
- }
156
-
157
- if (functionPath) {
158
- // Avoid unsafe functions
159
- if ((functionPath.node as NodeSymbol)[UNSAFE]) return;
160
-
161
- if (functionPath.node.async || functionPath.node.generator) return;
162
- }
163
- programOrFunctionPath.scope.crawl();
164
-
165
- let hasIllegalNode = false;
166
- const bindingNames = new Set<string>();
167
- blockPath.traverse({
168
- Identifier(path) {
169
- if (!path.isBindingIdentifier()) return;
170
- const binding = path.scope.getBinding(path.node.name);
171
- if (!binding) return;
172
-
173
- let fnParent = path.getFunctionParent();
174
- if (
175
- path.key === "id" &&
176
- path.parentPath.isFunctionDeclaration()
177
- ) {
178
- fnParent = path.parentPath.getFunctionParent();
179
- }
180
-
181
- if (fnParent !== functionPath) return;
182
-
183
- if (!isDefiningIdentifier(path)) {
184
- return;
185
- }
186
-
187
- if (bindingNames.has(path.node.name)) {
188
- hasIllegalNode = true;
189
- path.stop();
190
- return;
191
- }
192
- bindingNames.add(path.node.name);
193
- },
194
- });
195
-
196
- if (hasIllegalNode) {
197
- return;
198
- }
199
-
200
- me.changeData.blocks++;
201
-
202
- // Limit how many numbers get entangled
203
- let mangledLiteralsCreated = 0;
204
-
205
- const cffIndex = ++cffCounter; // Start from 1
206
- const prefix = cffPrefix + "_" + cffIndex;
207
-
208
- const withIdentifier = (suffix) => {
209
- var name;
210
- if (isDebug) {
211
- name = prefix + "_" + suffix;
212
- } else {
213
- name = me.obfuscator.nameGen.generate(false);
214
- }
215
-
216
- var id = t.identifier(name);
217
-
218
- (id as NodeSymbol)[NO_RENAME] = cffIndex;
219
- return id;
220
- };
221
-
222
- const mainFnName = withIdentifier("main");
223
-
224
- const scopeVar = withIdentifier("scope");
225
-
226
- const stateVars = new Array(isDebug ? 1 : getRandomInteger(2, 5))
227
- .fill("")
228
- .map((_, i) => withIdentifier(`state_${i}`));
229
-
230
- const argVar = withIdentifier("_arg");
231
- let usedArgVar = false;
232
-
233
- const didReturnVar = withIdentifier("return");
234
-
235
- const basicBlocks = new Map<string, BasicBlock>();
236
-
237
- // Map labels to states
238
- const stateIntGen = new IntGen();
239
-
240
- const defaultBlockPath = blockPath;
241
-
242
- let scopeCounter = 0;
243
-
244
- let scopeNameGen = new NameGen(me.options.identifierGenerator);
245
- if (!isDebug) {
246
- scopeNameGen = me.obfuscator.nameGen;
247
- }
248
-
249
- // Create 'with' object - Determines which scope gets top-level variable access
250
- const withProperty = isDebug ? "with" : scopeNameGen.generate(false);
251
- const withMemberExpression = new Template(
252
- `${scopeVar.name}["${withProperty}"]`
253
- ).expression<t.MemberExpression>();
254
- withMemberExpression.object[NO_RENAME] = cffIndex;
255
-
256
- class ScopeManager {
257
- isNotUsed = true;
258
- requiresInitializing = true;
259
-
260
- nameMap = new Map<string, string>();
261
- nameGen = addWithStatement
262
- ? me.obfuscator.nameGen
263
- : new NameGen(me.options.identifierGenerator);
264
-
265
- findBestWithDiscriminant(basicBlock: BasicBlock): ScopeManager {
266
- // This initializing block is forbidden to have a with discriminant
267
- // (As no previous code is able to prepare the with discriminant)
268
- if (basicBlock !== this.initializingBasicBlock) {
269
- // If no variables were defined in this scope, don't use it
270
- if (Object.keys(this.scope.bindings).length > 0) return this;
271
- }
272
-
273
- return this.parent?.findBestWithDiscriminant(basicBlock);
274
- }
275
-
276
- getNewName(name: string, originalNode?: t.Node) {
277
- if (!this.nameMap.has(name)) {
278
- let newName = this.nameGen.generate(false);
279
- if (isDebug) {
280
- newName = "_" + name;
281
- }
282
-
283
- // console.log(name, newName);
284
-
285
- this.nameMap.set(name, newName);
286
-
287
- me.changeData.variables++;
288
-
289
- // console.log(
290
- // "Renaming " +
291
- // name +
292
- // " to " +
293
- // newName +
294
- // " : " +
295
- // this.scope.path.type
296
- // );
297
-
298
- return newName;
299
- }
300
- return this.nameMap.get(name);
301
- }
302
-
303
- getScopeObject() {
304
- return t.memberExpression(
305
- deepClone(scopeVar),
306
- t.stringLiteral(this.propertyName),
307
- true
308
- );
309
- }
310
-
311
- getInitializingStatement() {
312
- return t.expressionStatement(
313
- t.assignmentExpression(
314
- "=",
315
- this.getScopeObject(),
316
- this.getInitializingObjectExpression()
317
- )
318
- );
319
- }
320
-
321
- getInitializingObjectExpression() {
322
- return isDebug
323
- ? new Template(`
324
- ({
325
- identity: "${this.propertyName}"
326
- })
327
- `).expression()
328
- : new Template(`({})`).expression();
329
- }
330
-
331
- getMemberExpression(
332
- name: string,
333
- object: t.Expression = this.getScopeObject()
334
- ) {
335
- const memberExpression = t.memberExpression(
336
- object,
337
- t.stringLiteral(name),
338
- true
339
- );
340
-
341
- return memberExpression;
342
- }
343
-
344
- propertyName: string;
345
- constructor(
346
- public scope: Scope,
347
- public initializingBasicBlock: BasicBlock
348
- ) {
349
- this.propertyName = isDebug
350
- ? "_" + cffIndex + "_" + scopeCounter++
351
- : scopeNameGen.generate();
352
- }
353
-
354
- get parent() {
355
- return scopeToScopeManager.get(this.scope.parent);
356
- }
357
-
358
- getObjectExpression(refreshLabel: string) {
359
- let refreshScope = basicBlocks.get(refreshLabel).scopeManager;
360
- let propertyMap: { [property: string]: t.Expression } = {};
361
-
362
- let cursor = this.scope;
363
- while (cursor) {
364
- let parentScopeManager = scopeToScopeManager.get(cursor);
365
- if (parentScopeManager) {
366
- propertyMap[parentScopeManager.propertyName] =
367
- t.memberExpression(
368
- deepClone(scopeVar),
369
- t.stringLiteral(parentScopeManager.propertyName),
370
- true
371
- );
372
- }
373
-
374
- cursor = cursor.parent;
375
- }
376
-
377
- propertyMap[refreshScope.propertyName] =
378
- refreshScope.getInitializingObjectExpression();
379
-
380
- const properties: t.ObjectProperty[] = [];
381
- for (const key in propertyMap) {
382
- properties.push(
383
- t.objectProperty(t.stringLiteral(key), propertyMap[key], true)
384
- );
385
- }
386
-
387
- return t.objectExpression(properties);
388
- }
389
-
390
- hasOwnName(name: string) {
391
- return this.nameMap.has(name);
392
- }
393
- }
394
-
395
- const getImpossibleBasicBlocks = () => {
396
- return Array.from(basicBlocks.values()).filter(
397
- (block) => block.options.impossible
398
- );
399
- };
400
-
401
- const scopeToScopeManager = new Map<Scope, ScopeManager>();
402
- /**
403
- * A Basic Block is a sequence of instructions with no diversion except at the entry and exit points.
404
- */
405
- class BasicBlock {
406
- totalState: number;
407
- stateValues: number[];
408
- allowWithDiscriminant = true;
409
- bestWithDiscriminant: ScopeManager;
410
-
411
- get withDiscriminant() {
412
- if (!this.allowWithDiscriminant) return;
413
-
414
- return this.bestWithDiscriminant;
415
- }
416
-
417
- private createPath() {
418
- const newPath = NodePath.get<t.BlockStatement, any>({
419
- hub: this.parentPath.hub,
420
- parentPath: this.parentPath,
421
- parent: this.parentPath.node,
422
- container: this.parentPath.node.body,
423
- listKey: "body", // Set the correct list key
424
- key: "virtual", // Set the index of the new node
425
- } as any);
426
-
427
- newPath.scope = this.parentPath.scope;
428
- newPath.parentPath = this.parentPath;
429
- newPath.node = t.blockStatement([]);
430
-
431
- this.thisPath = newPath;
432
- this.thisNode = newPath.node;
433
- }
434
-
435
- insertAfter(newNode: t.Statement) {
436
- this.body.push(newNode);
437
- }
438
-
439
- get scope() {
440
- return this.parentPath.scope;
441
- }
442
-
443
- get scopeManager() {
444
- return scopeToScopeManager.get(this.scope);
445
- }
446
-
447
- thisPath: NodePath<t.BlockStatement>;
448
- thisNode: t.BlockStatement;
449
-
450
- get body(): t.Statement[] {
451
- return this.thisPath.node.body;
452
- }
453
-
454
- createFalsePredicate(): t.Expression {
455
- var predicate = this.createPredicate();
456
- if (predicate.value) {
457
- // Make predicate false
458
- return t.unaryExpression("!", predicate.node);
459
- }
460
- return predicate.node;
461
- }
462
-
463
- createTruePredicate(): t.Expression {
464
- var predicate = this.createPredicate();
465
- if (!predicate.value) {
466
- // Make predicate true
467
- return t.unaryExpression("!", predicate.node);
468
- }
469
- return predicate.node;
470
- }
471
-
472
- createPredicate() {
473
- var stateVarIndex = getRandomInteger(0, stateVars.length);
474
- var stateValue = this.stateValues[stateVarIndex];
475
- var compareValue = choice([
476
- stateValue,
477
- getRandomInteger(-250, 250),
478
- ]);
479
-
480
- var operator: t.BinaryExpression["operator"] = choice([
481
- "==",
482
- "!=",
483
- "<",
484
- ">",
485
- ]);
486
- var compareResult;
487
- switch (operator) {
488
- case "==":
489
- compareResult = stateValue === compareValue;
490
- break;
491
- case "!=":
492
- compareResult = stateValue !== compareValue;
493
- break;
494
- case "<":
495
- compareResult = stateValue < compareValue;
496
- break;
497
- case ">":
498
- compareResult = stateValue > compareValue;
499
- break;
500
- }
501
-
502
- return {
503
- node: t.binaryExpression(
504
- operator,
505
- deepClone(stateVars[stateVarIndex]),
506
- numericLiteral(compareValue)
507
- ),
508
- value: compareResult,
509
- };
510
- }
511
-
512
- identifier(identifierName: string, scopeManager: ScopeManager) {
513
- if (
514
- this.withDiscriminant &&
515
- this.withDiscriminant === scopeManager
516
- ) {
517
- var id = t.identifier(identifierName);
518
- (id as NodeSymbol)[NO_RENAME] = cffIndex;
519
- (id as NodeSymbol)[WITH_STATEMENT] = true;
520
- return id;
521
- }
522
-
523
- return scopeManager.getMemberExpression(identifierName);
524
- }
525
-
526
- initializedScope: ScopeManager;
527
-
528
- constructor(
529
- public label: string,
530
- public parentPath: NodePath<t.Block>,
531
- public options: { impossible?: boolean } = {}
532
- ) {
533
- this.createPath();
534
-
535
- if (isDebug) {
536
- // States in debug mode are just 1, 2, 3, ...
537
- this.totalState = basicBlocks.size + 1;
538
- } else {
539
- this.totalState = stateIntGen.generate();
540
- }
541
-
542
- // Correct state values
543
- // Start with random numbers
544
- this.stateValues = stateVars.map(() =>
545
- getRandomInteger(-250, 250)
546
- );
547
-
548
- // Try to re-use old state values to make diffs smaller
549
- if (basicBlocks.size > 1) {
550
- const lastBlock = [...basicBlocks.values()].at(-1);
551
- this.stateValues = lastBlock.stateValues.map((oldValue, i) => {
552
- return choice([oldValue, this.stateValues[i]]);
553
- });
554
- }
555
-
556
- // Correct one of the values so that the accumulated sum is equal to the state
557
- const correctIndex = getRandomInteger(0, this.stateValues.length);
558
-
559
- const getCurrentState = () =>
560
- this.stateValues.reduce((a, b) => a + b, 0);
561
-
562
- // Correct the value
563
- this.stateValues[correctIndex] =
564
- this.totalState -
565
- (getCurrentState() - this.stateValues[correctIndex]);
566
-
567
- ok(getCurrentState() === this.totalState);
568
-
569
- // Store basic block
570
- basicBlocks.set(label, this);
571
-
572
- // Create a new scope manager if it doesn't exist
573
- if (!scopeToScopeManager.has(this.scope)) {
574
- scopeToScopeManager.set(
575
- this.scope,
576
- new ScopeManager(this.scope, this)
577
- );
578
- }
579
-
580
- this.initializedScope = this.scopeManager;
581
- }
582
- }
583
-
584
- /**
585
- * Stage 1: Flatten the code into Basic Blocks
586
- *
587
- * This involves transforming the Control Flow / Scopes into blocks with 'goto' statements
588
- *
589
- * - A block is simply a sequence of statements
590
- * - A block can have a 'goto' statement to another block
591
- * - A block original scope is preserved
592
- *
593
- * 'goto' & Scopes are transformed in Stage 2
594
- */
595
-
596
- const switchLabel = me.getPlaceholder();
597
- const breakStatement = () => {
598
- return t.breakStatement(t.identifier(switchLabel));
599
- };
600
-
601
- const startLabel = me.getPlaceholder();
602
- const endLabel = me.getPlaceholder();
603
-
604
- let currentBasicBlock = new BasicBlock(startLabel, blockPath);
605
- currentBasicBlock.allowWithDiscriminant = false;
606
-
607
- const gotoFunctionName =
608
- "GOTO__" +
609
- me.getPlaceholder() +
610
- "__IF_YOU_CAN_READ_THIS_THERE_IS_A_BUG";
611
-
612
- function GotoControlStatement(label: string) {
613
- return new Template(`
614
- ${gotoFunctionName}("${label}");
615
- `).single();
616
- }
617
-
618
- // Ends the current block and starts a new one
619
- function endCurrentBasicBlock({
620
- jumpToNext = true,
621
- nextLabel = me.getPlaceholder(),
622
- prevJumpTo = null,
623
- nextBlockPath = null,
624
- } = {}) {
625
- ok(nextBlockPath);
626
-
627
- if (prevJumpTo) {
628
- currentBasicBlock.insertAfter(GotoControlStatement(prevJumpTo));
629
- } else if (jumpToNext) {
630
- currentBasicBlock.insertAfter(GotoControlStatement(nextLabel));
631
- }
632
-
633
- currentBasicBlock = new BasicBlock(nextLabel, nextBlockPath);
634
- }
635
-
636
- const prependNodes: t.Statement[] = [];
637
- const functionExpressions: [
638
- string,
639
- string,
640
- BasicBlock,
641
- t.FunctionExpression
642
- ][] = [];
643
-
644
- function flattenIntoBasicBlocks(
645
- bodyIn: NodePath<t.Statement>[] | NodePath<t.Block>
646
- ) {
647
- // if (!Array.isArray(bodyIn) && bodyIn.isBlock()) {
648
- // currentBasicBlock.parentPath = bodyIn;
649
- // }
650
- const body = Array.isArray(bodyIn) ? bodyIn : bodyIn.get("body");
651
- const nextBlockPath = Array.isArray(bodyIn)
652
- ? currentBasicBlock.parentPath
653
- : bodyIn;
654
-
655
- for (const index in body) {
656
- const statement = body[index];
657
-
658
- // Keep Imports before everything else
659
- if (statement.isImportDeclaration()) {
660
- prependNodes.push(statement.node);
661
- continue;
662
- }
663
-
664
- if (statement.isFunctionDeclaration()) {
665
- const fnName = statement.node.id.name;
666
- let isIllegal = false;
667
-
668
- if (
669
- !flattenFunctionDeclarations ||
670
- statement.node.async ||
671
- statement.node.generator ||
672
- (statement.node as NodeSymbol)[UNSAFE] ||
673
- (statement.node as NodeSymbol)[CFF_UNSAFE] ||
674
- isStrictMode(statement)
675
- ) {
676
- isIllegal = true;
677
- }
678
- let oldBasicBlock = currentBasicBlock;
679
- let fnLabel = me.getPlaceholder();
680
-
681
- let sm = currentBasicBlock.scopeManager;
682
- let rename = sm.getNewName(fnName);
683
-
684
- sm.scope.bindings[fnName].kind = "var";
685
-
686
- const hoistedBasicBlock = Array.from(basicBlocks.values()).find(
687
- (block) => block.parentPath === currentBasicBlock.parentPath
688
- );
689
-
690
- if (isIllegal) {
691
- hoistedBasicBlock.body.unshift(statement.node);
692
- continue;
693
- }
694
-
695
- me.changeData.functions++;
696
-
697
- const functionExpression = t.functionExpression(
698
- null,
699
- [],
700
- t.blockStatement([])
701
- );
702
- functionExpressions.push([
703
- fnName,
704
- fnLabel,
705
- currentBasicBlock,
706
- functionExpression,
707
- ]);
708
-
709
- // Change the function declaration to a variable declaration
710
- hoistedBasicBlock.body.unshift(
711
- t.variableDeclaration("var", [
712
- t.variableDeclarator(
713
- t.identifier(fnName),
714
- functionExpression
715
- ),
716
- ])
717
- );
718
-
719
- const blockStatement = statement.get("body");
720
-
721
- endCurrentBasicBlock({
722
- nextLabel: fnLabel,
723
- nextBlockPath: blockStatement,
724
- jumpToNext: false,
725
- });
726
- let fnTopBlock = currentBasicBlock;
727
-
728
- // Implicit return
729
- blockStatement.node.body.push(
730
- t.returnStatement(t.identifier("undefined"))
731
- );
732
-
733
- flattenIntoBasicBlocks(blockStatement);
734
- scopeToScopeManager.get(statement.scope).requiresInitializing =
735
- false;
736
- basicBlocks.get(fnLabel).allowWithDiscriminant = false;
737
-
738
- // Debug label
739
- if (isDebug) {
740
- fnTopBlock.body.unshift(
741
- t.expressionStatement(
742
- t.stringLiteral(
743
- "Function " +
744
- statement.node.id.name +
745
- " -> Renamed to " +
746
- rename
747
- )
748
- )
749
- );
750
- }
751
-
752
- // Unpack parameters from the parameter 'argVar'
753
- if (statement.node.params.length > 0) {
754
- usedArgVar = true;
755
- fnTopBlock.body.unshift(
756
- t.variableDeclaration("var", [
757
- t.variableDeclarator(
758
- t.arrayPattern(statement.node.params),
759
- deepClone(argVar)
760
- ),
761
- ])
762
- );
763
-
764
- // Change bindings from 'param' to 'var'
765
- statement.get("params").forEach((param) => {
766
- let ids = param.getBindingIdentifierPaths();
767
- // Loop over the record of binding identifiers
768
- for (const identifierName in ids) {
769
- const identifierPath = ids[identifierName];
770
- if (identifierPath.getFunctionParent() === statement) {
771
- const binding =
772
- statement.scope.getBinding(identifierName);
773
-
774
- if (binding) {
775
- binding.kind = "var";
776
- }
777
- }
778
- }
779
- });
780
- }
781
-
782
- currentBasicBlock = oldBasicBlock;
783
- continue;
784
- }
785
-
786
- // Convert IF statements into Basic Blocks
787
- if (statement.isIfStatement() && flattenIfStatements) {
788
- const test = statement.get("test");
789
- const consequent = statement.get("consequent");
790
- const alternate = statement.get("alternate");
791
-
792
- // Both consequent and alternate are blocks
793
- if (
794
- consequent.isBlockStatement() &&
795
- (!alternate.node || alternate.isBlockStatement())
796
- ) {
797
- me.changeData.ifStatements++;
798
-
799
- const consequentLabel = me.getPlaceholder();
800
- const alternateLabel = alternate.node
801
- ? me.getPlaceholder()
802
- : null;
803
- const afterPath = me.getPlaceholder();
804
-
805
- currentBasicBlock.insertAfter(
806
- t.ifStatement(
807
- test.node,
808
- GotoControlStatement(consequentLabel),
809
- alternateLabel
810
- ? GotoControlStatement(alternateLabel)
811
- : GotoControlStatement(afterPath)
812
- )
813
- );
814
-
815
- const oldBasicBlock = currentBasicBlock;
816
-
817
- endCurrentBasicBlock({
818
- jumpToNext: false,
819
- nextLabel: consequentLabel,
820
- nextBlockPath: consequent,
821
- });
822
-
823
- flattenIntoBasicBlocks(consequent);
824
- currentBasicBlock.initializedScope =
825
- oldBasicBlock.scopeManager;
826
-
827
- if (alternate.isBlockStatement()) {
828
- endCurrentBasicBlock({
829
- prevJumpTo: afterPath,
830
- nextLabel: alternateLabel,
831
- nextBlockPath: alternate,
832
- });
833
-
834
- flattenIntoBasicBlocks(alternate);
835
- }
836
-
837
- endCurrentBasicBlock({
838
- prevJumpTo: afterPath,
839
- nextLabel: afterPath,
840
- nextBlockPath: oldBasicBlock.parentPath,
841
- });
842
-
843
- continue;
844
- }
845
- }
846
-
847
- if (
848
- Number(index) === body.length - 1 &&
849
- statement.isExpressionStatement() &&
850
- statement.findParent((p) => p.isBlock()) === blockPath
851
- ) {
852
- // Return the result of the last expression for eval() purposes
853
- currentBasicBlock.insertAfter(
854
- t.returnStatement(statement.get("expression").node)
855
- );
856
- continue;
857
- }
858
-
859
- // 3 or more statements should be split more
860
- if (
861
- currentBasicBlock.body.length > 1 &&
862
- chance(50 + currentBasicBlock.body.length)
863
- ) {
864
- endCurrentBasicBlock({
865
- nextBlockPath: nextBlockPath,
866
- });
867
- }
868
-
869
- // console.log(currentBasicBlock.thisPath.type);
870
- // console.log(currentBasicBlock.body);
871
- currentBasicBlock.body.push(statement.node);
872
- }
873
- }
874
-
875
- // Convert our code into Basic Blocks
876
- flattenIntoBasicBlocks(blockPath.get("body"));
877
-
878
- // Ensure always jumped to the Program end
879
- endCurrentBasicBlock({
880
- jumpToNext: true,
881
- nextLabel: endLabel,
882
- nextBlockPath: defaultBlockPath,
883
- });
884
-
885
- basicBlocks.get(endLabel).allowWithDiscriminant = false;
886
-
887
- if (!isDebug && addDeadCode) {
888
- // DEAD CODE 1/3: Add fake chunks that are never reached
889
- const fakeChunkCount = getRandomInteger(1, 5);
890
- for (let i = 0; i < fakeChunkCount; i++) {
891
- // These chunks just jump somewhere random, they are never executed
892
- // so it could contain any code
893
- const fakeBlock = new BasicBlock(me.getPlaceholder(), blockPath, {
894
- impossible: true,
895
- });
896
- let fakeJump;
897
- do {
898
- fakeJump = choice(Array.from(basicBlocks.keys()));
899
- } while (fakeJump === fakeBlock.label);
900
-
901
- fakeBlock.insertAfter(GotoControlStatement(fakeJump));
902
- me.changeData.deadCode++;
903
- }
904
-
905
- // DEAD CODE 2/3: Add fake jumps to really mess with deobfuscators
906
- // "irreducible control flow"
907
- basicBlocks.forEach((basicBlock) => {
908
- if (chance(30 - basicBlocks.size)) {
909
- let randomLabel = choice(Array.from(basicBlocks.keys()));
910
-
911
- // The `false` literal will be mangled
912
- basicBlock.insertAfter(
913
- new Template(`
914
- if({predicate}){
915
- {goto}
916
- }
917
- `).single({
918
- goto: GotoControlStatement(randomLabel),
919
- predicate: basicBlock.createFalsePredicate(),
920
- })
921
- );
922
-
923
- me.changeData.deadCode++;
924
- }
925
- });
926
- // DEAD CODE 3/3: Clone chunks but these chunks are never ran
927
- const cloneChunkCount = getRandomInteger(1, 5);
928
- for (let i = 0; i < cloneChunkCount; i++) {
929
- let randomChunk = choice(Array.from(basicBlocks.values()));
930
-
931
- // Don't double define functions
932
- let hasDeclaration = randomChunk.body.find((stmt) => {
933
- return t.isDeclaration(stmt);
934
- });
935
-
936
- if (!hasDeclaration) {
937
- let clonedChunk = new BasicBlock(
938
- me.getPlaceholder(),
939
- randomChunk.parentPath,
940
- {
941
- impossible: true,
942
- }
943
- );
944
-
945
- randomChunk.thisNode.body
946
- .map((x) => deepClone(x))
947
- .forEach((node) => {
948
- if (node.type === "EmptyStatement") return;
949
- clonedChunk.insertAfter(node);
950
- });
951
-
952
- me.changeData.deadCode++;
953
- }
954
- }
955
- }
956
-
957
- // Select scope managers for the with statement
958
- for (const basicBlock of basicBlocks.values()) {
959
- basicBlock.bestWithDiscriminant =
960
- basicBlock.initializedScope?.findBestWithDiscriminant(basicBlock);
961
-
962
- if (isDebug && basicBlock.withDiscriminant) {
963
- basicBlock.body.unshift(
964
- t.expressionStatement(
965
- t.stringLiteral(
966
- "With " + basicBlock.withDiscriminant.propertyName
967
- )
968
- )
969
- );
970
- }
971
- }
972
-
973
- /**
974
- * Stage 2: Transform 'goto' statements into valid JavaScript
975
- *
976
- * - 'goto' is replaced with equivalent state updates and break statements
977
- * - Original identifiers are converted into member expressions
978
- */
979
-
980
- // Remap 'GotoStatement' to actual state assignments and Break statements
981
- for (const basicBlock of basicBlocks.values()) {
982
- const { stateValues: currentStateValues } = basicBlock;
983
- // Wrap the statement in a Babel path to allow traversal
984
-
985
- const outerFn = getParentFunctionOrProgram(basicBlock.parentPath);
986
-
987
- function isWithinSameFunction(path: NodePath) {
988
- let fn = getParentFunctionOrProgram(path);
989
- return fn.node === outerFn.node;
990
- }
991
-
992
- let visitor: Visitor = {
993
- BooleanLiteral: {
994
- exit(boolPath) {
995
- // Don't mangle booleans in debug mode
996
- if (
997
- isDebug ||
998
- !mangleBooleanLiterals ||
999
- me.isSkipped(boolPath)
1000
- )
1001
- return;
1002
-
1003
- if (!isWithinSameFunction(boolPath)) return;
1004
- if (chance(50 + mangledLiteralsCreated)) return;
1005
-
1006
- mangledLiteralsCreated++;
1007
-
1008
- const index = getRandomInteger(0, stateVars.length - 1);
1009
- const stateVar = stateVars[index];
1010
- const stateVarValue = currentStateValues[index];
1011
-
1012
- const compareValue = choice([
1013
- getRandomInteger(-250, 250),
1014
- stateVarValue,
1015
- ]);
1016
- const compareResult = stateVarValue === compareValue;
1017
-
1018
- const newExpression = t.binaryExpression(
1019
- boolPath.node.value === compareResult ? "==" : "!=",
1020
- deepClone(stateVar),
1021
- numericLiteral(compareValue)
1022
- );
1023
-
1024
- ensureComputedExpression(boolPath);
1025
- boolPath.replaceWith(newExpression);
1026
- },
1027
- },
1028
- // Mangle numbers with the state values
1029
- NumericLiteral: {
1030
- exit(numPath) {
1031
- // Don't mangle numbers in debug mode
1032
- if (
1033
- isDebug ||
1034
- !mangleNumericalLiterals ||
1035
- me.isSkipped(numPath)
1036
- )
1037
- return;
1038
-
1039
- const num = numPath.node.value;
1040
- if (
1041
- Math.floor(num) !== num ||
1042
- Math.abs(num) > 100_000 ||
1043
- !Number.isFinite(num) ||
1044
- Number.isNaN(num)
1045
- )
1046
- return;
1047
-
1048
- if (!isWithinSameFunction(numPath)) return;
1049
- if (chance(50 + mangledLiteralsCreated)) return;
1050
-
1051
- mangledLiteralsCreated++;
1052
-
1053
- const index = getRandomInteger(0, stateVars.length - 1);
1054
- const stateVar = stateVars[index];
1055
-
1056
- // num = 50
1057
- // stateVar = 30
1058
- // stateVar + 30
1059
-
1060
- const diff = t.binaryExpression(
1061
- "+",
1062
- deepClone(stateVar),
1063
- me.skip(numericLiteral(num - currentStateValues[index]))
1064
- );
1065
-
1066
- ensureComputedExpression(numPath);
1067
-
1068
- numPath.replaceWith(diff);
1069
- numPath.skip();
1070
- },
1071
- },
1072
-
1073
- Identifier: {
1074
- exit(path: NodePath<t.Identifier>) {
1075
- if (!isVariableIdentifier(path)) return;
1076
- if (me.isSkipped(path)) return;
1077
- if ((path.node as NodeSymbol)[NO_RENAME] === cffIndex) return;
1078
- // For identifiers using implicit with discriminant, skip
1079
- if ((path.node as NodeSymbol)[WITH_STATEMENT]) return;
1080
-
1081
- const identifierName = path.node.name;
1082
- if (identifierName === gotoFunctionName) return;
1083
-
1084
- var binding = path.scope.getBinding(identifierName);
1085
- if (!binding) {
1086
- return;
1087
- }
1088
-
1089
- if (
1090
- binding.kind === "var" ||
1091
- binding.kind === "let" ||
1092
- binding.kind === "const"
1093
- ) {
1094
- } else {
1095
- return;
1096
- }
1097
-
1098
- var scopeManager = scopeToScopeManager.get(binding.scope);
1099
- if (!scopeManager) return;
1100
-
1101
- let newName = scopeManager.getNewName(
1102
- identifierName,
1103
- path.node
1104
- );
1105
-
1106
- let memberExpression: t.MemberExpression | t.Identifier =
1107
- scopeManager.getMemberExpression(newName);
1108
-
1109
- scopeManager.isNotUsed = false;
1110
-
1111
- // Scope object as with discriminant? Use identifier
1112
- if (typeof basicBlock.withDiscriminant === "undefined") {
1113
- const id = t.identifier(scopeManager.propertyName);
1114
- (id as NodeSymbol)[WITH_STATEMENT] = true;
1115
- (id as NodeSymbol)[NO_RENAME] = cffIndex;
1116
-
1117
- memberExpression = scopeManager.getMemberExpression(
1118
- newName,
1119
- id
1120
- );
1121
- }
1122
-
1123
- if (isDefiningIdentifier(path)) {
1124
- replaceDefiningIdentifierToMemberExpression(
1125
- path,
1126
- memberExpression
1127
- );
1128
- return;
1129
- }
1130
-
1131
- if (!path.container) return;
1132
-
1133
- if (
1134
- basicBlock.withDiscriminant &&
1135
- basicBlock.withDiscriminant === scopeManager &&
1136
- basicBlock.withDiscriminant.hasOwnName(identifierName)
1137
- ) {
1138
- // The defining mode must directly append to the scope object
1139
- // Subsequent uses can use the identifier
1140
- const isDefiningNode = path.node === binding.identifier;
1141
-
1142
- if (!isDefiningNode) {
1143
- memberExpression = basicBlock.identifier(
1144
- newName,
1145
- scopeManager
1146
- );
1147
- }
1148
- }
1149
-
1150
- me.skip(memberExpression);
1151
-
1152
- path.replaceWith(memberExpression);
1153
- path.skip();
1154
-
1155
- // Preserve proper 'this' context when directly calling functions
1156
- // X.Y.Z() -> (1, X.Y.Z)()
1157
- if (
1158
- path.parentPath.isCallExpression() &&
1159
- path.key === "callee"
1160
- ) {
1161
- path.replaceWith(
1162
- t.sequenceExpression([t.numericLiteral(1), path.node])
1163
- );
1164
- }
1165
- },
1166
- },
1167
-
1168
- // Top-level returns set additional flag to indicate that the function has returned
1169
- ReturnStatement: {
1170
- exit(path) {
1171
- var functionParent = path.getFunctionParent();
1172
- if (
1173
- !functionParent ||
1174
- functionParent.get("body") !== blockPath
1175
- )
1176
- return;
1177
-
1178
- const returnArgument =
1179
- path.node.argument || t.identifier("undefined");
1180
-
1181
- path.node.argument = new Template(`
1182
- ({didReturnVar} = true, {returnArgument})
1183
- `).expression({
1184
- returnArgument,
1185
- didReturnVar: deepClone(didReturnVar),
1186
- });
1187
- },
1188
- },
1189
-
1190
- // goto() calls are replaced with state updates and break statements
1191
- CallExpression: {
1192
- exit(path) {
1193
- if (
1194
- t.isIdentifier(path.node.callee) &&
1195
- path.node.callee.name === gotoFunctionName
1196
- ) {
1197
- const [labelNode] = path.node.arguments;
1198
-
1199
- ok(t.isStringLiteral(labelNode));
1200
- const label = labelNode.value;
1201
-
1202
- const jumpBlock = basicBlocks.get(label);
1203
- ok(jumpBlock, "Label not found: " + label);
1204
-
1205
- const {
1206
- stateValues: newStateValues,
1207
- totalState: newTotalState,
1208
- } = jumpBlock;
1209
-
1210
- const assignments: t.Expression[] = [];
1211
-
1212
- if (jumpBlock.withDiscriminant) {
1213
- assignments.push(
1214
- t.assignmentExpression(
1215
- "=",
1216
- deepClone(withMemberExpression),
1217
- jumpBlock.withDiscriminant.getScopeObject()
1218
- )
1219
- );
1220
- } else if (basicBlock.withDiscriminant) {
1221
- // Reset the with discriminant to undefined using fake property
1222
- // scope["fake"] -> undefined
1223
-
1224
- const fakeProperty = scopeNameGen.generate();
1225
-
1226
- assignments.push(
1227
- t.assignmentExpression(
1228
- "=",
1229
- deepClone(withMemberExpression),
1230
- t.memberExpression(
1231
- deepClone(scopeVar),
1232
- t.stringLiteral(fakeProperty),
1233
- true
1234
- )
1235
- )
1236
- );
1237
- }
1238
-
1239
- for (let i = 0; i < stateVars.length; i++) {
1240
- const oldValue = currentStateValues[i];
1241
- const newValue = newStateValues[i];
1242
-
1243
- // console.log(oldValue, newValue);
1244
- if (oldValue === newValue) continue; // No diff needed if the value doesn't change
1245
-
1246
- let assignment = t.assignmentExpression(
1247
- "=",
1248
- deepClone(stateVars[i]),
1249
- numericLiteral(newValue)
1250
- );
1251
-
1252
- if (!isDebug && addRelativeAssignments) {
1253
- // Use diffs to create confusing code
1254
- assignment = t.assignmentExpression(
1255
- "+=",
1256
- deepClone(stateVars[i]),
1257
- numericLiteral(newValue - oldValue)
1258
- );
1259
- }
1260
-
1261
- assignments.push(assignment);
1262
- }
1263
-
1264
- // Add debug label
1265
- if (isDebug) {
1266
- assignments.unshift(
1267
- t.stringLiteral("Goto " + newTotalState)
1268
- );
1269
- }
1270
-
1271
- path.parentPath
1272
- .replaceWith(
1273
- t.expressionStatement(t.sequenceExpression(assignments))
1274
- )[0]
1275
- .skip();
1276
-
1277
- // Add break after updating state variables
1278
- path.insertAfter(breakStatement());
1279
- }
1280
- },
1281
- },
1282
- };
1283
-
1284
- basicBlock.thisPath.traverse(visitor);
1285
- }
1286
-
1287
- /**
1288
- * Stage 3: Create a switch statement to handle the control flow
1289
- *
1290
- * - Add fake / impossible blocks
1291
- * - Add fake / predicates to the switch cases tests
1292
- */
1293
-
1294
- // Create global numbers for predicates
1295
- const mainScope = basicBlocks.get(startLabel).scopeManager;
1296
- const predicateNumbers = new Map<string, number>();
1297
- const predicateNumberCount =
1298
- isDebug || !addPredicateTests ? 0 : getRandomInteger(1, 4);
1299
-
1300
- for (let i = 0; i < predicateNumberCount; i++) {
1301
- const name = mainScope.getNewName(
1302
- me.getPlaceholder("predicate_" + i)
1303
- );
1304
-
1305
- const number = getRandomInteger(-250, 250);
1306
- predicateNumbers.set(name, number);
1307
- }
1308
-
1309
- const predicateSymbol = Symbol("predicate");
1310
-
1311
- const createAssignment = (values: number[]) => {
1312
- var exprStmt = new Template(`
1313
- ({predicateVariables} = {values})
1314
- `).single({
1315
- predicateVariables: t.arrayPattern(
1316
- Array.from(predicateNumbers.keys()).map((name) =>
1317
- mainScope.getMemberExpression(name)
1318
- )
1319
- ),
1320
- values: t.arrayExpression(
1321
- values.map((value) => numericLiteral(value))
1322
- ),
1323
- });
1324
-
1325
- exprStmt[predicateSymbol] = true;
1326
-
1327
- return exprStmt;
1328
- };
1329
-
1330
- basicBlocks
1331
- .get(startLabel)
1332
- .body.unshift(
1333
- createAssignment(Array.from(predicateNumbers.values()))
1334
- );
1335
-
1336
- // Add random assignments to impossible blocks
1337
- var fakeAssignmentCount = getRandomInteger(1, 3);
1338
-
1339
- for (let i = 0; i < fakeAssignmentCount; i++) {
1340
- var impossibleBlock = choice(getImpossibleBasicBlocks());
1341
- if (impossibleBlock) {
1342
- if (impossibleBlock.body[0]?.[predicateSymbol]) continue;
1343
-
1344
- var fakeValues = new Array(predicateNumberCount)
1345
- .fill(0)
1346
- .map(() => getRandomInteger(-250, 250));
1347
- impossibleBlock.body.unshift(createAssignment(fakeValues));
1348
- }
1349
- }
1350
-
1351
- // Add scope initializations: scope["_0"] = {identity: "_0"}
1352
- for (const scopeManager of scopeToScopeManager.values()) {
1353
- if (scopeManager.isNotUsed) continue;
1354
- if (!scopeManager.requiresInitializing) continue;
1355
- if (scopeManager.initializingBasicBlock.label === startLabel)
1356
- continue;
1357
-
1358
- scopeManager.initializingBasicBlock.body.unshift(
1359
- scopeManager.getInitializingStatement()
1360
- );
1361
- }
1362
-
1363
- let switchCases: t.SwitchCase[] = [];
1364
- let blocks = Array.from(basicBlocks.values());
1365
- if (!isDebug && addFakeTests) {
1366
- shuffle(blocks);
1367
- }
1368
- for (const block of blocks) {
1369
- if (block.label === endLabel) {
1370
- // ok(block.body.length === 0);
1371
- continue;
1372
- }
1373
-
1374
- let test: t.Expression = numericLiteral(block.totalState);
1375
-
1376
- // Predicate tests cannot apply to the start label
1377
- // As that's when the numbers are initialized
1378
- if (
1379
- !isDebug &&
1380
- addPredicateTests &&
1381
- block.label !== startLabel &&
1382
- chance(50)
1383
- ) {
1384
- let predicateName = choice(Array.from(predicateNumbers.keys()));
1385
- if (predicateName) {
1386
- let number = predicateNumbers.get(predicateName);
1387
- let diff = block.totalState - number;
1388
-
1389
- test = t.binaryExpression(
1390
- "+",
1391
- mainScope.getMemberExpression(predicateName),
1392
- numericLiteral(diff)
1393
- );
1394
- }
1395
- }
1396
-
1397
- // Add complex tests
1398
- if (!isDebug && addComplexTests && chance(50)) {
1399
- // Create complex test expressions for each switch case
1400
-
1401
- // case STATE+X:
1402
- let stateVarIndex = getRandomInteger(0, stateVars.length);
1403
-
1404
- let stateValues = block.stateValues;
1405
- let difference = stateValues[stateVarIndex] - block.totalState;
1406
-
1407
- let conditionNodes: t.Expression[] = [];
1408
- let alreadyConditionedItems = new Set<string>();
1409
-
1410
- // This code finds clash conditions and adds them to 'conditionNodes' array
1411
- Array.from(basicBlocks.keys()).forEach((label) => {
1412
- if (label !== block.label) {
1413
- let labelStates = basicBlocks.get(label).stateValues;
1414
- let totalState = labelStates.reduce((a, b) => a + b, 0);
1415
-
1416
- if (totalState === labelStates[stateVarIndex] - difference) {
1417
- let differentIndex = labelStates.findIndex(
1418
- (v, i) => v !== stateValues[i]
1419
- );
1420
- if (differentIndex !== -1) {
1421
- let expressionAsString =
1422
- stateVars[differentIndex].name +
1423
- "!=" +
1424
- labelStates[differentIndex];
1425
- if (!alreadyConditionedItems.has(expressionAsString)) {
1426
- alreadyConditionedItems.add(expressionAsString);
1427
-
1428
- conditionNodes.push(
1429
- t.binaryExpression(
1430
- "!=",
1431
- deepClone(stateVars[differentIndex]),
1432
- numericLiteral(labelStates[differentIndex])
1433
- )
1434
- );
1435
- }
1436
- } else {
1437
- conditionNodes.push(
1438
- t.binaryExpression(
1439
- "!=",
1440
- deepClone(discriminant),
1441
- numericLiteral(totalState)
1442
- )
1443
- );
1444
- }
1445
- }
1446
- }
1447
- });
1448
-
1449
- // case STATE!=Y && STATE+X
1450
- test = t.binaryExpression(
1451
- "-",
1452
- deepClone(stateVars[stateVarIndex]),
1453
- numericLiteral(difference)
1454
- );
1455
-
1456
- // Use the 'conditionNodes' to not cause state clashing issues
1457
- conditionNodes.forEach((conditionNode) => {
1458
- test = t.logicalExpression("&&", conditionNode, test);
1459
- });
1460
- }
1461
-
1462
- const tests = [test];
1463
-
1464
- if (!isDebug && addFakeTests && chance(50)) {
1465
- // Add fake tests
1466
- let fakeTestCount = getRandomInteger(1, 3);
1467
- for (let i = 0; i < fakeTestCount; i++) {
1468
- tests.push(numericLiteral(stateIntGen.generate()));
1469
- }
1470
-
1471
- shuffle(tests);
1472
- }
1473
-
1474
- const lastTest = tests.pop();
1475
-
1476
- for (const test of tests) {
1477
- switchCases.push(t.switchCase(test, []));
1478
- }
1479
-
1480
- switchCases.push(t.switchCase(lastTest, block.thisPath.node.body));
1481
- }
1482
-
1483
- if (!isDebug && addFakeTests) {
1484
- // A random test can be 'default'
1485
- choice(switchCases).test = null;
1486
- }
1487
-
1488
- const discriminant = new Template(`
1489
- ${stateVars.map((x) => x.name).join(" + ")}
1490
- `).expression<t.Expression>();
1491
-
1492
- traverse(t.program([t.expressionStatement(discriminant)]), {
1493
- Identifier(path) {
1494
- (path.node as NodeSymbol)[NO_RENAME] = cffIndex;
1495
- },
1496
- });
1497
-
1498
- // Create a new SwitchStatement
1499
- const switchStatement = t.labeledStatement(
1500
- t.identifier(switchLabel),
1501
- t.switchStatement(discriminant, switchCases)
1502
- );
1503
-
1504
- const startStateValues = basicBlocks.get(startLabel).stateValues;
1505
- const endTotalState = basicBlocks.get(endLabel).totalState;
1506
-
1507
- const whileStatement = t.whileStatement(
1508
- t.binaryExpression(
1509
- "!==",
1510
- deepClone(discriminant),
1511
- numericLiteral(endTotalState)
1512
- ),
1513
- t.blockStatement([
1514
- t.withStatement(
1515
- new Template(`{withDiscriminant} || {scopeVar}`).expression({
1516
- withDiscriminant: deepClone(withMemberExpression),
1517
- scopeVar: deepClone(scopeVar),
1518
- }),
1519
- t.blockStatement([switchStatement])
1520
- ),
1521
- ])
1522
- );
1523
-
1524
- const parameters: t.Identifier[] = [
1525
- ...stateVars,
1526
- scopeVar,
1527
- argVar,
1528
- ].map((id) => deepClone(id));
1529
-
1530
- const parametersNames: string[] = parameters.map((id) => id.name);
1531
-
1532
- for (var [
1533
- originalFnName,
1534
- fnLabel,
1535
- basicBlock,
1536
- fn,
1537
- ] of functionExpressions) {
1538
- const { scopeManager } = basicBlock;
1539
- const { stateValues } = basicBlocks.get(fnLabel);
1540
-
1541
- const argumentsRestName = me.getPlaceholder();
1542
-
1543
- const argumentsNodes = [];
1544
- for (const parameterName of parametersNames) {
1545
- const stateIndex = stateVars
1546
- .map((x) => x.name)
1547
- .indexOf(parameterName);
1548
- if (stateIndex !== -1) {
1549
- argumentsNodes.push(numericLiteral(stateValues[stateIndex]));
1550
- } else if (parameterName === argVar.name) {
1551
- argumentsNodes.push(t.identifier(argumentsRestName));
1552
- } else if (parameterName === scopeVar.name) {
1553
- argumentsNodes.push(scopeManager.getObjectExpression(fnLabel));
1554
- } else {
1555
- ok(false);
1556
- }
1557
- }
1558
-
1559
- // Ensure parameter is added (No effect if not added in this case)
1560
- usedArgVar = true;
1561
-
1562
- Object.assign(
1563
- fn,
1564
- new Template(`
1565
- (function (...${argumentsRestName}){
1566
- ${
1567
- isDebug
1568
- ? `"Calling ${originalFnName}, Label: ${fnLabel}";`
1569
- : ""
1570
- }
1571
- return {callExpression}
1572
- })
1573
-
1574
- `).expression({
1575
- callExpression: t.callExpression(
1576
- deepClone(mainFnName),
1577
- argumentsNodes
1578
- ),
1579
- })
1580
- );
1581
- }
1582
-
1583
- const startProgramObjectExpression = basicBlocks
1584
- .get(startLabel)
1585
- .scopeManager.getObjectExpression(startLabel);
1586
-
1587
- const mainParameters: t.FunctionDeclaration["params"] = parameters;
1588
-
1589
- // First state values use the default parameter for initialization
1590
- // function main(..., scope = { mainScope: {} }, ...){...}
1591
- mainParameters.splice(
1592
- (mainParameters as t.Identifier[]).findIndex(
1593
- (p) => p.name === scopeVar.name
1594
- ),
1595
- 1,
1596
- t.assignmentPattern(
1597
- deepClone(scopeVar),
1598
- startProgramObjectExpression
1599
- )
1600
- );
1601
-
1602
- // Remove parameter 'argVar' if never used (No function calls obfuscated)
1603
- if (!usedArgVar) {
1604
- mainParameters.pop();
1605
- }
1606
-
1607
- const mainFnDeclaration = t.functionDeclaration(
1608
- deepClone(mainFnName),
1609
- parameters,
1610
- t.blockStatement([whileStatement])
1611
- );
1612
-
1613
- // The main function is always called with same number of arguments
1614
- (mainFnDeclaration as NodeSymbol)[PREDICTABLE] = true;
1615
-
1616
- var startProgramExpression = t.callExpression(deepClone(mainFnName), [
1617
- ...startStateValues.map((stateValue) => numericLiteral(stateValue)),
1618
- ]);
1619
-
1620
- const resultVar = withIdentifier("result");
1621
-
1622
- const isTopLevel = blockPath.isProgram();
1623
- const allowReturns =
1624
- !isTopLevel && blockPath.find((p) => p.isFunction());
1625
-
1626
- const startPrefix = allowReturns ? `var {resultVar} = ` : "";
1627
-
1628
- const startProgramStatements = new Template(`
1629
- ${allowReturns ? `var {didReturnVar};` : ""}
1630
- ${startPrefix}{startProgramExpression};
1631
- ${
1632
- allowReturns
1633
- ? `
1634
- if({didReturnVar}){
1635
- return {resultVar};
1636
- }`
1637
- : ""
1638
- }
1639
- `).compile({
1640
- startProgramExpression,
1641
- didReturnVar: () => deepClone(didReturnVar),
1642
- resultVar: () => deepClone(resultVar),
1643
- });
1644
-
1645
- blockPath.node.body = [
1646
- ...prependNodes,
1647
- mainFnDeclaration,
1648
- ...startProgramStatements,
1649
- ];
1650
-
1651
- functionsModified.add(programOrFunctionPath.node);
1652
-
1653
- // Reset all bindings here
1654
- blockPath.scope.bindings = Object.create(null);
1655
-
1656
- // Register new declarations
1657
- for (var node of blockPath.get("body")) {
1658
- blockPath.scope.registerDeclaration(node);
1659
- }
1660
- },
1661
- },
1662
- },
1663
- };
1664
- };
1
+ import traverse, { NodePath, Scope, Visitor } from "@babel/traverse";
2
+ import { PluginArg, PluginObject } from "./plugin";
3
+ import { Order } from "../order";
4
+ import {
5
+ ensureComputedExpression,
6
+ getParentFunctionOrProgram,
7
+ isDefiningIdentifier,
8
+ isStrictMode,
9
+ isVariableIdentifier,
10
+ replaceDefiningIdentifierToMemberExpression,
11
+ } from "../utils/ast-utils";
12
+ import * as t from "@babel/types";
13
+ import { numericLiteral, deepClone } from "../utils/node";
14
+ import Template from "../templates/template";
15
+ import {
16
+ chance,
17
+ choice,
18
+ getRandomInteger,
19
+ shuffle,
20
+ } from "../utils/random-utils";
21
+ import { IntGen } from "../utils/IntGen";
22
+ import { ok } from "assert";
23
+ import { NameGen } from "../utils/NameGen";
24
+ import {
25
+ NodeSymbol,
26
+ UNSAFE,
27
+ NO_RENAME,
28
+ PREDICTABLE,
29
+ variableFunctionName,
30
+ WITH_STATEMENT,
31
+ } from "../constants";
32
+
33
+ // Function deemed unsafe for CFF
34
+ const CFF_UNSAFE = Symbol("CFF_UNSAFE");
35
+
36
+ /**
37
+ * Breaks functions into DAGs (Directed Acyclic Graphs)
38
+ *
39
+ * - 1. Break functions into chunks
40
+ * - 2. Shuffle chunks but remember their original position
41
+ * - 3. Create a Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition.
42
+ *
43
+ * The Switch statement:
44
+ *
45
+ * - 1. The state variable controls which case will run next
46
+ * - 2. At the end of each case, the state variable is updated to the next block of code.
47
+ * - 3. The while loop continues until the the state variable is the end state.
48
+ */
49
+ export default ({ Plugin }: PluginArg): PluginObject => {
50
+ const me = Plugin(Order.ControlFlowFlattening, {
51
+ changeData: {
52
+ functions: 0,
53
+ blocks: 0,
54
+ ifStatements: 0,
55
+ deadCode: 0,
56
+ variables: 0,
57
+ },
58
+ });
59
+
60
+ // in Debug mode, the output is much easier to read
61
+ const isDebug = false;
62
+ const flattenIfStatements = true; // Converts IF-statements into equivalent 'goto style of code'
63
+ const flattenFunctionDeclarations = true; // Converts Function Declarations into equivalent 'goto style of code'
64
+ const addRelativeAssignments = true; // state += (NEW_STATE - CURRENT_STATE)
65
+ const addDeadCode = true; // add fakes chunks of code
66
+ const addFakeTests = true; // case 100: case 490: case 510: ...
67
+ const addComplexTests = true; // case s != 49 && s - 10:
68
+ const addPredicateTests = true; // case scope.A + 10: ...
69
+ const mangleNumericalLiterals = true; // 50 => state + X
70
+ const mangleBooleanLiterals = true; // true => state == X
71
+ const addWithStatement = true; // Disabling not supported yet
72
+ const addGeneratorFunction = true; // Wrap in generator function?
73
+
74
+ const cffPrefix = me.getPlaceholder();
75
+
76
+ // Amount of blocks changed by Control Flow Flattening
77
+ let cffCounter = 0;
78
+
79
+ const functionsModified: t.Node[] = [];
80
+
81
+ function flagFunctionToAvoid(path: NodePath, reason: string) {
82
+ var fnOrProgram = getParentFunctionOrProgram(path);
83
+
84
+ fnOrProgram.node[CFF_UNSAFE] = reason;
85
+ }
86
+
87
+ return {
88
+ post: () => {
89
+ for (var node of functionsModified) {
90
+ (node as NodeSymbol)[UNSAFE] = true;
91
+ }
92
+ },
93
+
94
+ visitor: {
95
+ // Unsafe detection
96
+ ThisExpression(path) {
97
+ flagFunctionToAvoid(path, "this");
98
+ },
99
+ VariableDeclaration(path) {
100
+ if (path.node.declarations.length !== 1) {
101
+ path.getAncestry().forEach((p) => {
102
+ p.node[CFF_UNSAFE] = "multipleDeclarations";
103
+ });
104
+ }
105
+ },
106
+ Identifier(path) {
107
+ if (
108
+ path.node.name === variableFunctionName ||
109
+ path.node.name === "arguments"
110
+ ) {
111
+ flagFunctionToAvoid(path, "arguments");
112
+ }
113
+ },
114
+ "Super|MetaProperty|AwaitExpression|YieldExpression"(path) {
115
+ flagFunctionToAvoid(path, "functionSpecific");
116
+ },
117
+
118
+ // Main CFF transformation
119
+ "Program|Function": {
120
+ exit(_path) {
121
+ let programOrFunctionPath = _path as NodePath<t.Program | t.Function>;
122
+
123
+ // Exclude loops
124
+ if (
125
+ programOrFunctionPath.find((p) => p.isForStatement() || p.isWhile())
126
+ )
127
+ return;
128
+
129
+ // Exclude 'CFF_UNSAFE' functions
130
+ if (programOrFunctionPath.node[CFF_UNSAFE]) return;
131
+
132
+ let programPath = _path.isProgram() ? _path : null;
133
+ let functionPath = _path.isFunction() ? _path : null;
134
+
135
+ let blockPath: NodePath<t.Block>;
136
+ if (programPath) {
137
+ blockPath = programPath;
138
+ } else {
139
+ let fnBlockPath = functionPath.get("body");
140
+ if (!fnBlockPath.isBlock()) return;
141
+ blockPath = fnBlockPath;
142
+ }
143
+
144
+ // Don't apply to strict mode blocks
145
+ const strictModeEnforcingBlock = programOrFunctionPath.find((path) =>
146
+ isStrictMode(path as NodePath<t.Block>),
147
+ );
148
+ if (strictModeEnforcingBlock) return;
149
+
150
+ // Must be at least 3 statements or more
151
+ if (blockPath.node.body.length < 3) return;
152
+
153
+ // Check user's threshold setting
154
+ if (!me.computeProbabilityMap(me.options.controlFlowFlattening)) {
155
+ return;
156
+ }
157
+
158
+ if (functionPath) {
159
+ // Avoid unsafe functions
160
+ if ((functionPath.node as NodeSymbol)[UNSAFE]) return;
161
+
162
+ if (functionPath.node.async || functionPath.node.generator) return;
163
+ }
164
+ programOrFunctionPath.scope.crawl();
165
+
166
+ let hasIllegalNode = false;
167
+ const bindingNames = new Set<string>();
168
+ blockPath.traverse({
169
+ Identifier(path) {
170
+ if (!path.isBindingIdentifier()) return;
171
+ const binding = path.scope.getBinding(path.node.name);
172
+ if (!binding) return;
173
+
174
+ let fnParent = path.getFunctionParent();
175
+ if (
176
+ path.key === "id" &&
177
+ path.parentPath.isFunctionDeclaration()
178
+ ) {
179
+ fnParent = path.parentPath.getFunctionParent();
180
+ }
181
+
182
+ if (fnParent !== functionPath) return;
183
+
184
+ if (!isDefiningIdentifier(path)) {
185
+ return;
186
+ }
187
+
188
+ if (bindingNames.has(path.node.name)) {
189
+ hasIllegalNode = true;
190
+ path.stop();
191
+ return;
192
+ }
193
+ bindingNames.add(path.node.name);
194
+ },
195
+ });
196
+
197
+ if (hasIllegalNode) {
198
+ return;
199
+ }
200
+
201
+ me.changeData.blocks++;
202
+
203
+ // Limit how many numbers get entangled
204
+ let mangledLiteralsCreated = 0;
205
+
206
+ const cffIndex = ++cffCounter; // Start from 1
207
+ const prefix = cffPrefix + "_" + cffIndex;
208
+
209
+ const withIdentifier = (suffix) => {
210
+ var name;
211
+ if (isDebug) {
212
+ name = prefix + "_" + suffix;
213
+ } else {
214
+ name = me.obfuscator.nameGen.generate(false);
215
+ }
216
+
217
+ var id = t.identifier(name);
218
+
219
+ (id as NodeSymbol)[NO_RENAME] = cffIndex;
220
+ return id;
221
+ };
222
+
223
+ const mainFnName = withIdentifier("main");
224
+
225
+ const scopeVar = withIdentifier("scope");
226
+
227
+ const stateVars = new Array(isDebug ? 1 : getRandomInteger(2, 5))
228
+ .fill("")
229
+ .map((_, i) => withIdentifier(`state_${i}`));
230
+
231
+ const argVar = withIdentifier("_arg");
232
+ let usedArgVar = false;
233
+
234
+ const didReturnVar = withIdentifier("return");
235
+
236
+ const basicBlocks = new Map<string, BasicBlock>();
237
+
238
+ // Map labels to states
239
+ const stateIntGen = new IntGen();
240
+
241
+ const defaultBlockPath = blockPath;
242
+
243
+ let scopeCounter = 0;
244
+
245
+ let scopeNameGen = new NameGen(me.options.identifierGenerator);
246
+ if (!isDebug) {
247
+ scopeNameGen = me.obfuscator.nameGen;
248
+ }
249
+
250
+ // Create 'with' object - Determines which scope gets top-level variable access
251
+ const withProperty = isDebug ? "with" : scopeNameGen.generate(false);
252
+ const withMemberExpression = new Template(
253
+ `${scopeVar.name}["${withProperty}"]`,
254
+ ).expression<t.MemberExpression>();
255
+ withMemberExpression.object[NO_RENAME] = cffIndex;
256
+
257
+ class ScopeManager {
258
+ isNotUsed = true;
259
+ requiresInitializing = true;
260
+
261
+ nameMap = new Map<string, string>();
262
+ nameGen = addWithStatement
263
+ ? me.obfuscator.nameGen
264
+ : new NameGen(me.options.identifierGenerator);
265
+
266
+ findBestWithDiscriminant(basicBlock: BasicBlock): ScopeManager {
267
+ // This initializing block is forbidden to have a with discriminant
268
+ // (As no previous code is able to prepare the with discriminant)
269
+ if (basicBlock !== this.initializingBasicBlock) {
270
+ // If no variables were defined in this scope, don't use it
271
+ if (Object.keys(this.scope.bindings).length > 0) return this;
272
+ }
273
+
274
+ return this.parent?.findBestWithDiscriminant(basicBlock);
275
+ }
276
+
277
+ getNewName(name: string, originalNode?: t.Node) {
278
+ if (!this.nameMap.has(name)) {
279
+ let newName = this.nameGen.generate(false);
280
+ if (isDebug) {
281
+ newName = "_" + name;
282
+ }
283
+
284
+ // console.log(name, newName);
285
+
286
+ this.nameMap.set(name, newName);
287
+
288
+ me.changeData.variables++;
289
+
290
+ // console.log(
291
+ // "Renaming " +
292
+ // name +
293
+ // " to " +
294
+ // newName +
295
+ // " : " +
296
+ // this.scope.path.type
297
+ // );
298
+
299
+ return newName;
300
+ }
301
+ return this.nameMap.get(name);
302
+ }
303
+
304
+ getScopeObject() {
305
+ return t.memberExpression(
306
+ deepClone(scopeVar),
307
+ t.stringLiteral(this.propertyName),
308
+ true,
309
+ );
310
+ }
311
+
312
+ getInitializingStatement() {
313
+ return t.expressionStatement(
314
+ t.assignmentExpression(
315
+ "=",
316
+ this.getScopeObject(),
317
+ this.getInitializingObjectExpression(),
318
+ ),
319
+ );
320
+ }
321
+
322
+ getInitializingObjectExpression() {
323
+ return isDebug
324
+ ? new Template(`
325
+ ({
326
+ identity: "${this.propertyName}"
327
+ })
328
+ `).expression()
329
+ : new Template(`({})`).expression();
330
+ }
331
+
332
+ getMemberExpression(
333
+ name: string,
334
+ object: t.Expression = this.getScopeObject(),
335
+ ) {
336
+ const memberExpression = t.memberExpression(
337
+ object,
338
+ t.stringLiteral(name),
339
+ true,
340
+ );
341
+
342
+ return memberExpression;
343
+ }
344
+
345
+ propertyName: string;
346
+ constructor(
347
+ public scope: Scope,
348
+ public initializingBasicBlock: BasicBlock,
349
+ ) {
350
+ this.propertyName = isDebug
351
+ ? "_" + cffIndex + "_" + scopeCounter++
352
+ : scopeNameGen.generate();
353
+ }
354
+
355
+ get parent() {
356
+ return scopeToScopeManager.get(this.scope.parent);
357
+ }
358
+
359
+ getObjectExpression(refreshLabel: string) {
360
+ let refreshScope = basicBlocks.get(refreshLabel).scopeManager;
361
+ let propertyMap: { [property: string]: t.Expression } = {};
362
+
363
+ let cursor = this.scope;
364
+ while (cursor) {
365
+ let parentScopeManager = scopeToScopeManager.get(cursor);
366
+ if (parentScopeManager) {
367
+ propertyMap[parentScopeManager.propertyName] =
368
+ t.memberExpression(
369
+ deepClone(scopeVar),
370
+ t.stringLiteral(parentScopeManager.propertyName),
371
+ true,
372
+ );
373
+ }
374
+
375
+ cursor = cursor.parent;
376
+ }
377
+
378
+ propertyMap[refreshScope.propertyName] =
379
+ refreshScope.getInitializingObjectExpression();
380
+
381
+ const properties: t.ObjectProperty[] = [];
382
+ for (const key in propertyMap) {
383
+ properties.push(
384
+ t.objectProperty(
385
+ t.stringLiteral(key),
386
+ propertyMap[key],
387
+ true,
388
+ ),
389
+ );
390
+ }
391
+
392
+ return t.objectExpression(properties);
393
+ }
394
+
395
+ hasOwnName(name: string) {
396
+ return this.nameMap.has(name);
397
+ }
398
+ }
399
+
400
+ const getImpossibleBasicBlocks = () => {
401
+ return Array.from(basicBlocks.values()).filter(
402
+ (block) => block.options.impossible,
403
+ );
404
+ };
405
+
406
+ const scopeToScopeManager = new Map<Scope, ScopeManager>();
407
+ /**
408
+ * A Basic Block is a sequence of instructions with no diversion except at the entry and exit points.
409
+ */
410
+ class BasicBlock {
411
+ totalState: number;
412
+ stateValues: number[];
413
+ allowWithDiscriminant = true;
414
+ bestWithDiscriminant: ScopeManager;
415
+
416
+ get withDiscriminant() {
417
+ if (!this.allowWithDiscriminant) return;
418
+
419
+ return this.bestWithDiscriminant;
420
+ }
421
+
422
+ private createPath() {
423
+ const newPath = NodePath.get<t.BlockStatement, any>({
424
+ hub: this.parentPath.hub,
425
+ parentPath: this.parentPath,
426
+ parent: this.parentPath.node,
427
+ container: this.parentPath.node.body,
428
+ listKey: "body", // Set the correct list key
429
+ key: "virtual", // Set the index of the new node
430
+ } as any);
431
+
432
+ newPath.scope = this.parentPath.scope;
433
+ newPath.parentPath = this.parentPath;
434
+ newPath.node = t.blockStatement([]);
435
+
436
+ this.thisPath = newPath;
437
+ this.thisNode = newPath.node;
438
+ }
439
+
440
+ insertAfter(newNode: t.Statement) {
441
+ this.body.push(newNode);
442
+ }
443
+
444
+ get scope() {
445
+ return this.parentPath.scope;
446
+ }
447
+
448
+ get scopeManager() {
449
+ return scopeToScopeManager.get(this.scope);
450
+ }
451
+
452
+ thisPath: NodePath<t.BlockStatement>;
453
+ thisNode: t.BlockStatement;
454
+
455
+ get body(): t.Statement[] {
456
+ return this.thisPath.node.body;
457
+ }
458
+
459
+ createFalsePredicate(): t.Expression {
460
+ var predicate = this.createPredicate();
461
+ if (predicate.value) {
462
+ // Make predicate false
463
+ return t.unaryExpression("!", predicate.node);
464
+ }
465
+ return predicate.node;
466
+ }
467
+
468
+ createTruePredicate(): t.Expression {
469
+ var predicate = this.createPredicate();
470
+ if (!predicate.value) {
471
+ // Make predicate true
472
+ return t.unaryExpression("!", predicate.node);
473
+ }
474
+ return predicate.node;
475
+ }
476
+
477
+ createPredicate() {
478
+ var stateVarIndex = getRandomInteger(0, stateVars.length);
479
+ var stateValue = this.stateValues[stateVarIndex];
480
+ var compareValue = choice([
481
+ stateValue,
482
+ getRandomInteger(-250, 250),
483
+ ]);
484
+
485
+ var operator: t.BinaryExpression["operator"] = choice([
486
+ "==",
487
+ "!=",
488
+ "<",
489
+ ">",
490
+ ]);
491
+ var compareResult;
492
+ switch (operator) {
493
+ case "==":
494
+ compareResult = stateValue === compareValue;
495
+ break;
496
+ case "!=":
497
+ compareResult = stateValue !== compareValue;
498
+ break;
499
+ case "<":
500
+ compareResult = stateValue < compareValue;
501
+ break;
502
+ case ">":
503
+ compareResult = stateValue > compareValue;
504
+ break;
505
+ }
506
+
507
+ return {
508
+ node: t.binaryExpression(
509
+ operator,
510
+ deepClone(stateVars[stateVarIndex]),
511
+ numericLiteral(compareValue),
512
+ ),
513
+ value: compareResult,
514
+ };
515
+ }
516
+
517
+ identifier(identifierName: string, scopeManager: ScopeManager) {
518
+ if (
519
+ this.withDiscriminant &&
520
+ this.withDiscriminant === scopeManager
521
+ ) {
522
+ var id = t.identifier(identifierName);
523
+ (id as NodeSymbol)[NO_RENAME] = cffIndex;
524
+ (id as NodeSymbol)[WITH_STATEMENT] = true;
525
+ return id;
526
+ }
527
+
528
+ return scopeManager.getMemberExpression(identifierName);
529
+ }
530
+
531
+ initializedScope: ScopeManager;
532
+
533
+ constructor(
534
+ public label: string,
535
+ public parentPath: NodePath<t.Block>,
536
+ public options: { impossible?: boolean } = {},
537
+ ) {
538
+ this.createPath();
539
+
540
+ if (isDebug) {
541
+ // States in debug mode are just 1, 2, 3, ...
542
+ this.totalState = basicBlocks.size + 1;
543
+ } else {
544
+ this.totalState = stateIntGen.generate();
545
+ }
546
+
547
+ // Correct state values
548
+ // Start with random numbers
549
+ this.stateValues = stateVars.map(() =>
550
+ getRandomInteger(-250, 250),
551
+ );
552
+
553
+ // Try to re-use old state values to make diffs smaller
554
+ if (basicBlocks.size > 1) {
555
+ const lastBlock = [...basicBlocks.values()].at(-1);
556
+ this.stateValues = lastBlock.stateValues.map((oldValue, i) => {
557
+ return choice([oldValue, this.stateValues[i]]);
558
+ });
559
+ }
560
+
561
+ // Correct one of the values so that the accumulated sum is equal to the state
562
+ const correctIndex = getRandomInteger(0, this.stateValues.length);
563
+
564
+ const getCurrentState = () =>
565
+ this.stateValues.reduce((a, b) => a + b, 0);
566
+
567
+ // Correct the value
568
+ this.stateValues[correctIndex] =
569
+ this.totalState -
570
+ (getCurrentState() - this.stateValues[correctIndex]);
571
+
572
+ ok(getCurrentState() === this.totalState);
573
+
574
+ // Store basic block
575
+ basicBlocks.set(label, this);
576
+
577
+ // Create a new scope manager if it doesn't exist
578
+ if (!scopeToScopeManager.has(this.scope)) {
579
+ scopeToScopeManager.set(
580
+ this.scope,
581
+ new ScopeManager(this.scope, this),
582
+ );
583
+ }
584
+
585
+ this.initializedScope = this.scopeManager;
586
+ }
587
+ }
588
+
589
+ /**
590
+ * Stage 1: Flatten the code into Basic Blocks
591
+ *
592
+ * This involves transforming the Control Flow / Scopes into blocks with 'goto' statements
593
+ *
594
+ * - A block is simply a sequence of statements
595
+ * - A block can have a 'goto' statement to another block
596
+ * - A block original scope is preserved
597
+ *
598
+ * 'goto' & Scopes are transformed in Stage 2
599
+ */
600
+
601
+ const switchLabel = me.getPlaceholder();
602
+ const breakStatement = () => {
603
+ return t.breakStatement(t.identifier(switchLabel));
604
+ };
605
+
606
+ const startLabel = me.getPlaceholder();
607
+ const endLabel = me.getPlaceholder();
608
+
609
+ let currentBasicBlock = new BasicBlock(startLabel, blockPath);
610
+ currentBasicBlock.allowWithDiscriminant = false;
611
+
612
+ const gotoFunctionName =
613
+ "GOTO__" +
614
+ me.getPlaceholder() +
615
+ "__IF_YOU_CAN_READ_THIS_THERE_IS_A_BUG";
616
+
617
+ function GotoControlStatement(label: string) {
618
+ return new Template(`
619
+ ${gotoFunctionName}("${label}");
620
+ `).single();
621
+ }
622
+
623
+ // Ends the current block and starts a new one
624
+ function endCurrentBasicBlock({
625
+ jumpToNext = true,
626
+ nextLabel = me.getPlaceholder(),
627
+ prevJumpTo = null,
628
+ nextBlockPath = null,
629
+ } = {}) {
630
+ ok(nextBlockPath);
631
+
632
+ if (prevJumpTo) {
633
+ currentBasicBlock.insertAfter(GotoControlStatement(prevJumpTo));
634
+ } else if (jumpToNext) {
635
+ currentBasicBlock.insertAfter(GotoControlStatement(nextLabel));
636
+ }
637
+
638
+ currentBasicBlock = new BasicBlock(nextLabel, nextBlockPath);
639
+ }
640
+
641
+ const prependNodes: t.Statement[] = [];
642
+ const functionExpressions: [
643
+ string,
644
+ string,
645
+ BasicBlock,
646
+ t.FunctionExpression,
647
+ ][] = [];
648
+
649
+ function flattenIntoBasicBlocks(
650
+ bodyIn: NodePath<t.Statement>[] | NodePath<t.Block>,
651
+ ) {
652
+ // if (!Array.isArray(bodyIn) && bodyIn.isBlock()) {
653
+ // currentBasicBlock.parentPath = bodyIn;
654
+ // }
655
+ const body = Array.isArray(bodyIn) ? bodyIn : bodyIn.get("body");
656
+ const nextBlockPath = Array.isArray(bodyIn)
657
+ ? currentBasicBlock.parentPath
658
+ : bodyIn;
659
+
660
+ for (const index in body) {
661
+ const statement = body[index];
662
+
663
+ // Keep Imports before everything else
664
+ if (statement.isImportDeclaration()) {
665
+ prependNodes.push(statement.node);
666
+ continue;
667
+ }
668
+
669
+ if (statement.isFunctionDeclaration()) {
670
+ const fnName = statement.node.id.name;
671
+ let isIllegal = false;
672
+
673
+ if (
674
+ !flattenFunctionDeclarations ||
675
+ statement.node.async ||
676
+ statement.node.generator ||
677
+ (statement.node as NodeSymbol)[UNSAFE] ||
678
+ (statement.node as NodeSymbol)[CFF_UNSAFE] ||
679
+ isStrictMode(statement)
680
+ ) {
681
+ isIllegal = true;
682
+ }
683
+ let oldBasicBlock = currentBasicBlock;
684
+ let fnLabel = me.getPlaceholder();
685
+
686
+ let sm = currentBasicBlock.scopeManager;
687
+ let rename = sm.getNewName(fnName);
688
+
689
+ sm.scope.bindings[fnName].kind = "var";
690
+
691
+ const hoistedBasicBlock = Array.from(basicBlocks.values()).find(
692
+ (block) => block.parentPath === currentBasicBlock.parentPath,
693
+ );
694
+
695
+ if (isIllegal) {
696
+ hoistedBasicBlock.body.unshift(statement.node);
697
+ continue;
698
+ }
699
+
700
+ me.changeData.functions++;
701
+
702
+ const functionExpression = t.functionExpression(
703
+ null,
704
+ [],
705
+ t.blockStatement([]),
706
+ );
707
+ functionExpressions.push([
708
+ fnName,
709
+ fnLabel,
710
+ currentBasicBlock,
711
+ functionExpression,
712
+ ]);
713
+
714
+ // Change the function declaration to a variable declaration
715
+ hoistedBasicBlock.body.unshift(
716
+ t.variableDeclaration("var", [
717
+ t.variableDeclarator(
718
+ t.identifier(fnName),
719
+ functionExpression,
720
+ ),
721
+ ]),
722
+ );
723
+
724
+ const blockStatement = statement.get("body");
725
+
726
+ endCurrentBasicBlock({
727
+ nextLabel: fnLabel,
728
+ nextBlockPath: blockStatement,
729
+ jumpToNext: false,
730
+ });
731
+ let fnTopBlock = currentBasicBlock;
732
+
733
+ // Implicit return
734
+ blockStatement.node.body.push(
735
+ t.returnStatement(t.identifier("undefined")),
736
+ );
737
+
738
+ flattenIntoBasicBlocks(blockStatement);
739
+ scopeToScopeManager.get(statement.scope).requiresInitializing =
740
+ false;
741
+ basicBlocks.get(fnLabel).allowWithDiscriminant = false;
742
+
743
+ // Debug label
744
+ if (isDebug) {
745
+ fnTopBlock.body.unshift(
746
+ t.expressionStatement(
747
+ t.stringLiteral(
748
+ "Function " +
749
+ statement.node.id.name +
750
+ " -> Renamed to " +
751
+ rename,
752
+ ),
753
+ ),
754
+ );
755
+ }
756
+
757
+ // Unpack parameters from the parameter 'argVar'
758
+ if (statement.node.params.length > 0) {
759
+ usedArgVar = true;
760
+ fnTopBlock.body.unshift(
761
+ t.variableDeclaration("var", [
762
+ t.variableDeclarator(
763
+ t.arrayPattern(statement.node.params),
764
+ deepClone(argVar),
765
+ ),
766
+ ]),
767
+ );
768
+
769
+ // Change bindings from 'param' to 'var'
770
+ statement.get("params").forEach((param) => {
771
+ let ids = param.getBindingIdentifierPaths();
772
+ // Loop over the record of binding identifiers
773
+ for (const identifierName in ids) {
774
+ const identifierPath = ids[identifierName];
775
+ if (identifierPath.getFunctionParent() === statement) {
776
+ const binding =
777
+ statement.scope.getBinding(identifierName);
778
+
779
+ if (binding) {
780
+ binding.kind = "var";
781
+ }
782
+ }
783
+ }
784
+ });
785
+ }
786
+
787
+ currentBasicBlock = oldBasicBlock;
788
+ continue;
789
+ }
790
+
791
+ // Convert IF statements into Basic Blocks
792
+ if (statement.isIfStatement() && flattenIfStatements) {
793
+ const test = statement.get("test");
794
+ const consequent = statement.get("consequent");
795
+ const alternate = statement.get("alternate");
796
+
797
+ // Both consequent and alternate are blocks
798
+ if (
799
+ consequent.isBlockStatement() &&
800
+ (!alternate.node || alternate.isBlockStatement())
801
+ ) {
802
+ me.changeData.ifStatements++;
803
+
804
+ const consequentLabel = me.getPlaceholder();
805
+ const alternateLabel = alternate.node
806
+ ? me.getPlaceholder()
807
+ : null;
808
+ const afterPath = me.getPlaceholder();
809
+
810
+ currentBasicBlock.insertAfter(
811
+ t.ifStatement(
812
+ test.node,
813
+ GotoControlStatement(consequentLabel),
814
+ alternateLabel
815
+ ? GotoControlStatement(alternateLabel)
816
+ : GotoControlStatement(afterPath),
817
+ ),
818
+ );
819
+
820
+ const oldBasicBlock = currentBasicBlock;
821
+
822
+ endCurrentBasicBlock({
823
+ jumpToNext: false,
824
+ nextLabel: consequentLabel,
825
+ nextBlockPath: consequent,
826
+ });
827
+
828
+ flattenIntoBasicBlocks(consequent);
829
+ currentBasicBlock.initializedScope =
830
+ oldBasicBlock.scopeManager;
831
+
832
+ if (alternate.isBlockStatement()) {
833
+ endCurrentBasicBlock({
834
+ prevJumpTo: afterPath,
835
+ nextLabel: alternateLabel,
836
+ nextBlockPath: alternate,
837
+ });
838
+
839
+ flattenIntoBasicBlocks(alternate);
840
+ }
841
+
842
+ endCurrentBasicBlock({
843
+ prevJumpTo: afterPath,
844
+ nextLabel: afterPath,
845
+ nextBlockPath: oldBasicBlock.parentPath,
846
+ });
847
+
848
+ continue;
849
+ }
850
+ }
851
+
852
+ if (
853
+ Number(index) === body.length - 1 &&
854
+ statement.isExpressionStatement() &&
855
+ statement.findParent((p) => p.isBlock()) === blockPath &&
856
+ programPath // <- ONLY APPLY TO TOP-LEVEL NEVER FUNCTIONS!
857
+ ) {
858
+ // Return the result of the last expression for eval() purposes
859
+ currentBasicBlock.insertAfter(
860
+ t.returnStatement(statement.get("expression").node),
861
+ );
862
+ continue;
863
+ }
864
+
865
+ // 3 or more statements should be split more
866
+ if (
867
+ currentBasicBlock.body.length > 1 &&
868
+ chance(50 + currentBasicBlock.body.length)
869
+ ) {
870
+ endCurrentBasicBlock({
871
+ nextBlockPath: nextBlockPath,
872
+ });
873
+ }
874
+
875
+ // console.log(currentBasicBlock.thisPath.type);
876
+ // console.log(currentBasicBlock.body);
877
+ currentBasicBlock.body.push(statement.node);
878
+ }
879
+ }
880
+
881
+ // Convert our code into Basic Blocks
882
+ flattenIntoBasicBlocks(blockPath.get("body"));
883
+
884
+ // Ensure always jumped to the Program end
885
+ endCurrentBasicBlock({
886
+ jumpToNext: true,
887
+ nextLabel: endLabel,
888
+ nextBlockPath: defaultBlockPath,
889
+ });
890
+
891
+ basicBlocks.get(endLabel).allowWithDiscriminant = false;
892
+
893
+ if (!isDebug && addDeadCode) {
894
+ // DEAD CODE 1/3: Add fake chunks that are never reached
895
+ const fakeChunkCount = getRandomInteger(1, 5);
896
+ for (let i = 0; i < fakeChunkCount; i++) {
897
+ // These chunks just jump somewhere random, they are never executed
898
+ // so it could contain any code
899
+ const fakeBlock = new BasicBlock(me.getPlaceholder(), blockPath, {
900
+ impossible: true,
901
+ });
902
+ let fakeJump;
903
+ do {
904
+ fakeJump = choice(Array.from(basicBlocks.keys()));
905
+ } while (fakeJump === fakeBlock.label);
906
+
907
+ fakeBlock.insertAfter(GotoControlStatement(fakeJump));
908
+ me.changeData.deadCode++;
909
+ }
910
+
911
+ // DEAD CODE 2/3: Add fake jumps to really mess with deobfuscators
912
+ // "irreducible control flow"
913
+ basicBlocks.forEach((basicBlock) => {
914
+ if (chance(30 - basicBlocks.size)) {
915
+ let randomLabel = choice(Array.from(basicBlocks.keys()));
916
+
917
+ // The `false` literal will be mangled
918
+ basicBlock.insertAfter(
919
+ new Template(`
920
+ if({predicate}){
921
+ {goto}
922
+ }
923
+ `).single({
924
+ goto: GotoControlStatement(randomLabel),
925
+ predicate: basicBlock.createFalsePredicate(),
926
+ }),
927
+ );
928
+
929
+ me.changeData.deadCode++;
930
+ }
931
+ });
932
+ // DEAD CODE 3/3: Clone chunks but these chunks are never ran
933
+ const cloneChunkCount = getRandomInteger(1, 5);
934
+ for (let i = 0; i < cloneChunkCount; i++) {
935
+ let randomChunk = choice(Array.from(basicBlocks.values()));
936
+
937
+ // Don't double define functions
938
+ let hasDeclaration = randomChunk.body.find((stmt) => {
939
+ return t.isDeclaration(stmt);
940
+ });
941
+
942
+ if (!hasDeclaration) {
943
+ let clonedChunk = new BasicBlock(
944
+ me.getPlaceholder(),
945
+ randomChunk.parentPath,
946
+ {
947
+ impossible: true,
948
+ },
949
+ );
950
+
951
+ randomChunk.thisNode.body
952
+ .map((x) => deepClone(x))
953
+ .forEach((node) => {
954
+ if (node.type === "EmptyStatement") return;
955
+ clonedChunk.insertAfter(node);
956
+ });
957
+
958
+ me.changeData.deadCode++;
959
+ }
960
+ }
961
+ }
962
+
963
+ // Select scope managers for the with statement
964
+ for (const basicBlock of basicBlocks.values()) {
965
+ basicBlock.bestWithDiscriminant =
966
+ basicBlock.initializedScope?.findBestWithDiscriminant(basicBlock);
967
+
968
+ if (isDebug && basicBlock.withDiscriminant) {
969
+ basicBlock.body.unshift(
970
+ t.expressionStatement(
971
+ t.stringLiteral(
972
+ "With " + basicBlock.withDiscriminant.propertyName,
973
+ ),
974
+ ),
975
+ );
976
+ }
977
+ }
978
+
979
+ /**
980
+ * Stage 2: Transform 'goto' statements into valid JavaScript
981
+ *
982
+ * - 'goto' is replaced with equivalent state updates and break statements
983
+ * - Original identifiers are converted into member expressions
984
+ */
985
+
986
+ // Remap 'GotoStatement' to actual state assignments and Break statements
987
+ for (const basicBlock of basicBlocks.values()) {
988
+ const { stateValues: currentStateValues } = basicBlock;
989
+ // Wrap the statement in a Babel path to allow traversal
990
+
991
+ const outerFn = getParentFunctionOrProgram(basicBlock.parentPath);
992
+
993
+ function isWithinSameFunction(path: NodePath) {
994
+ let fn = getParentFunctionOrProgram(path);
995
+ return fn.node === outerFn.node;
996
+ }
997
+
998
+ let visitor: Visitor = {
999
+ BooleanLiteral: {
1000
+ exit(boolPath) {
1001
+ // Don't mangle booleans in debug mode
1002
+ if (
1003
+ isDebug ||
1004
+ !mangleBooleanLiterals ||
1005
+ me.isSkipped(boolPath)
1006
+ )
1007
+ return;
1008
+
1009
+ if (!isWithinSameFunction(boolPath)) return;
1010
+ if (chance(50 + mangledLiteralsCreated)) return;
1011
+
1012
+ mangledLiteralsCreated++;
1013
+
1014
+ const index = getRandomInteger(0, stateVars.length - 1);
1015
+ const stateVar = stateVars[index];
1016
+ const stateVarValue = currentStateValues[index];
1017
+
1018
+ const compareValue = choice([
1019
+ getRandomInteger(-250, 250),
1020
+ stateVarValue,
1021
+ ]);
1022
+ const compareResult = stateVarValue === compareValue;
1023
+
1024
+ const newExpression = t.binaryExpression(
1025
+ boolPath.node.value === compareResult ? "==" : "!=",
1026
+ deepClone(stateVar),
1027
+ numericLiteral(compareValue),
1028
+ );
1029
+
1030
+ ensureComputedExpression(boolPath);
1031
+ boolPath.replaceWith(newExpression);
1032
+ },
1033
+ },
1034
+ // Mangle numbers with the state values
1035
+ NumericLiteral: {
1036
+ exit(numPath) {
1037
+ // Don't mangle numbers in debug mode
1038
+ if (
1039
+ isDebug ||
1040
+ !mangleNumericalLiterals ||
1041
+ me.isSkipped(numPath)
1042
+ )
1043
+ return;
1044
+
1045
+ const num = numPath.node.value;
1046
+ if (
1047
+ Math.floor(num) !== num ||
1048
+ Math.abs(num) > 100_000 ||
1049
+ !Number.isFinite(num) ||
1050
+ Number.isNaN(num)
1051
+ )
1052
+ return;
1053
+
1054
+ if (!isWithinSameFunction(numPath)) return;
1055
+ if (chance(50 + mangledLiteralsCreated)) return;
1056
+
1057
+ mangledLiteralsCreated++;
1058
+
1059
+ const index = getRandomInteger(0, stateVars.length - 1);
1060
+ const stateVar = stateVars[index];
1061
+
1062
+ // num = 50
1063
+ // stateVar = 30
1064
+ // stateVar + 30
1065
+
1066
+ const diff = t.binaryExpression(
1067
+ "+",
1068
+ deepClone(stateVar),
1069
+ me.skip(numericLiteral(num - currentStateValues[index])),
1070
+ );
1071
+
1072
+ ensureComputedExpression(numPath);
1073
+
1074
+ numPath.replaceWith(diff);
1075
+ numPath.skip();
1076
+ },
1077
+ },
1078
+
1079
+ Identifier: {
1080
+ exit(path: NodePath<t.Identifier>) {
1081
+ if (!isVariableIdentifier(path)) return;
1082
+ if (me.isSkipped(path)) return;
1083
+ if ((path.node as NodeSymbol)[NO_RENAME] === cffIndex) return;
1084
+ // For identifiers using implicit with discriminant, skip
1085
+ if ((path.node as NodeSymbol)[WITH_STATEMENT]) return;
1086
+
1087
+ const identifierName = path.node.name;
1088
+ if (identifierName === gotoFunctionName) return;
1089
+
1090
+ var binding = path.scope.getBinding(identifierName);
1091
+ if (!binding) {
1092
+ return;
1093
+ }
1094
+
1095
+ if (
1096
+ binding.kind === "var" ||
1097
+ binding.kind === "let" ||
1098
+ binding.kind === "const"
1099
+ ) {
1100
+ } else {
1101
+ return;
1102
+ }
1103
+
1104
+ var scopeManager = scopeToScopeManager.get(binding.scope);
1105
+ if (!scopeManager) return;
1106
+
1107
+ let newName = scopeManager.getNewName(
1108
+ identifierName,
1109
+ path.node,
1110
+ );
1111
+
1112
+ let memberExpression: t.MemberExpression | t.Identifier =
1113
+ scopeManager.getMemberExpression(newName);
1114
+
1115
+ scopeManager.isNotUsed = false;
1116
+
1117
+ // Scope object as with discriminant? Use identifier
1118
+ if (typeof basicBlock.withDiscriminant === "undefined") {
1119
+ const id = t.identifier(scopeManager.propertyName);
1120
+ (id as NodeSymbol)[WITH_STATEMENT] = true;
1121
+ (id as NodeSymbol)[NO_RENAME] = cffIndex;
1122
+
1123
+ memberExpression = scopeManager.getMemberExpression(
1124
+ newName,
1125
+ id,
1126
+ );
1127
+ }
1128
+
1129
+ if (isDefiningIdentifier(path)) {
1130
+ replaceDefiningIdentifierToMemberExpression(
1131
+ path,
1132
+ memberExpression,
1133
+ );
1134
+ return;
1135
+ }
1136
+
1137
+ if (!path.container) return;
1138
+
1139
+ if (
1140
+ basicBlock.withDiscriminant &&
1141
+ basicBlock.withDiscriminant === scopeManager &&
1142
+ basicBlock.withDiscriminant.hasOwnName(identifierName)
1143
+ ) {
1144
+ // The defining mode must directly append to the scope object
1145
+ // Subsequent uses can use the identifier
1146
+ const isDefiningNode = path.node === binding.identifier;
1147
+
1148
+ if (!isDefiningNode) {
1149
+ memberExpression = basicBlock.identifier(
1150
+ newName,
1151
+ scopeManager,
1152
+ );
1153
+ }
1154
+ }
1155
+
1156
+ me.skip(memberExpression);
1157
+
1158
+ path.replaceWith(memberExpression);
1159
+ path.skip();
1160
+
1161
+ // Preserve proper 'this' context when directly calling functions
1162
+ // X.Y.Z() -> (1, X.Y.Z)()
1163
+ if (
1164
+ path.parentPath.isCallExpression() &&
1165
+ path.key === "callee"
1166
+ ) {
1167
+ path.replaceWith(
1168
+ t.sequenceExpression([t.numericLiteral(1), path.node]),
1169
+ );
1170
+ }
1171
+ },
1172
+ },
1173
+
1174
+ // Top-level returns set additional flag to indicate that the function has returned
1175
+ ReturnStatement: {
1176
+ exit(path) {
1177
+ var functionParent = path.getFunctionParent();
1178
+ if (
1179
+ !functionParent ||
1180
+ functionParent.get("body") !== blockPath
1181
+ )
1182
+ return;
1183
+
1184
+ const returnArgument =
1185
+ path.node.argument || t.identifier("undefined");
1186
+
1187
+ path.node.argument = new Template(`
1188
+ ({didReturnVar} = true, {returnArgument})
1189
+ `).expression({
1190
+ returnArgument,
1191
+ didReturnVar: deepClone(didReturnVar),
1192
+ });
1193
+ },
1194
+ },
1195
+
1196
+ // goto() calls are replaced with state updates and break statements
1197
+ CallExpression: {
1198
+ exit(path) {
1199
+ if (
1200
+ t.isIdentifier(path.node.callee) &&
1201
+ path.node.callee.name === gotoFunctionName
1202
+ ) {
1203
+ const [labelNode] = path.node.arguments;
1204
+
1205
+ ok(t.isStringLiteral(labelNode));
1206
+ const label = labelNode.value;
1207
+
1208
+ const jumpBlock = basicBlocks.get(label);
1209
+ ok(jumpBlock, "Label not found: " + label);
1210
+
1211
+ const {
1212
+ stateValues: newStateValues,
1213
+ totalState: newTotalState,
1214
+ } = jumpBlock;
1215
+
1216
+ const assignments: t.Expression[] = [];
1217
+
1218
+ if (jumpBlock.withDiscriminant) {
1219
+ assignments.push(
1220
+ t.assignmentExpression(
1221
+ "=",
1222
+ deepClone(withMemberExpression),
1223
+ jumpBlock.withDiscriminant.getScopeObject(),
1224
+ ),
1225
+ );
1226
+ } else if (basicBlock.withDiscriminant) {
1227
+ // Reset the with discriminant to undefined using fake property
1228
+ // scope["fake"] -> undefined
1229
+
1230
+ const fakeProperty = scopeNameGen.generate();
1231
+
1232
+ assignments.push(
1233
+ t.assignmentExpression(
1234
+ "=",
1235
+ deepClone(withMemberExpression),
1236
+ t.memberExpression(
1237
+ deepClone(scopeVar),
1238
+ t.stringLiteral(fakeProperty),
1239
+ true,
1240
+ ),
1241
+ ),
1242
+ );
1243
+ }
1244
+
1245
+ let mutatingStateValues = [...currentStateValues];
1246
+
1247
+ for (let i = 0; i < stateVars.length; i++) {
1248
+ const oldValue = currentStateValues[i];
1249
+ const newValue = newStateValues[i];
1250
+
1251
+ // console.log(oldValue, newValue);
1252
+ if (oldValue === newValue) continue; // No diff needed if the value doesn't change
1253
+
1254
+ let assignment = t.assignmentExpression(
1255
+ "=",
1256
+ deepClone(stateVars[i]),
1257
+ numericLiteral(newValue),
1258
+ );
1259
+
1260
+ if (!isDebug && addRelativeAssignments) {
1261
+ // Use two level of diffs to create confusing code:
1262
+ // stateVar += anotherStateVar - diff
1263
+ // has the effect of:
1264
+ // stateVar = newState
1265
+
1266
+ function mangledNumericLiteral(value: number) {
1267
+ let stateVarIndex;
1268
+ do {
1269
+ stateVarIndex = getRandomInteger(
1270
+ 0,
1271
+ stateVars.length,
1272
+ );
1273
+ } while (stateVarIndex === i && stateVars.length > 1);
1274
+
1275
+ const stateVarId = stateVars[stateVarIndex];
1276
+ const stateVarValue =
1277
+ mutatingStateValues[stateVarIndex];
1278
+ const diff = stateVarValue - value;
1279
+
1280
+ return t.binaryExpression(
1281
+ "-",
1282
+ deepClone(stateVarId),
1283
+ numericLiteral(diff),
1284
+ );
1285
+ }
1286
+
1287
+ assignment = t.assignmentExpression(
1288
+ "+=",
1289
+ deepClone(stateVars[i]),
1290
+ mangledNumericLiteral(newValue - oldValue),
1291
+ );
1292
+ }
1293
+
1294
+ mutatingStateValues[i] = newValue;
1295
+
1296
+ assignments.push(assignment);
1297
+ }
1298
+
1299
+ // Add debug label
1300
+ if (isDebug) {
1301
+ assignments.unshift(
1302
+ t.stringLiteral("Goto " + newTotalState),
1303
+ );
1304
+ }
1305
+
1306
+ path.parentPath
1307
+ .replaceWith(
1308
+ t.expressionStatement(
1309
+ t.sequenceExpression(assignments),
1310
+ ),
1311
+ )[0]
1312
+ .skip();
1313
+
1314
+ // Add break after updating state variables
1315
+ path.insertAfter(breakStatement());
1316
+ }
1317
+ },
1318
+ },
1319
+ };
1320
+
1321
+ basicBlock.thisPath.traverse(visitor);
1322
+ }
1323
+
1324
+ /**
1325
+ * Stage 3: Create a switch statement to handle the control flow
1326
+ *
1327
+ * - Add fake / impossible blocks
1328
+ * - Add fake / predicates to the switch cases tests
1329
+ */
1330
+
1331
+ // Create global numbers for predicates
1332
+ const mainScope = basicBlocks.get(startLabel).scopeManager;
1333
+ const predicateNumbers = new Map<string, number>();
1334
+ const predicateNumberCount =
1335
+ isDebug || !addPredicateTests ? 0 : getRandomInteger(1, 4);
1336
+
1337
+ for (let i = 0; i < predicateNumberCount; i++) {
1338
+ const name = mainScope.getNewName(
1339
+ me.getPlaceholder("predicate_" + i),
1340
+ );
1341
+
1342
+ const number = getRandomInteger(-250, 250);
1343
+ predicateNumbers.set(name, number);
1344
+ }
1345
+
1346
+ const predicateSymbol = Symbol("predicate");
1347
+
1348
+ const createAssignment = (values: number[]) => {
1349
+ var exprStmt = new Template(`
1350
+ ({predicateVariables} = {values})
1351
+ `).single({
1352
+ predicateVariables: t.arrayPattern(
1353
+ Array.from(predicateNumbers.keys()).map((name) =>
1354
+ mainScope.getMemberExpression(name),
1355
+ ),
1356
+ ),
1357
+ values: t.arrayExpression(
1358
+ values.map((value) => numericLiteral(value)),
1359
+ ),
1360
+ });
1361
+
1362
+ exprStmt[predicateSymbol] = true;
1363
+
1364
+ return exprStmt;
1365
+ };
1366
+
1367
+ basicBlocks
1368
+ .get(startLabel)
1369
+ .body.unshift(
1370
+ createAssignment(Array.from(predicateNumbers.values())),
1371
+ );
1372
+
1373
+ // Add random assignments to impossible blocks
1374
+ var fakeAssignmentCount = getRandomInteger(1, 3);
1375
+
1376
+ for (let i = 0; i < fakeAssignmentCount; i++) {
1377
+ var impossibleBlock = choice(getImpossibleBasicBlocks());
1378
+ if (impossibleBlock) {
1379
+ if (impossibleBlock.body[0]?.[predicateSymbol]) continue;
1380
+
1381
+ var fakeValues = new Array(predicateNumberCount)
1382
+ .fill(0)
1383
+ .map(() => getRandomInteger(-250, 250));
1384
+ impossibleBlock.body.unshift(createAssignment(fakeValues));
1385
+ }
1386
+ }
1387
+
1388
+ // Add scope initializations: scope["_0"] = {identity: "_0"}
1389
+ for (const scopeManager of scopeToScopeManager.values()) {
1390
+ if (scopeManager.isNotUsed) continue;
1391
+ if (!scopeManager.requiresInitializing) continue;
1392
+ if (scopeManager.initializingBasicBlock.label === startLabel)
1393
+ continue;
1394
+
1395
+ scopeManager.initializingBasicBlock.body.unshift(
1396
+ scopeManager.getInitializingStatement(),
1397
+ );
1398
+ }
1399
+
1400
+ let switchCases: t.SwitchCase[] = [];
1401
+ let blocks = Array.from(basicBlocks.values());
1402
+ if (!isDebug && addFakeTests) {
1403
+ shuffle(blocks);
1404
+ }
1405
+ for (const block of blocks) {
1406
+ if (block.label === endLabel) {
1407
+ // ok(block.body.length === 0);
1408
+ continue;
1409
+ }
1410
+
1411
+ let test: t.Expression = numericLiteral(block.totalState);
1412
+
1413
+ // Predicate tests cannot apply to the start label
1414
+ // As that's when the numbers are initialized
1415
+ if (
1416
+ !isDebug &&
1417
+ addPredicateTests &&
1418
+ block.label !== startLabel &&
1419
+ chance(50)
1420
+ ) {
1421
+ let predicateName = choice(Array.from(predicateNumbers.keys()));
1422
+ if (predicateName) {
1423
+ let number = predicateNumbers.get(predicateName);
1424
+ let diff = block.totalState - number;
1425
+
1426
+ test = t.binaryExpression(
1427
+ "+",
1428
+ mainScope.getMemberExpression(predicateName),
1429
+ numericLiteral(diff),
1430
+ );
1431
+ }
1432
+ }
1433
+
1434
+ // Add complex tests
1435
+ if (!isDebug && addComplexTests && chance(50)) {
1436
+ // Create complex test expressions for each switch case
1437
+
1438
+ // case STATE+X:
1439
+ let stateVarIndex = getRandomInteger(0, stateVars.length);
1440
+
1441
+ let stateValues = block.stateValues;
1442
+ let difference = stateValues[stateVarIndex] - block.totalState;
1443
+
1444
+ let conditionNodes: t.Expression[] = [];
1445
+ let alreadyConditionedItems = new Set<string>();
1446
+
1447
+ // This code finds clash conditions and adds them to 'conditionNodes' array
1448
+ Array.from(basicBlocks.keys()).forEach((label) => {
1449
+ if (label !== block.label) {
1450
+ let labelStates = basicBlocks.get(label).stateValues;
1451
+ let totalState = labelStates.reduce((a, b) => a + b, 0);
1452
+
1453
+ if (totalState === labelStates[stateVarIndex] - difference) {
1454
+ let differentIndex = labelStates.findIndex(
1455
+ (v, i) => v !== stateValues[i],
1456
+ );
1457
+ if (differentIndex !== -1) {
1458
+ let expressionAsString =
1459
+ stateVars[differentIndex].name +
1460
+ "!=" +
1461
+ labelStates[differentIndex];
1462
+ if (!alreadyConditionedItems.has(expressionAsString)) {
1463
+ alreadyConditionedItems.add(expressionAsString);
1464
+
1465
+ conditionNodes.push(
1466
+ t.binaryExpression(
1467
+ "!=",
1468
+ deepClone(stateVars[differentIndex]),
1469
+ numericLiteral(labelStates[differentIndex]),
1470
+ ),
1471
+ );
1472
+ }
1473
+ } else {
1474
+ conditionNodes.push(
1475
+ t.binaryExpression(
1476
+ "!=",
1477
+ deepClone(discriminant),
1478
+ numericLiteral(totalState),
1479
+ ),
1480
+ );
1481
+ }
1482
+ }
1483
+ }
1484
+ });
1485
+
1486
+ // case STATE!=Y && STATE+X
1487
+ test = t.binaryExpression(
1488
+ "-",
1489
+ deepClone(stateVars[stateVarIndex]),
1490
+ numericLiteral(difference),
1491
+ );
1492
+
1493
+ // Use the 'conditionNodes' to not cause state clashing issues
1494
+ conditionNodes.forEach((conditionNode) => {
1495
+ test = t.logicalExpression("&&", conditionNode, test);
1496
+ });
1497
+ }
1498
+
1499
+ const tests = [test];
1500
+
1501
+ if (!isDebug && addFakeTests && chance(50)) {
1502
+ // Add fake tests
1503
+ let fakeTestCount = getRandomInteger(1, 3);
1504
+ for (let i = 0; i < fakeTestCount; i++) {
1505
+ tests.push(numericLiteral(stateIntGen.generate()));
1506
+ }
1507
+
1508
+ shuffle(tests);
1509
+ }
1510
+
1511
+ const lastTest = tests.pop();
1512
+
1513
+ for (const test of tests) {
1514
+ switchCases.push(t.switchCase(test, []));
1515
+ }
1516
+
1517
+ switchCases.push(t.switchCase(lastTest, block.thisPath.node.body));
1518
+ }
1519
+
1520
+ if (!isDebug && addFakeTests) {
1521
+ // A random test can be 'default'
1522
+ choice(switchCases).test = null;
1523
+ }
1524
+
1525
+ const discriminant = new Template(`
1526
+ ${stateVars.map((x) => x.name).join(" + ")}
1527
+ `).expression<t.Expression>();
1528
+
1529
+ traverse(t.program([t.expressionStatement(discriminant)]), {
1530
+ Identifier(path) {
1531
+ (path.node as NodeSymbol)[NO_RENAME] = cffIndex;
1532
+ },
1533
+ });
1534
+
1535
+ // Create a new SwitchStatement
1536
+ const switchStatement = t.labeledStatement(
1537
+ t.identifier(switchLabel),
1538
+ t.switchStatement(discriminant, switchCases),
1539
+ );
1540
+
1541
+ const startStateValues = basicBlocks.get(startLabel).stateValues;
1542
+ const endTotalState = basicBlocks.get(endLabel).totalState;
1543
+
1544
+ const whileStatement = t.whileStatement(
1545
+ t.binaryExpression(
1546
+ "!==",
1547
+ deepClone(discriminant),
1548
+ numericLiteral(endTotalState),
1549
+ ),
1550
+ t.blockStatement([
1551
+ t.withStatement(
1552
+ new Template(`{withDiscriminant} || {scopeVar}`).expression({
1553
+ withDiscriminant: deepClone(withMemberExpression),
1554
+ scopeVar: deepClone(scopeVar),
1555
+ }),
1556
+ t.blockStatement([switchStatement]),
1557
+ ),
1558
+ ]),
1559
+ );
1560
+
1561
+ const parameters: t.Identifier[] = [
1562
+ ...stateVars,
1563
+ scopeVar,
1564
+ argVar,
1565
+ ].map((id) => deepClone(id));
1566
+
1567
+ const parametersNames: string[] = parameters.map((id) => id.name);
1568
+
1569
+ for (var [
1570
+ originalFnName,
1571
+ fnLabel,
1572
+ basicBlock,
1573
+ fn,
1574
+ ] of functionExpressions) {
1575
+ const { scopeManager } = basicBlock;
1576
+ const { stateValues } = basicBlocks.get(fnLabel);
1577
+
1578
+ const argumentsRestName = me.getPlaceholder();
1579
+
1580
+ const argumentsNodes = [];
1581
+ for (const parameterName of parametersNames) {
1582
+ const stateIndex = stateVars
1583
+ .map((x) => x.name)
1584
+ .indexOf(parameterName);
1585
+ if (stateIndex !== -1) {
1586
+ argumentsNodes.push(numericLiteral(stateValues[stateIndex]));
1587
+ } else if (parameterName === argVar.name) {
1588
+ argumentsNodes.push(t.identifier(argumentsRestName));
1589
+ } else if (parameterName === scopeVar.name) {
1590
+ argumentsNodes.push(scopeManager.getObjectExpression(fnLabel));
1591
+ } else {
1592
+ ok(false);
1593
+ }
1594
+ }
1595
+
1596
+ // Ensure parameter is added (No effect if not added in this case)
1597
+ usedArgVar = true;
1598
+
1599
+ Object.assign(
1600
+ fn,
1601
+ new Template(`
1602
+ (function (...${argumentsRestName}){
1603
+ ${
1604
+ isDebug
1605
+ ? `"Calling ${originalFnName}, Label: ${fnLabel}";`
1606
+ : ""
1607
+ }
1608
+ return {callExpression}
1609
+ })
1610
+
1611
+ `).expression({
1612
+ callExpression: createCallExpression(argumentsNodes),
1613
+ }),
1614
+ );
1615
+ }
1616
+
1617
+ const startProgramObjectExpression = basicBlocks
1618
+ .get(startLabel)
1619
+ .scopeManager.getObjectExpression(startLabel);
1620
+
1621
+ const mainParameters: t.FunctionDeclaration["params"] = parameters;
1622
+
1623
+ // First state values use the default parameter for initialization
1624
+ // function main(..., scope = { mainScope: {} }, ...){...}
1625
+ mainParameters.splice(
1626
+ (mainParameters as t.Identifier[]).findIndex(
1627
+ (p) => p.name === scopeVar.name,
1628
+ ),
1629
+ 1,
1630
+ t.assignmentPattern(
1631
+ deepClone(scopeVar),
1632
+ startProgramObjectExpression,
1633
+ ),
1634
+ );
1635
+
1636
+ // Remove parameter 'argVar' if never used (No function calls obfuscated)
1637
+ if (!usedArgVar) {
1638
+ mainParameters.pop();
1639
+ }
1640
+
1641
+ const mainFnDeclaration = t.functionDeclaration(
1642
+ deepClone(mainFnName),
1643
+ parameters,
1644
+ t.blockStatement([whileStatement]),
1645
+ addGeneratorFunction,
1646
+ );
1647
+
1648
+ // The main function is always called with same number of arguments
1649
+ (mainFnDeclaration as NodeSymbol)[PREDICTABLE] = true;
1650
+
1651
+ function createCallExpression(argumentNodes: t.Expression[]) {
1652
+ const callExpression = t.callExpression(
1653
+ deepClone(mainFnName),
1654
+ argumentNodes,
1655
+ );
1656
+
1657
+ if (!addGeneratorFunction) {
1658
+ return callExpression;
1659
+ }
1660
+
1661
+ return new Template(`
1662
+ ({callExpression})["next"]()["value"];
1663
+ `).expression<t.CallExpression>({
1664
+ callExpression: callExpression,
1665
+ });
1666
+ }
1667
+
1668
+ var startProgramExpression = createCallExpression(
1669
+ startStateValues.map((stateValue) => numericLiteral(stateValue)),
1670
+ );
1671
+
1672
+ const resultVar = withIdentifier("result");
1673
+
1674
+ const isTopLevel = blockPath.isProgram();
1675
+ const allowReturns =
1676
+ !isTopLevel && blockPath.find((p) => p.isFunction());
1677
+
1678
+ const startPrefix = allowReturns ? `var {resultVar} = ` : "";
1679
+
1680
+ const startProgramStatements = new Template(`
1681
+ ${allowReturns ? `var {didReturnVar};` : ""}
1682
+ ${startPrefix}{startProgramExpression};
1683
+ ${
1684
+ allowReturns
1685
+ ? `
1686
+ if({didReturnVar}){
1687
+ return {resultVar};
1688
+ }`
1689
+ : ""
1690
+ }
1691
+ `).compile({
1692
+ startProgramExpression,
1693
+ didReturnVar: () => deepClone(didReturnVar),
1694
+ resultVar: () => deepClone(resultVar),
1695
+ });
1696
+
1697
+ blockPath.node.body = [
1698
+ ...prependNodes,
1699
+ mainFnDeclaration,
1700
+ ...startProgramStatements,
1701
+ ];
1702
+
1703
+ functionsModified.push(programOrFunctionPath.node);
1704
+
1705
+ // Reset all bindings here
1706
+ blockPath.scope.bindings = Object.create(null);
1707
+
1708
+ // Register new declarations
1709
+ for (var node of blockPath.get("body")) {
1710
+ blockPath.scope.registerDeclaration(node);
1711
+ }
1712
+ },
1713
+ },
1714
+ },
1715
+ };
1716
+ };