diff --git a/CHANGELOG.md b/CHANGELOG.md index a797ec5..023b624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +# `1.7.2` +Updates + +- `Anti Tooling` & `Expression Obfuscation` improvements +- - No longer expanded by [webcrack](https://github.com/j4k0xb/webcrack), [synchrony](https://github.com/relative/synchrony) & [REstringer](https://github.com/PerimeterX/restringer) + +- `String Concealing` improvements +- - Randomizes the charset for each obfuscation +- - Place multiple decryption functions throughout the code +- - These changes aim to defeat [JSConfuser-String-Decryptor](https://github.com/0v41n/JSConfuser-String-Decryptor) and any other RegEx-based decoders + +- `Moved Declarations` improvements +- - Now moves some variables as unused parameters on certain functions + +- `RGF` improvements +- - More likely to transform functions containing functions + +- Fixed [#96](https://github.com/MichaelXF/js-confuser/issues/96) +- - Removed hardcoded limits on `String Concealing`, `String Compression`, and `Duplicate Literals Removal` + +- Fixed [#106](https://github.com/MichaelXF/js-confuser/issues/106) +- - Final fix with const variables for `Object Extraction` + +- Fixed [#131](https://github.com/MichaelXF/js-confuser/issues/131) +- - __dirname is no longer changed by `Global Concealing` + +**New Option** + +### `preserveFunctionLength` +- Modified functions will retain the correct `function.length` property. (`true/false`) +Enabled by default. + +Minor improvements +- Preserve `function.length` +- Preserve Strict Mode behaviors +- Preserve indirect vs. direct [`eval`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) use + + # `1.7.1` Updates diff --git a/README.md b/README.md index dad0a1d..54801e2 100644 --- a/README.md +++ b/README.md @@ -131,6 +131,8 @@ Converts output to ES5-compatible code. (`true/false`) Does not cover all cases such as Promises or Generator functions. Use [Babel](https://babel.dev/). +[Learn more here.](https://github.com/MichaelXF/js-confuser/blob/master/docs/ES5.md) + ### `renameVariables` Determines if variables should be renamed. (`true/false`) @@ -185,29 +187,6 @@ qFaI6S(); Renames top-level variables, turn this off for web-related scripts. Enabled by default. (`true/false`) -```js -// Output (Same input from above) -var twoSum = function (Oc4nmjB, Fk3nptX) { - var on_KnCm = {}; - var lqAauc = Oc4nmjB["length"]; - for (var mALijp8 = 0; mALijp8 < lqAauc; mALijp8++) { - if (Oc4nmjB[mALijp8] in on_KnCm) { - return [on_KnCm[Oc4nmjB[mALijp8]], mALijp8]; - } - on_KnCm[Fk3nptX - Oc4nmjB[mALijp8]] = mALijp8; - } - return [-1, -1]; -}; -var test = function () { - var y5ySeZ = [2, 7, 11, 15]; - var gHYMOm = 9; - var aAdj3v = [0, 1]; - var GnLVHX = twoSum(y5ySeZ, gHYMOm); - !(ok(GnLVHX[0] === aAdj3v[0]), ok(GnLVHX[1] === aAdj3v[1])); -}; -test(); -``` - ### `identifierGenerator` Determines how variables are renamed. @@ -392,8 +371,11 @@ yAt1T_y(-93)["log"]("Hello World"); ``` ### `stringCompression` + String Compression uses LZW's compression algorithm to compress strings. (`true/false/0-1`) +Use a number to control the percentage of strings. + `"console"` -> `inflate('replaĕ!ğğuģģ<~@')` ### `stringConcealing` @@ -666,9 +648,8 @@ function getAreaOfCircle(radius) { } // Output -function getAreaOfCircle(yLu5YB1) { - var eUf7Wle, XVYH4D; - var F8QuPL = Math["PI"]; +function getAreaOfCircle(yLu5YB1, eUf7Wle, XVYH4D, F8QuPL) { + F8QuPL = Math["PI"]; typeof ((eUf7Wle = Math["pow"](yLu5YB1, 2)), (XVYH4D = F8QuPL * eUf7Wle)); return XVYH4D; } @@ -835,7 +816,11 @@ These features are experimental or a security concern. // experimental identifierGenerator: function(){ return "myvar_" + (counter++); - } + }, + + // Modified functions will retain the correct `function.length` property. + // Enabled by default. + preserveFunctionLength: false } ``` diff --git a/package.json b/package.json index f56b42c..4d3398c 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "author": "MichaelXF", "license": "MIT", "dependencies": { - "acorn": "^8.10.0", - "escodegen": "^2.0.0" + "acorn": "^8.12.1", + "escodegen": "^2.1.0" }, "devDependencies": { "@babel/cli": "^7.17.6", diff --git a/src/constants.ts b/src/constants.ts index dc65988..142c536 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -82,3 +82,15 @@ export const reservedIdentifiers = new Set([ export const noRenameVariablePrefix = "__NO_JS_CONFUSER_RENAME__"; export const placeholderVariablePrefix = "__p_"; + +/** + * Tells the obfuscator this function is predictable: + * - Never called with extraneous parameters + */ +export const predictableFunctionTag = "__JS_PREDICT__"; + +/** + * Tells the obfuscator this function is critical for the Obfuscated code. + * - Example: string decryption function + */ +export const criticalFunctionTag = "__JS_CRITICAL__"; diff --git a/src/options.ts b/src/options.ts index 4f12305..b76bf7b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -585,6 +585,15 @@ export interface ObfuscateOptions { * [See all settings here](https://github.com/MichaelXF/js-confuser/blob/master/README.md#options) */ debugComments?: boolean; + + /** + * ### `preserveFunctionLength` + * + * Modified functions will retain the correct `function.length` property. Enabled by default. (`true/false`) + * + * [See all settings here](https://github.com/MichaelXF/js-confuser/blob/master/README.md#options) + */ + preserveFunctionLength?: boolean; } const validProperties = new Set([ @@ -619,6 +628,7 @@ const validProperties = new Set([ "verbose", "globalVariables", "debugComments", + "preserveFunctionLength", ]); const validOses = new Set(["windows", "linux", "osx", "ios", "android"]); @@ -764,6 +774,9 @@ export async function correctOptions( if (!options.hasOwnProperty("renameGlobals")) { options.renameGlobals = true; // RenameGlobals is on by default } + if (!options.hasOwnProperty("preserveFunctionLength")) { + options.preserveFunctionLength = true; // preserveFunctionLength is on by default + } if (options.globalVariables && !(options.globalVariables instanceof Set)) { options.globalVariables = new Set(Object.keys(options.globalVariables)); diff --git a/src/order.ts b/src/order.ts index 927085a..69e7aa4 100644 --- a/src/order.ts +++ b/src/order.ts @@ -46,11 +46,11 @@ export enum ObfuscateOrder { Minify = 28, + AntiTooling = 29, + RenameVariables = 30, ES5 = 31, - AntiTooling = 34, - Finalizer = 35, } diff --git a/src/templates/bufferToString.ts b/src/templates/bufferToString.ts index 76c3f9a..35b6bd4 100644 --- a/src/templates/bufferToString.ts +++ b/src/templates/bufferToString.ts @@ -1,19 +1,59 @@ +import { + placeholderVariablePrefix, + predictableFunctionTag, +} from "../constants"; import Template from "./template"; -export const BufferToStringTemplate = Template(` - function __getGlobal(){ +export const GetGlobalTemplate = Template(` + function ${placeholderVariablePrefix}CFG__getGlobalThis${predictableFunctionTag}(){ + return globalThis + } + + function ${placeholderVariablePrefix}CFG__getGlobal${predictableFunctionTag}(){ + return global + } + + function ${placeholderVariablePrefix}CFG__getWindow${predictableFunctionTag}(){ + return window + } + + function ${placeholderVariablePrefix}CFG__getThisFunction${predictableFunctionTag}(){ + return new Function("return this")() + } + + function {getGlobalFnName}(array = [ + ${placeholderVariablePrefix}CFG__getGlobalThis${predictableFunctionTag}, + ${placeholderVariablePrefix}CFG__getGlobal${predictableFunctionTag}, + ${placeholderVariablePrefix}CFG__getWindow${predictableFunctionTag}, + ${placeholderVariablePrefix}CFG__getThisFunction${predictableFunctionTag} + ]){ + var bestMatch + var itemsToSearch = [] try { - return global||window|| ( new Function("return this") )(); - } catch ( e ) { + bestMatch = Object + itemsToSearch["push"](("")["__proto__"]["constructor"]["name"]) + } catch(e) { + + } + A: for(var i = 0; i < array["length"]; i++) { try { - return this; - } catch ( e ) { - return {}; - } + bestMatch = array[i]() + for(var j = 0; j < itemsToSearch["length"]; j++) { + if(typeof bestMatch[itemsToSearch[j]] === "undefined") continue A; + } + return bestMatch + } catch(e) {} } + + return bestMatch || this; } +`); - var __globalObject = __getGlobal() || {}; +export const BufferToStringTemplate = Template(` + + ${GetGlobalTemplate.source} + + var __globalObject = {getGlobalFnName}() || {}; var __TextDecoder = __globalObject["TextDecoder"]; var __Uint8Array = __globalObject["Uint8Array"]; var __Buffer = __globalObject["Buffer"]; @@ -63,6 +103,4 @@ export const BufferToStringTemplate = Template(` return utf8ArrayToStr(buffer); } } - - `); diff --git a/src/templates/functionLength.ts b/src/templates/functionLength.ts index 71dd7aa..23884e4 100644 --- a/src/templates/functionLength.ts +++ b/src/templates/functionLength.ts @@ -3,12 +3,30 @@ import Template from "./template"; /** * Helper function to set `function.length` property. */ -export const FunctionLengthTemplate = Template(` +export const FunctionLengthTemplate = Template( + ` function {name}(functionObject, functionLength){ - Object["defineProperty"](functionObject, "length", { + {ObjectDefineProperty}(functionObject, "length", { "value": functionLength, "configurable": true }); return functionObject; } -`); +`, + ` +function {name}(functionObject, functionLength){ + return {ObjectDefineProperty}(functionObject, "length", { + "value": functionLength, + "configurable": true + }); +} +`, + ` +function {name}(functionObject, functionLength){ + return {ObjectDefineProperty}["call"](null, functionObject, "length", { + "value": functionLength, + "configurable": true + }); +} +` +); diff --git a/src/templates/globals.ts b/src/templates/globals.ts new file mode 100644 index 0000000..6ff97e9 --- /dev/null +++ b/src/templates/globals.ts @@ -0,0 +1,3 @@ +import Template from "./template"; + +export const ObjectDefineProperty = Template(`Object["defineProperty"]`); diff --git a/src/templates/template.ts b/src/templates/template.ts index 225d97e..90c13f8 100644 --- a/src/templates/template.ts +++ b/src/templates/template.ts @@ -1,57 +1,103 @@ import { Node } from "../util/gen"; import { parseSnippet, parseSync } from "../parser"; +import { ok } from "assert"; +import { choice } from "../util/random"; +import { placeholderVariablePrefix } from "../constants"; export interface ITemplate { - fill(variables?: { [name: string]: string | number }): string; + fill(variables?: { [name: string]: string | number }): { + output: string; + template: string; + }; compile(variables?: { [name: string]: string | number }): Node[]; single(variables?: { [name: string]: string | number }): Node; + variables(variables): ITemplate; + + ignoreMissingVariables(): ITemplate; + + templates: string[]; source: string; } -export default function Template(template: string): ITemplate { - var neededVariables = 0; - while (template.includes(`{$${neededVariables + 1}}`)) { - neededVariables++; +export default function Template(...templates: string[]): ITemplate { + ok(templates.length); + + var requiredVariables = new Set(); + var providedVariables = {}; + var defaultVariables: { [key: string]: string } = Object.create(null); + + // This may picked up "$mb[pP`x]" from String Encoding function + // ignoreMissingVariables() prevents this + var matches = templates[0].match(/{[$A-z0-9_]+}/g); + if (matches !== null) { + matches.forEach((variable) => { + var name = variable.slice(1, -1); + + // $ variables are for default variables + if (name.startsWith("$")) { + defaultVariables[name] = + placeholderVariablePrefix + + "td_" + + Object.keys(defaultVariables).length; + } else { + requiredVariables.add(name); + } + }); } - var vars = Object.create(null); - new Array(neededVariables + 1).fill(0).forEach((x, i) => { - vars["\\$" + i] = "temp_" + i; - }); - - function fill(variables?: { [name: string]: string | number }): string { - if (!variables) { - variables = Object.create(null); - } - var cloned = template; + function fill( + variables: { [name: string]: string | number } = Object.create(null) + ) { + var userVariables = { ...providedVariables, ...variables }; + + // Validate all variables were passed in + for (var requiredVariable of requiredVariables) { + if (typeof userVariables[requiredVariable] === "undefined") { + throw new Error( + templates[0] + + " missing variable: " + + requiredVariable + + " from " + + JSON.stringify(userVariables) + ); + } + } - var keys = { ...variables, ...vars }; + var template = choice(templates); + var output = template; + var allVariables = { + ...defaultVariables, + ...userVariables, + }; - Object.keys(keys).forEach((name) => { - var bracketName = "{" + name + "}"; - var value = keys[name] + ""; + Object.keys(allVariables).forEach((name) => { + var bracketName = "{" + name.replace("$", "\\$") + "}"; + var value = allVariables[name] + ""; var reg = new RegExp(bracketName, "g"); - cloned = cloned.replace(reg, value); + output = output.replace(reg, value); }); - return cloned; + return { output, template }; } function compile(variables: { [name: string]: string | number }): Node[] { - var code = fill(variables); + var { output, template } = fill(variables); try { - var program = parseSnippet(code); + var program = parseSnippet(output); return program.body; } catch (e) { console.error(e); console.error(template); - throw new Error("Template failed to parse"); + console.error({ ...providedVariables, ...variables }); + throw new Error( + "Template failed to parse: OUTPUT= " + output + " SOURCE= " + template + ); } } @@ -60,11 +106,25 @@ export default function Template(template: string): ITemplate { return nodes[0]; } + function variables(newVariables) { + Object.assign(providedVariables, newVariables); + return obj; + } + + function ignoreMissingVariables() { + defaultVariables = Object.create(null); + requiredVariables.clear(); + return obj; + } + var obj: ITemplate = { fill, compile, single, - source: template, + templates, + variables, + ignoreMissingVariables, + source: templates[0], }; return obj; diff --git a/src/transforms/antiTooling.ts b/src/transforms/antiTooling.ts index 0de57a3..6871b14 100644 --- a/src/transforms/antiTooling.ts +++ b/src/transforms/antiTooling.ts @@ -1,32 +1,51 @@ import { ObfuscateOrder } from "../order"; +import Template from "../templates/template"; import { isBlock } from "../traverse"; import { + Node, ExpressionStatement, - SequenceExpression, - UnaryExpression, + CallExpression, + Identifier, } from "../util/gen"; -import { choice } from "../util/random"; +import { prepend } from "../util/insert"; import Transform from "./transform"; // JsNice.org tries to separate sequence expressions into multiple lines, this stops that. export default class AntiTooling extends Transform { + fnName: string; + constructor(o) { super(o, ObfuscateOrder.AntiTooling); } + apply(tree: Node) { + super.apply(tree); + + if (typeof this.fnName === "string") { + prepend( + tree, + Template(` + function {fnName}(){ + } + `).single({ fnName: this.fnName }) + ); + } + } + match(object, parents) { return isBlock(object) || object.type == "SwitchCase"; } transform(object, parents) { return () => { - var exprs = []; - var deleteExprs = []; + var exprs: Node[] = []; + var deleteExprs: Node[] = []; - var body = object.type == "SwitchCase" ? object.consequent : object.body; + var body: Node[] = + object.type == "SwitchCase" ? object.consequent : object.body; const end = () => { - function flatten(expr) { + function flatten(expr: Node) { if (expr.type == "ExpressionStatement") { flatten(expr.expression); } else if (expr.type == "SequenceExpression") { @@ -41,13 +60,16 @@ export default class AntiTooling extends Transform { if (flattened.length > 1) { flattened[0] = { ...flattened[0] }; + + if (!this.fnName) { + this.fnName = this.getPlaceholder(); + } + + // (expr1,expr2,expr3) -> F(expr1, expr2, expr3) this.replace( exprs[0], ExpressionStatement( - UnaryExpression( - choice(["typeof", "void", "!"]), - SequenceExpression(flattened) - ) + CallExpression(Identifier(this.fnName), [...flattened]) ) ); diff --git a/src/transforms/controlFlowFlattening/controlFlowFlattening.ts b/src/transforms/controlFlowFlattening/controlFlowFlattening.ts index 07e4b91..c282753 100644 --- a/src/transforms/controlFlowFlattening/controlFlowFlattening.ts +++ b/src/transforms/controlFlowFlattening/controlFlowFlattening.ts @@ -47,9 +47,8 @@ import { import { chance, choice, getRandomInteger, shuffle } from "../../util/random"; import Transform from "../transform"; import ExpressionObfuscation from "./expressionObfuscation"; -import { isModuleSource } from "../string/stringConcealing"; import { reservedIdentifiers } from "../../constants"; -import { isDirective } from "../../util/compare"; +import { isDirective, isModuleSource } from "../../util/compare"; const flattenStructures = new Set([ "IfStatement", @@ -1571,6 +1570,7 @@ export default class ControlFlowFlattening extends Transform { !this.isDebug && o.type === "Identifier" && this.mangleIdentifiers && + !reservedIdentifiers.has(o.name) && chance(50 - this.mangledExpressionsMade / 100) && !p.find((x) => isVarContext(x)) ) { diff --git a/src/transforms/controlFlowFlattening/expressionObfuscation.ts b/src/transforms/controlFlowFlattening/expressionObfuscation.ts index 7267144..1273546 100644 --- a/src/transforms/controlFlowFlattening/expressionObfuscation.ts +++ b/src/transforms/controlFlowFlattening/expressionObfuscation.ts @@ -1,15 +1,48 @@ +import { criticalFunctionTag } from "../../constants"; +import Template from "../../templates/template"; import { isBlock } from "../../traverse"; -import { Identifier, SequenceExpression } from "../../util/gen"; +import { + CallExpression, + Identifier, + Node, + SequenceExpression, +} from "../../util/gen"; +import { prepend } from "../../util/insert"; import Transform from "../transform"; /** * Expression Obfuscation runs before Control Flow Flattening */ export default class ExpressionObfuscation extends Transform { + fnName: string; + constructor(o) { super(o); } + apply(tree: Node): void { + super.apply(tree); + + if (typeof this.fnName === "string") { + prepend( + tree, + Template(` + function {fnName}(...args){ + return args[args["length"] - 1] + } + `).single({ fnName: this.fnName }) + ); + } + } + + createSequenceExpression(expressions: Node[]): Node { + if (!this.fnName) { + this.fnName = this.getPlaceholder() + criticalFunctionTag; + } + + return CallExpression(Identifier(this.fnName), [...expressions]); + } + match(object, parents) { return isBlock(object); } @@ -54,12 +87,12 @@ export default class ExpressionObfuscation extends Transform { stmt.test.left.argument.type === "Identifier" ) // typeof is special ) { - stmt.test.left.argument = SequenceExpression([ + stmt.test.left.argument = this.createSequenceExpression([ ...exprs, { ...stmt.test.left.argument }, ]); } else { - stmt.test.left = SequenceExpression([ + stmt.test.left = this.createSequenceExpression([ ...exprs, { ...stmt.test.left }, ]); @@ -70,12 +103,15 @@ export default class ExpressionObfuscation extends Transform { stmt.test.operator !== "**" && stmt.test.left.left.type == "UnaryExpression" ) { - stmt.test.left.left.argument = SequenceExpression([ + stmt.test.left.left.argument = this.createSequenceExpression([ ...exprs, { ...stmt.test.left.left.argument }, ]); } else { - stmt.test = SequenceExpression([...exprs, { ...stmt.test }]); + stmt.test = this.createSequenceExpression([ + ...exprs, + { ...stmt.test }, + ]); } deleteExprs.push(...exprs); } else if ( @@ -88,7 +124,7 @@ export default class ExpressionObfuscation extends Transform { if (init) { if (init.type == "VariableDeclaration") { - init.declarations[0].init = SequenceExpression([ + init.declarations[0].init = this.createSequenceExpression([ ...exprs, { ...(init.declarations[0].init || Identifier("undefined")), @@ -96,7 +132,7 @@ export default class ExpressionObfuscation extends Transform { ]); deleteExprs.push(...exprs); } else if (init.type == "AssignmentExpression") { - init.right = SequenceExpression([ + init.right = this.createSequenceExpression([ ...exprs, { ...(init.right || Identifier("undefined")), @@ -106,7 +142,7 @@ export default class ExpressionObfuscation extends Transform { } } } else if (stmt.type == "VariableDeclaration") { - stmt.declarations[0].init = SequenceExpression([ + stmt.declarations[0].init = this.createSequenceExpression([ ...exprs, { ...(stmt.declarations[0].init || Identifier("undefined")), @@ -114,13 +150,13 @@ export default class ExpressionObfuscation extends Transform { ]); deleteExprs.push(...exprs); } else if (stmt.type == "ThrowStatement") { - stmt.argument = SequenceExpression([ + stmt.argument = this.createSequenceExpression([ ...exprs, { ...stmt.argument }, ]); deleteExprs.push(...exprs); } else if (stmt.type == "ReturnStatement") { - stmt.argument = SequenceExpression([ + stmt.argument = this.createSequenceExpression([ ...exprs, { ...(stmt.argument || Identifier("undefined")) }, ]); diff --git a/src/transforms/deadCode.ts b/src/transforms/deadCode.ts index 09fc6ff..bc42c82 100644 --- a/src/transforms/deadCode.ts +++ b/src/transforms/deadCode.ts @@ -188,10 +188,6 @@ function setCookie(cname, cvalue, exdays) { return true; } - /** - * @param {TreeNode} root - * @return {boolean} - */ function isBalanced(root) { const height = getHeightBalanced(root); return height !== Infinity; @@ -426,9 +422,6 @@ function setCookie(cname, cvalue, exdays) { console.log(maximumGap); `), Template(` - /** - * @param {number} capacity - */ var LRUCache = function(capacity) { this.capacity = capacity; this.length = 0; @@ -437,10 +430,6 @@ function setCookie(cname, cvalue, exdays) { this.tail = null; }; - /** - * @param {number} key - * @return {number} - */ LRUCache.prototype.get = function(key) { var node = this.map[key]; if (node) { @@ -452,11 +441,6 @@ function setCookie(cname, cvalue, exdays) { } }; - /** - * @param {number} key - * @param {number} value - * @return {void} - */ LRUCache.prototype.put = function(key, value) { if (this.map[key]) { this.remove(this.map[key]); diff --git a/src/transforms/dispatcher.ts b/src/transforms/dispatcher.ts index 4c739fe..53c5782 100644 --- a/src/transforms/dispatcher.ts +++ b/src/transforms/dispatcher.ts @@ -34,14 +34,19 @@ import { isVarContext, prepend, append, + computeFunctionLength, + isFunction, } from "../util/insert"; import Transform from "./transform"; import { isInsideType } from "../util/compare"; import { choice, shuffle } from "../util/random"; import { ComputeProbabilityMap } from "../probability"; -import { reservedIdentifiers } from "../constants"; +import { predictableFunctionTag, reservedIdentifiers } from "../constants"; import { ObfuscateOrder } from "../order"; import Template from "../templates/template"; +import { FunctionLengthTemplate } from "../templates/functionLength"; +import { ObjectDefineProperty } from "../templates/globals"; +import { getLexicalScope } from "../util/scope"; /** * A Dispatcher processes function calls. All the function declarations are brought into a dictionary. @@ -72,12 +77,30 @@ export default class Dispatcher extends Transform { isDebug = false; count: number; + functionLengthName: string; + constructor(o) { super(o, ObfuscateOrder.Dispatcher); this.count = 0; } + apply(tree: Node): void { + super.apply(tree); + + if (this.options.preserveFunctionLength && this.functionLengthName) { + prepend( + tree, + FunctionLengthTemplate.single({ + name: this.functionLengthName, + ObjectDefineProperty: this.createInitVariable(ObjectDefineProperty, [ + tree, + ]), + }) + ); + } + } + match(object: Node, parents: Node[]) { if (isInsideType("AwaitExpression", object, parents)) { return false; @@ -113,6 +136,8 @@ export default class Dispatcher extends Transform { ? object : getVarContext(object, parents); + var lexicalScope = isFunction(context) ? context.body : context; + walk(object, parents, (o: Node, p: Node[]) => { if (object == o) { // Fix 1 @@ -138,6 +163,15 @@ export default class Dispatcher extends Transform { o.body.type != "BlockStatement" ) { illegalFnNames.add(name); + return; + } + + // Must defined in the same block as the current function being scanned + // Solves 'let' and 'class' declaration issue + var ls = getLexicalScope(o, p); + if (ls !== lexicalScope) { + illegalFnNames.add(name); + return; } // If dupe, no routing @@ -217,11 +251,18 @@ export default class Dispatcher extends Transform { // Only make a dispatcher function if it caught any functions if (set.size > 0) { + if (!this.functionLengthName) { + this.functionLengthName = this.getPlaceholder(); + } + var payloadArg = this.getPlaceholder() + "_dispatcher_" + this.count + "_payload"; var dispatcherFnName = - this.getPlaceholder() + "_dispatcher_" + this.count; + this.getPlaceholder() + + "_dispatcher_" + + this.count + + predictableFunctionTag; this.log(dispatcherFnName, set); this.count++; @@ -253,6 +294,8 @@ export default class Dispatcher extends Transform { expression: false, type: "FunctionExpression", id: null, + params: [], + [predictableFunctionTag]: true, }; this.addComment(functionExpression, name); @@ -272,48 +315,6 @@ export default class Dispatcher extends Transform { ); prepend(def.body, variableDeclaration); - - // replace params with random identifiers - var args = [0, 1, 2].map((x) => this.getPlaceholder()); - functionExpression.params = args.map((x) => Identifier(x)); - - var deadCode = choice(["fakeReturn", "ifStatement"]); - - switch (deadCode) { - case "fakeReturn": - // Dead code... - var ifStatement = IfStatement( - UnaryExpression("!", Identifier(args[0])), - [ - ReturnStatement( - CallExpression(Identifier(args[1]), [ - ThisExpression(), - Identifier(args[2]), - ]) - ), - ], - null - ); - - body.unshift(ifStatement); - break; - - case "ifStatement": - var test = LogicalExpression( - "||", - Identifier(args[0]), - AssignmentExpression( - "=", - Identifier(args[1]), - CallExpression(Identifier(args[2]), []) - ) - ); - def.body = BlockStatement([ - IfStatement(test, [...body], null), - ReturnStatement(Identifier(args[1])), - ]); - break; - } } // For logging purposes @@ -380,6 +381,48 @@ export default class Dispatcher extends Transform { null ), + VariableDeclaration( + VariableDeclarator( + Identifier("lengths"), + ObjectExpression( + !this.options.preserveFunctionLength + ? [] + : shuffledKeys + .map((name) => { + var [def, defParents] = functionDeclarations[name]; + + return { + key: newFnNames[name], + value: computeFunctionLength(def.params), + }; + }) + .filter((item) => item.value !== 0) + .map((item) => + Property(Literal(item.key), Literal(item.value)) + ) + ) + ) + ), + + Template(` + function makeFn${predictableFunctionTag}(){ + var fn = function(...args){ + ${payloadArg} = args; + return ${mapName}[${x}].call(this) + }, a = lengths[${x}] + + ${ + this.options.preserveFunctionLength + ? `if(a){ + return ${this.functionLengthName}(fn, a) + }` + : "" + } + + return fn + } + `).single(), + // Arg to get a function reference IfStatement( BinaryExpression("==", Identifier(y), Literal(expectedGet)), @@ -403,30 +446,9 @@ export default class Dispatcher extends Transform { Identifier(x), true ), - FunctionExpression( - [RestElement(Identifier(getterArgName))], - [ - // Arg setter - ExpressionStatement( - AssignmentExpression( - "=", - Identifier(payloadArg), - Identifier(getterArgName) - ) - ), - - // Call fn & return - ReturnStatement( - CallExpression( - MemberExpression( - getAccessor(), - Identifier("call"), - false - ), - [ThisExpression(), Literal(gen.generate())] - ) - ), - ] + CallExpression( + Identifier(`makeFn${predictableFunctionTag}`), + [] ) ) ) @@ -439,7 +461,7 @@ export default class Dispatcher extends Transform { AssignmentExpression( "=", Identifier(returnProp), - CallExpression(getAccessor(), [Literal(gen.generate())]) + CallExpression(getAccessor(), []) ) ), ] diff --git a/src/transforms/es5/antiClass.ts b/src/transforms/es5/antiClass.ts index b1e1026..23f0cc6 100644 --- a/src/transforms/es5/antiClass.ts +++ b/src/transforms/es5/antiClass.ts @@ -94,8 +94,17 @@ export default class AntiClass extends Transform { first.expression.type == "CallExpression" ) { if (first.expression.callee.type == "Super") { + /// super(...args) superArguments = first.expression.arguments; value.body.body.shift(); + } else if ( + // F(super(...args)) + first.expression.arguments[0] && + first.expression.arguments[0].type === "CallExpression" && + first.expression.arguments[0].callee.type === "Super" + ) { + superArguments = first.expression.arguments[0].arguments; + first.expression.arguments[0] = Identifier("undefined"); } } @@ -198,7 +207,7 @@ export default class AntiClass extends Transform { ); if (superName) { - ok(superArguments, "Super class with no super arguments"); + ok(superArguments, "Failed to find super() arguments"); // save the super state virtualBody.unshift( diff --git a/src/transforms/extraction/classExtraction.ts b/src/transforms/extraction/classExtraction.ts new file mode 100644 index 0000000..e13bcc8 --- /dev/null +++ b/src/transforms/extraction/classExtraction.ts @@ -0,0 +1,168 @@ +import { ok } from "assert"; +import { ExitCallback, getBlock, walk } from "../../traverse"; +import { + CallExpression, + FunctionDeclaration, + FunctionExpression, + Identifier, + Literal, + MemberExpression, + MethodDefinition, + Node, + ReturnStatement, + Super, + ThisExpression, +} from "../../util/gen"; +import { isStringLiteral } from "../../util/guard"; +import { isClass, prepend } from "../../util/insert"; +import { getLexicalScope } from "../../util/scope"; +import Transform from "../transform"; + +export default class ClassExtraction extends Transform { + constructor(o) { + super(o); + } + + match(object: Node, parents: Node[]): boolean { + return ( + object.type === "ClassDeclaration" || object.type === "ClassExpression" + ); + } + + extractKeyString(property: Node): string | null { + if (property.key.type === "Identifier" && !property.key.computed) { + return property.key.name; + } + + if (isStringLiteral(property.key)) { + return property.key.value; + } + + return null; + } + + transform(object: Node, parents: Node[]): void | ExitCallback { + return () => { + var classBody = object.body; + var className = object.id?.type === "Identifier" && object.id?.name; + + if (!className) className = this.getPlaceholder(); + + var lexicalScope = getLexicalScope(object, parents); + + var superMethodName: string; + + for (var methodDefinition of classBody.body) { + if ( + methodDefinition.type === "MethodDefinition" && + methodDefinition.value.type === "FunctionExpression" + ) { + // Don't change constructors calling super() + if (methodDefinition.kind === "constructor" && object.superClass) + continue; + + var functionExpression: Node = methodDefinition.value; + + var fnName = + className + + "_" + + methodDefinition.kind + + "_" + + this.extractKeyString(methodDefinition) || this.getPlaceholder(); + + walk( + functionExpression, + [methodDefinition, object, ...parents], + (o, p) => { + if (o.type === "Super") { + var classContext = p.find((node) => isClass(node)); + if (classContext !== object) return; + + return () => { + if (!superMethodName) { + superMethodName = + this.getGenerator("randomized").generate(); + } + + var memberExpression = p[0]; + if (memberExpression.type === "CallExpression") { + throw new Error("Failed to detect super() usage"); + } + ok(memberExpression.type === "MemberExpression"); + + var propertyArg = memberExpression.computed + ? memberExpression.property + : (ok(memberExpression.property.type === "Identifier"), + Literal(memberExpression.property.name)); + + var getSuperExpression = CallExpression( + MemberExpression( + ThisExpression(), + Literal(superMethodName), + true + ), + [propertyArg] + ); + + if (p[1].type === "CallExpression" && p[1].callee === p[0]) { + getSuperExpression = CallExpression( + MemberExpression( + getSuperExpression, + Literal("bind"), + true + ), + [ThisExpression()] + ); + } + + this.replace(p[0], getSuperExpression); + }; + } + } + ); + + var originalParams = functionExpression.params; + var originalBody = functionExpression.body.body; + + functionExpression.body.body = [ + ReturnStatement( + CallExpression( + MemberExpression(Identifier(fnName), Literal("apply"), true), + [ThisExpression(), Identifier("arguments")] + ) + ), + ]; + + functionExpression.params = []; + if (methodDefinition.kind === "set") { + functionExpression.params = [Identifier(this.getPlaceholder())]; + } + + prepend( + lexicalScope, + FunctionDeclaration(fnName, [...originalParams], [...originalBody]) + ); + } + } + + if (superMethodName) { + classBody.body.push( + MethodDefinition( + Literal(superMethodName), + FunctionExpression( + [Identifier("key")], + [ + ReturnStatement( + MemberExpression(Super(), Identifier("key"), true) + ), + ] + ), + "method", + false, + true + ) + ); + } + }; + } +} diff --git a/src/transforms/extraction/duplicateLiteralsRemoval.ts b/src/transforms/extraction/duplicateLiteralsRemoval.ts index cbe13ee..7fcec11 100644 --- a/src/transforms/extraction/duplicateLiteralsRemoval.ts +++ b/src/transforms/extraction/duplicateLiteralsRemoval.ts @@ -16,15 +16,14 @@ import { ConditionalExpression, } from "../../util/gen"; import { append, clone, prepend } from "../../util/insert"; -import { isDirective, isPrimitive } from "../../util/compare"; - +import { isDirective, isModuleSource, isPrimitive } from "../../util/compare"; import { ObfuscateOrder } from "../../order"; -import { isModuleSource } from "../string/stringConcealing"; import { ComputeProbabilityMap } from "../../probability"; import { ok } from "assert"; import { chance, choice, getRandomInteger } from "../../util/random"; import { getBlock } from "../../traverse"; import { getIdentifierInfo } from "../../util/identifiers"; +import { predictableFunctionTag } from "../../constants"; /** * [Duplicate Literals Removal](https://docs.jscrambler.com/code-integrity/documentation/transformations/duplicate-literals-removal) replaces duplicate literals with a variable name. @@ -119,7 +118,7 @@ export default class DuplicateLiteralsRemoval extends Transform { ]; // The function uses mangling to hide the index being accessed - var mangleCount = getRandomInteger(1, 10); + var mangleCount = getRandomInteger(1, 5); for (var i = 0; i < mangleCount; i++) { var operator = choice([">", "<"]); var compareValue = choice(indexRangeInclusive); @@ -188,7 +187,7 @@ export default class DuplicateLiteralsRemoval extends Transform { var root = parents[parents.length - 1]; var rootFunctionName = this.getPlaceholder() + "_dLR_0"; this.functions.set(root, { - functionName: rootFunctionName, + functionName: rootFunctionName + predictableFunctionTag, indexShift: getRandomInteger(-100, 100), }); @@ -199,7 +198,10 @@ export default class DuplicateLiteralsRemoval extends Transform { var block = getBlock(object, parents); if (!this.functions.has(block) && chance(50 - this.functions.size)) { var newFunctionName = - this.getPlaceholder() + "_dLR_" + this.functions.size; + this.getPlaceholder() + + "_dLR_" + + this.functions.size + + predictableFunctionTag; this.functions.set(block, { functionName: newFunctionName, @@ -224,7 +226,7 @@ export default class DuplicateLiteralsRemoval extends Transform { return () => { if (object.type === "Identifier") { var info = getIdentifierInfo(object, parents); - if (info.isLabel || info.spec.isDefined) return; + if (info.isLabel || info.spec.isDefined || info.spec.isModified) return; } if (object.regex) { return; @@ -234,9 +236,6 @@ export default class DuplicateLiteralsRemoval extends Transform { return; } - // HARD CODED LIMIT of 10,000 (after 1,000 elements) - if (this.map.size > 1000 && chance(this.map.size / 100)) return; - if ( this.arrayName && parents[0].object && diff --git a/src/transforms/extraction/objectExtraction.ts b/src/transforms/extraction/objectExtraction.ts index 7c500ac..5d3bfba 100644 --- a/src/transforms/extraction/objectExtraction.ts +++ b/src/transforms/extraction/objectExtraction.ts @@ -1,19 +1,7 @@ import Transform from "../transform"; import { walk } from "../../traverse"; -import { - Node, - Location, - Identifier, - VariableDeclaration, - VariableDeclarator, -} from "../../util/gen"; -import { - clone, - deleteDeclaration, - getVarContext, - isVarContext, - prepend, -} from "../../util/insert"; +import { Node, Location, Identifier, VariableDeclarator } from "../../util/gen"; +import { getVarContext, isVarContext } from "../../util/insert"; import { ObfuscateOrder } from "../../order"; import { getIdentifierInfo } from "../../util/identifiers"; import { isValidIdentifier } from "../../util/compare"; @@ -316,8 +304,9 @@ export default class ObjectExtraction extends Transform { ...variableDeclarators ); + // const can only be safely changed to let if (declaration.kind === "const") { - declaration.kind = "var"; + declaration.kind = "let"; } // update all identifiers that pointed to the old object diff --git a/src/transforms/flatten.ts b/src/transforms/flatten.ts index eef00ed..99aa47f 100644 --- a/src/transforms/flatten.ts +++ b/src/transforms/flatten.ts @@ -1,5 +1,9 @@ import { ok } from "assert"; -import { noRenameVariablePrefix, reservedIdentifiers } from "../constants"; +import { + noRenameVariablePrefix, + predictableFunctionTag, + reservedIdentifiers, +} from "../constants"; import { ObfuscateOrder } from "../order"; import { walk } from "../traverse"; import { @@ -35,6 +39,7 @@ import { import { shuffle } from "../util/random"; import Transform from "./transform"; import { FunctionLengthTemplate } from "../templates/functionLength"; +import { ObjectDefineProperty } from "../templates/globals"; /** * Flatten takes functions and isolates them from their original scope, and brings it to the top level of the program. @@ -235,7 +240,11 @@ export default class Flatten extends Transform { return; } - var newFnName = this.getPlaceholder() + "_flat_" + currentFnName; + var newFnName = + this.getPlaceholder() + + "_flat_" + + currentFnName + + predictableFunctionTag; var flatObjectName = this.getPlaceholder() + "_flat_object"; const getFlatObjectMember = (propertyName: string) => { @@ -498,15 +507,21 @@ export default class Flatten extends Transform { // Preserve function.length property var originalFunctionLength = computeFunctionLength(object.params); - object.params = [SpreadElement(Identifier(argumentsName))]; + object.params = [RestElement(Identifier(argumentsName))]; - if (originalFunctionLength !== 0) { + if (this.options.preserveFunctionLength && originalFunctionLength !== 0) { if (!this.functionLengthName) { this.functionLengthName = this.getPlaceholder(); prepend( parents[parents.length - 1] || object, - FunctionLengthTemplate.single({ name: this.functionLengthName }) + FunctionLengthTemplate.single({ + name: this.functionLengthName, + ObjectDefineProperty: this.createInitVariable( + ObjectDefineProperty, + parents + ), + }) ); } diff --git a/src/transforms/identifier/globalConcealing.ts b/src/transforms/identifier/globalConcealing.ts index c3c37fa..b476c56 100644 --- a/src/transforms/identifier/globalConcealing.ts +++ b/src/transforms/identifier/globalConcealing.ts @@ -22,9 +22,10 @@ import { } from "../../util/gen"; import { append, prepend } from "../../util/insert"; import { chance, getRandomInteger } from "../../util/random"; -import { reservedIdentifiers } from "../../constants"; +import { predictableFunctionTag, reservedIdentifiers } from "../../constants"; import { ComputeProbabilityMap } from "../../probability"; import GlobalAnalysis from "./globalAnalysis"; +import { GetGlobalTemplate } from "../../templates/bufferToString"; /** * Global Concealing hides global variables being accessed. @@ -33,6 +34,7 @@ import GlobalAnalysis from "./globalAnalysis"; */ export default class GlobalConcealing extends Transform { globalAnalysis: GlobalAnalysis; + ignoreGlobals = new Set(["require", "__dirname", "eval"]); constructor(o) { super(o, ObfuscateOrder.GlobalConcealing); @@ -52,7 +54,9 @@ export default class GlobalConcealing extends Transform { delete globals[del]; }); - delete globals["require"]; + for (var varName of this.ignoreGlobals) { + delete globals[varName]; + } reservedIdentifiers.forEach((x) => { delete globals[x]; @@ -69,55 +73,33 @@ export default class GlobalConcealing extends Transform { }); if (Object.keys(globals).length > 0) { - var used = new Set(); + var usedStates = new Set(); // Make getter function // holds "window" or "global" var globalVar = this.getPlaceholder(); - // holds outermost "this" - var thisVar = this.getPlaceholder(); - - // "window" or "global" in node - var global = - this.options.globalVariables.values().next().value || "window"; - var alternateGlobal = global === "window" ? "global" : "window"; - - var getGlobalVariableFnName = this.getPlaceholder(); - var getThisVariableFnName = this.getPlaceholder(); + var getGlobalVariableFnName = + this.getPlaceholder() + predictableFunctionTag; // Returns global variable or fall backs to `this` - var getGlobalVariableFn = Template(` - var ${getGlobalVariableFnName} = function(){ - try { - return ${global} || ${alternateGlobal} || (new Function("return this"))(); - } catch (e){ - return ${getThisVariableFnName}["call"](this); - } - }`).single(); - - var getThisVariableFn = Template(` - var ${getThisVariableFnName} = function(){ - try { - return this; - } catch (e){ - return null; - } - }`).single(); + var getGlobalVariableFn = GetGlobalTemplate.compile({ + getGlobalFnName: getGlobalVariableFnName, + }); // 2. Replace old accessors - var globalFn = this.getPlaceholder(); + var globalFn = this.getPlaceholder() + predictableFunctionTag; var newNames: { [globalVarName: string]: number } = Object.create(null); Object.keys(globals).forEach((name) => { var locations: Location[] = globals[name]; - var state; + var state: number; do { - state = getRandomInteger(-1000, 1000 + used.size); - } while (used.has(state)); - used.add(state); + state = getRandomInteger(-1000, 1000 + usedStates.size); + } while (usedStates.has(state)); + usedStates.add(state); newNames[name] = state; @@ -136,10 +118,10 @@ export default class GlobalConcealing extends Transform { do { state = getRandomInteger( 0, - 1000 + used.size + this.options.globalVariables.size * 100 + 1000 + usedStates.size + this.options.globalVariables.size * 100 ); - } while (used.has(state)); - used.add(state); + } while (usedStates.has(state)); + usedStates.add(state); newNames[name] = state; } @@ -159,15 +141,7 @@ export default class GlobalConcealing extends Transform { var code = newNames[name]; var body: Node[] = [ ReturnStatement( - LogicalExpression( - "||", - MemberExpression( - Identifier(globalVar), - Literal(name), - true - ), - MemberExpression(Identifier(thisVar), Literal(name), true) - ) + MemberExpression(Identifier(globalVar), Literal(name), true) ), ]; if (chance(50)) { @@ -180,7 +154,7 @@ export default class GlobalConcealing extends Transform { "||", Literal(name), MemberExpression( - Identifier(thisVar), + Identifier(globalVar), Literal(name), true ) @@ -195,18 +169,10 @@ export default class GlobalConcealing extends Transform { }) ), ReturnStatement( - LogicalExpression( - "||", - MemberExpression( - Identifier(globalVar), - Identifier(returnName), - true - ), - MemberExpression( - Identifier(thisVar), - Identifier(returnName), - true - ) + MemberExpression( + Identifier(globalVar), + Identifier(returnName), + true ) ), ] @@ -215,7 +181,7 @@ export default class GlobalConcealing extends Transform { var tempVar = this.getPlaceholder(); var variableDeclaration = Template(` - var ${globalVar}, ${thisVar}; + var ${globalVar}; `).single(); variableDeclaration.declarations.push( @@ -226,11 +192,9 @@ export default class GlobalConcealing extends Transform { FunctionExpression( [], [ - getGlobalVariableFn, - getThisVariableFn, - + ...getGlobalVariableFn, Template( - `return ${thisVar} = ${getThisVariableFnName}["call"](this, ${globalFn}), ${globalVar} = ${getGlobalVariableFnName}["call"](this)` + `return ${globalVar} = ${getGlobalVariableFnName}["call"](this)` ).single(), ] ), diff --git a/src/transforms/identifier/movedDeclarations.ts b/src/transforms/identifier/movedDeclarations.ts index 60e332b..a86208b 100644 --- a/src/transforms/identifier/movedDeclarations.ts +++ b/src/transforms/identifier/movedDeclarations.ts @@ -6,10 +6,21 @@ import { Identifier, Node, VariableDeclarator, + AssignmentPattern, } from "../../util/gen"; -import { isForInitialize, prepend } from "../../util/insert"; +import { + isForInitialize, + isFunction, + isStrictModeFunction, + prepend, +} from "../../util/insert"; import { ok } from "assert"; import { ObfuscateOrder } from "../../order"; +import { choice } from "../../util/random"; +import { predictableFunctionTag } from "../../constants"; +import { isIndependent, isMoveable } from "../../util/compare"; +import { getFunctionParameters } from "../../util/identifiers"; +import { isLexicalScope } from "../../util/scope"; /** * Defines all the names at the top of every lexical block. @@ -33,35 +44,86 @@ export default class MovedDeclarations extends Transform { var forInitializeType = isForInitialize(object, parents); // Get the block statement or Program node - var blockIndex = parents.findIndex((x) => isBlock(x)); + var blockIndex = parents.findIndex((x) => isLexicalScope(x)); var block = parents[blockIndex]; - var body = block.body; - var bodyObject = parents[blockIndex - 2] || object; + var body: Node[] = + block.type === "SwitchCase" ? block.consequent : block.body; + ok(Array.isArray(body), "No body array found."); - // Make sure in the block statement, and not already at the top of it + var bodyObject = parents[blockIndex - 2] || object; var index = body.indexOf(bodyObject); - if (index === -1 || index === 0) return; - var topVariableDeclaration; - if (body[0].type === "VariableDeclaration" && body[0].kind === "var") { - topVariableDeclaration = body[0]; + var varName = object.declarations[0].id.name; + ok(typeof varName === "string"); + + var predictableFunctionIndex = parents.findIndex((x) => isFunction(x)); + var predictableFunction = parents[predictableFunctionIndex]; + + var deleteStatement = false; + + if ( + predictableFunction && + ((predictableFunction.id && + predictableFunction.id.name.includes(predictableFunctionTag)) || + predictableFunction[predictableFunctionTag]) && // Must have predictableFunctionTag in the name, or on object + predictableFunction[predictableFunctionTag] !== false && // If === false, the function is deemed not predictable + predictableFunction.params.length < 1000 && // Max 1,000 parameters + !predictableFunction.params.find((x) => x.type === "RestElement") && // Cannot add parameters after spread operator + !( + ["Property", "MethodDefinition"].includes( + parents[predictableFunctionIndex + 1]?.type + ) && parents[predictableFunctionIndex + 1]?.kind !== "init" + ) && // Preserve getter/setter methods + !getFunctionParameters( + predictableFunction, + parents.slice(predictableFunctionIndex) + ).find((entry) => entry[0].name === varName) // Ensure not duplicate param name + ) { + // Use function f(..., x, y, z) to declare name + + var value = object.declarations[0].init; + var isPredictablyComputed = + predictableFunction.body === block && + !isStrictModeFunction(predictableFunction) && + value && + isIndependent(value, []) && + isMoveable(value, [object.declarations[0], object, ...parents]); + + var defineWithValue = isPredictablyComputed; + + if (defineWithValue) { + predictableFunction.params.push( + AssignmentPattern(Identifier(varName), value) + ); + object.declarations[0].init = null; + deleteStatement = true; + } else { + predictableFunction.params.push(Identifier(varName)); + } } else { - topVariableDeclaration = { - type: "VariableDeclaration", - declarations: [], - kind: "var", - }; + // Use 'var x, y, z' to declare name - prepend(block, topVariableDeclaration); - } + // Make sure in the block statement, and not already at the top of it + if (index === -1 || index === 0) return; - var varName = object.declarations[0].id.name; - ok(typeof varName === "string"); + var topVariableDeclaration; + if (body[0].type === "VariableDeclaration" && body[0].kind === "var") { + topVariableDeclaration = body[0]; + } else { + topVariableDeclaration = { + type: "VariableDeclaration", + declarations: [], + kind: "var", + }; - // Add `var x` at the top of the block - topVariableDeclaration.declarations.push( - VariableDeclarator(Identifier(varName)) - ); + prepend(block, topVariableDeclaration); + } + + // Add `var x` at the top of the block + topVariableDeclaration.declarations.push( + VariableDeclarator(Identifier(varName)) + ); + } var assignmentExpression = AssignmentExpression( "=", @@ -79,8 +141,12 @@ export default class MovedDeclarations extends Transform { this.replace(object, Identifier(varName)); } } else { - // Replace `var x = value` to `x = value` - this.replace(object, ExpressionStatement(assignmentExpression)); + if (deleteStatement && index !== -1) { + body.splice(index, 1); + } else { + // Replace `var x = value` to `x = value` + this.replace(object, ExpressionStatement(assignmentExpression)); + } } }; } diff --git a/src/transforms/minify.ts b/src/transforms/minify.ts index 8e0658c..e07a00d 100644 --- a/src/transforms/minify.ts +++ b/src/transforms/minify.ts @@ -29,6 +29,8 @@ import { walk, isBlock } from "../traverse"; import { ok } from "assert"; import { isLexicalScope } from "../util/scope"; import Template from "../templates/template"; +import { ObjectDefineProperty } from "../templates/globals"; +import { getIdentifierInfo } from "../util/identifiers"; /** * Basic transformations to reduce code size. @@ -259,25 +261,34 @@ export default class Minify extends Transform { append( parents[parents.length - 1] || object, Template(` - function ${this.arrowFunctionName}(arrowFn, functionLength){ + function ${this.arrowFunctionName}(arrowFn, functionLength = 0){ var functionObject = function(){ return arrowFn(...arguments) }; - Object["defineProperty"](functionObject, "length", { - "value": functionLength, - "configurable": true - }); - - return functionObject; + ${ + this.options.preserveFunctionLength + ? `return {ObjectDefineProperty}(functionObject, "length", { + "value": functionLength, + "configurable": true + });` + : `return functionObject` + } + } - `).single() + `).single({ + ObjectDefineProperty: this.options.preserveFunctionLength + ? this.createInitVariable(ObjectDefineProperty, parents) + : undefined, + }) ); } const wrap = (object: Node) => { - return CallExpression(Identifier(this.arrowFunctionName), [ - clone(object), - Literal(computeFunctionLength(object.params)), - ]); + var args: Node[] = [clone(object)]; + var fnLength = computeFunctionLength(object.params); + if (this.options.preserveFunctionLength && fnLength != 0) { + args.push(Literal(fnLength)); + } + return CallExpression(Identifier(this.arrowFunctionName), args); }; if (object.type == "FunctionExpression") { @@ -619,6 +630,7 @@ export default class Minify extends Transform { if ( object.id.type == "ObjectPattern" && + object.init && object.init.type == "ObjectExpression" ) { if ( @@ -673,6 +685,9 @@ export default class Minify extends Transform { } if (object.type == "Identifier") { return () => { + var info = getIdentifierInfo(object, parents); + if (info.spec.isDefined || info.spec.isModified) return; + if (object.name == "undefined" && !isForInitialize(object, parents)) { this.replaceIdentifierOrLiteral( object, diff --git a/src/transforms/rgf.ts b/src/transforms/rgf.ts index 63f6c4a..e708f11 100644 --- a/src/transforms/rgf.ts +++ b/src/transforms/rgf.ts @@ -1,13 +1,16 @@ import { compileJsSync } from "../compiler"; -import { reservedIdentifiers } from "../constants"; +import { predictableFunctionTag, reservedIdentifiers } from "../constants"; import Obfuscator from "../obfuscator"; import { ObfuscateOrder } from "../order"; import { ComputeProbabilityMap } from "../probability"; +import { FunctionLengthTemplate } from "../templates/functionLength"; +import { ObjectDefineProperty } from "../templates/globals"; import { walk } from "../traverse"; import { ArrayExpression, BlockStatement, CallExpression, + ExpressionStatement, Identifier, Literal, MemberExpression, @@ -19,7 +22,11 @@ import { VariableDeclarator, } from "../util/gen"; import { getIdentifierInfo } from "../util/identifiers"; -import { prepend, getDefiningContext } from "../util/insert"; +import { + prepend, + getDefiningContext, + computeFunctionLength, +} from "../util/insert"; import Integrity from "./lock/integrity"; import Transform from "./transform"; @@ -38,6 +45,16 @@ export default class RGF extends Transform { // The name of the array holding all the `new Function` expressions arrayExpressionName: string; + functionLengthName: string; + + getFunctionLengthName(parents: Node[]) { + if (!this.functionLengthName) { + this.functionLengthName = this.getPlaceholder(); + } + + return this.functionLengthName; + } + constructor(o) { super(o, ObfuscateOrder.RGF); @@ -60,6 +77,19 @@ export default class RGF extends Transform { ) ); } + + // The function.length helper function must be placed last + if (this.functionLengthName) { + prepend( + tree, + FunctionLengthTemplate.single({ + name: this.functionLengthName, + ObjectDefineProperty: this.createInitVariable(ObjectDefineProperty, [ + tree, + ]), + }) + ); + } } match(object, parents) { @@ -141,6 +171,7 @@ export default class RGF extends Transform { walk(object, parents, (o, p) => { if ( o.type === "Identifier" && + o.name !== this.arrayExpressionName && !reservedIdentifiers.has(o.name) && !this.options.globalVariables.has(o.name) ) { @@ -226,9 +257,26 @@ export default class RGF extends Transform { generator: false, }; + // The new program will look like this + // new Function(` + // var rgf_array = this[0] + // function greet(message){ + // console.log(message) + // } + // return greet.apply(this[1], arguments) + // `) + // + // And called like + // f.apply([ rgf_array, this ], arguments) var tree = { type: "Program", body: [ + VariableDeclaration( + VariableDeclarator( + this.arrayExpressionName, + MemberExpression(ThisExpression(), Literal(0)) + ) + ), embeddedFunction, ReturnStatement( CallExpression( @@ -237,7 +285,10 @@ export default class RGF extends Transform { Literal("apply"), true ), - [ThisExpression(), Identifier("arguments")] + [ + MemberExpression(ThisExpression(), Literal(1)), + Identifier("arguments"), + ] ) ), ], @@ -261,12 +312,14 @@ export default class RGF extends Transform { this.arrayExpressionElements.push(newFunctionExpression); // The member expression to retrieve this function - var memberExpression = MemberExpression( + var memberExpression: Node = MemberExpression( Identifier(this.arrayExpressionName), Literal(newFunctionExpressionIndex), true ); + var originalFunctionLength = computeFunctionLength(object.params); + // Replace based on type // (1) Function Declaration: @@ -276,19 +329,55 @@ export default class RGF extends Transform { ReturnStatement( CallExpression( MemberExpression(memberExpression, Literal("apply"), true), - [ThisExpression(), Identifier("arguments")] + [ + ArrayExpression([ + Identifier(this.arrayExpressionName), + ThisExpression(), + ]), + Identifier("arguments"), + ] ) ), ]); // The parameters are no longer needed ('arguments' is used to capture them) object.params = []; + + // The function is no longer guaranteed to not have extraneous parameters passed in + object[predictableFunctionTag] = false; + + if ( + this.options.preserveFunctionLength && + originalFunctionLength !== 0 + ) { + var body = parents[0] as unknown as Node[]; + + body.splice( + body.indexOf(object), + 0, + ExpressionStatement( + CallExpression(Identifier(this.getFunctionLengthName(parents)), [ + Identifier(object.id.name), + Literal(originalFunctionLength), + ]) + ) + ); + } return; } // (2) Function Expression: // - Replace expression with member expression pointing to new function if (object.type === "FunctionExpression") { + if ( + this.options.preserveFunctionLength && + originalFunctionLength !== 0 + ) { + memberExpression = CallExpression( + Identifier(this.getFunctionLengthName(parents)), + [memberExpression, Literal(originalFunctionLength)] + ); + } this.replace(object, memberExpression); return; } diff --git a/src/transforms/stack.ts b/src/transforms/stack.ts index 6d80873..0084eb3 100644 --- a/src/transforms/stack.ts +++ b/src/transforms/stack.ts @@ -1,7 +1,7 @@ import { ok } from "assert"; import { ObfuscateOrder } from "../order"; import { ComputeProbabilityMap } from "../probability"; -import Template from "../templates/template"; +import Template, { ITemplate } from "../templates/template"; import { walk } from "../traverse"; import { AssignmentExpression, @@ -16,6 +16,8 @@ import { RestElement, ReturnStatement, SequenceExpression, + VariableDeclaration, + VariableDeclarator, } from "../util/gen"; import { getIdentifierInfo } from "../util/identifiers"; import { @@ -32,6 +34,7 @@ import { chance, choice, getRandomInteger } from "../util/random"; import Transform from "./transform"; import { noRenameVariablePrefix } from "../constants"; import { FunctionLengthTemplate } from "../templates/functionLength"; +import { ObjectDefineProperty } from "../templates/globals"; export default class Stack extends Transform { mangledExpressionsMade: number; @@ -497,12 +500,18 @@ export default class Stack extends Transform { Template(`${stackName}["length"] = ${startingSize}`).single() ); - if (originalFunctionLength !== 0) { + if (this.options.preserveFunctionLength && originalFunctionLength !== 0) { if (!this.functionLengthName) { this.functionLengthName = this.getPlaceholder(); prepend( parents[parents.length - 1] || object, - FunctionLengthTemplate.single({ name: this.functionLengthName }) + FunctionLengthTemplate.single({ + name: this.functionLengthName, + ObjectDefineProperty: this.createInitVariable( + ObjectDefineProperty, + parents + ), + }) ); } diff --git a/src/transforms/string/encoding.ts b/src/transforms/string/encoding.ts index 7eab1ec..bb9f5cf 100644 --- a/src/transforms/string/encoding.ts +++ b/src/transforms/string/encoding.ts @@ -1,16 +1,47 @@ -import Template from "../../templates/template"; - -const Encoding: { - [encoding_name: string]: { - encode: (s) => string; - decode: (s) => string; - template: ReturnType; - }; -} = { - base91: { +import Template, { ITemplate } from "../../templates/template"; +import { choice, shuffle } from "../../util/random"; + +/** + * Defines an encoding implementation the Obfuscator + */ +export interface EncodingImplementation { + identity: string; + + encode(s): string; + decode(s): string; + template: ITemplate; +} + +let _hasAllEncodings = false; +export function hasAllEncodings() { + return _hasAllEncodings; +} + +export function createEncodingImplementation(): EncodingImplementation { + if (_hasAllEncodings) { + return EncodingImplementations[ + choice(Object.keys(EncodingImplementations)) + ]; + } + + // create base91 encoding + let strTable = + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"'; + + // shuffle table + strTable = shuffle(strTable.split("")).join(""); + + let identity = "base91_" + strTable; + + if (EncodingImplementations.hasOwnProperty(identity)) { + _hasAllEncodings = true; + return EncodingImplementations[identity]; + } + + var encodingImplementation = { + identity, encode(str) { - const table = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"'; + const table = strTable; const raw = Buffer.from(str, "utf-8"); const len = raw.length; @@ -45,8 +76,7 @@ const Encoding: { return ret; }, decode(str) { - const table = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_`{|}~"'; + const table = strTable; const raw = "" + (str || ""); const len = raw.length; @@ -81,45 +111,51 @@ const Encoding: { return Buffer.from(ret).toString("utf-8"); }, template: Template(` - function {name}(str){ - const table = - 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!#$%&()*+,./:;<=>?@[]^_\`{|}~"'; - - const raw = "" + (str || ""); - const len = raw.length; - const ret = []; - - let b = 0; - let n = 0; - let v = -1; - - for (let i = 0; i < len; i++) { - const p = table.indexOf(raw[i]); - if (p === -1) continue; - if (v < 0) { - v = p; - } else { - v += p * 91; - b |= v << n; - n += (v & 8191) > 88 ? 13 : 14; - do { - ret.push(b & 0xff); - b >>= 8; - n -= 8; - } while (n > 7); - v = -1; + function {__fnName__}(str){ + var table = '${strTable}'; + + var raw = "" + (str || ""); + var len = raw.length; + var ret = []; + + var b = 0; + var n = 0; + var v = -1; + + for (var i = 0; i < len; i++) { + var p = table.indexOf(raw[i]); + if (p === -1) continue; + if (v < 0) { + v = p; + } else { + v += p * 91; + b |= v << n; + n += (v & 8191) > 88 ? 13 : 14; + do { + ret.push(b & 0xff); + b >>= 8; + n -= 8; + } while (n > 7); + v = -1; + } } + + if (v > -1) { + ret.push((b | (v << n)) & 0xff); + } + + return {__bufferToString__}(ret); } + `).ignoreMissingVariables(), + }; - if (v > -1) { - ret.push((b | (v << n)) & 0xff); - } - - return {bufferToString}(ret); - } - `), - }, + EncodingImplementations[identity] = encodingImplementation; + return encodingImplementation; +} +export const EncodingImplementations: { + [encodingIdentity: string]: EncodingImplementation; +} = { /* ascii85: { This implementation is flaky and causes decoding errors encode(a) { var b, c, d, e, f, g, h, i, j, k; @@ -209,5 +245,3 @@ const Encoding: { `), }, */ }; - -export default Encoding; diff --git a/src/transforms/string/stringCompression.ts b/src/transforms/string/stringCompression.ts index 330cfe9..585a35b 100644 --- a/src/transforms/string/stringCompression.ts +++ b/src/transforms/string/stringCompression.ts @@ -2,7 +2,7 @@ import { ok } from "assert"; import { ObfuscateOrder } from "../../order"; import { ComputeProbabilityMap } from "../../probability"; import Template from "../../templates/template"; -import { isDirective } from "../../util/compare"; +import { isDirective, isModuleSource } from "../../util/compare"; import { CallExpression, FunctionDeclaration, @@ -16,8 +16,8 @@ import { } from "../../util/gen"; import { append, prepend } from "../../util/insert"; import Transform from "../transform"; -import { isModuleSource } from "./stringConcealing"; -import { chance } from "../../util/random"; +import { predictableFunctionTag } from "../../constants"; + function LZ_encode(c) { ok(c); var x = "charCodeAt", @@ -94,7 +94,7 @@ export default class StringCompression extends Transform { this.map = new Map(); this.ignore = new Set(); this.string = ""; - this.fnName = this.getPlaceholder(); + this.fnName = this.getPlaceholder() + predictableFunctionTag; } apply(tree) { @@ -107,7 +107,7 @@ export default class StringCompression extends Transform { var split = this.getPlaceholder(); var decoder = this.getPlaceholder(); - var getStringName = this.getPlaceholder(); + var getStringName = this.getPlaceholder() + predictableFunctionTag; // Returns the string payload var encoded = LZ_encode(this.string); if (LZ_decode(encoded) !== this.string) { @@ -210,9 +210,6 @@ export default class StringCompression extends Transform { return; } - // HARD CODED LIMIT of 10,000 (after 1,000 elements) - if (this.map.size > 1000 && !chance(this.map.size / 100)) return; - var index = this.map.get(object.value); // New string, add it! diff --git a/src/transforms/string/stringConcealing.ts b/src/transforms/string/stringConcealing.ts index 43d7698..6cf418d 100644 --- a/src/transforms/string/stringConcealing.ts +++ b/src/transforms/string/stringConcealing.ts @@ -1,12 +1,11 @@ import { ok } from "assert"; import { ObfuscateOrder } from "../../order"; import Template from "../../templates/template"; -import { isBlock } from "../../traverse"; -import { isDirective } from "../../util/compare"; +import { getBlock } from "../../traverse"; +import { isDirective, isModuleSource } from "../../util/compare"; import { ArrayExpression, CallExpression, - FunctionExpression, Identifier, Literal, MemberExpression, @@ -22,54 +21,36 @@ import { choice, getRandomInteger, getRandomString, + shuffle, } from "../../util/random"; import Transform from "../transform"; -import Encoding from "./encoding"; +import { + EncodingImplementation, + EncodingImplementations, + createEncodingImplementation, + hasAllEncodings, +} from "./encoding"; import { ComputeProbabilityMap } from "../../probability"; import { BufferToStringTemplate } from "../../templates/bufferToString"; +import { criticalFunctionTag, predictableFunctionTag } from "../../constants"; -export function isModuleSource(object: Node, parents: Node[]) { - if (!parents[0]) { - return false; - } - - if (parents[0].type == "ImportDeclaration" && parents[0].source == object) { - return true; - } - - if (parents[0].type == "ImportExpression" && parents[0].source == object) { - return true; - } - - if ( - parents[1] && - parents[1].type == "CallExpression" && - parents[1].arguments[0] === object && - parents[1].callee.type == "Identifier" - ) { - if ( - parents[1].callee.name == "require" || - parents[1].callee.name == "import" - ) { - return true; - } - } - - return false; +interface FunctionObject { + block: Node; + fnName: string; + encodingImplementation: EncodingImplementation; } export default class StringConcealing extends Transform { arrayExpression: Node; set: Set; - index: { [str: string]: [number, string] }; + index: { [str: string]: [number, string, Node] }; // index, fnName, block arrayName = this.getPlaceholder(); ignore = new Set(); variablesMade = 1; - encoding: { [type: string]: string } = Object.create(null); gen: ReturnType; - hasAllEncodings: boolean; + functionObjects: FunctionObject[] = []; constructor(o) { super(o, ObfuscateOrder.StringConcealing); @@ -77,84 +58,110 @@ export default class StringConcealing extends Transform { this.set = new Set(); this.index = Object.create(null); this.arrayExpression = ArrayExpression([]); - this.hasAllEncodings = false; this.gen = this.getGenerator(); + } + + apply(tree) { + super.apply(tree); // Pad array with useless strings var dead = getRandomInteger(5, 15); for (var i = 0; i < dead; i++) { var str = getRandomString(getRandomInteger(5, 40)); - var fn = this.transform(Literal(str), []); + var fn = this.transform(Literal(str), [tree]); if (fn) { fn(); } } - } - - apply(tree) { - super.apply(tree); var cacheName = this.getPlaceholder(); - var bufferToStringName = this.getPlaceholder(); + var bufferToStringName = this.getPlaceholder() + predictableFunctionTag; // This helper functions convert UInt8 Array to UTf-string prepend( tree, - ...BufferToStringTemplate.compile({ name: bufferToStringName }) + ...BufferToStringTemplate.compile({ + name: bufferToStringName, + getGlobalFnName: this.getPlaceholder() + predictableFunctionTag, + }) ); - Object.keys(this.encoding).forEach((type) => { - var { template } = Encoding[type]; - var decodeFn = this.getPlaceholder(); - var getterFn = this.encoding[type]; + for (var functionObject of this.functionObjects) { + var { + block, + fnName: getterFnName, + encodingImplementation, + } = functionObject; + + var decodeFn = + this.getPlaceholder() + predictableFunctionTag + criticalFunctionTag; append( - tree, - template.single({ name: decodeFn, bufferToString: bufferToStringName }) + block, + encodingImplementation.template.single({ + __fnName__: decodeFn, + __bufferToString__: bufferToStringName, + }) ); + // All these are fake and never ran + var ifStatements = Template(`if ( z == x ) { + return y[${cacheName}[z]] = ${getterFnName}(x, y); + } + if ( y ) { + [b, y] = [a(b), x || z] + return ${getterFnName}(x, b, z) + } + if ( z && a !== ${decodeFn} ) { + ${getterFnName} = ${decodeFn} + return ${getterFnName}(x, -1, z, a, b) + } + if ( a === ${getterFnName} ) { + ${decodeFn} = y + return ${decodeFn}(z) + } + if( a === undefined ) { + ${getterFnName} = b + } + if( z == a ) { + return y ? x[b[y]] : ${cacheName}[x] || (z=(b[x] || a), ${cacheName}[x] = z(${this.arrayName}[x])) + } + `).compile(); - append( - tree, + // Not all fake if-statements are needed + ifStatements = ifStatements.filter(() => chance(50)); + + // This one is always used + ifStatements.push( Template(` - - function ${getterFn}(x, y, z, a = ${decodeFn}, b = ${cacheName}){ - if ( z ) { - return y[${cacheName}[z]] = ${getterFn}(x, y); - } else if ( y ) { - [b, y] = [a(b), x || z] - } - - return y ? x[b[y]] : ${cacheName}[x] || (z=(b[x], a), ${cacheName}[x] = z(${this.arrayName}[x])) - } - - `).single() + if ( x !== y ) { + return b[x] || (b[x] = a(${this.arrayName}[x])) + } + `).single() ); - }); - var flowIntegrity = this.getPlaceholder(); + shuffle(ifStatements); + + var varDeclaration = Template(` + var ${getterFnName} = (x, y, z, a, b)=>{ + if(typeof a === "undefined") { + a = ${decodeFn} + } + if(typeof b === "undefined") { + b = ${cacheName} + } + } + `).single(); + + varDeclaration.declarations[0].init.body.body.push(...ifStatements); + + prepend(block, varDeclaration); + } prepend( tree, VariableDeclaration([ VariableDeclarator(cacheName, ArrayExpression([])), - VariableDeclarator(flowIntegrity, Literal(0)), - VariableDeclarator( - this.arrayName, - CallExpression( - FunctionExpression( - [], - [ - VariableDeclaration( - VariableDeclarator("a", this.arrayExpression) - ), - Template( - `return (${flowIntegrity} ? a["pop"]() : ${flowIntegrity}++, a)` - ).single(), - ] - ), - [] - ) - ), + VariableDeclarator(this.arrayName, this.arrayExpression), ]) ); } @@ -192,48 +199,67 @@ export default class StringConcealing extends Transform { return; } - // HARD CODED LIMIT of 10,000 (after 1,000 elements) - if (this.set.size > 1000 && !chance(this.set.size / 100)) return; + var currentBlock = getBlock(object, parents); - var types = Object.keys(this.encoding); + // Find created functions + var functionObjects: FunctionObject[] = parents + .filter((node) => node.$stringConcealingFunctionObject) + .map((item) => item.$stringConcealingFunctionObject); - var type = choice(types); - if (!type || (!this.hasAllEncodings && chance(10))) { - var allowed = Object.keys(Encoding).filter( - (type) => !this.encoding[type] - ); + // Choose random functionObject to use + var functionObject = choice(functionObjects); + + if ( + !functionObject || + (!hasAllEncodings() && + chance(25 / this.functionObjects.length) && + !currentBlock.$stringConcealingFunctionObject) + ) { + // No functions, create one - if (!allowed.length) { - this.hasAllEncodings = true; - } else { - var random = choice(allowed); - type = random; + var newFunctionObject: FunctionObject = { + block: currentBlock, + encodingImplementation: createEncodingImplementation(), + fnName: this.getPlaceholder() + predictableFunctionTag, + }; - this.encoding[random] = this.getPlaceholder(); - } + this.functionObjects.push(newFunctionObject); + currentBlock.$stringConcealingFunctionObject = newFunctionObject; + functionObject = newFunctionObject; } - var fnName = this.encoding[type]; - var encoder = Encoding[type]; + var { fnName, encodingImplementation } = functionObject; - // The decode function must return correct result - var encoded = encoder.encode(object.value); - if (encoder.decode(encoded) != object.value) { - this.ignore.add(object.value); - this.warn(type, object.value.slice(0, 100)); - return; + var index = -1; + + // String already decoded? + if (this.set.has(object.value)) { + var row = this.index[object.value]; + if (parents.includes(row[2])) { + [index, fnName] = row; + ok(typeof index === "number"); + } } - var index = -1; - if (!this.set.has(object.value)) { + if (index == -1) { + // The decode function must return correct result + var encoded = encodingImplementation.encode(object.value); + if (encodingImplementation.decode(encoded) !== object.value) { + this.ignore.add(object.value); + this.warn( + encodingImplementation.identity, + object.value.slice(0, 100) + ); + delete EncodingImplementations[encodingImplementation.identity]; + + return; + } + this.arrayExpression.elements.push(Literal(encoded)); index = this.arrayExpression.elements.length - 1; - this.index[object.value] = [index, fnName]; + this.index[object.value] = [index, fnName, currentBlock]; this.set.add(object.value); - } else { - [index, fnName] = this.index[object.value]; - ok(typeof index === "number"); } ok(index != -1, "index == -1"); @@ -269,7 +295,7 @@ export default class StringConcealing extends Transform { var constantReferenceType = choice(["variable", "array", "object"]); - var place = choice(parents.filter((node) => isBlock(node))); + var place = currentBlock; if (!place) { this.error(new Error("No lexical block to insert code")); } diff --git a/src/transforms/string/stringEncoding.ts b/src/transforms/string/stringEncoding.ts index 21651b8..04418e9 100644 --- a/src/transforms/string/stringEncoding.ts +++ b/src/transforms/string/stringEncoding.ts @@ -1,7 +1,6 @@ import Transform from "../transform"; import { choice } from "../../util/random"; -import { isDirective } from "../../util/compare"; -import { isModuleSource } from "./stringConcealing"; +import { isDirective, isModuleSource } from "../../util/compare"; import { ComputeProbabilityMap } from "../../probability"; import { Identifier } from "../../util/gen"; diff --git a/src/transforms/string/stringSplitting.ts b/src/transforms/string/stringSplitting.ts index 3264c15..765ceb1 100644 --- a/src/transforms/string/stringSplitting.ts +++ b/src/transforms/string/stringSplitting.ts @@ -3,8 +3,7 @@ import { Node, Literal, BinaryExpression } from "../../util/gen"; import { clone } from "../../util/insert"; import { getRandomInteger, shuffle, splitIntoChunks } from "../../util/random"; import { ObfuscateOrder } from "../../order"; -import { isModuleSource } from "./stringConcealing"; -import { isDirective } from "../../util/compare"; +import { isDirective, isModuleSource } from "../../util/compare"; import { ok } from "assert"; import { ComputeProbabilityMap } from "../../probability"; diff --git a/src/transforms/transform.ts b/src/transforms/transform.ts index 00b7050..3b756b4 100644 --- a/src/transforms/transform.ts +++ b/src/transforms/transform.ts @@ -1,5 +1,10 @@ import traverse, { ExitCallback } from "../traverse"; -import { AddComment, Node } from "../util/gen"; +import { + AddComment, + Node, + VariableDeclaration, + VariableDeclarator, +} from "../util/gen"; import { alphabeticalGenerator, choice, @@ -15,6 +20,8 @@ import { reservedKeywords, } from "../constants"; import { ObfuscateOrder } from "../order"; +import { ITemplate } from "../templates/template"; +import { prepend } from "../util/insert"; /** * Base-class for all transformations. @@ -78,6 +85,8 @@ export default class Transform { */ after: Transform[]; + initVariables = new Map(); + constructor(obfuscator, priority: number = -1) { ok(obfuscator instanceof Obfuscator, "obfuscator should be an Obfuscator"); @@ -336,6 +345,26 @@ export default class Transform { return identifier; } + createInitVariable = (value: ITemplate, parents: Node[]) => { + var key = value.templates[0]; + if (this.initVariables.has(key)) { + return this.initVariables.get(key); + } + + var root = parents[parents.length - 1]; + ok(root.type === "Program"); + + var name = this.getPlaceholder(); + this.initVariables.set(key, name); + + prepend( + root, + VariableDeclaration(VariableDeclarator(name, value.single().expression)) + ); + + return name; + }; + /** * Smartly appends a comment to a Node. * - Includes the transformation's name. diff --git a/src/util/compare.ts b/src/util/compare.ts index 1188f38..6524b11 100644 --- a/src/util/compare.ts +++ b/src/util/compare.ts @@ -74,19 +74,52 @@ export function isDirective(object: Node, parents: Node[]) { return parents[dIndex].expression == (parents[dIndex - 1] || object); } +export function isModuleSource(object: Node, parents: Node[]) { + if (!parents[0]) { + return false; + } + + if (parents[0].type == "ImportDeclaration" && parents[0].source == object) { + return true; + } + + if (parents[0].type == "ImportExpression" && parents[0].source == object) { + return true; + } + + if ( + parents[1] && + parents[1].type == "CallExpression" && + parents[1].arguments[0] === object && + parents[1].callee.type == "Identifier" + ) { + if ( + parents[1].callee.name == "require" || + parents[1].callee.name == "import" + ) { + return true; + } + } + + return false; +} + +export function isMoveable(object: Node, parents: Node[]) { + return !isDirective(object, parents) && !isModuleSource(object, parents); +} + export function isIndependent(object: Node, parents: Node[]) { if (object.type == "Literal") { return true; } - var parent = parents[0]; - if (object.type == "Identifier") { - var set = new Set(["null", "undefined"]); - if (set.has(object.name)) { + if (primitiveIdentifiers.has(object.name)) { return true; } - if (parent.type == "Property") { + + var parent = parents[0]; + if (parent && parent.type == "Property") { if (!parent.computed && parent.key == object) { return true; } @@ -105,6 +138,7 @@ export function isIndependent(object: Node, parents: Node[]) { if (object != $object) { if (!Array.isArray($object) && !isIndependent($object, $parents)) { allowIt = false; + return "EXIT"; } } }); diff --git a/src/util/gen.ts b/src/util/gen.ts index d3546cb..14f622e 100644 --- a/src/util/gen.ts +++ b/src/util/gen.ts @@ -1,4 +1,5 @@ import { ok } from "assert"; +import { predictableFunctionTag } from "../constants"; import { isValidIdentifier } from "./compare"; export type Type = @@ -306,6 +307,7 @@ export function FunctionExpression(params: Node[], body: any[]) { generator: false, expression: false, async: false, + [predictableFunctionTag]: true, }; } @@ -340,6 +342,7 @@ export function FunctionDeclaration( generator: false, expression: false, async: false, + [predictableFunctionTag]: true, }; } @@ -529,16 +532,20 @@ export function AddComment(node: Node, text: string) { return node; } +export function Super() { + return { type: "Super" }; +} + export function MethodDefinition( - identifier: Node, + key: Node, functionExpression: Node, kind: "method" | "constructor" | "get" | "set", - isStatic = true, + isStatic = false, computed = false ) { return { type: "MethodDefinition", - key: identifier, + key: key, computed: computed, value: functionExpression, kind: kind, diff --git a/src/util/insert.ts b/src/util/insert.ts index 5930d24..ffae140 100644 --- a/src/util/insert.ts +++ b/src/util/insert.ts @@ -3,6 +3,12 @@ import { getBlock, isBlock } from "../traverse"; import { Node } from "./gen"; import { getIdentifierInfo, validateChain } from "./identifiers"; +export function isClass(object: Node): boolean { + return ( + object.type === "ClassDeclaration" || object.type === "ClassExpression" + ); +} + /** * - `FunctionDeclaration` * - `FunctionExpression` @@ -18,6 +24,17 @@ export function isFunction(object: Node): boolean { ].includes(object && object.type); } +export function isStrictModeFunction(object: Node): boolean { + ok(isFunction(object)); + + return ( + object.body.type === "BlockStatement" && + object.body.body[0] && + object.body.body[0].type === "ExpressionStatement" && + object.body.body[0].directive === "use strict" + ); +} + /** * The function context where the object is. * diff --git a/src/util/scope.ts b/src/util/scope.ts index fb6bec5..1fed3cc 100644 --- a/src/util/scope.ts +++ b/src/util/scope.ts @@ -1,9 +1,21 @@ +import { ok } from "assert"; import { isBlock } from "../traverse"; +import { Node } from "./gen"; -export function isLexicalScope(object) { +export function isLexicalScope(object: Node) { return isBlock(object) || object.type == "SwitchCase"; } -export function getLexicalScope(object, parents) { +export function getLexicalScope(object: Node, parents: Node[]): Node { return [object, ...parents].find((node) => isLexicalScope(node)); } + +export function getLexicalScopeBody(object: Node): Node[] { + ok(isLexicalScope(object)); + + return isBlock(object) + ? object.body + : object.type === "SwitchCase" + ? object.consequent + : ok("Unhandled case"); +} diff --git a/test/code/Cash.test.ts b/test/code/Cash.test.ts index c0ca767..a3b2828 100644 --- a/test/code/Cash.test.ts +++ b/test/code/Cash.test.ts @@ -32,12 +32,18 @@ test("Variant #1: Cash.js on High Preset (Strict Mode)", async () => { $: false, } as any; window.window = window; + global.window = window; - // writeFileSync(join(__dirname, "Cash.output.js"), output, { - // encoding: "utf-8", - // }); + try { + eval(output); + } catch (e) { + console.error(e); + writeFileSync("dev.output.js", output, { + encoding: "utf-8", + }); - eval(output); + expect(true).toStrictEqual(false); + } expect(window).toHaveProperty("cash"); }); diff --git a/test/code/StrictMode.src.js b/test/code/StrictMode.src.js new file mode 100644 index 0000000..2355733 --- /dev/null +++ b/test/code/StrictMode.src.js @@ -0,0 +1,65 @@ +"use strict"; + +function TestStrictMode() { + "use strict"; + + var isStrictMode = () => { + try { + undefined = true; + } catch (E) { + return true; + } + return false; + }; + + // filler code to make transformations more likely to affect this function + var x, y, z; + var string1 = "Hello World"; + var string2 = "use strict"; + + var chars = string2.split(""); + var count = 0; + for (var char of chars) { + count++; + } + + expect(count).toStrictEqual(10); + + // This function should be in strict mode + expect(isStrictMode()).toStrictEqual(true); +} + +var isStrictMode = () => { + try { + undefined = true; + } catch (E) { + return true; + } + return false; +}; + +// Global level should be in strict mode +expect(isStrictMode()).toStrictEqual(true); +TestStrictMode(); + +// Direct vs. Indirect eval usage +var evalString = ` +var isStrictMode = () => { + try { + undefined = true; + } catch (E) { + return true; + } + return false; +}; + +isStrictMode();`; + +// Direct eval -> Preserve global strict-mode +var directEvalResult = eval(evalString); +expect(directEvalResult).toStrictEqual(true); + +// Indirect eval -> Does not inherit context strict-mode +var _eval_ = eval; +var indirectEvalResult = _eval_(evalString); +expect(indirectEvalResult).toStrictEqual(false); diff --git a/test/code/StrictMode.test.js b/test/code/StrictMode.test.js new file mode 100644 index 0000000..3b6a3b3 --- /dev/null +++ b/test/code/StrictMode.test.js @@ -0,0 +1,37 @@ +import { readFileSync, writeFileSync } from "fs"; +import { join } from "path"; +import JsConfuser from "../../src/index"; + +var StrictMode_JS = readFileSync( + join(__dirname, "./StrictMode.src.js"), + "utf-8" +); + +test("Variant #1: StrictMode on High Preset", async () => { + var output = await JsConfuser(StrictMode_JS, { + target: "node", + preset: "high", + }); + + //writeFileSync("./dev.output.js", output); + + eval(output); +}); + +test("Variant #2: StrictMode on 2x High Preset", async () => { + var output = await JsConfuser(StrictMode_JS, { + target: "node", + preset: "high", + }); + + //writeFileSync("./dev.output1.js", output); + + var output2 = await JsConfuser(output, { + target: "node", + preset: "high", + }); + + //writeFileSync("./dev.output2.js", output2); + + eval(output2); +}); diff --git a/test/compare.test.ts b/test/compare.test.ts index 47e2b47..0f1b7ab 100644 --- a/test/compare.test.ts +++ b/test/compare.test.ts @@ -1,3 +1,4 @@ +import Template from "../src/templates/template"; import { isIndependent } from "../src/util/compare"; import { ArrayExpression, @@ -17,9 +18,9 @@ describe("isIndependent", () => { ).toStrictEqual(false); }); - it("should return true for reserved identifiers (null, undefined, etc)", () => { + it("should return true for reserved identifiers (undefined, NaN, etc)", () => { expect( - isIndependent(Identifier("null"), [{ type: "Program" }]) + isIndependent(Identifier("undefined"), [{ type: "Program" }]) ).toStrictEqual(true); }); @@ -41,4 +42,63 @@ describe("isIndependent", () => { it("should return false for everything else", () => { expect(isIndependent(FunctionExpression([], []), [])).toStrictEqual(false); }); + + it("various cases", () => { + expect( + isIndependent( + Template(`({ + x: 1, + y: 2, + z: 3, + })`).single().expression, + [] + ) + ).toStrictEqual(true); + + expect( + isIndependent( + Template(`({ + x: 1, + y: 2, + z: [3,4,5,6,7,"My String",undefined,null,NaN], + })`).single().expression, + [] + ) + ).toStrictEqual(true); + + expect( + isIndependent( + Template(`({ + x: 1, + y: 2, + z: 3, + _: function(){return value} + })`).single().expression, + [] + ) + ).toStrictEqual(false); + + expect( + isIndependent( + Template(`({ + x: 1, + y: 2, + z: 3, + _: [value] + })`).single().expression, + [] + ) + ).toStrictEqual(false); + + expect( + isIndependent( + Template(`([ + { + x: value + } + ])`).single().expression, + [] + ) + ).toStrictEqual(false); + }); }); diff --git a/test/options.test.ts b/test/options.test.ts index 226c60a..777997c 100644 --- a/test/options.test.ts +++ b/test/options.test.ts @@ -1,76 +1,132 @@ import JsConfuser from "../src/index"; -it("should accept percentages", async () => { - var output = await JsConfuser(`var TEST_VARIABLE;`, { - target: "node", - renameGlobals: true, - renameVariables: true, - stringConcealing: 0.5, +describe("options", () => { + test("Variant #1: Accept percentages", async () => { + var output = await JsConfuser(`var TEST_VARIABLE;`, { + target: "node", + renameGlobals: true, + renameVariables: true, + stringConcealing: 0.5, + }); + + expect(output).not.toContain("TEST_VARIABLE"); }); - expect(output).not.toContain("TEST_VARIABLE"); -}); + test("Variant #2: Accept probability arrays", async () => { + var output = await JsConfuser(`var TEST_VARIABLE;`, { + target: "node", + renameVariables: true, + renameGlobals: true, + identifierGenerator: ["hexadecimal", "mangled"], // half hexadecimal, half randomized + }); -it("should accept probability arrays", async () => { - var output = await JsConfuser(`var TEST_VARIABLE;`, { - target: "node", - renameVariables: true, - renameGlobals: true, - identifierGenerator: ["hexadecimal", "mangled"], // half hexadecimal, half randomized + expect(output).not.toContain("TEST_VARIABLE"); }); - expect(output).not.toContain("TEST_VARIABLE"); -}); + test("Variant #3: Accept probability maps", async () => { + var output = await JsConfuser(`var TEST_VARIABLE;`, { + target: "node", + renameVariables: true, + renameGlobals: true, + identifierGenerator: { + // 25% each + hexadecimal: 0.25, + randomized: 0.25, + mangled: 0.25, + number: 0.25, + }, + }); -it("should accept probability maps", async () => { - var output = await JsConfuser(`var TEST_VARIABLE;`, { - target: "node", - renameVariables: true, - renameGlobals: true, - identifierGenerator: { - // 25% each - hexadecimal: 0.25, - randomized: 0.25, - mangled: 0.25, - number: 0.25, - }, + expect(output).not.toContain("TEST_VARIABLE"); }); - expect(output).not.toContain("TEST_VARIABLE"); -}); + test("Variant #4: Work with compact false", async () => { + var output = await JsConfuser(`var TEST_VARIABLE;`, { + target: "node", + renameGlobals: true, + renameVariables: true, + compact: false, + }); -it("should work with compact false", async () => { - var output = await JsConfuser(`var TEST_VARIABLE;`, { - target: "node", - renameGlobals: true, - renameVariables: true, - compact: false, + expect(output).not.toContain("TEST_VARIABLE"); }); - expect(output).not.toContain("TEST_VARIABLE"); -}); + test("Variant #5: Work with indent set to 2 spaces", async () => { + var output = await JsConfuser(`var TEST_VARIABLE;`, { + target: "node", + renameGlobals: true, + renameVariables: true, + compact: false, + indent: 2, + }); -it("should work with indent set to 2 spaces", async () => { - var output = await JsConfuser(`var TEST_VARIABLE;`, { - target: "node", - renameGlobals: true, - renameVariables: true, - compact: false, - indent: 2, + expect(output).not.toContain("TEST_VARIABLE"); }); - expect(output).not.toContain("TEST_VARIABLE"); + test("Variant #6: Work with debugComments enabled", async () => { + var output = await JsConfuser(`var TEST_VARIABLE;`, { + target: "node", + renameGlobals: true, + renameVariables: true, + compact: false, + indent: 2, + debugComments: true, + }); + + expect(output).not.toContain("TEST_VARIABLE"); + }); }); -it("should work with debugComments enabled", async () => { - var output = await JsConfuser(`var TEST_VARIABLE;`, { - target: "node", - renameGlobals: true, - renameVariables: true, - compact: false, - indent: 2, - debugComments: true, +describe("options.preserveFunctionLength", () => { + test("Variant #1: Enabled by default", async () => { + var output = await JsConfuser( + ` + function myFunction(a, b, c, d = "") { + // Function.length = 3 + } + + TEST_OUTPUT = myFunction.length; // 3 + `, + { + target: "node", + preset: "high", + } + ); + + var TEST_OUTPUT; + eval(output); + expect(TEST_OUTPUT).toStrictEqual(3); }); - expect(output).not.toContain("TEST_VARIABLE"); + test("Variant #2: Disabled", async () => { + var output = await JsConfuser( + ` + function myFunction(a, b, c, d = "") { + // Function.length = 3 + } + + TEST_OUTPUT = myFunction.length; // 3 + `, + { + target: "node", + preset: "high", + preserveFunctionLength: false, + + stringEncoding: false, + stringCompression: false, + stringConcealing: false, + stringSplitting: false, + deadCode: false, + duplicateLiteralsRemoval: false, + + rgf: true, + } + ); + + expect(output).not.toContain("defineProperty"); + + var TEST_OUTPUT; + eval(output); + expect(TEST_OUTPUT).not.toStrictEqual(3); + }); }); diff --git a/test/transforms/controlFlowFlattening/expressionObfuscation.test.ts b/test/transforms/controlFlowFlattening/expressionObfuscation.test.ts index 1efef51..2a5cc26 100644 --- a/test/transforms/controlFlowFlattening/expressionObfuscation.test.ts +++ b/test/transforms/controlFlowFlattening/expressionObfuscation.test.ts @@ -1,15 +1,21 @@ +import { criticalFunctionTag } from "../../../src/constants"; import JsConfuser from "../../../src/index"; +// Enable Control Flow Flattening's 'Expression Obfuscation' but skips all CFF switch transformations +function fakeEnabled() { + return false; +} + test("Variant #1: Join expressions in a sequence expression", async () => { var output = await JsConfuser( ` TEST_OUTPUT=0; TEST_OUTPUT++; `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("(TEST_OUTPUT=0,TEST_OUTPUT++"); + expect(output).toContain("TEST_OUTPUT=0,TEST_OUTPUT++"); var TEST_OUTPUT; eval(output); @@ -25,10 +31,10 @@ test("Variant #2: If Statement", async () => { TEST_OUTPUT++; } `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("if(TEST_OUTPUT=0,true)"); + expect(output).toContain("TEST_OUTPUT=0,true"); var TEST_OUTPUT; eval(output); @@ -44,10 +50,10 @@ test("Variant #3: ForStatement (Variable Declaration initializer)", async () => TEST_OUTPUT++; } `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("for(var i=(TEST_OUTPUT=0,0)"); + expect(output).toContain("TEST_OUTPUT=0,0"); var TEST_OUTPUT; eval(output); @@ -63,10 +69,10 @@ test("Variant #4: ForStatement (Assignment expression initializer)", async () => TEST_OUTPUT++; } `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("for(i=(TEST_OUTPUT=0,0)"); + expect(output).toContain("TEST_OUTPUT=0,0"); var TEST_OUTPUT, i; eval(output); @@ -83,10 +89,10 @@ test("Variant #5: Return statement", async () => { } fn(); `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("return TEST_OUTPUT=10,'Value'}"); + expect(output).toContain("TEST_OUTPUT=10,'Value'"); var TEST_OUTPUT; eval(output); @@ -103,10 +109,10 @@ test("Variant #6: Return statement (no argument)", async () => { } fn(); `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("return TEST_OUTPUT=10,undefined}"); + expect(output).toContain("TEST_OUTPUT=10,undefined"); var TEST_OUTPUT; eval(output); @@ -127,10 +133,10 @@ test("Variant #7: Throw statement", async () => { } `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("throw TEST_OUTPUT='Correct Value',new Error"); + expect(output).toContain("TEST_OUTPUT='Correct Value',new Error"); var TEST_OUTPUT; eval(output); @@ -144,10 +150,10 @@ test("Variant #8: Variable declaration", async () => { TEST_OUTPUT = "Correct Value"; var x = 1, y = 2; `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("var x=(TEST_OUTPUT='Correct Value',1)"); + expect(output).toContain("(TEST_OUTPUT='Correct Value'"); var TEST_OUTPUT; eval(output); @@ -161,13 +167,26 @@ test("Variant #9: Variable declaration (no initializer)", async () => { TEST_OUTPUT = "Correct Value"; var x,y; `, - { target: "node", controlFlowFlattening: true } + { target: "node", controlFlowFlattening: fakeEnabled } ); - expect(output).toContain("var x=(TEST_OUTPUT='Correct Value',undefined)"); + expect(output).toContain(`(TEST_OUTPUT='Correct Value',undefined)`); var TEST_OUTPUT; eval(output); expect(TEST_OUTPUT).toStrictEqual("Correct Value"); }); + +test("Variant #10: Use function call", async () => { + var output = await JsConfuser( + ` + TEST_OUTPUT = "Correct Value"; + var x,y; + `, + { target: "node", controlFlowFlattening: fakeEnabled } + ); + + expect(output).toContain("function"); + expect(output).toContain(criticalFunctionTag); +}); diff --git a/test/transforms/dispatcher.test.ts b/test/transforms/dispatcher.test.ts index b69614c..2cd6370 100644 --- a/test/transforms/dispatcher.test.ts +++ b/test/transforms/dispatcher.test.ts @@ -400,3 +400,58 @@ printX(); expect(TEST_OUTPUT).toStrictEqual("Correct Value"); }); + +test("Variant #18: Preserve function.length property", async () => { + var output = await JsConfuser( + ` + function myFunction1(){ + // Function.length = 0 + } + function myFunction2(a, b, c, d = "") { + // Function.length = 3 + } + + myFunction1(); + myFunction2(); + TEST_OUTPUT_1 = myFunction1.length; + TEST_OUTPUT_2 = myFunction2.length; + + `, + { target: "node", dispatcher: true } + ); + + expect(output).toContain("dispatcher_0"); + + var TEST_OUTPUT_1, TEST_OUTPUT_2; + eval(output); + + expect(TEST_OUTPUT_1).toStrictEqual(0); + expect(TEST_OUTPUT_2).toStrictEqual(3); +}); + +test("Variant #19: Lexically bound variables", async () => { + var output = await JsConfuser( + ` + switch (true) { + case true: + let message = "Hello World"; + + function logMessage() { + TEST_OUTPUT = message; + } + + logMessage(); + } + `, + { + target: "node", + dispatcher: true, + } + ); + + var TEST_OUTPUT; + + eval(output); + + expect(TEST_OUTPUT).toStrictEqual("Hello World"); +}); diff --git a/test/transforms/extraction/classExtraction.test.ts b/test/transforms/extraction/classExtraction.test.ts new file mode 100644 index 0000000..f8f7fb2 --- /dev/null +++ b/test/transforms/extraction/classExtraction.test.ts @@ -0,0 +1,86 @@ +import JsConfuser from "../../../src/index"; + +// TODO +test.skip("Variant #1: Extract class methods", async () => { + var code = ` + function nested() { + if (true) { + switch (true) { + case true: + let dimension2D = "2D"; + + class Square { + constructor(size) { + this.size = size; + } + + static fromJSON(json) { + return new Square(json.size); + } + + getArea() { + return this.size * this.size; + } + + get dimensions() { + return dimension2D; + } + + set dimensions(value) { + if (value !== "2D") { + throw new Error("Only supports 2D"); + } + } + } + + class Rectangle extends Square { + constructor(width, length) { + super(null); + this.width = width; + this.length = length; + } + + static fromJSON(json) { + return new Rectangle(json.width, json.height); + } + + getArea() { + return this.width * this.length; + } + + myMethod(dim = super.dimensions) { + console.log(dim); + } + } + + var rectangle = Rectangle.fromJSON({ width: 10, height: 5 }); + + console.log(rectangle.getArea()); + rectangle.myMethod(); + + rectangle.dimensions = "2D"; // Allowed + + try { + rectangle.dimensions = "3D"; // Not allowed + } catch (e) { + if (e.message.includes("Only supports 2D")) { + // console.log("Failed to set dimensions"); + TEST_OUTPUT = true; + } + } + + } + } + } + + nested(); + + `; + + var output = await JsConfuser(code, { target: "node" }); + + var TEST_OUTPUT; + eval(output); + + expect(TEST_OUTPUT).toStrictEqual(true); +}); diff --git a/test/transforms/extraction/duplicateLiteralsRemoval.test.ts b/test/transforms/extraction/duplicateLiteralsRemoval.test.ts index 82456c6..5760fa2 100644 --- a/test/transforms/extraction/duplicateLiteralsRemoval.test.ts +++ b/test/transforms/extraction/duplicateLiteralsRemoval.test.ts @@ -184,9 +184,17 @@ test("Variant #9: Undefined as variable name", async () => { ` var undefined = 0; var undefined = 1; + + try { + (undefined = 0); + undefined = 1; + } catch (e) { + } `, { target: "node", duplicateLiteralsRemoval: true } ); + expect(output).toContain("(undefined="); + eval(output); }); diff --git a/test/transforms/extraction/objectExtraction.test.ts b/test/transforms/extraction/objectExtraction.test.ts index 7b52fe9..76711b9 100644 --- a/test/transforms/extraction/objectExtraction.test.ts +++ b/test/transforms/extraction/objectExtraction.test.ts @@ -482,6 +482,8 @@ test("Variant #16: Handle const declarations", async () => { } ); + expect(output).toContain("let"); + var TEST_OUTPUT; eval(output); diff --git a/test/transforms/identifier/globalConcealing.test.ts b/test/transforms/identifier/globalConcealing.test.ts index 9125e7d..57a0ea1 100644 --- a/test/transforms/identifier/globalConcealing.test.ts +++ b/test/transforms/identifier/globalConcealing.test.ts @@ -51,3 +51,22 @@ test("Variant #3: Properly hide in default parameter, function expression", asyn expect(TEST_OUTPUT).toStrictEqual(true); }); + +// https://github.com/MichaelXF/js-confuser/issues/131 +test("Variant #4: Don't change __dirname", async function () { + var code = ` + TEST_OUTPUT = __dirname; + `; + + var output = await JsConfuser(code, { + target: "node", + globalConcealing: true, + }); + + expect(output).toContain("__dirname"); + + var TEST_OUTPUT; + eval(output); + + expect(typeof TEST_OUTPUT).toStrictEqual("string"); +}); diff --git a/test/transforms/identifier/movedDeclarations.test.ts b/test/transforms/identifier/movedDeclarations.test.ts index 0fa5007..8ab9bfd 100644 --- a/test/transforms/identifier/movedDeclarations.test.ts +++ b/test/transforms/identifier/movedDeclarations.test.ts @@ -1,3 +1,4 @@ +import { predictableFunctionTag } from "../../../src/constants"; import JsConfuser from "../../../src/index"; test("Variant #1: Move variable 'y' to top", async () => { @@ -212,3 +213,63 @@ test("Variant #9: Defined variable without an initializer", async () => { eval(output2); expect(TEST_OUTPUT).toStrictEqual(3); }); + +test("Variant #10: Move parameters to predictable function", async () => { + var code = ` + function testFunction${predictableFunctionTag}_FN(){ + var values = [10,20,35,"40", null] + var parseIntKey = "parseInt" + var output = 0 + var utils = { + get parser${predictableFunctionTag}(){ + var fn = (value)=>{ + return global[parseIntKey](value) + } + return fn + }, + + set setter_fn${predictableFunctionTag}(newValue){ + var fakeVar + } + } + + class FakeClass { + constructor(){ + } + + get fakeGet${predictableFunctionTag}(){ + var fakeVar + } + } + + for(var value of values) { + switch(value){ + case null: + break; + + default: + var stringifiedValue = "" + value + var parsedValue = utils.parser${predictableFunctionTag}(stringifiedValue) + output += parsedValue; + break; + } + } + + return output + } + + TEST_OUTPUT = testFunction${predictableFunctionTag}_FN() + `; + + var output = await JsConfuser(code, { + target: "node", + movedDeclarations: true, + }); + + expect(output).toContain("_FN(values"); + + var TEST_OUTPUT; + eval(output); + + expect(TEST_OUTPUT).toStrictEqual(105); +}); diff --git a/test/transforms/minify.test.ts b/test/transforms/minify.test.ts index aac4240..bb07330 100644 --- a/test/transforms/minify.test.ts +++ b/test/transforms/minify.test.ts @@ -211,6 +211,16 @@ test("Variant #11: Shorten 'undefined' to 'void 0'", async () => { }); expect(output2).toContain("var x={[void 0]:1}"); + + var output3 = await JsConfuser( + `try { var undefined; (undefined) = true } catch(e) {}`, + { + target: "node", + minify: true, + } + ); + + eval(output3); }); test("Variant #11: Shorten 'Infinity' to 1/0", async () => { @@ -536,3 +546,30 @@ test("Variant #27: Preserve function.length property", async () => { expect(TEST_OUTPUT).toStrictEqual(6); }); + +// https://github.com/MichaelXF/js-confuser/issues/125 +test("Variant #28: Don't break destructuring assignment", async () => { + var output = await JsConfuser( + ` + let objectSlice = []; + objectSlice.push({ + a: 1, + b: 2, + c: 3, + }) + for (let { + a, + b, + c + } of objectSlice) { + TEST_OUTPUT = a + b + c; + } + `, + { target: "node", minify: true } + ); + + var TEST_OUTPUT; + eval(output); + + expect(TEST_OUTPUT).toStrictEqual(6); +}); diff --git a/test/transforms/rgf.test.ts b/test/transforms/rgf.test.ts index 868b929..5757f0c 100644 --- a/test/transforms/rgf.test.ts +++ b/test/transforms/rgf.test.ts @@ -326,3 +326,53 @@ test("Variant #10: Configurable by custom function option", async () => { expect(TEST_OUTPUT_1).toStrictEqual(false); expect(TEST_OUTPUT_2).toStrictEqual(true); }); + +test("Variant #11: Function containing function should both be changed", async function () { + var output = await JsConfuser( + ` + function FunctionA(){ + function FunctionB(){ + var bVar = 10; + return bVar + } + + var bFn = FunctionB; + var aVar = bFn(); + return aVar + 1 + } + + TEST_OUTPUT = FunctionA(); + `, + { target: "node", rgf: true } + ); + + // 2 means one Function changed, 3 means two Functions changed + expect(output.split("new Function").length).toStrictEqual(3); + + var TEST_OUTPUT; + eval(output); + + expect(TEST_OUTPUT).toStrictEqual(11); +}); + +test("Variant #12: Preserve Function.length", async function () { + var output = await JsConfuser( + ` + function myFunction(a,b,c,d = ""){ // Function.length = 3 + + } + + myFunction() + TEST_OUTPUT = myFunction.length + `, + { + target: "node", + rgf: true, + } + ); + + var TEST_OUTPUT; + eval(output); + + expect(TEST_OUTPUT).toStrictEqual(3); +});