From 74c28e9810057af50c2a2bdec4f9847720d297df Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 23 Jul 2024 20:47:50 -0400 Subject: [PATCH 01/19] Fix #131 --- README.md | 2 + src/transforms/identifier/globalConcealing.ts | 429 +++++++++--------- .../identifier/globalConcealing.test.ts | 63 ++- 3 files changed, 259 insertions(+), 235 deletions(-) diff --git a/README.md b/README.md index dad0a1d..db96ead 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`) diff --git a/src/transforms/identifier/globalConcealing.ts b/src/transforms/identifier/globalConcealing.ts index c3c37fa..aadff8a 100644 --- a/src/transforms/identifier/globalConcealing.ts +++ b/src/transforms/identifier/globalConcealing.ts @@ -2,23 +2,23 @@ import Template from "../../templates/template"; import Transform from "../transform"; import { ObfuscateOrder } from "../../order"; import { - Node, - Location, - CallExpression, - Identifier, - Literal, - FunctionDeclaration, - ReturnStatement, - MemberExpression, - SwitchStatement, - SwitchCase, - LogicalExpression, - VariableDeclarator, - FunctionExpression, - ExpressionStatement, - AssignmentExpression, - VariableDeclaration, - BreakStatement, + Node, + Location, + CallExpression, + Identifier, + Literal, + FunctionDeclaration, + ReturnStatement, + MemberExpression, + SwitchStatement, + SwitchCase, + LogicalExpression, + VariableDeclarator, + FunctionExpression, + ExpressionStatement, + AssignmentExpression, + VariableDeclaration, + BreakStatement, } from "../../util/gen"; import { append, prepend } from "../../util/insert"; import { chance, getRandomInteger } from "../../util/random"; @@ -32,63 +32,66 @@ import GlobalAnalysis from "./globalAnalysis"; * - Any variable that is not defined is considered "global" */ export default class GlobalConcealing extends Transform { - globalAnalysis: GlobalAnalysis; - - constructor(o) { - super(o, ObfuscateOrder.GlobalConcealing); - - this.globalAnalysis = new GlobalAnalysis(o); - this.before.push(this.globalAnalysis); - } - - match(object: Node, parents: Node[]) { - return object.type == "Program"; - } - - transform(object: Node, parents: Node[]) { - return () => { - var globals: { [name: string]: Location[] } = this.globalAnalysis.globals; - this.globalAnalysis.notGlobals.forEach((del) => { - delete globals[del]; - }); - - delete globals["require"]; - - reservedIdentifiers.forEach((x) => { - delete globals[x]; - }); - - Object.keys(globals).forEach((x) => { - if (this.globalAnalysis.globals[x].length < 1) { - delete globals[x]; - } else if ( - !ComputeProbabilityMap(this.options.globalConcealing, (x) => x, x) - ) { - delete globals[x]; - } - }); - - if (Object.keys(globals).length > 0) { - var used = 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(); - - // Returns global variable or fall backs to `this` - var getGlobalVariableFn = Template(` + globalAnalysis: GlobalAnalysis; + ignoreGlobals = new Set(["require", "__dirname"]); + + constructor(o) { + super(o, ObfuscateOrder.GlobalConcealing); + + this.globalAnalysis = new GlobalAnalysis(o); + this.before.push(this.globalAnalysis); + } + + match(object: Node, parents: Node[]) { + return object.type == "Program"; + } + + transform(object: Node, parents: Node[]) { + return () => { + var globals: { [name: string]: Location[] } = this.globalAnalysis.globals; + this.globalAnalysis.notGlobals.forEach((del) => { + delete globals[del]; + }); + + for (var varName of this.ignoreGlobals) { + delete globals[varName]; + } + + reservedIdentifiers.forEach((x) => { + delete globals[x]; + }); + + Object.keys(globals).forEach((x) => { + if (this.globalAnalysis.globals[x].length < 1) { + delete globals[x]; + } else if ( + !ComputeProbabilityMap(this.options.globalConcealing, (x) => x, x) + ) { + delete globals[x]; + } + }); + + if (Object.keys(globals).length > 0) { + var used = 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(); + + // Returns global variable or fall backs to `this` + var getGlobalVariableFn = Template(` var ${getGlobalVariableFnName} = function(){ try { return ${global} || ${alternateGlobal} || (new Function("return this"))(); @@ -97,7 +100,7 @@ export default class GlobalConcealing extends Transform { } }`).single(); - var getThisVariableFn = Template(` + var getThisVariableFn = Template(` var ${getThisVariableFnName} = function(){ try { return this; @@ -106,145 +109,145 @@ export default class GlobalConcealing extends Transform { } }`).single(); - // 2. Replace old accessors - var globalFn = this.getPlaceholder(); - - var newNames: { [globalVarName: string]: number } = Object.create(null); - - Object.keys(globals).forEach((name) => { - var locations: Location[] = globals[name]; - var state; - do { - state = getRandomInteger(-1000, 1000 + used.size); - } while (used.has(state)); - used.add(state); - - newNames[name] = state; - - locations.forEach(([node, parents]) => { - this.replace( - node, - CallExpression(Identifier(globalFn), [Literal(state)]) - ); - }); - }); - - // Adds all global variables to the switch statement - this.options.globalVariables.forEach((name) => { - if (!newNames[name]) { - var state; - do { - state = getRandomInteger( - 0, - 1000 + used.size + this.options.globalVariables.size * 100 - ); - } while (used.has(state)); - used.add(state); - - newNames[name] = state; - } - }); - - var indexParamName = this.getPlaceholder(); - var returnName = this.getPlaceholder(); - - var functionDeclaration = FunctionDeclaration( - globalFn, - [Identifier(indexParamName)], - [ - VariableDeclaration(VariableDeclarator(returnName)), - SwitchStatement( - Identifier(indexParamName), - Object.keys(newNames).map((name) => { - var code = newNames[name]; - var body: Node[] = [ - ReturnStatement( - LogicalExpression( - "||", - MemberExpression( - Identifier(globalVar), - Literal(name), - true - ), - MemberExpression(Identifier(thisVar), Literal(name), true) - ) - ), - ]; - if (chance(50)) { - body = [ - ExpressionStatement( - AssignmentExpression( - "=", - Identifier(returnName), - LogicalExpression( - "||", - Literal(name), - MemberExpression( - Identifier(thisVar), - Literal(name), - true - ) - ) - ) - ), - BreakStatement(), - ]; - } - - return SwitchCase(Literal(code), body); - }) - ), - ReturnStatement( - LogicalExpression( - "||", - MemberExpression( - Identifier(globalVar), - Identifier(returnName), - true - ), - MemberExpression( - Identifier(thisVar), - Identifier(returnName), - true - ) - ) - ), - ] - ); - - var tempVar = this.getPlaceholder(); - - var variableDeclaration = Template(` + // 2. Replace old accessors + var globalFn = this.getPlaceholder(); + + var newNames: { [globalVarName: string]: number } = Object.create(null); + + Object.keys(globals).forEach((name) => { + var locations: Location[] = globals[name]; + var state; + do { + state = getRandomInteger(-1000, 1000 + used.size); + } while (used.has(state)); + used.add(state); + + newNames[name] = state; + + locations.forEach(([node, parents]) => { + this.replace( + node, + CallExpression(Identifier(globalFn), [Literal(state)]) + ); + }); + }); + + // Adds all global variables to the switch statement + this.options.globalVariables.forEach((name) => { + if (!newNames[name]) { + var state; + do { + state = getRandomInteger( + 0, + 1000 + used.size + this.options.globalVariables.size * 100 + ); + } while (used.has(state)); + used.add(state); + + newNames[name] = state; + } + }); + + var indexParamName = this.getPlaceholder(); + var returnName = this.getPlaceholder(); + + var functionDeclaration = FunctionDeclaration( + globalFn, + [Identifier(indexParamName)], + [ + VariableDeclaration(VariableDeclarator(returnName)), + SwitchStatement( + Identifier(indexParamName), + Object.keys(newNames).map((name) => { + var code = newNames[name]; + var body: Node[] = [ + ReturnStatement( + LogicalExpression( + "||", + MemberExpression( + Identifier(globalVar), + Literal(name), + true + ), + MemberExpression(Identifier(thisVar), Literal(name), true) + ) + ), + ]; + if (chance(50)) { + body = [ + ExpressionStatement( + AssignmentExpression( + "=", + Identifier(returnName), + LogicalExpression( + "||", + Literal(name), + MemberExpression( + Identifier(thisVar), + Literal(name), + true + ) + ) + ) + ), + BreakStatement(), + ]; + } + + return SwitchCase(Literal(code), body); + }) + ), + ReturnStatement( + LogicalExpression( + "||", + MemberExpression( + Identifier(globalVar), + Identifier(returnName), + true + ), + MemberExpression( + Identifier(thisVar), + Identifier(returnName), + true + ) + ) + ), + ] + ); + + var tempVar = this.getPlaceholder(); + + var variableDeclaration = Template(` var ${globalVar}, ${thisVar}; `).single(); - variableDeclaration.declarations.push( - VariableDeclarator( - tempVar, - CallExpression( - MemberExpression( - FunctionExpression( - [], - [ - getGlobalVariableFn, - getThisVariableFn, - - Template( - `return ${thisVar} = ${getThisVariableFnName}["call"](this, ${globalFn}), ${globalVar} = ${getGlobalVariableFnName}["call"](this)` - ).single(), - ] - ), - Literal("call"), - true - ), - [] - ) - ) - ); - - prepend(object, variableDeclaration); - append(object, functionDeclaration); - } - }; - } + variableDeclaration.declarations.push( + VariableDeclarator( + tempVar, + CallExpression( + MemberExpression( + FunctionExpression( + [], + [ + getGlobalVariableFn, + getThisVariableFn, + + Template( + `return ${thisVar} = ${getThisVariableFnName}["call"](this, ${globalFn}), ${globalVar} = ${getGlobalVariableFnName}["call"](this)` + ).single(), + ] + ), + Literal("call"), + true + ), + [] + ) + ) + ); + + prepend(object, variableDeclaration); + append(object, functionDeclaration); + } + }; + } } diff --git a/test/transforms/identifier/globalConcealing.test.ts b/test/transforms/identifier/globalConcealing.test.ts index 9125e7d..92eb355 100644 --- a/test/transforms/identifier/globalConcealing.test.ts +++ b/test/transforms/identifier/globalConcealing.test.ts @@ -1,39 +1,39 @@ import JsConfuser from "../../../src/index"; test("Variant #1: Hide global names (such as Math)", async () => { - var code = ` + var code = ` var TEST_RESULT = Math.floor(10.1); `; - var output = await JsConfuser(code, { - target: "browser", - globalConcealing: true, - }); + var output = await JsConfuser(code, { + target: "browser", + globalConcealing: true, + }); - expect(output).not.toContain("Math.floor"); - expect(output).not.toContain("=Math"); - expect(output).toContain("['Math']"); - expect(output).toContain("window"); + expect(output).not.toContain("Math.floor"); + expect(output).not.toContain("=Math"); + expect(output).toContain("['Math']"); + expect(output).toContain("window"); }); test("Variant #2: Do not hide modified identifiers", async () => { - var code = ` + var code = ` var Math = 50; console.log(Math); `; - var output = await JsConfuser(code, { - target: "browser", - globalConcealing: true, - }); + var output = await JsConfuser(code, { + target: "browser", + globalConcealing: true, + }); - expect(output).toContain("log'](Math)"); + expect(output).toContain("log'](Math)"); }); test("Variant #3: Properly hide in default parameter, function expression", async () => { - var output = await JsConfuser( - ` + var output = await JsConfuser( + ` function myFunction( myParameter = function(){ var myVariable = true; return myVariable; @@ -43,11 +43,30 @@ test("Variant #3: Properly hide in default parameter, function expression", asyn TEST_OUTPUT = myFunction(); // true `, - { target: "node", globalConcealing: true } - ); + { target: "node", globalConcealing: true } + ); - var TEST_OUTPUT; - eval(output); + var TEST_OUTPUT; + eval(output); - expect(TEST_OUTPUT).toStrictEqual(true); + 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"); }); From 73bf828f78f538051cd7cabc29405897c6aa2480 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 23 Jul 2024 20:57:57 -0400 Subject: [PATCH 02/19] Fix #106 --- src/transforms/extraction/objectExtraction.ts | 19 +- src/transforms/identifier/globalConcealing.ts | 432 +++++++++--------- .../extraction/objectExtraction.test.ts | 2 + .../identifier/globalConcealing.test.ts | 62 +-- 4 files changed, 253 insertions(+), 262 deletions(-) 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/identifier/globalConcealing.ts b/src/transforms/identifier/globalConcealing.ts index aadff8a..cb4aa7a 100644 --- a/src/transforms/identifier/globalConcealing.ts +++ b/src/transforms/identifier/globalConcealing.ts @@ -2,23 +2,23 @@ import Template from "../../templates/template"; import Transform from "../transform"; import { ObfuscateOrder } from "../../order"; import { - Node, - Location, - CallExpression, - Identifier, - Literal, - FunctionDeclaration, - ReturnStatement, - MemberExpression, - SwitchStatement, - SwitchCase, - LogicalExpression, - VariableDeclarator, - FunctionExpression, - ExpressionStatement, - AssignmentExpression, - VariableDeclaration, - BreakStatement, + Node, + Location, + CallExpression, + Identifier, + Literal, + FunctionDeclaration, + ReturnStatement, + MemberExpression, + SwitchStatement, + SwitchCase, + LogicalExpression, + VariableDeclarator, + FunctionExpression, + ExpressionStatement, + AssignmentExpression, + VariableDeclaration, + BreakStatement, } from "../../util/gen"; import { append, prepend } from "../../util/insert"; import { chance, getRandomInteger } from "../../util/random"; @@ -32,66 +32,66 @@ import GlobalAnalysis from "./globalAnalysis"; * - Any variable that is not defined is considered "global" */ export default class GlobalConcealing extends Transform { - globalAnalysis: GlobalAnalysis; - ignoreGlobals = new Set(["require", "__dirname"]); - - constructor(o) { - super(o, ObfuscateOrder.GlobalConcealing); - - this.globalAnalysis = new GlobalAnalysis(o); - this.before.push(this.globalAnalysis); - } - - match(object: Node, parents: Node[]) { - return object.type == "Program"; - } - - transform(object: Node, parents: Node[]) { - return () => { - var globals: { [name: string]: Location[] } = this.globalAnalysis.globals; - this.globalAnalysis.notGlobals.forEach((del) => { - delete globals[del]; - }); - - for (var varName of this.ignoreGlobals) { - delete globals[varName]; - } - - reservedIdentifiers.forEach((x) => { - delete globals[x]; - }); - - Object.keys(globals).forEach((x) => { - if (this.globalAnalysis.globals[x].length < 1) { - delete globals[x]; - } else if ( - !ComputeProbabilityMap(this.options.globalConcealing, (x) => x, x) - ) { - delete globals[x]; - } - }); - - if (Object.keys(globals).length > 0) { - var used = 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(); - - // Returns global variable or fall backs to `this` - var getGlobalVariableFn = Template(` + globalAnalysis: GlobalAnalysis; + ignoreGlobals = new Set(["require", "__dirname"]); + + constructor(o) { + super(o, ObfuscateOrder.GlobalConcealing); + + this.globalAnalysis = new GlobalAnalysis(o); + this.before.push(this.globalAnalysis); + } + + match(object: Node, parents: Node[]) { + return object.type == "Program"; + } + + transform(object: Node, parents: Node[]) { + return () => { + var globals: { [name: string]: Location[] } = this.globalAnalysis.globals; + this.globalAnalysis.notGlobals.forEach((del) => { + delete globals[del]; + }); + + for (var varName of this.ignoreGlobals) { + delete globals[varName]; + } + + reservedIdentifiers.forEach((x) => { + delete globals[x]; + }); + + Object.keys(globals).forEach((x) => { + if (this.globalAnalysis.globals[x].length < 1) { + delete globals[x]; + } else if ( + !ComputeProbabilityMap(this.options.globalConcealing, (x) => x, x) + ) { + delete globals[x]; + } + }); + + if (Object.keys(globals).length > 0) { + var used = 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(); + + // Returns global variable or fall backs to `this` + var getGlobalVariableFn = Template(` var ${getGlobalVariableFnName} = function(){ try { return ${global} || ${alternateGlobal} || (new Function("return this"))(); @@ -100,7 +100,7 @@ export default class GlobalConcealing extends Transform { } }`).single(); - var getThisVariableFn = Template(` + var getThisVariableFn = Template(` var ${getThisVariableFnName} = function(){ try { return this; @@ -109,145 +109,145 @@ export default class GlobalConcealing extends Transform { } }`).single(); - // 2. Replace old accessors - var globalFn = this.getPlaceholder(); - - var newNames: { [globalVarName: string]: number } = Object.create(null); - - Object.keys(globals).forEach((name) => { - var locations: Location[] = globals[name]; - var state; - do { - state = getRandomInteger(-1000, 1000 + used.size); - } while (used.has(state)); - used.add(state); - - newNames[name] = state; - - locations.forEach(([node, parents]) => { - this.replace( - node, - CallExpression(Identifier(globalFn), [Literal(state)]) - ); - }); - }); - - // Adds all global variables to the switch statement - this.options.globalVariables.forEach((name) => { - if (!newNames[name]) { - var state; - do { - state = getRandomInteger( - 0, - 1000 + used.size + this.options.globalVariables.size * 100 - ); - } while (used.has(state)); - used.add(state); - - newNames[name] = state; - } - }); - - var indexParamName = this.getPlaceholder(); - var returnName = this.getPlaceholder(); - - var functionDeclaration = FunctionDeclaration( - globalFn, - [Identifier(indexParamName)], - [ - VariableDeclaration(VariableDeclarator(returnName)), - SwitchStatement( - Identifier(indexParamName), - Object.keys(newNames).map((name) => { - var code = newNames[name]; - var body: Node[] = [ - ReturnStatement( - LogicalExpression( - "||", - MemberExpression( - Identifier(globalVar), - Literal(name), - true - ), - MemberExpression(Identifier(thisVar), Literal(name), true) - ) - ), - ]; - if (chance(50)) { - body = [ - ExpressionStatement( - AssignmentExpression( - "=", - Identifier(returnName), - LogicalExpression( - "||", - Literal(name), - MemberExpression( - Identifier(thisVar), - Literal(name), - true - ) - ) - ) - ), - BreakStatement(), - ]; - } - - return SwitchCase(Literal(code), body); - }) - ), - ReturnStatement( - LogicalExpression( - "||", - MemberExpression( - Identifier(globalVar), - Identifier(returnName), - true - ), - MemberExpression( - Identifier(thisVar), - Identifier(returnName), - true - ) - ) - ), - ] - ); - - var tempVar = this.getPlaceholder(); - - var variableDeclaration = Template(` + // 2. Replace old accessors + var globalFn = this.getPlaceholder(); + + var newNames: { [globalVarName: string]: number } = Object.create(null); + + Object.keys(globals).forEach((name) => { + var locations: Location[] = globals[name]; + var state; + do { + state = getRandomInteger(-1000, 1000 + used.size); + } while (used.has(state)); + used.add(state); + + newNames[name] = state; + + locations.forEach(([node, parents]) => { + this.replace( + node, + CallExpression(Identifier(globalFn), [Literal(state)]) + ); + }); + }); + + // Adds all global variables to the switch statement + this.options.globalVariables.forEach((name) => { + if (!newNames[name]) { + var state; + do { + state = getRandomInteger( + 0, + 1000 + used.size + this.options.globalVariables.size * 100 + ); + } while (used.has(state)); + used.add(state); + + newNames[name] = state; + } + }); + + var indexParamName = this.getPlaceholder(); + var returnName = this.getPlaceholder(); + + var functionDeclaration = FunctionDeclaration( + globalFn, + [Identifier(indexParamName)], + [ + VariableDeclaration(VariableDeclarator(returnName)), + SwitchStatement( + Identifier(indexParamName), + Object.keys(newNames).map((name) => { + var code = newNames[name]; + var body: Node[] = [ + ReturnStatement( + LogicalExpression( + "||", + MemberExpression( + Identifier(globalVar), + Literal(name), + true + ), + MemberExpression(Identifier(thisVar), Literal(name), true) + ) + ), + ]; + if (chance(50)) { + body = [ + ExpressionStatement( + AssignmentExpression( + "=", + Identifier(returnName), + LogicalExpression( + "||", + Literal(name), + MemberExpression( + Identifier(thisVar), + Literal(name), + true + ) + ) + ) + ), + BreakStatement(), + ]; + } + + return SwitchCase(Literal(code), body); + }) + ), + ReturnStatement( + LogicalExpression( + "||", + MemberExpression( + Identifier(globalVar), + Identifier(returnName), + true + ), + MemberExpression( + Identifier(thisVar), + Identifier(returnName), + true + ) + ) + ), + ] + ); + + var tempVar = this.getPlaceholder(); + + var variableDeclaration = Template(` var ${globalVar}, ${thisVar}; `).single(); - variableDeclaration.declarations.push( - VariableDeclarator( - tempVar, - CallExpression( - MemberExpression( - FunctionExpression( - [], - [ - getGlobalVariableFn, - getThisVariableFn, - - Template( - `return ${thisVar} = ${getThisVariableFnName}["call"](this, ${globalFn}), ${globalVar} = ${getGlobalVariableFnName}["call"](this)` - ).single(), - ] - ), - Literal("call"), - true - ), - [] - ) - ) - ); - - prepend(object, variableDeclaration); - append(object, functionDeclaration); - } - }; - } + variableDeclaration.declarations.push( + VariableDeclarator( + tempVar, + CallExpression( + MemberExpression( + FunctionExpression( + [], + [ + getGlobalVariableFn, + getThisVariableFn, + + Template( + `return ${thisVar} = ${getThisVariableFnName}["call"](this, ${globalFn}), ${globalVar} = ${getGlobalVariableFnName}["call"](this)` + ).single(), + ] + ), + Literal("call"), + true + ), + [] + ) + ) + ); + + prepend(object, variableDeclaration); + append(object, functionDeclaration); + } + }; + } } 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 92eb355..57a0ea1 100644 --- a/test/transforms/identifier/globalConcealing.test.ts +++ b/test/transforms/identifier/globalConcealing.test.ts @@ -1,39 +1,39 @@ import JsConfuser from "../../../src/index"; test("Variant #1: Hide global names (such as Math)", async () => { - var code = ` + var code = ` var TEST_RESULT = Math.floor(10.1); `; - var output = await JsConfuser(code, { - target: "browser", - globalConcealing: true, - }); + var output = await JsConfuser(code, { + target: "browser", + globalConcealing: true, + }); - expect(output).not.toContain("Math.floor"); - expect(output).not.toContain("=Math"); - expect(output).toContain("['Math']"); - expect(output).toContain("window"); + expect(output).not.toContain("Math.floor"); + expect(output).not.toContain("=Math"); + expect(output).toContain("['Math']"); + expect(output).toContain("window"); }); test("Variant #2: Do not hide modified identifiers", async () => { - var code = ` + var code = ` var Math = 50; console.log(Math); `; - var output = await JsConfuser(code, { - target: "browser", - globalConcealing: true, - }); + var output = await JsConfuser(code, { + target: "browser", + globalConcealing: true, + }); - expect(output).toContain("log'](Math)"); + expect(output).toContain("log'](Math)"); }); test("Variant #3: Properly hide in default parameter, function expression", async () => { - var output = await JsConfuser( - ` + var output = await JsConfuser( + ` function myFunction( myParameter = function(){ var myVariable = true; return myVariable; @@ -43,30 +43,30 @@ test("Variant #3: Properly hide in default parameter, function expression", asyn TEST_OUTPUT = myFunction(); // true `, - { target: "node", globalConcealing: true } - ); + { target: "node", globalConcealing: true } + ); - var TEST_OUTPUT; - eval(output); + var TEST_OUTPUT; + eval(output); - expect(TEST_OUTPUT).toStrictEqual(true); + expect(TEST_OUTPUT).toStrictEqual(true); }); // https://github.com/MichaelXF/js-confuser/issues/131 test("Variant #4: Don't change __dirname", async function () { - var code = ` + var code = ` TEST_OUTPUT = __dirname; `; - var output = await JsConfuser(code, { - target: "node", - globalConcealing: true, - }); + var output = await JsConfuser(code, { + target: "node", + globalConcealing: true, + }); - expect(output).toContain("__dirname"); + expect(output).toContain("__dirname"); - var TEST_OUTPUT; - eval(output); + var TEST_OUTPUT; + eval(output); - expect(typeof TEST_OUTPUT).toStrictEqual("string"); + expect(typeof TEST_OUTPUT).toStrictEqual("string"); }); From a479aed0d89757aede5348b88bd721244ed176fb Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 23 Jul 2024 21:06:16 -0400 Subject: [PATCH 03/19] Fix #96 --- src/transforms/extraction/duplicateLiteralsRemoval.ts | 3 --- src/transforms/string/stringCompression.ts | 3 --- src/transforms/string/stringConcealing.ts | 3 --- 3 files changed, 9 deletions(-) diff --git a/src/transforms/extraction/duplicateLiteralsRemoval.ts b/src/transforms/extraction/duplicateLiteralsRemoval.ts index cbe13ee..f519a4b 100644 --- a/src/transforms/extraction/duplicateLiteralsRemoval.ts +++ b/src/transforms/extraction/duplicateLiteralsRemoval.ts @@ -234,9 +234,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/string/stringCompression.ts b/src/transforms/string/stringCompression.ts index 330cfe9..a08e182 100644 --- a/src/transforms/string/stringCompression.ts +++ b/src/transforms/string/stringCompression.ts @@ -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..db07904 100644 --- a/src/transforms/string/stringConcealing.ts +++ b/src/transforms/string/stringConcealing.ts @@ -192,9 +192,6 @@ 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 types = Object.keys(this.encoding); var type = choice(types); From 1525b51a78d6e04a21c10ba1ef444202735f2d98 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 23 Jul 2024 21:10:12 -0400 Subject: [PATCH 04/19] Fix #125 --- src/transforms/minify.ts | 1 + test/transforms/minify.test.ts | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/transforms/minify.ts b/src/transforms/minify.ts index 8e0658c..3718720 100644 --- a/src/transforms/minify.ts +++ b/src/transforms/minify.ts @@ -619,6 +619,7 @@ export default class Minify extends Transform { if ( object.id.type == "ObjectPattern" && + object.init && object.init.type == "ObjectExpression" ) { if ( diff --git a/test/transforms/minify.test.ts b/test/transforms/minify.test.ts index aac4240..e0780e6 100644 --- a/test/transforms/minify.test.ts +++ b/test/transforms/minify.test.ts @@ -536,3 +536,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); +}); From 21c1620da15f0abc45037da0a650f80a908f528a Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Thu, 25 Jul 2024 22:42:41 -0400 Subject: [PATCH 05/19] Object.defineProperty, Get global improvements --- CHANGELOG.md | 12 +++ src/templates/bufferToString.ts | 49 ++++++++--- src/templates/functionLength.ts | 24 +++++- src/templates/globals.ts | 3 + src/templates/template.ts | 82 +++++++++++++------ src/transforms/deadCode.ts | 16 ---- src/transforms/flatten.ts | 9 +- src/transforms/identifier/globalConcealing.ts | 82 +++++-------------- src/transforms/minify.ts | 24 ++++-- src/transforms/stack.ts | 13 ++- src/transforms/string/stringConcealing.ts | 5 +- src/transforms/transform.ts | 31 ++++++- 12 files changed, 219 insertions(+), 131 deletions(-) create mode 100644 src/templates/globals.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index a797ec5..b841a46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# `1.7.2` +Updates + +- Fixed [#131](https://github.com/MichaelXF/js-confuser/issues/131) +- - __dirname is no longer changed by Global Concealing + +- Fixed [#106](https://github.com/MichaelXF/js-confuser/issues/106) +- - Final fix for const variables for Object Extraction + +- Fixed [#96](https://github.com/MichaelXF/js-confuser/issues/96) +- - Remove hardcoded limits on String Concealing, String Compression, and Duplicate Literals Removal + # `1.7.1` Updates diff --git a/src/templates/bufferToString.ts b/src/templates/bufferToString.ts index 76c3f9a..66574d8 100644 --- a/src/templates/bufferToString.ts +++ b/src/templates/bufferToString.ts @@ -1,19 +1,46 @@ +import { placeholderVariablePrefix } from "../constants"; import Template from "./template"; -export const BufferToStringTemplate = Template(` - function __getGlobal(){ - try { - return global||window|| ( new Function("return this") )(); - } catch ( e ) { +export const GetGlobalTemplate = Template(` + function CFG__getGlobalThis(){ + return globalThis + } + + function CFG__getGlobal(){ + return global + } + + function CFG__getWindow(){ + return window + } + + function CFG__getThisFunction(){ + new Function("return this")() + } + + function {getGlobalFnName}(){ + var array = [ + CFG__getGlobalThis, + CFG__getGlobal, + CFG__getWindow, + CFG__getThisFunction + ] + + for(var i = 0; i < array.length; i++) { try { - return this; - } catch ( e ) { - return {}; - } + return array[i]() + } catch(e) {} } + + return 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 +90,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..639d975 100644 --- a/src/templates/template.ts +++ b/src/templates/template.ts @@ -1,57 +1,88 @@ 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; + 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 defaultVariables: { [key: string]: string } = Object.create(null); + + 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) + ) { + // Validate all variables were passed in + for (var requiredVariable of requiredVariables) { + if (typeof variables[requiredVariable] === "undefined") { + throw new Error( + templates[0] + + " missing variable: " + + requiredVariable + + " from " + + JSON.stringify(variables) + ); + } + } - var keys = { ...variables, ...vars }; + var template = choice(templates); + var output = template; + var allVariables = { ...defaultVariables, ...variables }; - 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"); + throw new Error("Template failed to parse: " + output); } } @@ -64,7 +95,8 @@ export default function Template(template: string): ITemplate { fill, compile, single, - source: template, + templates, + source: templates[0], }; return obj; 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/flatten.ts b/src/transforms/flatten.ts index eef00ed..777d18f 100644 --- a/src/transforms/flatten.ts +++ b/src/transforms/flatten.ts @@ -35,6 +35,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. @@ -506,7 +507,13 @@ export default class Flatten extends Transform { 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 cb4aa7a..7399d16 100644 --- a/src/transforms/identifier/globalConcealing.ts +++ b/src/transforms/identifier/globalConcealing.ts @@ -25,6 +25,7 @@ import { chance, getRandomInteger } from "../../util/random"; import { reservedIdentifiers } from "../../constants"; import { ComputeProbabilityMap } from "../../probability"; import GlobalAnalysis from "./globalAnalysis"; +import { GetGlobalTemplate } from "../../templates/bufferToString"; /** * Global Concealing hides global variables being accessed. @@ -72,42 +73,19 @@ 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(); // 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(); @@ -116,11 +94,11 @@ export default class GlobalConcealing extends Transform { 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; @@ -139,10 +117,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; } @@ -162,15 +140,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)) { @@ -183,7 +153,7 @@ export default class GlobalConcealing extends Transform { "||", Literal(name), MemberExpression( - Identifier(thisVar), + Identifier(globalVar), Literal(name), true ) @@ -198,18 +168,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 ) ), ] @@ -218,7 +180,7 @@ export default class GlobalConcealing extends Transform { var tempVar = this.getPlaceholder(); var variableDeclaration = Template(` - var ${globalVar}, ${thisVar}; + var ${globalVar}; `).single(); variableDeclaration.declarations.push( @@ -229,11 +191,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/minify.ts b/src/transforms/minify.ts index 3718720..aa4b32e 100644 --- a/src/transforms/minify.ts +++ b/src/transforms/minify.ts @@ -29,6 +29,7 @@ import { walk, isBlock } from "../traverse"; import { ok } from "assert"; import { isLexicalScope } from "../util/scope"; import Template from "../templates/template"; +import { ObjectDefineProperty } from "../templates/globals"; /** * Basic transformations to reduce code size. @@ -259,25 +260,30 @@ 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", { + return {ObjectDefineProperty}(functionObject, "length", { "value": functionLength, "configurable": true }); - - return functionObject; } - `).single() + `).single({ + ObjectDefineProperty: this.createInitVariable( + ObjectDefineProperty, + parents + ), + }) ); } 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 (fnLength != 0) { + args.push(Literal(fnLength)); + } + return CallExpression(Identifier(this.arrowFunctionName), args); }; if (object.type == "FunctionExpression") { diff --git a/src/transforms/stack.ts b/src/transforms/stack.ts index 6d80873..b79ce56 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; @@ -502,7 +505,13 @@ export default class Stack extends Transform { 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/stringConcealing.ts b/src/transforms/string/stringConcealing.ts index db07904..297dd1d 100644 --- a/src/transforms/string/stringConcealing.ts +++ b/src/transforms/string/stringConcealing.ts @@ -100,7 +100,10 @@ export default class StringConcealing extends Transform { // This helper functions convert UInt8 Array to UTf-string prepend( tree, - ...BufferToStringTemplate.compile({ name: bufferToStringName }) + ...BufferToStringTemplate.compile({ + name: bufferToStringName, + getGlobalFnName: this.getPlaceholder(), + }) ); Object.keys(this.encoding).forEach((type) => { 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. From fd0308c185e457d5f2ad7f9e19112b6a0ed42e61 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 30 Jul 2024 22:58:16 -0400 Subject: [PATCH 06/19] `Minify` undefined fix --- src/transforms/minify.ts | 4 ++++ test/transforms/minify.test.ts | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/transforms/minify.ts b/src/transforms/minify.ts index aa4b32e..dad1114 100644 --- a/src/transforms/minify.ts +++ b/src/transforms/minify.ts @@ -30,6 +30,7 @@ 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. @@ -680,6 +681,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/test/transforms/minify.test.ts b/test/transforms/minify.test.ts index e0780e6..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 () => { From 631f2ee0967070b3873af63075a1304830e3ccf7 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 30 Jul 2024 23:11:39 -0400 Subject: [PATCH 07/19] `Duplicate Literals Removal` undefined fix --- .../extraction/duplicateLiteralsRemoval.ts | 16 +++++++++------- .../extraction/duplicateLiteralsRemoval.test.ts | 8 ++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/transforms/extraction/duplicateLiteralsRemoval.ts b/src/transforms/extraction/duplicateLiteralsRemoval.ts index f519a4b..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; 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); }); From f63bd6fcc4df69df42fee01ee4a721f435a34152 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Tue, 30 Jul 2024 23:38:35 -0400 Subject: [PATCH 08/19] Strict Mode, Direct vs. Indirect eval() --- .../controlFlowFlattening.ts | 4 +- src/transforms/identifier/globalConcealing.ts | 9 +-- test/code/StrictMode.src.js | 65 +++++++++++++++++++ test/code/StrictMode.test.js | 37 +++++++++++ 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 test/code/StrictMode.src.js create mode 100644 test/code/StrictMode.test.js 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/identifier/globalConcealing.ts b/src/transforms/identifier/globalConcealing.ts index 7399d16..b476c56 100644 --- a/src/transforms/identifier/globalConcealing.ts +++ b/src/transforms/identifier/globalConcealing.ts @@ -22,7 +22,7 @@ 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"; @@ -34,7 +34,7 @@ import { GetGlobalTemplate } from "../../templates/bufferToString"; */ export default class GlobalConcealing extends Transform { globalAnalysis: GlobalAnalysis; - ignoreGlobals = new Set(["require", "__dirname"]); + ignoreGlobals = new Set(["require", "__dirname", "eval"]); constructor(o) { super(o, ObfuscateOrder.GlobalConcealing); @@ -80,7 +80,8 @@ export default class GlobalConcealing extends Transform { // holds "window" or "global" var globalVar = this.getPlaceholder(); - var getGlobalVariableFnName = this.getPlaceholder(); + var getGlobalVariableFnName = + this.getPlaceholder() + predictableFunctionTag; // Returns global variable or fall backs to `this` var getGlobalVariableFn = GetGlobalTemplate.compile({ @@ -88,7 +89,7 @@ export default class GlobalConcealing extends Transform { }); // 2. Replace old accessors - var globalFn = this.getPlaceholder(); + var globalFn = this.getPlaceholder() + predictableFunctionTag; var newNames: { [globalVarName: string]: number } = Object.create(null); 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); +}); From de8ae10ebbe0c9fb7890293972352dd7056d0536 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Wed, 31 Jul 2024 21:22:11 -0400 Subject: [PATCH 09/19] Tag predictable functions for further obfuscation --- src/constants.ts | 6 ++ src/transforms/dispatcher.ts | 55 +++---------------- src/transforms/flatten.ts | 14 ++++- src/transforms/string/stringCompression.ts | 10 ++-- src/transforms/string/stringConcealing.ts | 37 ++----------- src/transforms/string/stringEncoding.ts | 3 +- src/transforms/string/stringSplitting.ts | 3 +- src/util/compare.ts | 44 +++++++++++++-- src/util/gen.ts | 3 + src/util/insert.ts | 11 ++++ test/compare.test.ts | 64 +++++++++++++++++++++- 11 files changed, 152 insertions(+), 98 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index dc65988..4b8a524 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -82,3 +82,9 @@ 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__"; diff --git a/src/transforms/dispatcher.ts b/src/transforms/dispatcher.ts index 4c739fe..8643a5e 100644 --- a/src/transforms/dispatcher.ts +++ b/src/transforms/dispatcher.ts @@ -39,7 +39,7 @@ 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"; @@ -221,7 +221,10 @@ export default class Dispatcher extends Transform { 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 +256,8 @@ export default class Dispatcher extends Transform { expression: false, type: "FunctionExpression", id: null, + params: [], + [predictableFunctionTag]: true, }; this.addComment(functionExpression, name); @@ -272,48 +277,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 @@ -423,7 +386,7 @@ export default class Dispatcher extends Transform { Identifier("call"), false ), - [ThisExpression(), Literal(gen.generate())] + [ThisExpression()] ) ), ] @@ -439,7 +402,7 @@ export default class Dispatcher extends Transform { AssignmentExpression( "=", Identifier(returnProp), - CallExpression(getAccessor(), [Literal(gen.generate())]) + CallExpression(getAccessor(), []) ) ), ] diff --git a/src/transforms/flatten.ts b/src/transforms/flatten.ts index 777d18f..be7a4bf 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 { @@ -236,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) => { @@ -499,7 +507,7 @@ 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.functionLengthName) { diff --git a/src/transforms/string/stringCompression.ts b/src/transforms/string/stringCompression.ts index a08e182..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) { diff --git a/src/transforms/string/stringConcealing.ts b/src/transforms/string/stringConcealing.ts index 297dd1d..03fa7a0 100644 --- a/src/transforms/string/stringConcealing.ts +++ b/src/transforms/string/stringConcealing.ts @@ -2,7 +2,7 @@ import { ok } from "assert"; import { ObfuscateOrder } from "../../order"; import Template from "../../templates/template"; import { isBlock } from "../../traverse"; -import { isDirective } from "../../util/compare"; +import { isDirective, isModuleSource } from "../../util/compare"; import { ArrayExpression, CallExpression, @@ -27,36 +27,7 @@ import Transform from "../transform"; import Encoding from "./encoding"; import { ComputeProbabilityMap } from "../../probability"; import { BufferToStringTemplate } from "../../templates/bufferToString"; - -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; -} +import { predictableFunctionTag } from "../../constants"; export default class StringConcealing extends Transform { arrayExpression: Node; @@ -95,14 +66,14 @@ export default class StringConcealing extends Transform { 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, - getGlobalFnName: this.getPlaceholder(), + getGlobalFnName: this.getPlaceholder() + predictableFunctionTag, }) ); 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/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..a70a4d4 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, }; } diff --git a/src/util/insert.ts b/src/util/insert.ts index 5930d24..ff918c7 100644 --- a/src/util/insert.ts +++ b/src/util/insert.ts @@ -18,6 +18,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/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); + }); }); From 6a0aeaee0d33f2026481d6f241544d79098e45bf Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Wed, 31 Jul 2024 21:31:26 -0400 Subject: [PATCH 10/19] Variables to Unused Parameters Obfuscation --- CHANGELOG.md | 7 ++ .../identifier/movedDeclarations.ts | 113 ++++++++++++++---- .../identifier/movedDeclarations.test.ts | 61 ++++++++++ 3 files changed, 157 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b841a46..bfff623 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ Updates - Fixed [#96](https://github.com/MichaelXF/js-confuser/issues/96) - - Remove hardcoded limits on String Concealing, String Compression, and Duplicate Literals Removal +- Moved Declarations improvements +- - Now changes some variables to unused parameters on certain functions + +- Minor improvements +- - 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/src/transforms/identifier/movedDeclarations.ts b/src/transforms/identifier/movedDeclarations.ts index 60e332b..f700483 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,85 @@ 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.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 +140,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/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); +}); From ac8cc1bc2a6f95325bde562b5a9e9ccc400da87d Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Wed, 31 Jul 2024 23:47:26 -0400 Subject: [PATCH 11/19] Update dependencies --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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", From 380ed5b4ab52425dd7c24ccb111f7ac415ae0b38 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Wed, 31 Jul 2024 23:48:49 -0400 Subject: [PATCH 12/19] Improve Anti Tooling & Expression Obfuscation --- src/order.ts | 4 +- src/transforms/antiTooling.ts | 44 +++++++++++---- .../expressionObfuscation.ts | 56 +++++++++++++++---- src/transforms/es5/antiClass.ts | 11 +++- .../expressionObfuscation.test.ts | 55 ++++++++++++------ 5 files changed, 128 insertions(+), 42 deletions(-) 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/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/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/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/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); +}); From d9c6bfd57cc5cdd19b4c2d105289bbf889c92217 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Thu, 1 Aug 2024 00:12:16 -0400 Subject: [PATCH 13/19] Randomize String Concealing's charset --- CHANGELOG.md | 20 ++-- README.md | 26 +---- src/constants.ts | 6 + src/templates/template.ts | 35 +++++- src/transforms/string/encoding.ts | 136 ++++++++++++++-------- src/transforms/string/stringConcealing.ts | 67 ++++++----- 6 files changed, 172 insertions(+), 118 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfff623..3fc6067 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,17 +1,23 @@ # `1.7.2` Updates -- Fixed [#131](https://github.com/MichaelXF/js-confuser/issues/131) -- - __dirname is no longer changed by Global Concealing +- `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) -- Fixed [#106](https://github.com/MichaelXF/js-confuser/issues/106) -- - Final fix for const variables for Object Extraction +- `String Concealing` improvements +- - Randomizes the charset for each obfuscation - Defeating [JSConfuser-String-Decryptor](https://github.com/0v41n/JSConfuser-String-Decryptor) + +- `Moved Declarations` improvements +- - Now moves some variables as unused parameters on certain functions - Fixed [#96](https://github.com/MichaelXF/js-confuser/issues/96) -- - Remove hardcoded limits on String Concealing, String Compression, and Duplicate Literals Removal +- - Removed hardcoded limits on `String Concealing`, `String Compression`, and `Duplicate Literals Removal` -- Moved Declarations improvements -- - Now changes some variables to unused parameters on certain functions +- 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` - Minor improvements - - Preserve Strict Mode behaviors diff --git a/README.md b/README.md index db96ead..0dd2290 100644 --- a/README.md +++ b/README.md @@ -187,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. @@ -394,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` diff --git a/src/constants.ts b/src/constants.ts index 4b8a524..4bdbe5d 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -88,3 +88,9 @@ export const placeholderVariablePrefix = "__p_"; * - Never called with extraneous parameters */ export const predictableFunctionTag = "__JS_PREDICT__"; + +/** + * Tels the obfuscator this function is critical for the Obfuscated code. + * - Example: string decryption function + */ +export const criticalFunctionTag = "__JS_CRITICAL__"; diff --git a/src/templates/template.ts b/src/templates/template.ts index 639d975..bbea1e4 100644 --- a/src/templates/template.ts +++ b/src/templates/template.ts @@ -14,6 +14,10 @@ export interface ITemplate { single(variables?: { [name: string]: string | number }): Node; + variables(variables): ITemplate; + + ignoreMissingVariables(): ITemplate; + templates: string[]; source: string; } @@ -22,9 +26,10 @@ export default function Template(...templates: string[]): ITemplate { ok(templates.length); var requiredVariables = new Set(); + var providedVariables = {}; var defaultVariables: { [key: string]: string } = Object.create(null); - var matches = templates[0].match(/{[$A-z0-9]+}/g); + var matches = templates[0].match(/{[$A-z0-9_]+}/g); if (matches !== null) { matches.forEach((variable) => { var name = variable.slice(1, -1); @@ -44,22 +49,27 @@ export default function Template(...templates: string[]): ITemplate { 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 variables[requiredVariable] === "undefined") { + if (typeof userVariables[requiredVariable] === "undefined") { throw new Error( templates[0] + " missing variable: " + requiredVariable + " from " + - JSON.stringify(variables) + JSON.stringify(userVariables) ); } } var template = choice(templates); var output = template; - var allVariables = { ...defaultVariables, ...variables }; + var allVariables = { + ...defaultVariables, + ...userVariables, + }; Object.keys(allVariables).forEach((name) => { var bracketName = "{" + name.replace("$", "\\$") + "}"; @@ -82,7 +92,10 @@ export default function Template(...templates: string[]): ITemplate { } catch (e) { console.error(e); console.error(template); - throw new Error("Template failed to parse: " + output); + console.error({ ...providedVariables, ...variables }); + throw new Error( + "Template failed to parse: OUTPUT= " + output + " SOURCE= " + template + ); } } @@ -91,11 +104,23 @@ export default function Template(...templates: string[]): ITemplate { return nodes[0]; } + function variables(newVariables) { + Object.assign(providedVariables, newVariables); + return obj; + } + + function ignoreMissingVariables() { + requiredVariables.clear(); + return obj; + } + var obj: ITemplate = { fill, compile, single, templates, + variables, + ignoreMissingVariables, source: templates[0], }; diff --git a/src/transforms/string/encoding.ts b/src/transforms/string/encoding.ts index 7eab1ec..191b12d 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 + */ +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){ + const table = '${strTable}'; + + 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; + } } + + 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/stringConcealing.ts b/src/transforms/string/stringConcealing.ts index 03fa7a0..255784d 100644 --- a/src/transforms/string/stringConcealing.ts +++ b/src/transforms/string/stringConcealing.ts @@ -24,10 +24,14 @@ import { getRandomString, } from "../../util/random"; import Transform from "../transform"; -import Encoding from "./encoding"; +import { + EncodingImplementations, + createEncodingImplementation, + hasAllEncodings, +} from "./encoding"; import { ComputeProbabilityMap } from "../../probability"; import { BufferToStringTemplate } from "../../templates/bufferToString"; -import { predictableFunctionTag } from "../../constants"; +import { criticalFunctionTag, predictableFunctionTag } from "../../constants"; export default class StringConcealing extends Transform { arrayExpression: Node; @@ -40,15 +44,12 @@ export default class StringConcealing extends Transform { encoding: { [type: string]: string } = Object.create(null); gen: ReturnType; - hasAllEncodings: boolean; - constructor(o) { super(o, ObfuscateOrder.StringConcealing); this.set = new Set(); this.index = Object.create(null); this.arrayExpression = ArrayExpression([]); - this.hasAllEncodings = false; this.gen = this.getGenerator(); // Pad array with useless strings @@ -78,13 +79,16 @@ export default class StringConcealing extends Transform { ); Object.keys(this.encoding).forEach((type) => { - var { template } = Encoding[type]; - var decodeFn = this.getPlaceholder(); + var { template } = EncodingImplementations[type]; + var decodeFn = this.getPlaceholder() + criticalFunctionTag; var getterFn = this.encoding[type]; append( tree, - template.single({ name: decodeFn, bufferToString: bufferToStringName }) + template.single({ + __fnName__: decodeFn, + __bufferToString__: bufferToStringName, + }) ); append( @@ -166,37 +170,36 @@ export default class StringConcealing extends Transform { return; } - var types = Object.keys(this.encoding); - - var type = choice(types); - if (!type || (!this.hasAllEncodings && chance(10))) { - var allowed = Object.keys(Encoding).filter( - (type) => !this.encoding[type] - ); + var encodingIdentity = choice(Object.keys(EncodingImplementations)); - if (!allowed.length) { - this.hasAllEncodings = true; - } else { - var random = choice(allowed); - type = random; - - this.encoding[random] = this.getPlaceholder(); - } + if ( + !encodingIdentity || + (!hasAllEncodings() && + chance(25 / Object.keys(EncodingImplementations).length)) + ) { + encodingIdentity = createEncodingImplementation().identity; } - var fnName = this.encoding[type]; - var encoder = Encoding[type]; - - // 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; + if (!this.encoding[encodingIdentity]) { + this.encoding[encodingIdentity] = + this.getPlaceholder() + predictableFunctionTag; } + var fnName = this.encoding[encodingIdentity]; + var encodingImplementation = EncodingImplementations[encodingIdentity]; + var index = -1; if (!this.set.has(object.value)) { + // 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(encodingIdentity, object.value.slice(0, 100)); + delete EncodingImplementations[encodingIdentity]; + + return; + } + this.arrayExpression.elements.push(Literal(encoded)); index = this.arrayExpression.elements.length - 1; this.index[object.value] = [index, fnName]; From 2ab0ad9817635233d926545451a391f746559f94 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Fri, 2 Aug 2024 00:56:56 -0400 Subject: [PATCH 14/19] Place multiple decryption functions throughout the code --- CHANGELOG.md | 6 +- src/constants.ts | 2 +- src/templates/bufferToString.ts | 45 ++++-- src/transforms/string/encoding.ts | 2 +- src/transforms/string/stringConcealing.ts | 186 ++++++++++++++-------- 5 files changed, 154 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3fc6067..deaa43e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,9 @@ Updates - - 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 - Defeating [JSConfuser-String-Decryptor](https://github.com/0v41n/JSConfuser-String-Decryptor) +- - 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 @@ -21,7 +23,7 @@ Updates - Minor improvements - - Preserve Strict Mode behaviors -- - Preserve indirect vs direct [`eval`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) use +- - 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/src/constants.ts b/src/constants.ts index 4bdbe5d..142c536 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -90,7 +90,7 @@ export const placeholderVariablePrefix = "__p_"; export const predictableFunctionTag = "__JS_PREDICT__"; /** - * Tels the obfuscator this function is critical for the Obfuscated code. + * Tells the obfuscator this function is critical for the Obfuscated code. * - Example: string decryption function */ export const criticalFunctionTag = "__JS_CRITICAL__"; diff --git a/src/templates/bufferToString.ts b/src/templates/bufferToString.ts index 66574d8..35b6bd4 100644 --- a/src/templates/bufferToString.ts +++ b/src/templates/bufferToString.ts @@ -1,38 +1,51 @@ -import { placeholderVariablePrefix } from "../constants"; +import { + placeholderVariablePrefix, + predictableFunctionTag, +} from "../constants"; import Template from "./template"; export const GetGlobalTemplate = Template(` - function CFG__getGlobalThis(){ + function ${placeholderVariablePrefix}CFG__getGlobalThis${predictableFunctionTag}(){ return globalThis } - function CFG__getGlobal(){ + function ${placeholderVariablePrefix}CFG__getGlobal${predictableFunctionTag}(){ return global } - function CFG__getWindow(){ + function ${placeholderVariablePrefix}CFG__getWindow${predictableFunctionTag}(){ return window } - function CFG__getThisFunction(){ - new Function("return this")() + function ${placeholderVariablePrefix}CFG__getThisFunction${predictableFunctionTag}(){ + return new Function("return this")() } - function {getGlobalFnName}(){ - var array = [ - CFG__getGlobalThis, - CFG__getGlobal, - CFG__getWindow, - CFG__getThisFunction - ] + function {getGlobalFnName}(array = [ + ${placeholderVariablePrefix}CFG__getGlobalThis${predictableFunctionTag}, + ${placeholderVariablePrefix}CFG__getGlobal${predictableFunctionTag}, + ${placeholderVariablePrefix}CFG__getWindow${predictableFunctionTag}, + ${placeholderVariablePrefix}CFG__getThisFunction${predictableFunctionTag} + ]){ + var bestMatch + var itemsToSearch = [] + try { + bestMatch = Object + itemsToSearch["push"](("")["__proto__"]["constructor"]["name"]) + } catch(e) { - for(var i = 0; i < array.length; i++) { + } + A: for(var i = 0; i < array["length"]; i++) { try { - return array[i]() + bestMatch = array[i]() + for(var j = 0; j < itemsToSearch["length"]; j++) { + if(typeof bestMatch[itemsToSearch[j]] === "undefined") continue A; + } + return bestMatch } catch(e) {} } - return this; + return bestMatch || this; } `); diff --git a/src/transforms/string/encoding.ts b/src/transforms/string/encoding.ts index 191b12d..d81a20c 100644 --- a/src/transforms/string/encoding.ts +++ b/src/transforms/string/encoding.ts @@ -4,7 +4,7 @@ import { choice, shuffle } from "../../util/random"; /** * Defines an encoding implementation the Obfuscator */ -interface EncodingImplementation { +export interface EncodingImplementation { identity: string; encode(s): string; diff --git a/src/transforms/string/stringConcealing.ts b/src/transforms/string/stringConcealing.ts index 255784d..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 { getBlock } from "../../traverse"; import { isDirective, isModuleSource } from "../../util/compare"; import { ArrayExpression, CallExpression, - FunctionExpression, Identifier, Literal, MemberExpression, @@ -22,9 +21,11 @@ import { choice, getRandomInteger, getRandomString, + shuffle, } from "../../util/random"; import Transform from "../transform"; import { + EncodingImplementation, EncodingImplementations, createEncodingImplementation, hasAllEncodings, @@ -33,17 +34,24 @@ import { ComputeProbabilityMap } from "../../probability"; import { BufferToStringTemplate } from "../../templates/bufferToString"; import { criticalFunctionTag, predictableFunctionTag } from "../../constants"; +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; + functionObjects: FunctionObject[] = []; + constructor(o) { super(o, ObfuscateOrder.StringConcealing); @@ -51,20 +59,20 @@ export default class StringConcealing extends Transform { this.index = Object.create(null); this.arrayExpression = ArrayExpression([]); 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() + predictableFunctionTag; @@ -78,61 +86,82 @@ export default class StringConcealing extends Transform { }) ); - Object.keys(this.encoding).forEach((type) => { - var { template } = EncodingImplementations[type]; - var decodeFn = this.getPlaceholder() + criticalFunctionTag; - 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({ + 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), ]) ); } @@ -170,44 +199,67 @@ export default class StringConcealing extends Transform { return; } - var encodingIdentity = choice(Object.keys(EncodingImplementations)); + var currentBlock = getBlock(object, parents); + + // Find created functions + var functionObjects: FunctionObject[] = parents + .filter((node) => node.$stringConcealingFunctionObject) + .map((item) => item.$stringConcealingFunctionObject); + + // Choose random functionObject to use + var functionObject = choice(functionObjects); if ( - !encodingIdentity || + !functionObject || (!hasAllEncodings() && - chance(25 / Object.keys(EncodingImplementations).length)) + chance(25 / this.functionObjects.length) && + !currentBlock.$stringConcealingFunctionObject) ) { - encodingIdentity = createEncodingImplementation().identity; - } + // No functions, create one + + var newFunctionObject: FunctionObject = { + block: currentBlock, + encodingImplementation: createEncodingImplementation(), + fnName: this.getPlaceholder() + predictableFunctionTag, + }; - if (!this.encoding[encodingIdentity]) { - this.encoding[encodingIdentity] = - this.getPlaceholder() + predictableFunctionTag; + this.functionObjects.push(newFunctionObject); + currentBlock.$stringConcealingFunctionObject = newFunctionObject; + functionObject = newFunctionObject; } - var fnName = this.encoding[encodingIdentity]; - var encodingImplementation = EncodingImplementations[encodingIdentity]; + var { fnName, encodingImplementation } = functionObject; var index = -1; - if (!this.set.has(object.value)) { + + // 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"); + } + } + + 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(encodingIdentity, object.value.slice(0, 100)); - delete EncodingImplementations[encodingIdentity]; + 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"); @@ -243,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")); } From cfad75e5e330167eeeb1a2ef6e2e015894a1ce95 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Sat, 3 Aug 2024 16:01:47 -0400 Subject: [PATCH 15/19] RGF enhancements --- CHANGELOG.md | 4 + README.md | 5 +- .../identifier/movedDeclarations.ts | 1 + src/transforms/rgf.ts | 93 ++++++++++++++++++- src/transforms/string/encoding.ts | 18 ++-- test/transforms/rgf.test.ts | 50 ++++++++++ 6 files changed, 154 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deaa43e..c146f2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ Updates - `Moved Declarations` improvements - - Now moves some variables as unused parameters on certain functions +- `RGF` improvements +- - More likely to transform functions containing functions +- - Preserve `function.length` property + - Fixed [#96](https://github.com/MichaelXF/js-confuser/issues/96) - - Removed hardcoded limits on `String Concealing`, `String Compression`, and `Duplicate Literals Removal` diff --git a/README.md b/README.md index 0dd2290..c56a67d 100644 --- a/README.md +++ b/README.md @@ -648,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; } diff --git a/src/transforms/identifier/movedDeclarations.ts b/src/transforms/identifier/movedDeclarations.ts index f700483..a86208b 100644 --- a/src/transforms/identifier/movedDeclarations.ts +++ b/src/transforms/identifier/movedDeclarations.ts @@ -66,6 +66,7 @@ export default class MovedDeclarations extends Transform { ((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 !( diff --git a/src/transforms/rgf.ts b/src/transforms/rgf.ts index 63f6c4a..52944b9 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,49 @@ 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 (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 (originalFunctionLength !== 0) { + memberExpression = CallExpression( + Identifier(this.getFunctionLengthName(parents)), + [memberExpression, Literal(originalFunctionLength)] + ); + } this.replace(object, memberExpression); return; } diff --git a/src/transforms/string/encoding.ts b/src/transforms/string/encoding.ts index d81a20c..bb9f5cf 100644 --- a/src/transforms/string/encoding.ts +++ b/src/transforms/string/encoding.ts @@ -112,18 +112,18 @@ export function createEncodingImplementation(): EncodingImplementation { }, template: Template(` function {__fnName__}(str){ - const table = '${strTable}'; + var table = '${strTable}'; - const raw = "" + (str || ""); - const len = raw.length; - const ret = []; + var raw = "" + (str || ""); + var len = raw.length; + var ret = []; - let b = 0; - let n = 0; - let v = -1; + var b = 0; + var n = 0; + var v = -1; - for (let i = 0; i < len; i++) { - const p = table.indexOf(raw[i]); + for (var i = 0; i < len; i++) { + var p = table.indexOf(raw[i]); if (p === -1) continue; if (v < 0) { v = p; 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); +}); From 4668accc22a2a6d9ec75b1dacece06d582ba2569 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Sun, 4 Aug 2024 16:48:04 -0400 Subject: [PATCH 16/19] `Dispatcher` scope fix --- src/transforms/dispatcher.ts | 107 ++++++++++++++++++++++------- test/transforms/dispatcher.test.ts | 55 +++++++++++++++ 2 files changed, 138 insertions(+), 24 deletions(-) diff --git a/src/transforms/dispatcher.ts b/src/transforms/dispatcher.ts index 8643a5e..53c5782 100644 --- a/src/transforms/dispatcher.ts +++ b/src/transforms/dispatcher.ts @@ -34,6 +34,8 @@ import { isVarContext, prepend, append, + computeFunctionLength, + isFunction, } from "../util/insert"; import Transform from "./transform"; import { isInsideType } from "../util/compare"; @@ -42,6 +44,9 @@ import { ComputeProbabilityMap } from "../probability"; 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,6 +251,10 @@ 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"; @@ -343,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)), @@ -366,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()] - ) - ), - ] + CallExpression( + Identifier(`makeFn${predictableFunctionTag}`), + [] ) ) ) 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"); +}); From 2c70f57dc855f5748e32cf6611724ddcedefce6b Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Sun, 4 Aug 2024 16:49:58 -0400 Subject: [PATCH 17/19] New option: `preserveFunctionLength` --- src/options.ts | 13 +++ src/transforms/flatten.ts | 2 +- src/transforms/minify.ts | 22 ++--- src/transforms/rgf.ts | 10 ++- src/transforms/stack.ts | 2 +- src/util/scope.ts | 16 +++- test/options.test.ts | 166 +++++++++++++++++++++++++------------- 7 files changed, 161 insertions(+), 70 deletions(-) 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/transforms/flatten.ts b/src/transforms/flatten.ts index be7a4bf..99aa47f 100644 --- a/src/transforms/flatten.ts +++ b/src/transforms/flatten.ts @@ -509,7 +509,7 @@ export default class Flatten extends Transform { object.params = [RestElement(Identifier(argumentsName))]; - if (originalFunctionLength !== 0) { + if (this.options.preserveFunctionLength && originalFunctionLength !== 0) { if (!this.functionLengthName) { this.functionLengthName = this.getPlaceholder(); diff --git a/src/transforms/minify.ts b/src/transforms/minify.ts index dad1114..e07a00d 100644 --- a/src/transforms/minify.ts +++ b/src/transforms/minify.ts @@ -264,16 +264,20 @@ export default class Minify extends Transform { function ${this.arrowFunctionName}(arrowFn, functionLength = 0){ var functionObject = function(){ return arrowFn(...arguments) }; - return {ObjectDefineProperty}(functionObject, "length", { - "value": functionLength, - "configurable": true - }); + ${ + this.options.preserveFunctionLength + ? `return {ObjectDefineProperty}(functionObject, "length", { + "value": functionLength, + "configurable": true + });` + : `return functionObject` + } + } `).single({ - ObjectDefineProperty: this.createInitVariable( - ObjectDefineProperty, - parents - ), + ObjectDefineProperty: this.options.preserveFunctionLength + ? this.createInitVariable(ObjectDefineProperty, parents) + : undefined, }) ); } @@ -281,7 +285,7 @@ export default class Minify extends Transform { const wrap = (object: Node) => { var args: Node[] = [clone(object)]; var fnLength = computeFunctionLength(object.params); - if (fnLength != 0) { + if (this.options.preserveFunctionLength && fnLength != 0) { args.push(Literal(fnLength)); } return CallExpression(Identifier(this.arrowFunctionName), args); diff --git a/src/transforms/rgf.ts b/src/transforms/rgf.ts index 52944b9..e708f11 100644 --- a/src/transforms/rgf.ts +++ b/src/transforms/rgf.ts @@ -346,7 +346,10 @@ export default class RGF extends Transform { // The function is no longer guaranteed to not have extraneous parameters passed in object[predictableFunctionTag] = false; - if (originalFunctionLength !== 0) { + if ( + this.options.preserveFunctionLength && + originalFunctionLength !== 0 + ) { var body = parents[0] as unknown as Node[]; body.splice( @@ -366,7 +369,10 @@ export default class RGF extends Transform { // (2) Function Expression: // - Replace expression with member expression pointing to new function if (object.type === "FunctionExpression") { - if (originalFunctionLength !== 0) { + if ( + this.options.preserveFunctionLength && + originalFunctionLength !== 0 + ) { memberExpression = CallExpression( Identifier(this.getFunctionLengthName(parents)), [memberExpression, Literal(originalFunctionLength)] diff --git a/src/transforms/stack.ts b/src/transforms/stack.ts index b79ce56..0084eb3 100644 --- a/src/transforms/stack.ts +++ b/src/transforms/stack.ts @@ -500,7 +500,7 @@ 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( 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/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); + }); }); From 2eefdf05dfeb7f9da7532178724f7d5fe2d96244 Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Sun, 4 Aug 2024 16:50:32 -0400 Subject: [PATCH 18/19] New transform: `Class Extraction` --- CHANGELOG.md | 15 +- README.md | 6 +- src/transforms/extraction/classExtraction.ts | 168 ++++++++++++++++++ src/util/gen.ts | 10 +- src/util/insert.ts | 6 + .../extraction/classExtraction.test.ts | 86 +++++++++ 6 files changed, 283 insertions(+), 8 deletions(-) create mode 100644 src/transforms/extraction/classExtraction.ts create mode 100644 test/transforms/extraction/classExtraction.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index c146f2b..023b624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,6 @@ Updates - `RGF` improvements - - More likely to transform functions containing functions -- - Preserve `function.length` property - Fixed [#96](https://github.com/MichaelXF/js-confuser/issues/96) - - Removed hardcoded limits on `String Concealing`, `String Compression`, and `Duplicate Literals Removal` @@ -25,9 +24,17 @@ Updates - Fixed [#131](https://github.com/MichaelXF/js-confuser/issues/131) - - __dirname is no longer changed by `Global Concealing` -- Minor improvements -- - Preserve Strict Mode behaviors -- - Preserve indirect vs. direct [`eval`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval) use +**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 c56a67d..54801e2 100644 --- a/README.md +++ b/README.md @@ -816,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/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/util/gen.ts b/src/util/gen.ts index a70a4d4..14f622e 100644 --- a/src/util/gen.ts +++ b/src/util/gen.ts @@ -532,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 ff918c7..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` 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); +}); From ffe6c4cd6ebaf14407a9a91d71eeaca297131bca Mon Sep 17 00:00:00 2001 From: MichaelXF Date: Sun, 4 Aug 2024 18:00:37 -0400 Subject: [PATCH 19/19] Fix template error / Cash test --- src/templates/template.ts | 3 +++ test/code/Cash.test.ts | 14 ++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/templates/template.ts b/src/templates/template.ts index bbea1e4..90c13f8 100644 --- a/src/templates/template.ts +++ b/src/templates/template.ts @@ -29,6 +29,8 @@ export default function Template(...templates: string[]): ITemplate { 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) => { @@ -110,6 +112,7 @@ export default function Template(...templates: string[]): ITemplate { } function ignoreMissingVariables() { + defaultVariables = Object.create(null); requiredVariables.clear(); return obj; } 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"); });