js-confuser
Version:
JavaScript Obfuscation Tool.
499 lines (420 loc) • 14.5 kB
text/typescript
import { ok } from "assert";
import * as t from "@babel/types";
import generate from "@babel/generator";
import traverse from "@babel/traverse";
import { parse } from "@babel/parser";
import { ObfuscateOptions, ProbabilityMap } from "./options";
import { applyDefaultsToOptions, validateOptions } from "./validateOptions";
import { ObfuscationResult, ProfilerCallback } from "./obfuscationResult";
import { NameGen } from "./utils/NameGen";
import { Order } from "./order";
import {
PluginFunction,
PluginInstance,
PluginObject,
} from "./transforms/plugin";
// Transforms
import preparation from "./transforms/preparation";
import renameVariables from "./transforms/identifier/renameVariables";
import variableMasking from "./transforms/variableMasking";
import dispatcher from "./transforms/dispatcher";
import duplicateLiteralsRemoval from "./transforms/extraction/duplicateLiteralsRemoval";
import objectExtraction from "./transforms/extraction/objectExtraction";
import globalConcealing from "./transforms/identifier/globalConcealing";
import stringCompression from "./transforms/string/stringCompression";
import deadCode from "./transforms/deadCode";
import stringSplitting from "./transforms/string/stringSplitting";
import shuffle from "./transforms/shuffle";
import astScrambler from "./transforms/astScrambler";
import calculator from "./transforms/calculator";
import movedDeclarations from "./transforms/identifier/movedDeclarations";
import renameLabels from "./transforms/renameLabels";
import rgf from "./transforms/rgf";
import flatten from "./transforms/flatten";
import stringConcealing from "./transforms/string/stringConcealing";
import lock from "./transforms/lock/lock";
import controlFlowFlattening from "./transforms/controlFlowFlattening";
import opaquePredicates from "./transforms/opaquePredicates";
import minify from "./transforms/minify";
import finalizer from "./transforms/finalizer";
import integrity from "./transforms/lock/integrity";
import pack from "./transforms/pack";
import { createObject } from "./utils/object-utils";
export const DEFAULT_OPTIONS: ObfuscateOptions = {
target: "node",
compact: true,
};
export default class Obfuscator {
plugins: {
plugin: PluginObject;
pluginInstance: PluginInstance;
}[] = [];
options: ObfuscateOptions;
totalPossibleTransforms: number = 0;
globalState = {
lock: {
integrity: {
sensitivityRegex: / |\n|;|,|\{|\}|\(|\)|\.|\[|\]/g,
},
createCountermeasuresCode: (): t.Statement[] => {
throw new Error("Not implemented");
},
},
// After RenameVariables completes, this map will contain the renamed variables
// Most use cases involve grabbing the Program(global) mappings
renamedVariables: new Map<t.Node, Map<string, string>>(),
// Internal functions, should not be renamed/removed
internals: {
stringCompressionLibraryName: "",
nativeFunctionName: "",
integrityHashName: "",
invokeCountermeasuresFnName: "",
},
};
isInternalVariable(name: string) {
return Object.values(this.globalState.internals).includes(name);
}
shouldTransformNativeFunction(nameAndPropertyPath: string[]) {
if (!this.options.lock?.tamperProtection) {
return false;
}
// Custom implementation for Tamper Protection
if (typeof this.options.lock.tamperProtection === "function") {
return this.options.lock.tamperProtection(nameAndPropertyPath.join("."));
}
if (
this.options.target === "browser" &&
nameAndPropertyPath.length === 1 &&
nameAndPropertyPath[0] === "fetch"
) {
return true;
}
var globalObject = {};
try {
globalObject =
typeof globalThis !== "undefined"
? globalThis
: typeof window !== "undefined"
? window
: typeof global !== "undefined"
? global
: typeof self !== "undefined"
? self
: new Function("return this")();
} catch (e) {}
var fn = globalObject;
for (var item of nameAndPropertyPath) {
fn = fn?.[item];
if (typeof fn === "undefined") return false;
}
var hasNativeCode =
typeof fn === "function" && ("" + fn).includes("[native code]");
return hasNativeCode;
}
getStringCompressionLibraryName() {
if (this.parentObfuscator) {
return this.parentObfuscator.getStringCompressionLibraryName();
}
return this.globalState.internals.stringCompressionLibraryName;
}
getObfuscatedVariableName(originalName: string, programNode: t.Node) {
const renamedVariables = this.globalState.renamedVariables.get(programNode);
return renamedVariables?.get(originalName) || originalName;
}
/**
* The main Name Generator for `Rename Variables`
*/
nameGen: NameGen;
public constructor(
userOptions: ObfuscateOptions,
public parentObfuscator?: Obfuscator
) {
validateOptions(userOptions);
this.options = applyDefaultsToOptions({ ...userOptions });
this.nameGen = new NameGen(this.options.identifierGenerator);
const shouldAddLockTransform =
this.options.lock &&
(Object.keys(this.options.lock).filter(
(key) =>
key !== "customLocks" &&
this.isProbabilityMapProbable(this.options.lock[key])
).length > 0 ||
this.options.lock.customLocks.length > 0);
const allPlugins: PluginFunction[] = [];
const push = (probabilityMap, ...pluginFns) => {
this.totalPossibleTransforms += pluginFns.length;
if (!this.isProbabilityMapProbable(probabilityMap)) return;
allPlugins.push(...pluginFns);
};
push(true, preparation);
push(this.options.objectExtraction, objectExtraction);
push(this.options.flatten, flatten);
push(shouldAddLockTransform, lock);
push(this.options.rgf, rgf);
push(this.options.dispatcher, dispatcher);
push(this.options.deadCode, deadCode);
push(this.options.controlFlowFlattening, controlFlowFlattening);
push(this.options.calculator, calculator);
push(this.options.globalConcealing, globalConcealing);
push(this.options.opaquePredicates, opaquePredicates);
push(this.options.stringSplitting, stringSplitting);
push(this.options.stringConcealing, stringConcealing);
// String Compression is only applied to the main obfuscator
// Any RGF functions will not have string compression due to the size of the decompression function
push(
!parentObfuscator && this.options.stringCompression,
stringCompression
);
push(this.options.variableMasking, variableMasking);
push(this.options.duplicateLiteralsRemoval, duplicateLiteralsRemoval);
push(this.options.shuffle, shuffle);
push(this.options.movedDeclarations, movedDeclarations);
push(this.options.renameLabels, renameLabels);
push(this.options.minify, minify);
push(this.options.astScrambler, astScrambler);
push(this.options.renameVariables, renameVariables);
push(true, finalizer);
push(this.options.pack, pack);
push(this.options.lock?.integrity, integrity);
allPlugins.map((pluginFunction) => {
var pluginInstance: PluginInstance;
var plugin = pluginFunction({
Plugin: (order: Order, mergeObject?) => {
ok(typeof order === "number");
var pluginOptions = {
order,
name: Order[order],
};
const newPluginInstance = new PluginInstance(pluginOptions, this);
if (typeof mergeObject === "object" && mergeObject) {
Object.assign(newPluginInstance, mergeObject);
}
pluginInstance = newPluginInstance;
// @ts-ignore
return newPluginInstance as any;
},
});
ok(
pluginInstance,
"Plugin instance not created: " + pluginFunction.toString()
);
this.plugins.push({
plugin,
pluginInstance,
});
});
this.plugins = this.plugins.sort(
(a, b) => a.pluginInstance.order - b.pluginInstance.order
);
if (!parentObfuscator && this.hasPlugin(Order.StringCompression)) {
this.globalState.internals.stringCompressionLibraryName =
this.nameGen.generate(false);
}
}
index: number = 0;
obfuscateAST(
ast: babel.types.File,
options?: {
profiler?: ProfilerCallback;
disablePack?: boolean;
}
): babel.types.File {
let finalASTHandler: PluginObject["finalASTHandler"][] = [];
for (let i = 0; i < this.plugins.length; i++) {
this.index = i;
const { plugin, pluginInstance } = this.plugins[i];
// Skip pack if disabled
if (pluginInstance.order === Order.Pack && options?.disablePack) continue;
if (this.options.verbose) {
console.log(
`Applying ${pluginInstance.name} (${i + 1}/${this.plugins.length})`
);
}
traverse(ast, plugin.visitor);
plugin.post?.();
if (plugin.finalASTHandler) {
finalASTHandler.push(plugin.finalASTHandler);
}
if (options?.profiler) {
options?.profiler({
index: i,
currentTransform: pluginInstance.name,
nextTransform: this.plugins[i + 1]?.pluginInstance?.name,
totalTransforms: this.plugins.length,
});
}
}
for (const handler of finalASTHandler) {
ast = handler(ast);
}
return ast;
}
async obfuscate(sourceCode: string): Promise<ObfuscationResult> {
// Parse the source code into an AST
let ast = Obfuscator.parseCode(sourceCode);
ast = this.obfuscateAST(ast);
// Generate the transformed code from the modified AST with comments removed and compacted output
const code = this.generateCode(ast);
return {
code: code,
};
}
getPlugin(order: Order) {
return this.plugins.find((x) => x.pluginInstance.order === order);
}
hasPlugin(order: Order) {
return !!this.getPlugin(order);
}
/**
* Calls `Obfuscator.generateCode` with the current instance options
*/
generateCode<T extends t.Node = t.File>(ast: T): string {
return Obfuscator.generateCode(ast, this.options);
}
/**
* Generates code from an AST using `@babel/generator`
*/
static generateCode<T extends t.Node = t.File>(
ast: T,
options: ObfuscateOptions = DEFAULT_OPTIONS
): string {
const compact = !!options.compact;
const { code } = generate(ast, {
comments: false, // Remove comments
minified: compact,
// jsescOption: {
// String Encoding using Babel
// escapeEverything: true,
// },
});
return code;
}
/**
* Parses the source code into an AST using `babel.parseSync`
*/
static parseCode(sourceCode: string): babel.types.File {
// Parse the source code into an AST
let ast = parse(sourceCode, {
sourceType: "unambiguous",
});
return ast;
}
probabilityMapCounter = new WeakMap<Object, number>();
/**
* Evaluates a ProbabilityMap.
* @param map The setting object.
* @param customFnArgs Args given to user-implemented function, such as a variable name.
*/
computeProbabilityMap<
T,
F extends (...args: any[]) => any = (...args: any[]) => any
>(
map: ProbabilityMap<T, F>,
...customImplementationArgs: F extends (...args: infer P) => any ? P : never
): boolean | string {
// Check if this probability map uses the {value: ..., limit: ...} format
if (typeof map === "object" && map && "value" in map) {
// Check for the limit property
if ("limit" in map && typeof map.limit === "number" && map.limit >= 0) {
// Check if the limit has been reached
if (this.probabilityMapCounter.get(map) >= map.limit) {
return false;
}
}
var value = this.computeProbabilityMap(
map.value as ProbabilityMap<T, F>,
...customImplementationArgs
);
if (value) {
// Increment the counter for this map
this.probabilityMapCounter.set(
map,
this.probabilityMapCounter.get(map) + 1 || 1
);
}
return value;
}
if (!map) {
return false;
}
if (map === true || map === 1) {
return true;
}
if (typeof map === "number") {
return Math.random() < map;
}
if (typeof map === "function") {
return (map as Function)(...customImplementationArgs);
}
if (typeof map === "string") {
return map;
}
var asObject: { [mode: string]: number } = {};
if (Array.isArray(map)) {
map.forEach((x: any) => {
asObject[x.toString()] = 1;
});
} else {
asObject = map as any;
}
var total = Object.values(asObject).reduce((a, b) => a + b);
var percentages = createObject(
Object.keys(asObject),
Object.values(asObject).map((x) => x / total)
);
var ticket = Math.random();
var count = 0;
var winner = null;
Object.keys(percentages).forEach((key) => {
var x = Number(percentages[key]);
if (ticket >= count && ticket < count + x) {
winner = key;
}
count += x;
});
return winner;
}
/**
* Determines if a probability map can return a positive result (true, or some string mode).
* - Negative probability maps are used to remove transformations from running entirely.
* @param map
*/
isProbabilityMapProbable<T>(map: ProbabilityMap<T>): boolean {
ok(!Number.isNaN(map), "Numbers cannot be NaN");
if (!map || typeof map === "undefined") {
return false;
}
if (typeof map === "function") {
return true;
}
if (typeof map === "number") {
if (map > 1 || map < 0) {
throw new Error(`Numbers must be between 0 and 1 for 0% - 100%`);
}
}
if (Array.isArray(map)) {
ok(
map.length != 0,
"Empty arrays are not allowed for options. Use false instead."
);
if (map.length == 1) {
return !!map[0];
}
}
if (typeof map === "object") {
if (map instanceof Date) return true;
if (map instanceof RegExp) return true;
if ("value" in map && !map.value) return false;
if ("limit" in map && map.limit === 0) return false;
var keys = Object.keys(map);
ok(
keys.length != 0,
"Empty objects are not allowed for options. Use false instead."
);
if (keys.length == 1) {
return !!map[keys[0]];
}
}
return true;
}
}