distinctiomagnam
Version:
JavaScript Obfuscation Tool.
1,003 lines (867 loc) • 31.1 kB
text/typescript
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;
};
}
}