js-confuser
Version:
JavaScript Obfuscation Tool.
361 lines (318 loc) • 12 kB
text/typescript
import { NodePath } from "@babel/traverse";
import { PluginArg, PluginObject } from "./plugin";
import * as t from "@babel/types";
import { Order } from "../order";
import path from "path";
import {
NodeSymbol,
PREDICTABLE,
UNSAFE,
variableFunctionName,
} from "../constants";
import { ok } from "assert";
import {
getParentFunctionOrProgram,
getPatternIdentifierNames,
} from "../utils/ast-utils";
import { isVariableFunctionIdentifier } from "../utils/function-utils";
/**
* Preparation arranges the user's code into an AST the obfuscator can easily transform.
*
* ExplicitIdentifiers
* - `object.IDENTIFIER` -> `object['IDENTIFIER']` // Now String Concealing can apply on it
* - `{ IDENTIFIER: ... }` -> `{ "IDENTIFIER": ... }`
*
* ExplicitDeclarations
* - `var a,b,c` -> `var a; var b; var c;` // Now Stack can apply on it
*
* Block
* - `x => x * 2` -> `x => { return x * 2 }` // Change into Block Statements
* - `if(true) return` -> `if (true) { return }`
* - `while(a) a--;` -> `while(a) { a-- }`
*/
export default ({ Plugin }: PluginArg): PluginObject => {
const me = Plugin(Order.Preparation);
const markFunctionUnsafe = (path: NodePath<t.Node>) => {
const functionPath = path.findParent(
(path) => path.isFunction() || path.isProgram()
);
if (!functionPath) return;
const functionNode = functionPath.node;
(functionNode as NodeSymbol)[UNSAFE] = true;
};
return {
visitor: {
"ThisExpression|Super": {
exit(path) {
markFunctionUnsafe(path);
},
},
// `Hello ${username}` -> "Hello " + username
TemplateLiteral: {
exit(path) {
// Check if this is a tagged template literal, if yes, skip it
if (t.isTaggedTemplateExpression(path.parent)) {
return;
}
const { quasis, expressions } = path.node;
// Start with the first quasi (template string part)
let binaryExpression: t.Expression = t.stringLiteral(
quasis[0].value.cooked
);
// Loop over the remaining quasis and expressions, concatenating them
for (let i = 0; i < expressions.length; i++) {
// Add the expression as part of the binary concatenation
binaryExpression = t.binaryExpression(
"+",
binaryExpression,
expressions[i] as t.Expression
);
// Add the next quasi (template string part)
if (quasis[i + 1].value.cooked !== "") {
binaryExpression = t.binaryExpression(
"+",
binaryExpression,
t.stringLiteral(quasis[i + 1].value.cooked)
);
}
}
// Replace the template literal with the constructed binary expression
path.replaceWith(binaryExpression);
},
},
// /Hello World/g -> new RegExp("Hello World", "g")
RegExpLiteral: {
exit(path) {
const { pattern, flags } = path.node;
// Create a new RegExp() expression using the pattern and flags
const newRegExpCall = t.newExpression(
t.identifier("RegExp"), // Identifier for RegExp constructor
[
t.stringLiteral(pattern), // First argument: the pattern (no extra escaping needed)
flags ? t.stringLiteral(flags) : t.stringLiteral(""), // Second argument: the flags (if any)
]
);
// Replace the literal regex with the new RegExp() call
path.replaceWith(newRegExpCall);
},
},
ReferencedIdentifier: {
exit(path) {
const { name } = path.node;
if (["arguments", "eval"].includes(name)) {
markFunctionUnsafe(path);
}
// When Rename Variables is disabled, __JS_CONFUSER_VAR__ must still be removed
if (
!me.obfuscator.hasPlugin(Order.RenameVariables) &&
isVariableFunctionIdentifier(path)
) {
ok(
path.parentPath.isCallExpression(),
variableFunctionName + " must be directly called"
);
var argument = path.parentPath.node.arguments[0];
t.assertIdentifier(argument);
// Remove the variableFunctionName call
path.parentPath.replaceWith(t.stringLiteral(argument.name));
}
},
},
FunctionDeclaration: {
exit(path) {
// A function is 'predictable' if the parameter lengths are guaranteed to be known
// a(true) -> predictable
// (a || b)(true) -> unpredictable (Must be directly in a Call Expression)
// a(...args) -> unpredictable (Cannot use SpreadElement)
const { name } = path.node.id;
var binding = path.scope.getBinding(name);
var predictable = true;
var maxArgLength = 0;
for (var referencePath of binding.referencePaths) {
if (!referencePath.parentPath.isCallExpression()) {
predictable = false;
break;
}
var argsPath = referencePath.parentPath.get("arguments");
for (var arg of argsPath) {
if (arg.isSpreadElement()) {
predictable = false;
break;
}
}
if (argsPath.length > maxArgLength) {
maxArgLength = argsPath.length;
}
}
var definedArgLength = path.get("params").length;
if (predictable && definedArgLength >= maxArgLength) {
(path.node as NodeSymbol)[PREDICTABLE] = true;
}
},
},
// console.log() -> console["log"]();
MemberExpression: {
exit(path) {
if (!path.node.computed && path.node.property.type === "Identifier") {
path.node.property = t.stringLiteral(path.node.property.name);
path.node.computed = true;
}
},
},
// { key: true } -> { "key": true }
"Property|Method": {
exit(_path) {
let path = _path as NodePath<t.Property | t.Method>;
if (t.isClassPrivateProperty(path.node)) return;
if (!path.node.computed && path.node.key.type === "Identifier") {
// Don't change constructor key
if (t.isClassMethod(path.node) && path.node.kind === "constructor")
return;
path.node.key = t.stringLiteral(path.node.key.name);
path.node.computed = true;
}
},
},
// var a,b,c -> var a; var b; var c;
VariableDeclaration: {
exit(path) {
if (path.node.declarations.length > 1) {
// E.g. for (var i = 0, j = 1;;)
if (path.key === "init" && path.parentPath.isForStatement()) {
if (
!path.parentPath.node.test &&
!path.parentPath.node.update &&
path.node.kind === "var"
) {
path.parentPath.insertBefore(
path.node.declarations.map((declaration) =>
t.variableDeclaration(path.node.kind, [declaration])
)
);
path.remove();
}
} else {
if (path.parentPath.isExportNamedDeclaration()) {
path.parentPath.replaceWithMultiple(
path.node.declarations.map((declaration) =>
t.exportNamedDeclaration(
t.variableDeclaration(path.node.kind, [declaration])
)
)
);
} else {
path
.replaceWithMultiple(
path.node.declarations.map((declaration, i) => {
var names = Array.from(
getPatternIdentifierNames(path.get("declarations")[i])
);
names.forEach((name) => {
path.scope.removeBinding(name);
});
var newNode = t.variableDeclaration(path.node.kind, [
declaration,
]);
return newNode;
})
)
.forEach((newPath) => {
if (newPath.node.kind === "var") {
var functionOrProgram =
getParentFunctionOrProgram(newPath);
functionOrProgram.scope.registerDeclaration(newPath);
}
newPath.scope.registerDeclaration(newPath);
});
}
}
}
},
},
// () => a() -> () => { return a(); }
ArrowFunctionExpression: {
exit(path: NodePath<t.ArrowFunctionExpression>) {
if (path.node.body.type !== "BlockStatement") {
path.node.expression = false;
path.node.body = t.blockStatement([
t.returnStatement(path.node.body),
]);
}
},
},
// if (a) b() -> if (a) { b(); }
// if (a) {b()} else c() -> if (a) { b(); } else { c(); }
IfStatement: {
exit(path) {
if (path.node.consequent.type !== "BlockStatement") {
path.node.consequent = t.blockStatement([path.node.consequent]);
}
if (
path.node.alternate &&
path.node.alternate.type !== "BlockStatement"
) {
path.node.alternate = t.blockStatement([path.node.alternate]);
}
},
},
// for() d() -> for() { d(); }
// while(a) b() -> while(a) { b(); }
// with(a) b() -> with(a) { b(); }
"ForStatement|ForInStatement|ForOfStatement|WhileStatement|WithStatement":
{
exit(_path) {
var path = _path as NodePath<
| t.ForStatement
| t.ForInStatement
| t.ForOfStatement
| t.WhileStatement
| t.WithStatement
>;
if (path.node.body.type !== "BlockStatement") {
path.node.body = t.blockStatement([path.node.body]);
}
},
},
// function a(param = ()=>b)
// _getB = ()=> ()=>b
// function a(param = _getB())
// Basically Babel scope.rename misses this edge case, so we need to manually handle it
// Here were essentially making the variables easier to understand
Function: {
exit(path) {
for (var param of path.get("params")) {
param.traverse({
"FunctionExpression|ArrowFunctionExpression"(_innerPath) {
let innerPath = _innerPath as NodePath<
t.FunctionExpression | t.ArrowFunctionExpression
>;
const child = innerPath.find((path) =>
path.parentPath?.isAssignmentPattern()
);
if (!child) return;
if (
t.isAssignmentPattern(child.parent) &&
child.parent.right === child.node
) {
var creatorName = me.getPlaceholder();
var insertPath = path.insertBefore(
t.variableDeclaration("const", [
t.variableDeclarator(
t.identifier(creatorName),
t.arrowFunctionExpression([], innerPath.node, false)
),
])
)[0];
path.scope.parent.registerDeclaration(insertPath);
innerPath.replaceWith(
t.callExpression(t.identifier(creatorName), [])
);
}
},
});
}
},
},
},
};
};