js-confuser
Version:
JavaScript Obfuscation Tool.
421 lines (354 loc) • 12 kB
text/typescript
import * as t from "@babel/types";
import { NodePath } from "@babel/traverse";
import {
ensureComputedExpression,
getFunctionName,
isDefiningIdentifier,
isModifiedIdentifier,
isStrictMode,
isVariableIdentifier,
prepend,
prependProgram,
} from "../utils/ast-utils";
import { PluginArg, PluginObject } from "./plugin";
import { Order } from "../order";
import { NodeSymbol, PREDICTABLE, UNSAFE } from "../constants";
import {
computeFunctionLength,
isVariableFunctionIdentifier,
} from "../utils/function-utils";
import { ok } from "assert";
import { Scope } from "@babel/traverse";
import { NameGen } from "../utils/NameGen";
export default ({ Plugin }: PluginArg): PluginObject => {
const me = Plugin(Order.Flatten, {
changeData: {
functions: 0,
},
});
const isDebug = false;
function flattenFunction(fnPath: NodePath<t.Function>) {
// Skip if already processed
if (me.isSkipped(fnPath)) return;
// Don't apply to generator functions
if (fnPath.node.generator) return;
// Skip getter/setter methods
if (fnPath.isObjectMethod() || fnPath.isClassMethod()) {
if (fnPath.node.kind !== "method") return;
}
// Do not apply to arrow functions
if (t.isArrowFunctionExpression(fnPath.node)) return;
if (!t.isBlockStatement(fnPath.node.body)) return;
// Skip if marked as unsafe
if ((fnPath.node as NodeSymbol)[UNSAFE]) return;
var program = fnPath.findParent((p) =>
p.isProgram()
) as NodePath<t.Program>;
let functionName = getFunctionName(fnPath);
if (!t.isValidIdentifier(functionName, true)) {
functionName = "anonymous";
}
if (!me.computeProbabilityMap(me.options.flatten, functionName)) {
return;
}
const strictMode = fnPath.find((path) => isStrictMode(path));
if (strictMode === fnPath) return;
me.log("Transforming", functionName);
const flatObjectName = `${me.getPlaceholder()}_flat_object`;
const newFnName = `${me.getPlaceholder()}_flat_${functionName}`;
const nameGen = new NameGen(me.options.identifierGenerator);
function generateProp(originalName: string, type: string) {
var newPropertyName: string;
do {
newPropertyName = isDebug
? type + "_" + originalName
: nameGen.generate();
} while (allPropertyNames.has(newPropertyName));
allPropertyNames.add(newPropertyName);
return newPropertyName;
}
const standardProps = new Map<string, string>();
const setterPropsNeeded = new Set<string>();
const typeofProps = new Map<string, string>();
const functionCallProps = new Map<string, string>();
const allPropertyNames = new Set();
const identifierPaths: NodePath<t.Identifier>[] = [];
// Traverse function to identify variables to be replaced with flat object properties
fnPath.traverse({
Identifier: {
exit(identifierPath) {
if (!isVariableIdentifier(identifierPath)) return;
if (
identifierPath.isBindingIdentifier() &&
isDefiningIdentifier(identifierPath)
)
return;
if (isVariableFunctionIdentifier(identifierPath)) return;
if ((identifierPath.node as NodeSymbol)[UNSAFE]) return;
const identifierName = identifierPath.node.name;
if (identifierName === "arguments") return;
var binding = identifierPath.scope.getBinding(identifierName);
if (!binding) {
return;
}
var isOutsideVariable =
fnPath.scope.parent.getBinding(identifierName) === binding;
if (!isOutsideVariable) {
return;
}
identifierPaths.push(identifierPath);
},
},
});
me.log(
`Function ${functionName}`,
"requires",
Array.from(new Set(identifierPaths.map((x) => x.node.name)))
);
for (var identifierPath of identifierPaths) {
const identifierName = identifierPath.node.name;
if (typeof identifierName !== "string") continue;
const isTypeof = identifierPath.parentPath.isUnaryExpression({
operator: "typeof",
});
const isFunctionCall =
identifierPath.parentPath.isCallExpression() &&
identifierPath.parentPath.node.callee === identifierPath.node;
if (isTypeof) {
var typeofProp = typeofProps.get(identifierName);
if (!typeofProp) {
typeofProp = generateProp(identifierName, "typeof");
typeofProps.set(identifierName, typeofProp);
}
ensureComputedExpression(identifierPath.parentPath);
identifierPath.parentPath
.replaceWith(
t.memberExpression(
t.identifier(flatObjectName),
t.stringLiteral(typeofProp),
true
)
)[0]
.skip();
} else if (isFunctionCall) {
let functionCallProp = functionCallProps.get(identifierName);
if (!functionCallProp) {
functionCallProp = generateProp(identifierName, "call");
functionCallProps.set(identifierName, functionCallProp);
}
ensureComputedExpression(identifierPath);
// Replace identifier with a reference to the flat object property
identifierPath
.replaceWith(
t.memberExpression(
t.identifier(flatObjectName),
t.stringLiteral(functionCallProp),
true
)
)[0]
.skip();
} else {
let standardProp = standardProps.get(identifierName);
if (!standardProp) {
standardProp = generateProp(identifierName, "standard");
standardProps.set(identifierName, standardProp);
}
if (!setterPropsNeeded.has(identifierName)) {
// Only provide 'set' method if the variable is modified
var isModification = isModifiedIdentifier(identifierPath);
if (isModification) {
setterPropsNeeded.add(identifierName);
}
}
ensureComputedExpression(identifierPath);
// Replace identifier with a reference to the flat object property
identifierPath
.replaceWith(
t.memberExpression(
t.identifier(flatObjectName),
t.stringLiteral(standardProp),
true
)
)[0]
.skip();
}
}
// for (const prop of [...typeofProps.keys(), ...functionCallProps.keys()]) {
// if (!standardProps.has(prop)) {
// standardProps.set(prop, generateProp());
// }
// }
const flatObjectProperties: t.ObjectMember[] = [];
for (var entry of standardProps) {
const [identifierName, objectProp] = entry;
flatObjectProperties.push(
me.skip(
t.objectMethod(
"get",
t.stringLiteral(objectProp),
[],
t.blockStatement([t.returnStatement(t.identifier(identifierName))]),
false,
false,
false
)
)
);
// Not all properties need a setter
if (setterPropsNeeded.has(identifierName)) {
var valueArgName = me.getPlaceholder() + "_value";
flatObjectProperties.push(
me.skip(
t.objectMethod(
"set",
t.stringLiteral(objectProp),
[t.identifier(valueArgName)],
t.blockStatement([
t.expressionStatement(
t.assignmentExpression(
"=",
t.identifier(identifierName),
t.identifier(valueArgName)
)
),
]),
false,
false,
false
)
)
);
}
}
for (const entry of typeofProps) {
const [identifierName, objectProp] = entry;
flatObjectProperties.push(
me.skip(
t.objectMethod(
"get",
t.stringLiteral(objectProp),
[],
t.blockStatement([
t.returnStatement(
t.unaryExpression("typeof", t.identifier(identifierName))
),
]),
false,
false,
false
)
)
);
}
for (const entry of functionCallProps) {
const [identifierName, objectProp] = entry;
flatObjectProperties.push(
me.skip(
t.objectMethod(
"method",
t.stringLiteral(objectProp),
[t.restElement(t.identifier("args"))],
t.blockStatement([
t.returnStatement(
t.callExpression(t.identifier(identifierName), [
t.spreadElement(t.identifier("args")),
])
),
]),
false,
false,
false
)
)
);
}
// Create the new flattened function
const flattenedFunctionDeclaration = t.functionDeclaration(
t.identifier(newFnName),
[t.arrayPattern([...fnPath.node.params]), t.identifier(flatObjectName)],
t.blockStatement([...[...fnPath.node.body.body]]),
false,
fnPath.node.async
);
// Create the flat object variable declaration
const flatObjectDeclaration = t.variableDeclaration("var", [
t.variableDeclarator(
t.identifier(flatObjectName),
t.objectExpression(flatObjectProperties)
),
]);
var argName = me.getPlaceholder() + "_args";
// Replace original function body with a call to the flattened function
fnPath.node.body = t.blockStatement([
flatObjectDeclaration,
t.returnStatement(
t.callExpression(t.identifier(newFnName), [
t.identifier(argName),
t.identifier(flatObjectName),
])
),
]);
const originalLength = computeFunctionLength(fnPath);
fnPath.node.params = [t.restElement(t.identifier(argName))];
// Ensure updated parameter gets registered in the function scope
fnPath.scope.crawl();
fnPath.skip();
// Add the new flattened function at the top level
var newPath = prependProgram(
program,
flattenedFunctionDeclaration
)[0] as NodePath<t.FunctionDeclaration>;
me.skip(newPath);
// Copy over all properties except the predictable flag
for (var symbol of Object.getOwnPropertySymbols(fnPath.node)) {
if (symbol !== PREDICTABLE) {
newPath.node[symbol] = fnPath.node[symbol];
}
}
// Old function is no longer predictable (rest element parameter)
(fnPath.node as NodeSymbol)[PREDICTABLE] = false;
// Old function is unsafe (uses arguments, this)
(fnPath.node as NodeSymbol)[UNSAFE] = true;
newPath.node[PREDICTABLE] = true;
// Carry over 'use strict' directive if not already present
if (strictMode) {
newPath.node.body.directives.push(
t.directive(t.directiveLiteral("use strict"))
);
// Non-simple parameter list conversion
prepend(
newPath,
t.variableDeclaration("var", [
t.variableDeclarator(
t.arrayPattern(newPath.node.params),
t.identifier("arguments")
),
])
);
newPath.node.params = [];
// Using 'arguments' is unsafe
(newPath.node as NodeSymbol)[UNSAFE] = true;
// Params changed and using 'arguments'
(newPath.node as NodeSymbol)[PREDICTABLE] = false;
}
// Ensure parameters are registered in the new function scope
newPath.scope.crawl();
newPath.skip();
me.skip(newPath);
// Set function length
me.setFunctionLength(fnPath, originalLength);
me.changeData.functions++;
}
return {
visitor: {
Function: {
exit(path: NodePath<t.Function>) {
flattenFunction(path);
},
},
Program(path) {
path.scope.crawl();
},
},
};
};