js-confuser
Version:
JavaScript Obfuscation Tool.
258 lines (212 loc) • 7.57 kB
text/typescript
import { Binding, NodePath } from "@babel/traverse";
import { PluginArg, PluginObject } from "./plugin";
import * as t from "@babel/types";
import Template from "../templates/template";
import { Order } from "../order";
import {
NodeSymbol,
PREDICTABLE,
reservedIdentifiers,
UNSAFE,
variableFunctionName,
} from "../constants";
import {
ensureComputedExpression,
getFunctionName,
isDefiningIdentifier,
isStrictMode,
isVariableIdentifier,
prepend,
replaceDefiningIdentifierToMemberExpression,
} from "../utils/ast-utils";
import {
computeFunctionLength,
isVariableFunctionIdentifier,
} from "../utils/function-utils";
import { ok } from "assert";
import { NameGen } from "../utils/NameGen";
import { choice, getRandomInteger } from "../utils/random-utils";
import { createLiteral } from "../utils/node";
export default ({ Plugin }: PluginArg): PluginObject => {
const me = Plugin(Order.VariableMasking, {
changeData: {
functions: 0,
},
});
const transformFunction = (fnPath: NodePath<t.Function>) => {
// Do not apply to getter/setter methods
if (fnPath.isObjectMethod() && fnPath.node.kind !== "method") {
return;
}
// Do not apply to class getters/setters
if (fnPath.isClassMethod() && fnPath.node.kind !== "method") {
return;
}
// Do not apply to async or generator functions
if (fnPath.node.generator || fnPath.node.async) {
return;
}
// Do not apply to functions with rest parameters or destructuring
if (fnPath.node.params.some((param) => !t.isIdentifier(param))) {
return;
}
// Do not apply to 'use strict' functions
if (isStrictMode(fnPath)) return;
// Do not apply to functions marked unsafe
if ((fnPath.node as NodeSymbol)[UNSAFE]) return;
const functionName = getFunctionName(fnPath);
if (!me.computeProbabilityMap(me.options.variableMasking, functionName)) {
return;
}
const stackName = me.getPlaceholder() + "_varMask";
const stackMap = new Map<Binding, number | string>();
const propertyGen = new NameGen("mangled");
const stackKeys = new Set<string>();
let needsStack = false;
const illegalBindings = new Set<Binding>();
function checkBinding(binding: Binding) {
// Custom illegal check
// Variable Declarations with more than one declarator are not supported
// They can be inserted from the user's code even though Preparation phase should prevent it
// String Compression library includes such code
// TODO: Support multiple declarators
var variableDeclaration = binding.path.find((p) =>
p.isVariableDeclaration()
) as NodePath<t.VariableDeclaration>;
if (
variableDeclaration &&
variableDeclaration.node.declarations.length > 1
) {
return false;
}
function checkForUnsafe(valuePath: NodePath) {
var hasUnsafeNode = false;
valuePath.traverse({
ThisExpression(path) {
hasUnsafeNode = true;
path.stop();
},
Function(path) {
if ((path.node as NodeSymbol)[UNSAFE]) {
hasUnsafeNode = true;
path.stop();
}
},
});
return hasUnsafeNode;
}
// Check function value for 'this'
// Adding function expression to the stack (member expression)
// would break the 'this' context
if (binding.path.isVariableDeclarator()) {
let init = binding.path.get("init");
if (init.node) {
if (checkForUnsafe(init)) return false;
}
}
// x = function(){ return this }
// Cannot be transformed to x = stack[0] as 'this' would change
for (var assignment of binding.constantViolations) {
if (checkForUnsafe(assignment)) return false;
}
// __JS_CONFUSER_VAR__(identifier) -> __JS_CONFUSER_VAR__(stack.identifier)
// This cannot be transformed as it would break the user's code
for (var referencePath of binding.referencePaths) {
if (isVariableFunctionIdentifier(referencePath)) {
return false;
}
}
return true;
}
for (const param of fnPath.get("params")) {
ok(param.isIdentifier());
const paramName = param.node.name;
const binding = param.scope.getBinding(paramName);
if (!binding || !checkBinding(binding)) return;
ok(!stackMap.has(binding));
stackKeys.add(stackMap.size.toString());
stackMap.set(binding, stackMap.size);
}
fnPath.traverse({
Identifier(path) {
if (!isVariableIdentifier(path)) return;
if (fnPath.get("id") === path) return; // Skip this function's name (Test #21)
if (reservedIdentifiers.has(path.node.name)) return;
if (me.options.globalVariables.has(path.node.name)) return;
if (path.node.name === stackName) return;
if (path.node.name === variableFunctionName) return;
const binding = path.scope.getBinding(path.node.name);
if (!binding || binding.scope !== fnPath.scope) return;
if (illegalBindings.has(binding)) return;
needsStack = true;
let index = stackMap.get(binding);
if (typeof index === "undefined") {
// Only transform var and let bindings
// Function declarations could be hoisted and changing them to declarations is breaking
if (!["var", "let"].includes(binding.kind)) {
illegalBindings.add(binding);
return;
}
if (!checkBinding(binding)) {
illegalBindings.add(binding);
return;
}
do {
index = choice([
stackMap.size,
propertyGen.generate(),
getRandomInteger(-250, 250),
]);
} while (!index || stackKeys.has(index.toString()));
stackMap.set(binding, index);
stackKeys.add(index.toString());
}
const memberExpression = t.memberExpression(
t.identifier(stackName),
createLiteral(index),
true
);
if (isDefiningIdentifier(path)) {
replaceDefiningIdentifierToMemberExpression(path, memberExpression);
return;
}
ensureComputedExpression(path);
path.replaceWith(memberExpression);
},
});
if (!needsStack) return;
const originalParamCount = fnPath.node.params.length;
const originalLength = computeFunctionLength(fnPath);
fnPath.node.params = [t.restElement(t.identifier(stackName))];
// Discard extraneous parameters
// Predictable functions are guaranteed to not have extraneous parameters
if (!(fnPath.node as NodeSymbol)[PREDICTABLE]) {
prepend(
fnPath,
new Template(`${stackName}["length"] = {originalParamCount};`).single({
originalParamCount: t.numericLiteral(originalParamCount),
})
);
}
// Function is no longer predictable
(fnPath.node as NodeSymbol)[PREDICTABLE] = false;
fnPath.scope.registerBinding("param", fnPath.get("params")[0], fnPath);
me.setFunctionLength(fnPath, originalLength);
me.changeData.functions++;
};
return {
visitor: {
Function: {
exit(path: NodePath<t.Function>) {
if (!path.get("body").isBlockStatement()) return;
transformFunction(path);
},
},
Program: {
enter(path) {
path.scope.crawl();
},
},
},
};
};