UNPKG

js-confuser

Version:

JavaScript Obfuscation Tool.

431 lines (363 loc) 14.5 kB
import { PluginArg, PluginObject } from "./plugin"; import { NodePath } from "@babel/traverse"; import * as t from "@babel/types"; import Template from "../templates/template"; import { ok } from "assert"; import { chance, getRandomString } from "../utils/random-utils"; import { Order } from "../order"; import { NodeSymbol, PREDICTABLE, UNSAFE } from "../constants"; import { computeFunctionLength, isVariableFunctionIdentifier, } from "../utils/function-utils"; import { SetFunctionLengthTemplate } from "../templates/setFunctionLengthTemplate"; import { numericLiteral } from "../utils/node"; import { isStrictMode, isVariableIdentifier, prependProgram, } from "../utils/ast-utils"; export default ({ Plugin }: PluginArg): PluginObject => { const me = Plugin(Order.Dispatcher, { changeData: { functions: 0, }, }); let dispatcherCounter = 0; const setFunctionLength = me.getPlaceholder("d_fnLength"); // in Debug mode, function names are preserved const isDebug = false; return { visitor: { "Program|Function": { exit(_path) { const blockPath = _path as NodePath<t.Program | t.Function>; if (blockPath.isProgram()) { blockPath.scope.crawl(); // Don't insert function length code when disabled // Instead insert empty function as the identifier is still referenced var insertNode = t.functionDeclaration( t.identifier(setFunctionLength), [], t.blockStatement([]) ); if (me.options.preserveFunctionLength) { // Insert function length code insertNode = SetFunctionLengthTemplate.single({ fnName: setFunctionLength, }); } me.skip(prependProgram(_path, insertNode)); } if ((blockPath.node as NodeSymbol)[UNSAFE]) return; // For testing // if (!blockPath.isProgram()) return; var blockStatement: NodePath<t.Block> = blockPath.isProgram() ? blockPath : (blockPath.get("body") as NodePath<t.BlockStatement>); // Track functions and illegal ones // A function is illegal if: // - the function is async or generator // - the function is redefined // - the function uses 'this', 'eval', or 'arguments' var functionPaths = new Map< string, NodePath<t.FunctionDeclaration> >(); var illegalNames = new Set<string>(); // Scan for function declarations blockPath.traverse({ // Check for reassigned / redefined functions BindingIdentifier: { exit(path: NodePath<t.Identifier>) { if (!isVariableIdentifier(path)) return; const name = path.node.name; if (!path.parentPath?.isFunctionDeclaration()) { illegalNames.add(name); } }, }, // Find functions eligible for dispatching FunctionDeclaration: { exit(path: NodePath<t.FunctionDeclaration>) { const name = path.node.id.name; // If the function is not named, we can't dispatch it if (!name) { return; } // Do not apply to async or generator functions if (path.node.async || path.node.generator) { return; } // Do not apply to functions in nested scopes if (path.parentPath !== blockStatement || me.isSkipped(path)) { illegalNames.add(name); return; } if (isStrictMode(path)) { illegalNames.add(name); return; } // Do not apply to unsafe functions, redefined functions, or internal obfuscator functions if ( (path.node as NodeSymbol)[UNSAFE] || functionPaths.has(name) || me.obfuscator.isInternalVariable(name) ) { illegalNames.add(name); return; } // Functions with default parameters are not fully supported // (Could be a Function Expression referencing outer scope) if ( path.node.params.find((x) => x.type === "AssignmentPattern") ) { illegalNames.add(name); return; } functionPaths.set(name, path); }, }, }); for (let name of illegalNames) { functionPaths.delete(name); } for (var name of functionPaths.keys()) { if (!me.computeProbabilityMap(me.options.dispatcher, name)) { functionPaths.delete(name); } } // No functions here to change if (functionPaths.size === 0) { return; } me.changeData.functions += functionPaths.size; const dispatcherName = me.getPlaceholder() + "_dispatcher_" + dispatcherCounter++; const payloadName = me.getPlaceholder() + "_payload"; const cacheName = me.getPlaceholder() + "_cache"; const newNameMapping = new Map<string, string>(); const keys = { placeholderNoMeaning: isDebug ? "noMeaning" : getRandomString(10), clearPayload: isDebug ? "clearPayload" : getRandomString(10), nonCall: isDebug ? "nonCall" : getRandomString(10), returnAsObject: isDebug ? "returnAsObject" : getRandomString(10), returnAsObjectProperty: isDebug ? "returnAsObjectProperty" : getRandomString(10), }; for (var name of functionPaths.keys()) { newNameMapping.set( name, isDebug ? "_" + name : getRandomString(6) /** "_" + name */ ); } // Find identifiers calling/referencing the functions blockPath.traverse({ ReferencedIdentifier: { exit(path: NodePath<t.Identifier | t.JSXIdentifier>) { if (path.isJSX()) return; if (isVariableFunctionIdentifier(path)) return; const name = path.node.name; var fnPath = functionPaths.get(name); if (!fnPath) return; var newName = newNameMapping.get(name); // Do not replace if not referencing the actual function if (path.scope.getBinding(name).path !== fnPath) { return; } const createDispatcherCall = (name, flagArg?) => { var dispatcherArgs = [t.stringLiteral(name)]; if (flagArg) { dispatcherArgs.push(t.stringLiteral(flagArg)); } var asObject = chance(50); if (asObject) { if (dispatcherArgs.length < 2) { dispatcherArgs.push( t.stringLiteral(keys.placeholderNoMeaning) ); } dispatcherArgs.push(t.stringLiteral(keys.returnAsObject)); } var callExpression: t.CallExpression | t.NewExpression = t.callExpression( t.identifier(dispatcherName), dispatcherArgs ); if (!asObject) { return callExpression; } if (chance(50)) { (callExpression as t.Node).type = "NewExpression"; } return t.memberExpression( callExpression, t.stringLiteral(keys.returnAsObjectProperty), true ); }; // Replace the identifier with a call to the function const parentPath = path.parentPath; if (path.key === "callee" && parentPath?.isCallExpression()) { var expressions: t.Expression[] = []; var callArguments = parentPath.node.arguments; if (callArguments.length === 0) { expressions.push( // Call the function createDispatcherCall(newName, keys.clearPayload) ); } else { expressions.push( // Prepare the payload arguments t.assignmentExpression( "=", t.identifier(payloadName), t.arrayExpression(callArguments as t.Expression[]) ), // Call the function createDispatcherCall(newName) ); } const output = expressions.length === 1 ? expressions[0] : t.sequenceExpression(expressions); if (!parentPath.container) return; parentPath.replaceWith(output); } else { if (!path.container) return; // Replace non-invocation references with a 'cached' version of the function path.replaceWith(createDispatcherCall(newName, keys.nonCall)); } }, }, }); const fnLengthProperties = []; // Create the dispatcher function const objectExpression = t.objectExpression( Array.from(newNameMapping).map(([name, newName]) => { const originalPath = functionPaths.get(name); const originalFn = originalPath.node; if (me.options.preserveFunctionLength) { const fnLength = computeFunctionLength(originalPath); if (fnLength >= 1) { // 0 is already the default fnLengthProperties.push( t.objectProperty( t.stringLiteral(newName), numericLiteral(fnLength) ) ); } } const newBody = [...originalFn.body.body]; ok(Array.isArray(newBody)); // Unpack parameters if (originalFn.params.length > 0) { newBody.unshift( t.variableDeclaration("var", [ t.variableDeclarator( t.arrayPattern([...originalFn.params]), t.identifier(payloadName) ), ]) ); } // Add debug label if (isDebug) { newBody.unshift( t.expressionStatement( t.stringLiteral(`Dispatcher: ${name} -> ${newName}`) ) ); } const functionExpression = t.functionExpression( null, [], t.blockStatement(newBody) ); for (var symbol of Object.getOwnPropertySymbols(originalFn)) { (functionExpression as any)[symbol] = (originalFn as any)[ symbol ]; } (functionExpression as NodeSymbol)[PREDICTABLE] = true; return t.objectProperty( t.stringLiteral(newName), functionExpression ); }) ); const fnLengths = t.objectExpression(fnLengthProperties); const dispatcher = new Template(` function ${dispatcherName}(name, flagArg, returnTypeArg, fnLengths = {fnLengthsObjectExpression}) { var output; var fns = {objectExpression}; if(flagArg === "${keys.clearPayload}") { ${payloadName} = []; } if(flagArg === "${keys.nonCall}") { function createFunction(){ var fn = function(...args){ ${payloadName} = args; return fns[name].apply(this); } var fnLength = fnLengths[name]; if(fnLength) { ${setFunctionLength}(fn, fnLength); } return fn; } output = ${cacheName}[name] || (${cacheName}[name] = createFunction()); } else { output = fns[name](); } if(returnTypeArg === "${keys.returnAsObject}") { return { "${keys.returnAsObjectProperty}": output }; } else { return output; } } `).single({ objectExpression, fnLengthsObjectExpression: fnLengths, }); (dispatcher as NodeSymbol)[PREDICTABLE] = true; /** * Prepends the node into the block. (And registers the declaration) * @param node */ function prepend(node: t.Statement) { var newPath = blockStatement.unshiftContainer<any, any, any>( "body", node )[0]; blockStatement.scope.registerDeclaration(newPath); } // Insert the dispatcher function prepend(dispatcher); // Insert the payload variable prepend( t.variableDeclaration("var", [ t.variableDeclarator(t.identifier(payloadName)), ]) ); // Insert the cache variable prepend( t.variableDeclaration("var", [ t.variableDeclarator( t.identifier(cacheName), new Template(`Object["create"](null)`).expression() ), ]) ); // Remove original functions for (let path of functionPaths.values()) { path.remove(); } }, }, }, }; };