distinctiomagnam
Version:
JavaScript Obfuscation Tool.
599 lines (530 loc) • 19.3 kB
text/typescript
import { walk } from "../traverse";
import {
ArrayExpression,
AssignmentExpression,
BinaryExpression,
CallExpression,
ExpressionStatement,
FunctionDeclaration,
FunctionExpression,
Identifier,
IfStatement,
Literal,
Node,
Location,
MemberExpression,
ObjectExpression,
Property,
ReturnStatement,
VariableDeclaration,
SequenceExpression,
NewExpression,
UnaryExpression,
BlockStatement,
LogicalExpression,
ThisExpression,
VariableDeclarator,
RestElement,
} from "../util/gen";
import { getIdentifierInfo } from "../util/identifiers";
import {
deleteDirect,
getBlockBody,
getVarContext,
isVarContext,
prepend,
append,
} from "../util/insert";
import Transform from "./transform";
import { isInsideType } from "../util/compare";
import { choice, shuffle } from "../util/random";
import { ComputeProbabilityMap } from "../probability";
import { reservedIdentifiers } from "../constants";
import { ObfuscateOrder } from "../order";
import Template from "../templates/template";
/**
* A Dispatcher processes function calls. All the function declarations are brought into a dictionary.
*
* ```js
* var param1;
* function dispatcher(key){
* var fns = {
* 'fn1': function(){
* var [arg1] = [param1];
* console.log(arg1);
* }
* }
* return fns[key]();
* };
* param1 = "Hello World";
* dispatcher('fn1'); // > "Hello World"
* ```
*
* Can break code with:
*
* 1. testing function equality,
* 2. using `arguments.callee`,
* 3. using `this`
*/
export default class Dispatcher extends Transform {
count: number;
constructor(o) {
super(o, ObfuscateOrder.Dispatcher);
this.count = 0;
}
match(object: Node, parents: Node[]) {
if (isInsideType("AwaitExpression", object, parents)) {
return false;
}
return (
isVarContext(object) &&
object.type !== "ArrowFunctionExpression" &&
!object.$dispatcherSkip &&
!parents.find((x) => x.$dispatcherSkip)
);
}
transform(object: Node, parents: Node[]) {
return () => {
if (ComputeProbabilityMap(this.options.dispatcher, (mode) => mode)) {
if (object.type != "Program" && object.body.type != "BlockStatement") {
return;
}
// Map of FunctionDeclarations
var functionDeclarations: { [name: string]: Location } = {};
// Array of Identifier nodes
var identifiers: Location[] = [];
var illegalFnNames: Set<string> = new Set();
// New Names for Functions
var newFnNames: { [name: string]: string } = {}; // [old name]: randomized name
var context = isVarContext(object)
? object
: getVarContext(object, parents);
walk(object, parents, (o: Node, p: Node[]) => {
if (object == o) {
// Fix 1
return;
}
var c = getVarContext(o, p);
if (o.type == "FunctionDeclaration") {
c = getVarContext(p[0], p.slice(1));
}
if (context === c) {
if (o.type == "FunctionDeclaration" && o.id.name) {
if (
o.$requiresEval ||
o.async ||
o.generator ||
p.find(
(x) => x.$dispatcherSkip || x.type == "MethodDefinition"
) ||
o.body.type != "BlockStatement"
) {
illegalFnNames.add(name);
}
var name = o.id.name;
// If dupe, no routing
if (functionDeclarations[name]) {
illegalFnNames.add(name);
return;
}
walk(o, p, (oo, pp) => {
if (
(oo.type == "Identifier" && oo.name == "arguments") ||
oo.type == "ThisExpression" ||
oo.type == "Super"
) {
if (getVarContext(oo, pp) === o) {
illegalFnNames.add(name);
return "EXIT";
}
}
});
functionDeclarations[name] = [o, p];
}
}
if (o.type == "Identifier") {
if (reservedIdentifiers.has(o.name)) {
return;
}
var info = getIdentifierInfo(o, p);
if (!info.spec.isReferenced) {
return;
}
if (info.spec.isDefined) {
if (info.isFunctionDeclaration) {
if (
p[0].id &&
(!functionDeclarations[p[0].id.name] ||
functionDeclarations[p[0].id.name][0] !== p[0])
) {
illegalFnNames.add(o.name);
}
} else {
illegalFnNames.add(o.name);
}
} else if (info.spec.isModified) {
illegalFnNames.add(o.name);
} else {
identifiers.push([o, p]);
}
}
});
illegalFnNames.forEach((name) => {
delete functionDeclarations[name];
});
// map original name->new game
var gen = this.getGenerator();
Object.keys(functionDeclarations).forEach((name) => {
newFnNames[name] = gen.generate();
});
// set containing new name
var set = new Set(Object.keys(newFnNames));
// Only make a dispatcher function if it caught any functions
if (set.size > 0) {
var payloadArg = `$jsc_d${this.count}_payload`;
var dispatcherFnName =
this.getPlaceholder() + "_dispatcher_" + this.count;
this.log(dispatcherFnName, set);
this.count++;
var expectedGet = gen.generate();
var expectedClearArgs = gen.generate();
var expectedNew = gen.generate();
var returnProp = gen.generate();
var newReturnMemberName = gen.generate();
var shuffledKeys = shuffle(Object.keys(functionDeclarations));
var mapName = this.getPlaceholder();
var cacheName = this.getPlaceholder();
// creating the dispatcher function
// 1. create function map
var map = VariableDeclaration(
VariableDeclarator(
mapName,
ObjectExpression(
shuffledKeys.map((name) => {
var [def, defParents] = functionDeclarations[name];
var body = getBlockBody(def.body);
var functionExpression: Node = {
...def,
expression: false,
type: "FunctionExpression",
id: null,
};
this.addComment(functionExpression, name);
if (def.params.length > 0) {
const fixParam = (param: Node) => {
return param;
};
var variableDeclaration = VariableDeclaration(
VariableDeclarator(
{
type: "ArrayPattern",
elements: def.params.map(fixParam),
},
Identifier(payloadArg)
)
);
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
var signature =
name +
"(" +
def.params.map((x) => x.name || "<>").join(",") +
")";
this.log("Added", signature);
// delete ref in block
if (defParents.length) {
deleteDirect(def, defParents[0]);
}
this.addComment(functionExpression, signature);
return Property(
Literal(newFnNames[name]),
functionExpression,
false
);
})
)
)
);
var getterArgName = this.getPlaceholder();
var x = this.getPlaceholder();
var y = this.getPlaceholder();
var z = this.getPlaceholder();
function getAccessor() {
return MemberExpression(Identifier(mapName), Identifier(x), true);
}
// 2. define it
var fn = FunctionDeclaration(
dispatcherFnName,
[Identifier(x), Identifier(y), Identifier(z)],
[
// Define map of callable functions
map,
// Set returning variable to undefined
VariableDeclaration(VariableDeclarator(returnProp)),
// Arg to clear the payload
IfStatement(
BinaryExpression(
"==",
Identifier(y),
Literal(expectedClearArgs)
),
[
ExpressionStatement(
AssignmentExpression(
"=",
Identifier(payloadArg),
ArrayExpression([])
)
),
],
null
),
// Arg to get a function reference
IfStatement(
BinaryExpression("==", Identifier(y), Literal(expectedGet)),
[
// Getter flag: return the function object
ExpressionStatement(
AssignmentExpression(
"=",
Identifier(returnProp),
LogicalExpression(
"||",
MemberExpression(
Identifier(cacheName),
Identifier(x),
true
),
AssignmentExpression(
"=",
MemberExpression(
Identifier(cacheName),
Identifier(x),
true
),
FunctionExpression(
[RestElement(Identifier(getterArgName))],
[
// Arg setter
ExpressionStatement(
AssignmentExpression(
"=",
Identifier(payloadArg),
Identifier(getterArgName)
)
),
// Call fn & return
ReturnStatement(
CallExpression(
MemberExpression(
getAccessor(),
Identifier("call"),
false
),
[ThisExpression(), Literal(gen.generate())]
)
),
]
)
)
)
)
),
],
[
// Call the function, return result
ExpressionStatement(
AssignmentExpression(
"=",
Identifier(returnProp),
CallExpression(getAccessor(), [Literal(gen.generate())])
)
),
]
),
// Check how the function was invoked (new () vs ())
IfStatement(
BinaryExpression("==", Identifier(z), Literal(expectedNew)),
[
// Wrap in object
ReturnStatement(
ObjectExpression([
Property(
Identifier(newReturnMemberName),
Identifier(returnProp),
false
),
])
),
],
[
// Return raw result
ReturnStatement(Identifier(returnProp)),
]
),
]
);
append(object, fn);
if (payloadArg) {
prepend(
object,
VariableDeclaration(
VariableDeclarator(payloadArg, ArrayExpression([]))
)
);
}
identifiers.forEach(([o, p]) => {
if (o.type != "Identifier") {
return;
}
var newName = newFnNames[o.name];
if (!newName) {
return;
}
if (!functionDeclarations[o.name]) {
this.error(new Error("newName, missing function declaration"));
}
var info = getIdentifierInfo(o, p);
if (
info.isFunctionCall &&
p[0].type == "CallExpression" &&
p[0].callee === o
) {
// Invoking call expression: `a();`
if (o.name == dispatcherFnName) {
return;
}
this.log(
`${o.name}(${p[0].arguments
.map((_) => "<>")
.join(",")}) -> ${dispatcherFnName}('${newName}')`
);
var assignmentExpressions: Node[] = [];
var dispatcherArgs: Node[] = [Literal(newName)];
if (p[0].arguments.length) {
assignmentExpressions = [
AssignmentExpression(
"=",
Identifier(payloadArg),
ArrayExpression(p[0].arguments)
),
];
} else {
dispatcherArgs.push(Literal(expectedClearArgs));
}
var type = choice(["CallExpression", "NewExpression"]);
var callExpression = null;
switch (type) {
case "CallExpression":
callExpression = CallExpression(
Identifier(dispatcherFnName),
dispatcherArgs
);
break;
case "NewExpression":
if (dispatcherArgs.length == 1) {
dispatcherArgs.push(Identifier("undefined"));
}
callExpression = MemberExpression(
NewExpression(Identifier(dispatcherFnName), [
...dispatcherArgs,
Literal(expectedNew),
]),
Identifier(newReturnMemberName),
false
);
break;
}
this.addComment(
callExpression,
"Calling " +
o.name +
"(" +
p[0].arguments.map((x) => x.name).join(", ") +
")"
);
var expr: Node = assignmentExpressions.length
? SequenceExpression([...assignmentExpressions, callExpression])
: callExpression;
// Replace the parent call expression
this.replace(p[0], expr);
} else {
// Non-invoking reference: `a`
if (info.spec.isDefined) {
if (info.isFunctionDeclaration) {
this.log(
"Skipped getter " + o.name + " (function declaration)"
);
} else {
this.log("Skipped getter " + o.name + " (defined)");
}
return;
}
if (info.spec.isModified) {
this.log("Skipped getter " + o.name + " (modified)");
return;
}
this.log(
`(getter) ${o.name} -> ${dispatcherFnName}('${newName}')`
);
this.replace(
o,
CallExpression(Identifier(dispatcherFnName), [
Literal(newName),
Literal(expectedGet),
])
);
}
});
prepend(
object,
VariableDeclaration(
VariableDeclarator(
Identifier(cacheName),
Template(`Object.create(null)`).single().expression
)
)
);
}
}
};
}
}