js-confuser
Version:
JavaScript Obfuscation Tool.
176 lines (152 loc) • 5.58 kB
text/typescript
import * as t from "@babel/types";
import { NodePath } from "@babel/traverse";
import { PluginArg, PluginObject } from "./plugin";
import { Order } from "../order";
import { NameGen } from "../utils/NameGen";
import { ok } from "assert";
const LABEL = Symbol("label");
interface LabelInterface {
label?: string;
renamed?: string;
removed: boolean;
required: boolean;
paths: NodePath<t.BreakStatement | t.ContinueStatement>[];
}
interface NodeLabel {
[LABEL]?: LabelInterface;
}
export default function ({ Plugin }: PluginArg): PluginObject {
const me = Plugin(Order.RenameLabels, {
changeData: {
labelsRenamed: 0,
labelsRemoved: 0,
},
});
return {
visitor: {
Program(path) {
const allLabelInterfaces: LabelInterface[] = [];
// First pass: Collect all label usages
path.traverse({
LabeledStatement(labelPath) {
const labelInterface = {
label: labelPath.node.label.name,
removed: false,
required: false,
paths: [],
};
allLabelInterfaces.push(labelInterface);
(labelPath.node as NodeLabel)[LABEL] = labelInterface;
},
"BreakStatement|ContinueStatement"(_path) {
const path = _path as NodePath<
t.BreakStatement | t.ContinueStatement
>;
if (path.node.label) {
const labelName = path.node.label.name;
let targets: NodePath<
t.For | t.While | t.BlockStatement | t.SwitchStatement
>[] = [];
let onlySearchLoops = path.isContinueStatement();
let currentPath: NodePath = path;
while (currentPath) {
if (
currentPath.isFor() ||
currentPath.isWhile() ||
currentPath.isSwitchStatement()
) {
targets.push(currentPath);
}
if (
currentPath.isBlockStatement() &&
currentPath.parentPath.isLabeledStatement()
) {
targets.push(currentPath);
}
currentPath = currentPath.parentPath;
}
const target = targets.find(
(label) =>
label.parentPath &&
label.parentPath.isLabeledStatement() &&
label.parentPath.node.label.name === labelName
);
if (onlySearchLoops) {
// Remove BlockStatements and SwitchStatements from the list of targets
// a continue statement only target loops
// This helps remove unnecessary labels when a continue is nested with a block statement
// ex: for-loop with if-statement continue
targets = targets.filter(
(target) =>
!target.isBlockStatement() && !target.isSwitchStatement()
);
}
ok(target);
const isRequired =
target.isBlockStatement() || targets[0] !== target;
const labelInterface = (target.parentPath.node as NodeLabel)[
LABEL
];
if (isRequired) {
labelInterface.required = true;
} else {
// Label is not required here, remove it for this particular break/continue statement
path.node.label = null;
}
if (!labelInterface.paths) {
labelInterface.paths = [];
}
labelInterface.paths.push(path);
}
},
});
const nameGen = new NameGen(me.options.identifierGenerator);
for (var labelInterface of allLabelInterfaces) {
const isRequired = labelInterface.required;
if (isRequired) {
var newName = labelInterface.label;
if (
me.computeProbabilityMap(
me.options.renameLabels,
labelInterface.label
)
) {
newName = nameGen.generate();
}
labelInterface.renamed = newName;
me.changeData.labelsRenamed++;
} else {
labelInterface.removed = true;
me.changeData.labelsRemoved++;
}
}
// Second pass: Rename labels and remove unused ones
path.traverse({
LabeledStatement(labelPath) {
const labelInterface = (labelPath.node as NodeLabel)[LABEL];
if (labelInterface) {
// Remove label but replace it with its body
if (labelInterface.removed) {
labelPath.replaceWith(labelPath.node.body);
}
// Else keep the label but rename it
if (typeof labelInterface.renamed === "string") {
labelPath.node.label.name = labelInterface.renamed;
}
// Update all break/continue statements
for (var breakPath of labelInterface.paths) {
// Remove label from break/continue statement
if (labelInterface.removed) {
breakPath.node.label = null;
} else {
// Update label name
breakPath.node.label = t.identifier(labelInterface.renamed);
}
}
}
},
});
},
},
};
}