UNPKG

distinctiomagnam

Version:
1,003 lines (867 loc) 31.1 kB
import { ok } from "assert"; import { compileJsSync } from "../../compiler"; import { ObfuscateOrder } from "../../order"; import { ComputeProbabilityMap } from "../../probability"; import Template from "../../templates/template"; import { getBlock, isBlock, walk } from "../../traverse"; import { ArrayExpression, ArrayPattern, AssignmentExpression, BinaryExpression, BreakStatement, CallExpression, ConditionalExpression, ExpressionStatement, FunctionDeclaration, Identifier, IfStatement, LabeledStatement, Literal, Node, ReturnStatement, SequenceExpression, SwitchCase, SwitchStatement, VariableDeclaration, VariableDeclarator, WhileStatement, } from "../../util/gen"; import { containsLexicallyBoundVariables, getIdentifierInfo, } from "../../util/identifiers"; import { clone, getBlockBody, getFunction, getVarContext, isVarContext, } from "../../util/insert"; import { choice, getRandomInteger, shuffle } from "../../util/random"; import Transform from "../transform"; import ChoiceFlowObfuscation from "./choiceFlowObfuscation"; import ControlFlowObfuscation from "./controlFlowObfuscation"; import ExpressionObfuscation from "./expressionObfuscation"; import SwitchCaseObfuscation from "./switchCaseObfuscation"; var flattenStructures = new Set([ "IfStatement", "ForStatement", "WhileStatement", "DoWhileStatement", ]); /** * Breaks functions into DAGs (Directed Acyclic Graphs) * * - 1. Break functions into chunks * - 2. Shuffle chunks but remember their original position * - 3. Create a Switch statement inside a While loop, each case is a chunk, and the while loops exits on the last transition. * * The Switch statement: * * - 1. The state variable controls which case will run next * - 2. At the end of each case, the state variable is updated to the next block of code. * - 3. The while loop continues until the the state variable is the end state. */ export default class ControlFlowFlattening extends Transform { isDebug = false; constructor(o) { super(o, ObfuscateOrder.ControlFlowFlattening); if (!this.isDebug) { this.before.push(new ExpressionObfuscation(o)); this.after.push(new ControlFlowObfuscation(o)); this.after.push(new SwitchCaseObfuscation(o)); } else { console.warn("Debug mode enabled"); } // this.after.push(new ChoiceFlowObfuscation(o)); } match(object, parents) { return ( isBlock(object) && (!parents[1] || !flattenStructures.has(parents[1].type)) && (!parents[2] || !flattenStructures.has(parents[2].type)) ); } transform(object, parents) { return () => { if (object.body.length < 3) { return; } if (containsLexicallyBoundVariables(object, parents)) { return; } if ( !ComputeProbabilityMap(this.options.controlFlowFlattening, (x) => x) ) { return; } var body = getBlockBody(object.body); if (!body.length) { return; } // First step is to reorder the body // Fix 1. Bring hoisted functions up to be declared first var functionDeclarations: Set<Node> = new Set(); var fnNames: Set<string> = new Set(); var illegalFnNames = new Set<string>(); /** * The variable names * * index -> var name */ var stateVars = Array(this.isDebug ? 1 : getRandomInteger(2, 5)) .fill(0) .map(() => this.getPlaceholder()); body.forEach((stmt, i) => { if (stmt.type == "FunctionDeclaration") { functionDeclarations.add(stmt); var name = stmt.id && stmt.id.name; fnNames.add(name); if (stmt.body.type !== "BlockStatement") { illegalFnNames.add(name); } else { walk(stmt, [body, object, ...parents], (o, p) => { if ( o.type == "ThisExpression" || o.type == "SuperExpression" || (o.type == "Identifier" && (o.name == "arguments" || o.name == "eval")) ) { illegalFnNames.add(name); return "EXIT"; } }); } } }); walk(object, parents, (o, p) => { if (o.type == "Identifier" && fnNames.has(o.name)) { var info = getIdentifierInfo(o, p); if (!info.spec.isReferenced) { return; } if (info.spec.isModified) { fnNames.delete(o.name); } else if (info.spec.isDefined) { if (info.isFunctionDeclaration) { if (!functionDeclarations.has(p[0])) { fnNames.delete(o.name); } } else { fnNames.delete(o.name); } } if (!info.spec.isDefined) { var b = getBlock(o, p); if (b !== object || !p[0] || p[0].type !== "CallExpression") { illegalFnNames.add(o.name); } else { var isExtractable = false; if (p[1]) { if ( p[1].type == "ExpressionStatement" && p[1].expression == p[0] && p[2] == object.body ) { isExtractable = true; p[1].$callExpression = "ExpressionStatement"; p[1].$fnName = o.name; } else if ( p[1].type == "VariableDeclarator" && p[1].init == p[0] && p[2].length === 1 && p[4] == object.body ) { isExtractable = true; p[3].$callExpression = "VariableDeclarator"; p[3].$fnName = o.name; } else if ( p[1].type == "AssignmentExpression" && p[1].operator == "=" && p[1].right === p[0] && p[2] && p[2].type == "ExpressionStatement" && p[3] == object.body ) { isExtractable = true; p[2].$callExpression = "AssignmentExpression"; p[2].$fnName = o.name; } } if (!isExtractable) { illegalFnNames.add(o.name); } } } } }); // redefined function, if (functionDeclarations.size !== fnNames.size) { return; } illegalFnNames.forEach((illegal) => { fnNames.delete(illegal); }); var fraction = 0.9; if (body.length > 20) { fraction /= Math.max(1.2, body.length - 18); } fraction = Math.min(0.1, fraction); if (isNaN(fraction) || !isFinite(fraction)) { fraction = 0.5; } var resultVar = this.getPlaceholder(); var argVar = this.getPlaceholder(); var needsResultAndArgVar = false; var fnToLabel: { [fnName: string]: string } = Object.create(null); fnNames.forEach((fnName) => { fnToLabel[fnName] = this.getPlaceholder(); }); const flattenBody = ( body: Node[], startingLabel = this.getPlaceholder() ): { label: string; body: Node[] }[] => { var chunks = []; var currentBody = []; var currentLabel = startingLabel; const finishCurrentChunk = ( pointingLabel?: string, newLabel?: string, addGotoStatement = true ) => { if (!newLabel) { newLabel = this.getPlaceholder(); } if (!pointingLabel) { pointingLabel = newLabel; } if (addGotoStatement) { currentBody.push({ type: "GotoStatement", label: pointingLabel }); } chunks.push({ label: currentLabel, body: [...currentBody], }); currentLabel = newLabel; currentBody = []; }; body.forEach((stmt, i) => { if (functionDeclarations.has(stmt)) { return; } if (stmt.$exit) { currentBody.push(stmt); currentBody.push(BreakStatement(switchLabel)); finishCurrentChunk(null, null, false); return; } if (stmt.$callExpression && fnToLabel[stmt.$fnName]) { var afterPath = this.getPlaceholder(); var args = []; switch (stmt.$callExpression) { // var a = fn(); case "VariableDeclarator": args = stmt.declarations[0].init.arguments; stmt.declarations[0].init = Identifier(resultVar); break; // fn(); case "ExpressionStatement": args = stmt.expression.arguments; stmt.expression = Identifier("undefined"); break; // a = fn(); case "AssignmentExpression": args = stmt.expression.right.arguments; stmt.expression.right = Identifier(resultVar); break; } needsResultAndArgVar = true; currentBody.push( ExpressionStatement( AssignmentExpression( "=", Identifier(argVar), ArrayExpression([ { type: "StateIdentifier", label: afterPath, }, ArrayExpression(args), ]) ) ) ); finishCurrentChunk(fnToLabel[stmt.$fnName], afterPath); } if (stmt.type == "GotoStatement" && i !== body.length - 1) { finishCurrentChunk(stmt.label); return; } if (stmt.type == "LabeledStatement") { var lbl = stmt.label.name; var control = stmt.body; var isSwitchStatement = control.type === "SwitchStatement"; if ( isSwitchStatement || ((control.type == "ForStatement" || control.type == "WhileStatement" || control.type == "DoWhileStatement") && control.body.type == "BlockStatement") ) { if (isSwitchStatement) { if ( control.cases.length == 0 || // at least 1 case control.cases.find( (x) => !x.test || // cant be default case !x.consequent.length || // must have body x.consequent.findIndex( (node) => node.type == "BreakStatement" ) !== x.consequent.length - 1 || // break statement must be at the end x.consequent[x.consequent.length - 1].type !== // must end with break statement "BreakStatement" || !x.consequent[x.consequent.length - 1].label || // must be labeled and correct x.consequent[x.consequent.length - 1].label.name != lbl ) ) { currentBody.push(stmt); return; } } var isLoop = !isSwitchStatement; var supportContinueStatement = isLoop; var testPath = this.getPlaceholder(); var updatePath = this.getPlaceholder(); var bodyPath = this.getPlaceholder(); var afterPath = this.getPlaceholder(); var possible = true; var toReplace = []; walk(control.body, [], (o, p) => { if ( o.type == "BreakStatement" || (supportContinueStatement && o.type == "ContinueStatement") ) { if (!o.label || o.label.name !== lbl) { possible = false; return "EXIT"; } if (o.label.name === lbl) { return () => { toReplace.push([ o, { type: "GotoStatement", label: o.type == "BreakStatement" ? afterPath : updatePath, }, ]); }; } } }); if (!possible) { currentBody.push(stmt); return; } toReplace.forEach((v) => this.replace(v[0], v[1])); if (isSwitchStatement) { var switchVarName = this.getPlaceholder(); currentBody.push( VariableDeclaration( VariableDeclarator(switchVarName, control.discriminant) ) ); var afterPath = this.getPlaceholder(); finishCurrentChunk(); control.cases.forEach((switchCase, i) => { var entryPath = this.getPlaceholder(); currentBody.push( IfStatement( BinaryExpression( "===", Identifier(switchVarName), switchCase.test ), [ { type: "GotoStatement", label: entryPath, }, ] ) ); chunks.push( ...flattenBody( [ ...switchCase.consequent.slice( 0, switchCase.consequent.length - 1 ), { type: "GotoStatement", label: afterPath, }, ], entryPath ) ); if (i === control.cases.length - 1) { } else { finishCurrentChunk(); } }); finishCurrentChunk(afterPath, afterPath); return; } else if (isLoop) { var isPostTest = control.type == "DoWhileStatement"; // add initializing section to current chunk if (control.init) { if (control.init.type == "VariableDeclaration") { currentBody.push(control.init); } else { currentBody.push(ExpressionStatement(control.init)); } } // create new label called `testPath` and have current chunk point to it (goto testPath) finishCurrentChunk(isPostTest ? bodyPath : testPath, testPath); currentBody.push( IfStatement(control.test || Literal(true), [ { type: "GotoStatement", label: bodyPath, }, ]) ); // create new label called `bodyPath` and have test body point to afterPath (goto afterPath) finishCurrentChunk(afterPath, bodyPath); var innerBothPath = this.getPlaceholder(); chunks.push( ...flattenBody( [ ...control.body.body, { type: "GotoStatement", label: updatePath, }, ], innerBothPath ) ); finishCurrentChunk(innerBothPath, updatePath); if (control.update) { currentBody.push(ExpressionStatement(control.update)); } finishCurrentChunk(testPath, afterPath); return; } } } if ( stmt.type == "IfStatement" && stmt.consequent.type == "BlockStatement" && (!stmt.alternate || stmt.alternate.type == "BlockStatement") ) { finishCurrentChunk(); var hasAlternate = !!stmt.alternate; ok(!(hasAlternate && stmt.alternate.type !== "BlockStatement")); var yesPath = this.getPlaceholder(); var noPath = this.getPlaceholder(); var afterPath = this.getPlaceholder(); currentBody.push( IfStatement(stmt.test, [ { type: "GotoStatement", label: yesPath, }, ]) ); chunks.push( ...flattenBody( [ ...stmt.consequent.body, { type: "GotoStatement", label: afterPath, }, ], yesPath ) ); if (hasAlternate) { chunks.push( ...flattenBody( [ ...stmt.alternate.body, { type: "GotoStatement", label: afterPath, }, ], noPath ) ); finishCurrentChunk(noPath, afterPath); } else { finishCurrentChunk(afterPath, afterPath); } return; } if (!currentBody.length || Math.random() < fraction) { currentBody.push(stmt); } else { // Start new chunk finishCurrentChunk(); currentBody.push(stmt); } }); finishCurrentChunk(); chunks[chunks.length - 1].body.pop(); return chunks; }; var chunks = []; /** * label: switch(a+b+c){...break label...} */ var switchLabel = this.getPlaceholder(); functionDeclarations.forEach((node) => { if (node.id && fnNames.has(node.id.name)) { var exitStateName = this.getPlaceholder(); var argumentsName = this.getPlaceholder(); needsResultAndArgVar = true; node.body.body.push(ReturnStatement()); walk(node.body, [], (o, p) => { if (o.type == "ReturnStatement") { if (!getFunction(o, p)) { return () => { var exitExpr = SequenceExpression([ AssignmentExpression( "=", ArrayPattern(stateVars.map(Identifier)), Identifier(exitStateName) ), AssignmentExpression( "=", Identifier(resultVar), o.argument || Identifier("undefined") ), ]); this.replace(o, ReturnStatement(exitExpr)); }; } } }); var declarations = [ VariableDeclarator( ArrayPattern([ Identifier(exitStateName), Identifier(argumentsName), ]), Identifier(argVar) ), ]; if (node.params.length) { declarations.push( VariableDeclarator( ArrayPattern(node.params), Identifier(argumentsName) ) ); } var innerName = this.getPlaceholder(); chunks.push( ...flattenBody( [ FunctionDeclaration( innerName, [], [VariableDeclaration(declarations), ...node.body.body] ), this.objectAssign( ExpressionStatement( CallExpression(Identifier(innerName), []) ), { $exit: true, } as any ), ], fnToLabel[node.id.name] ) ); } }); var startLabel = this.getPlaceholder(); chunks.push(...flattenBody(body, startLabel)); chunks[chunks.length - 1].body.push({ type: "GotoStatement", label: "END_LABEL", }); chunks.push({ label: "END_LABEL", body: [], }); var caseSelection: Set<number> = new Set(); var uniqueStatesNeeded = chunks.length; var endLabel = chunks[Object.keys(chunks).length - 1].label; do { var newState = getRandomInteger(1, chunks.length * 15); if (this.isDebug) { newState = caseSelection.size; } caseSelection.add(newState); } while (caseSelection.size !== uniqueStatesNeeded); ok(caseSelection.size == uniqueStatesNeeded); /** * The accumulated state values * * index -> total state value */ var caseStates = Array.from(caseSelection); /** * The individual state values for each label * * labels right now are just chunk indexes (numbers) * * but will expand to if statements and functions when `goto statement` obfuscation is added */ var labelToStates: { [label: string]: number[] } = Object.create(null); Object.values(chunks).forEach((chunk, i) => { var state = caseStates[i]; var stateValues = Array(stateVars.length) .fill(0) .map(() => getRandomInteger(-250, 250)); const getCurrentState = () => { return stateValues.reduce((a, b) => b + a, 0); }; var correctIndex = getRandomInteger(0, stateValues.length); stateValues[correctIndex] = state - (getCurrentState() - stateValues[correctIndex]); labelToStates[chunk.label] = stateValues; }); // console.log(labelToStates); var initStateValues = [...labelToStates[startLabel]]; var endState = labelToStates[endLabel].reduce((a, b) => b + a, 0); const numberLiteral = (num, depth, stateValues) => { ok(Array.isArray(stateValues)); if (depth > 10 || Math.random() > 0.8 / (depth * 4)) { return Literal(num); } var opposing = getRandomInteger(0, stateVars.length); if (Math.random() > 0.5) { var x = getRandomInteger(-250, 250); var operator = choice(["<", ">"]); var answer = operator == "<" ? x < stateValues[opposing] : x > stateValues[opposing]; var correct = numberLiteral(num, depth + 1, stateValues); var incorrect = numberLiteral( getRandomInteger(-250, 250), depth + 1, stateValues ); return ConditionalExpression( BinaryExpression( operator, numberLiteral(x, depth + 1, stateValues), Identifier(stateVars[opposing]) ), answer ? correct : incorrect, answer ? incorrect : correct ); } return BinaryExpression( "+", Identifier(stateVars[opposing]), numberLiteral(num - stateValues[opposing], depth + 1, stateValues) ); }; const createTransitionExpression = ( index: number, add: number, mutatingStateValues: number[] ) => { var newValue = mutatingStateValues[index] + add; var expr = null; if (this.isDebug) { expr = AssignmentExpression( "=", Identifier(stateVars[index]), Literal(newValue) ); } else if (Math.random() > 0.5) { expr = AssignmentExpression( "+=", Identifier(stateVars[index]), numberLiteral(add, 0, mutatingStateValues) ); } else { var double = mutatingStateValues[index] * 2; var diff = double - newValue; var first = AssignmentExpression( "*=", Identifier(stateVars[index]), numberLiteral(2, 0, mutatingStateValues) ); mutatingStateValues[index] = double; expr = SequenceExpression([ first, AssignmentExpression( "-=", Identifier(stateVars[index]), numberLiteral(diff, 0, mutatingStateValues) ), ]); } mutatingStateValues[index] = newValue; return expr; }; interface Case { state: number; body: Node[]; order: number; } var order = Object.create(null); var cases: Case[] = []; chunks.forEach((chunk, i) => { // skip last case, its empty and never ran if (chunk.label === endLabel) { return; } ok(labelToStates[chunk.label]); var state = caseStates[i]; var made = 1; var breaksInsertion = []; var staticStateValues = [...labelToStates[chunk.label]]; chunk.body.forEach((stmt, stmtIndex) => { var addBreak = false; walk(stmt, [], (o, p) => { if ( !this.isDebug && o.type == "Literal" && typeof o.value === "number" && Math.floor(o.value) === o.value && Math.abs(o.value) < 100_000 && Math.random() < 4 / made && !p.find((x) => isVarContext(x)) ) { made++; return () => { this.replaceIdentifierOrLiteral( o, numberLiteral(o.value, 0, staticStateValues), p ); }; } if (o.type == "StateIdentifier") { return () => { ok(labelToStates[o.label]); this.replace( o, ArrayExpression(labelToStates[o.label].map(Literal)) ); }; } if (o.type == "GotoStatement") { return () => { var blockIndex = p.findIndex((node) => isBlock(node)); if (blockIndex === -1) { addBreak = true; } else { var child = p[blockIndex - 2] || o; var childIndex = p[blockIndex].body.indexOf(child); p[blockIndex].body.splice( childIndex + 1, 0, BreakStatement(switchLabel) ); } var mutatingStateValues = [...labelToStates[chunk.label]]; var nextStateValues = labelToStates[o.label]; ok(nextStateValues, o.label); this.replace( o, ExpressionStatement( SequenceExpression( mutatingStateValues.map((_v, stateValueIndex) => { var diff = nextStateValues[stateValueIndex] - mutatingStateValues[stateValueIndex]; return createTransitionExpression( stateValueIndex, diff, mutatingStateValues ); }) ) ) ); }; } }); if (addBreak) { breaksInsertion.push(stmtIndex); } }); breaksInsertion.sort(); breaksInsertion.reverse(); breaksInsertion.forEach((index) => { chunk.body.splice(index + 1, 0, BreakStatement(switchLabel)); }); // var c = Identifier("undefined"); // this.addComment(c, stateValues.join(", ")); // transitionStatements.push(c); var caseObject = { body: chunk.body, state: state, order: i, }; order[i] = caseObject; cases.push(caseObject); }); if (!this.isDebug) { shuffle(cases); } var discriminant = Template(`${stateVars.join("+")}`).single().expression; body.length = 0; if (functionDeclarations.size) { functionDeclarations.forEach((x) => { if (!x.id || illegalFnNames.has(x.id.name)) { body.unshift(clone(x)); } }); } var switchStatement: Node = SwitchStatement( discriminant, cases.map((x, i) => { var statements = []; statements.push(...x.body); var test = Literal(x.state); return SwitchCase(test, statements); }) ); var declarations = []; if (needsResultAndArgVar) { declarations.push(VariableDeclarator(resultVar)); declarations.push(VariableDeclarator(argVar)); } declarations.push( ...stateVars.map((stateVar, i) => { return VariableDeclarator(stateVar, Literal(initStateValues[i])); }) ); body.push( VariableDeclaration(declarations), WhileStatement( BinaryExpression("!=", clone(discriminant), Literal(endState)), [LabeledStatement(switchLabel, switchStatement)] ) ); // mark this object for switch case obfuscation switchStatement.$controlFlowFlattening = true; }; } }