distinctiomagnam
Version:
JavaScript Obfuscation Tool.
419 lines (364 loc) • 10.4 kB
text/typescript
import traverse, { ExitCallback } from "../traverse";
import { AddComment, Node } from "../util/gen";
import {
alphabeticalGenerator,
choice,
getRandomInteger,
} from "../util/random";
import { ok } from "assert";
import Obfuscator from "../obfuscator";
import { ObfuscateOptions } from "../options";
import { ComputeProbabilityMap } from "../probability";
import { reservedIdentifiers, reservedKeywords } from "../constants";
import { ObfuscateOrder } from "../order";
/**
* Base-class for all transformations.
* - Transformations can have preparation transformations `.before`
* - Transformations can have cleanup transformations `.after`
*
* - `match()` function returns true/false if possible candidate
* - `transform()` function modifies the object
*
* ```js
* class Example extends Transform {
* constructor(o){
* super(o);
* }
*
* match(object, parents){
* return object.type == "...";
* }
*
* transform(object, parents){
* // onEnter
*
* return ()=>{
* // onExit
* }
* }
*
* apply(tree){
* // onStart
*
* super.apply(tree);
*
* // onEnd
* }
* }
* ```
*/
export default class Transform {
/**
* The obfuscator.
*/
obfuscator: Obfuscator;
/**
* The user's options.
*/
options: ObfuscateOptions;
/**
* Only required for top-level transformations.
*/
priority: number;
/**
* Transforms to run before, such as `Variable Analysis`.
*/
before: Transform[];
/**
* Transforms to run after.
*/
after: Transform[];
/**
* Transformations to run at the same time (can cause conflicts so use sparingly)
*/
concurrent: Transform[];
constructor(obfuscator, priority: number = -1) {
ok(obfuscator instanceof Obfuscator, "obfuscator should be an Obfuscator");
this.obfuscator = obfuscator;
this.options = this.obfuscator.options;
this.priority = priority;
this.before = [];
this.after = [];
this.concurrent = [];
}
/**
* The transformation name.
*/
get className() {
return (
ObfuscateOrder[this.priority] || (this as any).__proto__.constructor.name
);
}
/**
* Run an AST through the transformation (including `pre` and `post` transforms)
* @param tree
*/
apply(tree: Node) {
if (tree.type == "Program" && this.options.verbose) {
if (this.priority === -1) {
console.log("#", ">", this.className);
} else {
console.log("#", this.priority, this.className);
}
}
/**
* Run through pre-transformations
*/
this.before.forEach((x) => x.apply(tree));
traverse(tree, (object, parents) => {
var fns = [];
fns.push(this.input(object, parents));
// Fix 1. Increase performance with multiple transforms on one iteration.
this.concurrent.forEach((x) => fns.push(x.input(object, parents)));
return () => fns.forEach((x) => x && x());
});
/**
* Cleanup transformations
*/
this.after.forEach((x) => x.apply(tree));
}
/**
* The `match` function filters for possible candidates.
*
* - If `true`, the node is sent to the `transform()` method
* - else it's discarded.
*
* @param object
* @param parents
* @param block
*/
match(object: Node, parents: Node[]): boolean {
throw new Error("not implemented");
}
/**
* Modifies the given node.
*
* - Return a function to be ran when the node is exited.
* - The node is safe to modify in most cases.
*
* @param object - Current node
* @param parents - Array of ancestors `[Closest, ..., Root]`
* @param block
*/
transform(object: Node, parents: Node[]): ExitCallback | void {
throw new Error("not implemented");
}
/**
* Calls `.match` with the given parameters, and then `.transform` if satisfied.
* @private
*/
input(object: Node, parents: Node[]): ExitCallback | void {
if (this.match(object, parents)) {
return this.transform(object, parents);
}
}
/**
* Returns a random string.
*
* Used for creating temporary variables names, typically before RenameVariables has ran.
*
* These long temp names will be converted to short, mangled names by RenameVariables.
*/
getPlaceholder() {
const genRanHex = (size) =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 10).toString(10))
.join("");
return "__p_" + genRanHex(10);
}
/**
* Returns an independent name generator with it's own counter.
* @param offset
* @returns
*/
getGenerator(offset = 0) {
var count = offset;
return {
generate: () => {
count++;
return this.generateIdentifier(-1, count);
},
};
}
/**
* Generates a valid variable name.
* @param length Default length is 6 to 10 characters.
* @returns **`string`**
*/
generateIdentifier(length: number = -1, count = -1): string {
if (length == -1) {
length = getRandomInteger(6, 8);
}
var set = new Set();
if (count == -1) {
this.obfuscator.varCount++;
count = this.obfuscator.varCount;
set = this.obfuscator.generated;
}
var identifier;
do {
identifier = ComputeProbabilityMap(
this.options.identifierGenerator,
(mode = "randomized") => {
switch (mode) {
case "randomized":
var characters =
"_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".split(
""
);
var numbers = "0123456789".split("");
var combined = [...characters, ...numbers];
var result = "";
for (var i = 0; i < length; i++) {
result += choice(i == 0 ? characters : combined);
}
return result;
case "hexadecimal":
const genRanHex = (size) =>
[...Array(size)]
.map(() => Math.floor(Math.random() * 16).toString(16))
.join("");
return "_0x" + genRanHex(length).toUpperCase();
case "mangled":
while (1) {
var result = alphabeticalGenerator(count);
count++;
if (
reservedKeywords.has(result) ||
reservedIdentifiers.has(result)
) {
} else {
return result;
}
}
throw new Error("impossible but TypeScript insists");
case "number":
return "var_" + count;
case "zeroWidth":
var keyWords = [
"if",
"in",
"for",
"let",
"new",
"try",
"var",
"case",
"else",
"null",
"break",
"catch",
"class",
"const",
"super",
"throw",
"while",
"yield",
"delete",
"export",
"import",
"public",
"return",
"switch",
"default",
"finally",
"private",
"continue",
"debugger",
"function",
"arguments",
"protected",
"instanceof",
"function",
"await",
"async",
];
var safe = "\u200C".repeat(count + 1);
var base = choice(keyWords) + safe;
return base;
}
throw new Error("Invalid 'identifierGenerator' mode: " + mode);
}
);
} while (set.has(identifier));
if (!identifier) {
throw new Error("identifier null");
}
set.add(identifier);
return identifier;
}
/**
* Smartly appends a comment to a Node.
* - Includes the transformation's name.
* @param node
* @param text
* @param i
*/
addComment(node: Node, text: string) {
if (this.options.debugComments) {
return AddComment(node, `[${this.className}] ${text}`);
}
return node;
}
replace(node1: Node, node2: Node) {
for (var key in node1) {
delete node1[key];
}
this.objectAssign(node1, node2);
}
replaceIdentifierOrLiteral(node1: Node, node2: Node, parents: Node[]) {
// Fix 2. Make parent property key computed
if (
parents[0] &&
(parents[0].type == "Property" ||
parents[0].type == "MethodDefinition") &&
parents[0].key == node1
) {
parents[0].computed = true;
parents[0].shorthand = false;
}
this.replace(node1, node2);
}
/**
* Smartly merges two Nodes.
* - Null checking
* - Preserves comments
* @param node1
* @param node2
*/
objectAssign(node1: Node, node2: Node): Node {
ok(node1);
ok(node2);
var comments1 = node1.leadingComments || [];
var comments2 = node2.leadingComments || [];
var comments = [...comments1, ...comments2];
node2.leadingComments = comments;
node1._transform = node2._transform = this.className;
return Object.assign(node1, node2);
}
/**
* Verbose logging for this transformation.
* @param messages
*/
log(...messages: any[]) {
if (this.options.verbose) {
console.log("[" + this.className + "]", ...messages);
}
}
/**
* Verbose logging for warning/importing messages.
* @param messages
*/
warn(...messages: any[]) {
if (this.options.verbose) {
console.log("[ WARN " + this.className + " ]", ...messages);
}
}
/**
* Throws an error. Appends the transformation's name to the error's message.
* @param error
*/
error(error: Error): never {
throw new Error(`${this.className} Error: ${error.message}`);
}
}