quamvoluptatem
Version:
JavaScript Obfuscation Tool.
384 lines (320 loc) • 10.9 kB
text/typescript
import { ok } from "assert";
import { ObfuscateOrder } from "../../order";
import { ComputeProbabilityMap } from "../../probability";
import Template from "../../templates/template";
import { getBlock, isBlock, walk } from "../../traverse";
import {
AssignmentExpression,
BinaryExpression,
BreakStatement,
ConditionalExpression,
ExpressionStatement,
Identifier,
Literal,
Node,
SequenceExpression,
SwitchCase,
SwitchStatement,
VariableDeclaration,
VariableDeclarator,
WhileStatement,
} from "../../util/gen";
import {
containsLexicallyBoundVariables,
getIdentifierInfo,
} from "../../util/identifiers";
import {
clone,
getBlockBody,
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 "./switchCaseObfucation";
/**
* 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 {
constructor(o) {
super(o, ObfuscateOrder.ControlFlowFlattening);
this.before.push(new ExpressionObfuscation(o));
this.before.push(new ControlFlowObfuscation(o));
this.after.push(new SwitchCaseObfuscation(o));
// this.after.push(new ChoiceFlowObfuscation(o));
}
match(object, parents) {
return isBlock(object);
}
transform(object, parents) {
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();
body.forEach((stmt, i) => {
if (stmt.type == "FunctionDeclaration") {
functionDeclarations.add(stmt);
fnNames.add(stmt.id && stmt.id.name);
}
});
walk(object, parents, (o, p) => {
if (o.type == "Identifier") {
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);
}
}
}
});
// redefined function,
if (functionDeclarations.size !== fnNames.size) {
return;
}
var chunks: Node[][] = [[]];
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;
}
body.forEach((x, i) => {
if (functionDeclarations.has(x)) {
return;
}
var currentChunk = chunks[chunks.length - 1];
if (!currentChunk.length || Math.random() < fraction) {
currentChunk.push(x);
} else {
// Start new chunk
chunks.push([x]);
}
});
if (!chunks[chunks.length - 1].length) {
chunks.pop();
}
if (chunks.length < 2) {
return;
}
var caseSelection: Set<number> = new Set();
for (var i = 0; i < chunks.length + 1; i++) {
var newState;
do {
newState = getRandomInteger(1, chunks.length * 15);
} while (caseSelection.has(newState));
caseSelection.add(newState);
}
ok(caseSelection.size == chunks.length + 1);
var caseStates = Array.from(caseSelection);
var startState = caseStates[0];
var endState = caseStates[caseStates.length - 1];
var stateVars = Array(getRandomInteger(2, 7))
.fill(0)
.map(() => this.getPlaceholder());
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, stateVars.length);
stateValues[correctIndex] =
startState - (getCurrentState() - stateValues[correctIndex]);
var initStateValues = [...stateValues];
const numberLiteral = (num, depth) => {
if (depth > 12 || Math.random() > 0.9 / (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);
var incorrect = numberLiteral(getRandomInteger(-250, 250), depth + 1);
return ConditionalExpression(
BinaryExpression(
operator,
numberLiteral(x, depth + 1),
Identifier(stateVars[opposing])
),
answer ? correct : incorrect,
answer ? incorrect : correct
);
}
return BinaryExpression(
"+",
Identifier(stateVars[opposing]),
numberLiteral(num - stateValues[opposing], depth + 1)
);
};
const createTransitionStatement = (index, add) => {
var newValue = stateValues[index] + add;
var expr = null;
if (Math.random() > 0.5) {
expr = ExpressionStatement(
AssignmentExpression(
"+=",
Identifier(stateVars[index]),
numberLiteral(add, 0)
)
);
} else {
var double = stateValues[index] * 2;
var diff = double - newValue;
var first = AssignmentExpression(
"*=",
Identifier(stateVars[index]),
numberLiteral(2, 0)
);
stateValues[index] = double;
expr = ExpressionStatement(
SequenceExpression([
first,
AssignmentExpression(
"-=",
Identifier(stateVars[index]),
numberLiteral(diff, 0)
),
])
);
}
stateValues[index] = newValue;
return expr;
};
interface Case {
state: number;
nextState: number;
body: Node[];
order: number;
transitionStatements: Node[];
}
var order = Object.create(null);
var cases: Case[] = chunks.map((body, i) => {
body.forEach((stmt) => {
walk(stmt, [], (o, p) => {
if (
o.type == "Literal" &&
typeof o.value === "number" &&
Math.random() > 0.5 &&
!p.find((x) => isVarContext(x))
) {
return () => {
this.replace(o, numberLiteral(o.value, 0));
};
}
});
});
var state = caseStates[i];
var nextState = caseStates[i + 1];
var diff = nextState - state;
var transitionStatements = [];
ok(!isNaN(diff));
var modifying = getRandomInteger(0, stateVars.length);
var shift = 0;
// var c1 = Identifier("undefined");
// this.addComment(c1, stateValues.join(", "));
// transitionStatements.push(c1);
transitionStatements.push(
...Array.from(
new Set(
Array(getRandomInteger(0, stateVars.length - 2))
.fill(0)
.map(() => getRandomInteger(0, stateVars.length))
.filter((x) => x != modifying)
)
).map((x) => {
var randomNumber = getRandomInteger(-250, 250);
shift += randomNumber;
return createTransitionStatement(x, randomNumber);
})
);
transitionStatements.push(
createTransitionStatement(modifying, diff - shift)
);
// var c = Identifier("undefined");
// this.addComment(c, stateValues.join(", "));
// transitionStatements.push(c);
var caseObject = {
body: body,
state: state,
nextState: nextState,
order: i,
transitionStatements: transitionStatements,
};
order[i] = caseObject;
return caseObject;
});
shuffle(cases);
var discriminant = Template(`${stateVars.join("+")}`).single().expression;
body.length = 0;
if (functionDeclarations.size) {
functionDeclarations.forEach((x) => {
body.unshift(clone(x));
});
}
var switchStatement: Node = SwitchStatement(
discriminant,
cases.map((x, i) => {
var statements = [];
statements.push(...x.body);
statements.push(...x.transitionStatements);
statements.push(BreakStatement());
var test = Literal(x.state);
return SwitchCase(test, statements);
})
);
body.push(
VariableDeclaration(
stateVars.map((stateVar, i) => {
return VariableDeclarator(stateVar, Literal(initStateValues[i]));
})
),
WhileStatement(
BinaryExpression("!=", clone(discriminant), Literal(endState)),
[switchStatement]
)
);
};
}
}