UNPKG

distinctiomagnam

Version:
599 lines (530 loc) 19.3 kB
import { walk } from "../traverse"; import { ArrayExpression, AssignmentExpression, BinaryExpression, CallExpression, ExpressionStatement, FunctionDeclaration, FunctionExpression, Identifier, IfStatement, Literal, Node, Location, MemberExpression, ObjectExpression, Property, ReturnStatement, VariableDeclaration, SequenceExpression, NewExpression, UnaryExpression, BlockStatement, LogicalExpression, ThisExpression, VariableDeclarator, RestElement, } from "../util/gen"; import { getIdentifierInfo } from "../util/identifiers"; import { deleteDirect, getBlockBody, getVarContext, isVarContext, prepend, append, } from "../util/insert"; import Transform from "./transform"; import { isInsideType } from "../util/compare"; import { choice, shuffle } from "../util/random"; import { ComputeProbabilityMap } from "../probability"; import { reservedIdentifiers } from "../constants"; import { ObfuscateOrder } from "../order"; import Template from "../templates/template"; /** * A Dispatcher processes function calls. All the function declarations are brought into a dictionary. * * ```js * var param1; * function dispatcher(key){ * var fns = { * 'fn1': function(){ * var [arg1] = [param1]; * console.log(arg1); * } * } * return fns[key](); * }; * param1 = "Hello World"; * dispatcher('fn1'); // > "Hello World" * ``` * * Can break code with: * * 1. testing function equality, * 2. using `arguments.callee`, * 3. using `this` */ export default class Dispatcher extends Transform { count: number; constructor(o) { super(o, ObfuscateOrder.Dispatcher); this.count = 0; } match(object: Node, parents: Node[]) { if (isInsideType("AwaitExpression", object, parents)) { return false; } return ( isVarContext(object) && object.type !== "ArrowFunctionExpression" && !object.$dispatcherSkip && !parents.find((x) => x.$dispatcherSkip) ); } transform(object: Node, parents: Node[]) { return () => { if (ComputeProbabilityMap(this.options.dispatcher, (mode) => mode)) { if (object.type != "Program" && object.body.type != "BlockStatement") { return; } // Map of FunctionDeclarations var functionDeclarations: { [name: string]: Location } = {}; // Array of Identifier nodes var identifiers: Location[] = []; var illegalFnNames: Set<string> = new Set(); // New Names for Functions var newFnNames: { [name: string]: string } = {}; // [old name]: randomized name var context = isVarContext(object) ? object : getVarContext(object, parents); walk(object, parents, (o: Node, p: Node[]) => { if (object == o) { // Fix 1 return; } var c = getVarContext(o, p); if (o.type == "FunctionDeclaration") { c = getVarContext(p[0], p.slice(1)); } if (context === c) { if (o.type == "FunctionDeclaration" && o.id.name) { if ( o.$requiresEval || o.async || o.generator || p.find( (x) => x.$dispatcherSkip || x.type == "MethodDefinition" ) || o.body.type != "BlockStatement" ) { illegalFnNames.add(name); } var name = o.id.name; // If dupe, no routing if (functionDeclarations[name]) { illegalFnNames.add(name); return; } walk(o, p, (oo, pp) => { if ( (oo.type == "Identifier" && oo.name == "arguments") || oo.type == "ThisExpression" || oo.type == "Super" ) { if (getVarContext(oo, pp) === o) { illegalFnNames.add(name); return "EXIT"; } } }); functionDeclarations[name] = [o, p]; } } if (o.type == "Identifier") { if (reservedIdentifiers.has(o.name)) { return; } var info = getIdentifierInfo(o, p); if (!info.spec.isReferenced) { return; } if (info.spec.isDefined) { if (info.isFunctionDeclaration) { if ( p[0].id && (!functionDeclarations[p[0].id.name] || functionDeclarations[p[0].id.name][0] !== p[0]) ) { illegalFnNames.add(o.name); } } else { illegalFnNames.add(o.name); } } else if (info.spec.isModified) { illegalFnNames.add(o.name); } else { identifiers.push([o, p]); } } }); illegalFnNames.forEach((name) => { delete functionDeclarations[name]; }); // map original name->new game var gen = this.getGenerator(); Object.keys(functionDeclarations).forEach((name) => { newFnNames[name] = gen.generate(); }); // set containing new name var set = new Set(Object.keys(newFnNames)); // Only make a dispatcher function if it caught any functions if (set.size > 0) { var payloadArg = `$jsc_d${this.count}_payload`; var dispatcherFnName = this.getPlaceholder() + "_dispatcher_" + this.count; this.log(dispatcherFnName, set); this.count++; var expectedGet = gen.generate(); var expectedClearArgs = gen.generate(); var expectedNew = gen.generate(); var returnProp = gen.generate(); var newReturnMemberName = gen.generate(); var shuffledKeys = shuffle(Object.keys(functionDeclarations)); var mapName = this.getPlaceholder(); var cacheName = this.getPlaceholder(); // creating the dispatcher function // 1. create function map var map = VariableDeclaration( VariableDeclarator( mapName, ObjectExpression( shuffledKeys.map((name) => { var [def, defParents] = functionDeclarations[name]; var body = getBlockBody(def.body); var functionExpression: Node = { ...def, expression: false, type: "FunctionExpression", id: null, }; this.addComment(functionExpression, name); if (def.params.length > 0) { const fixParam = (param: Node) => { return param; }; var variableDeclaration = VariableDeclaration( VariableDeclarator( { type: "ArrayPattern", elements: def.params.map(fixParam), }, Identifier(payloadArg) ) ); prepend(def.body, variableDeclaration); // replace params with random identifiers var args = [0, 1, 2].map((x) => this.getPlaceholder()); functionExpression.params = args.map((x) => Identifier(x)); var deadCode = choice(["fakeReturn", "ifStatement"]); switch (deadCode) { case "fakeReturn": // Dead code... var ifStatement = IfStatement( UnaryExpression("!", Identifier(args[0])), [ ReturnStatement( CallExpression(Identifier(args[1]), [ ThisExpression(), Identifier(args[2]), ]) ), ], null ); body.unshift(ifStatement); break; case "ifStatement": var test = LogicalExpression( "||", Identifier(args[0]), AssignmentExpression( "=", Identifier(args[1]), CallExpression(Identifier(args[2]), []) ) ); def.body = BlockStatement([ IfStatement(test, [...body], null), ReturnStatement(Identifier(args[1])), ]); break; } } // For logging purposes var signature = name + "(" + def.params.map((x) => x.name || "<>").join(",") + ")"; this.log("Added", signature); // delete ref in block if (defParents.length) { deleteDirect(def, defParents[0]); } this.addComment(functionExpression, signature); return Property( Literal(newFnNames[name]), functionExpression, false ); }) ) ) ); var getterArgName = this.getPlaceholder(); var x = this.getPlaceholder(); var y = this.getPlaceholder(); var z = this.getPlaceholder(); function getAccessor() { return MemberExpression(Identifier(mapName), Identifier(x), true); } // 2. define it var fn = FunctionDeclaration( dispatcherFnName, [Identifier(x), Identifier(y), Identifier(z)], [ // Define map of callable functions map, // Set returning variable to undefined VariableDeclaration(VariableDeclarator(returnProp)), // Arg to clear the payload IfStatement( BinaryExpression( "==", Identifier(y), Literal(expectedClearArgs) ), [ ExpressionStatement( AssignmentExpression( "=", Identifier(payloadArg), ArrayExpression([]) ) ), ], null ), // Arg to get a function reference IfStatement( BinaryExpression("==", Identifier(y), Literal(expectedGet)), [ // Getter flag: return the function object ExpressionStatement( AssignmentExpression( "=", Identifier(returnProp), LogicalExpression( "||", MemberExpression( Identifier(cacheName), Identifier(x), true ), AssignmentExpression( "=", MemberExpression( Identifier(cacheName), Identifier(x), true ), FunctionExpression( [RestElement(Identifier(getterArgName))], [ // Arg setter ExpressionStatement( AssignmentExpression( "=", Identifier(payloadArg), Identifier(getterArgName) ) ), // Call fn & return ReturnStatement( CallExpression( MemberExpression( getAccessor(), Identifier("call"), false ), [ThisExpression(), Literal(gen.generate())] ) ), ] ) ) ) ) ), ], [ // Call the function, return result ExpressionStatement( AssignmentExpression( "=", Identifier(returnProp), CallExpression(getAccessor(), [Literal(gen.generate())]) ) ), ] ), // Check how the function was invoked (new () vs ()) IfStatement( BinaryExpression("==", Identifier(z), Literal(expectedNew)), [ // Wrap in object ReturnStatement( ObjectExpression([ Property( Identifier(newReturnMemberName), Identifier(returnProp), false ), ]) ), ], [ // Return raw result ReturnStatement(Identifier(returnProp)), ] ), ] ); append(object, fn); if (payloadArg) { prepend( object, VariableDeclaration( VariableDeclarator(payloadArg, ArrayExpression([])) ) ); } identifiers.forEach(([o, p]) => { if (o.type != "Identifier") { return; } var newName = newFnNames[o.name]; if (!newName) { return; } if (!functionDeclarations[o.name]) { this.error(new Error("newName, missing function declaration")); } var info = getIdentifierInfo(o, p); if ( info.isFunctionCall && p[0].type == "CallExpression" && p[0].callee === o ) { // Invoking call expression: `a();` if (o.name == dispatcherFnName) { return; } this.log( `${o.name}(${p[0].arguments .map((_) => "<>") .join(",")}) -> ${dispatcherFnName}('${newName}')` ); var assignmentExpressions: Node[] = []; var dispatcherArgs: Node[] = [Literal(newName)]; if (p[0].arguments.length) { assignmentExpressions = [ AssignmentExpression( "=", Identifier(payloadArg), ArrayExpression(p[0].arguments) ), ]; } else { dispatcherArgs.push(Literal(expectedClearArgs)); } var type = choice(["CallExpression", "NewExpression"]); var callExpression = null; switch (type) { case "CallExpression": callExpression = CallExpression( Identifier(dispatcherFnName), dispatcherArgs ); break; case "NewExpression": if (dispatcherArgs.length == 1) { dispatcherArgs.push(Identifier("undefined")); } callExpression = MemberExpression( NewExpression(Identifier(dispatcherFnName), [ ...dispatcherArgs, Literal(expectedNew), ]), Identifier(newReturnMemberName), false ); break; } this.addComment( callExpression, "Calling " + o.name + "(" + p[0].arguments.map((x) => x.name).join(", ") + ")" ); var expr: Node = assignmentExpressions.length ? SequenceExpression([...assignmentExpressions, callExpression]) : callExpression; // Replace the parent call expression this.replace(p[0], expr); } else { // Non-invoking reference: `a` if (info.spec.isDefined) { if (info.isFunctionDeclaration) { this.log( "Skipped getter " + o.name + " (function declaration)" ); } else { this.log("Skipped getter " + o.name + " (defined)"); } return; } if (info.spec.isModified) { this.log("Skipped getter " + o.name + " (modified)"); return; } this.log( `(getter) ${o.name} -> ${dispatcherFnName}('${newName}')` ); this.replace( o, CallExpression(Identifier(dispatcherFnName), [ Literal(newName), Literal(expectedGet), ]) ); } }); prepend( object, VariableDeclaration( VariableDeclarator( Identifier(cacheName), Template(`Object.create(null)`).single().expression ) ) ); } } }; } }