UNPKG

js-confuser

Version:

JavaScript Obfuscation Tool.

323 lines (277 loc) 10.3 kB
import { NodePath } from "@babel/traverse"; import { PluginArg, PluginObject } from "./plugin"; import { Order } from "../order"; import * as t from "@babel/types"; import Obfuscator from "../obfuscator"; import { append, getFunctionName, isDefiningIdentifier, isStrictMode, isVariableIdentifier, prepend, } from "../utils/ast-utils"; import { MULTI_TRANSFORM, NodeSymbol, PREDICTABLE, reservedIdentifiers, SKIP, UNSAFE, } from "../constants"; import { computeFunctionLength } from "../utils/function-utils"; import { numericLiteral } from "../utils/node"; import Template from "../templates/template"; import { createEvalIntegrityTemplate } from "../templates/tamperProtectionTemplates"; const RGF_ELIGIBLE = Symbol("rgfEligible"); /** * RGF (Runtime-Generated-Function) uses the `new Function("code")` syntax to create executable code from strings. * * Limitations: * * 1. Does not apply to async or generator functions * 2. Does not apply to functions that reference outside variables */ export default ({ Plugin }: PluginArg): PluginObject => { const me = Plugin(Order.RGF, { changeData: { functions: 0, }, }); const rgfArrayName = me.getPlaceholder() + "_rgf"; const rgfEvalName = me.getPlaceholder() + "_rgf_eval"; const rgfArrayExpression = t.arrayExpression([]); let active = true; return { visitor: { Program: { enter(path) { path.scope.crawl(); }, exit(path) { active = false; if (rgfArrayExpression.elements.length === 0) return; // Insert the RGF array at the top of the program prepend( path, t.variableDeclaration("var", [ t.variableDeclarator( t.identifier(rgfArrayName), rgfArrayExpression ), ]) ); var rgfEvalIntegrity = me.getPlaceholder() + "_rgf_eval_integrity"; prepend( path, new Template(` {EvalIntegrity} var ${rgfEvalIntegrity} = {EvalIntegrityName}(); `).compile({ EvalIntegrity: createEvalIntegrityTemplate(me, path), EvalIntegrityName: me.getPlaceholder(), }) ); append( path, new Template( ` function ${rgfEvalName}(code) { if (${rgfEvalIntegrity}) { return eval(code); } } ` ) .addSymbols(UNSAFE) .single() ); }, }, "FunctionDeclaration|FunctionExpression": { enter(_path) { if (!active) return; // On enter, determine if Function is eligible for RGF transformation const path = _path as NodePath< t.FunctionDeclaration | t.FunctionExpression >; if (me.isSkipped(path)) return; // Skip nested functions if the parent function is already deemed eligible if (path.find((p) => p.node[RGF_ELIGIBLE] || p.node[MULTI_TRANSFORM])) return; // Skip async and generator functions if (path.node.async || path.node.generator) return; const name = getFunctionName(path); if (name === me.options.lock?.countermeasures) return; if (me.obfuscator.isInternalVariable(name)) return; if ( !me.computeProbabilityMap( me.options.rgf, name, path.getFunctionParent() === null ) ) return; // Skip functions with references to outside variables // Check the scope to see if this function relies on any variables defined outside the function var identifierPreventingTransform: string; path.traverse({ Identifier(idPath) { if (!isVariableIdentifier(idPath)) return; if (idPath.isBindingIdentifier() && isDefiningIdentifier(idPath)) return; const { name } = idPath.node; // RGF array name is allowed, it is not considered an outside reference if (name === rgfArrayName) return; if (reservedIdentifiers.has(name)) return; if (me.options.globalVariables.has(name)) return; const binding = idPath.scope.getBinding(name); if (!binding) { // Global variables are allowed return; } var isOutsideVariable = path.scope.parent.getBinding(name) === binding; // If the binding is not in the current scope, it is an outside reference if (isOutsideVariable) { identifierPreventingTransform = name; idPath.stop(); } }, }); if (identifierPreventingTransform) { me.log( "Skipping function " + name + " due to reference to outside variable: " + identifierPreventingTransform ); return; } me.log("Function " + name + " is eligible for RGF transformation"); path.node[RGF_ELIGIBLE] = true; }, exit(_path) { if (!active) return; const path = _path as NodePath< t.FunctionDeclaration | t.FunctionExpression >; if (me.isSkipped(path)) return; // Function is not eligible for RGF transformation if (!path.node[RGF_ELIGIBLE]) return; const embeddedName = me.getPlaceholder() + "_embedded"; const replacementName = me.getPlaceholder() + "_replacement"; const argumentsName = me.getPlaceholder() + "_args"; const lastNode = t.expressionStatement(t.identifier(embeddedName)); (lastNode as NodeSymbol)[SKIP] = true; // Transform the function const evalProgram: t.Program = t.program([ t.functionDeclaration( t.identifier(embeddedName), [], t.blockStatement([ t.variableDeclaration("var", [ t.variableDeclarator( t.arrayPattern([ t.identifier(rgfArrayName), t.identifier(argumentsName), ]), t.identifier("arguments") ), ]), t.functionDeclaration( t.identifier(replacementName), path.node.params as (t.Identifier | t.Pattern)[], path.node.body ), t.returnStatement( t.callExpression( t.memberExpression( t.identifier(replacementName), t.identifier("apply") ), [t.thisExpression(), t.identifier(argumentsName)] ) ), ]) ), lastNode, ]); const strictModeEnforcingBlock = path.find((p) => isStrictMode(p)); if (strictModeEnforcingBlock) { // Preserve 'use strict' directive // This is necessary to enure subsequent transforms (Control Flow Flattening) are aware of the strict mode directive evalProgram.directives.push( t.directive(t.directiveLiteral("use strict")) ); } const evalFile = t.file(evalProgram); var newObfuscator = new Obfuscator(me.options, me.obfuscator); var hasRan = new Set( me.obfuscator.plugins .filter((plugin, i) => { return i <= me.obfuscator.index; }) .map((plugin) => plugin.pluginInstance.order) ); // Global Concealing will likely cause issues when Pack is also enabled const disallowedTransforms = new Set([Order.GlobalConcealing]); newObfuscator.plugins = newObfuscator.plugins.filter( ({ pluginInstance }) => { return ( (pluginInstance.order == Order.Preparation || !hasRan.has(pluginInstance.order)) && !disallowedTransforms.has(pluginInstance.order) ); } ); newObfuscator.obfuscateAST(evalFile); const generated = Obfuscator.generateCode(evalFile); var functionExpression = t.callExpression(t.identifier(rgfEvalName), [ t.stringLiteral(generated), ]); var index = rgfArrayExpression.elements.length; rgfArrayExpression.elements.push(functionExpression); // Params no longer needed, using 'arguments' instead const originalLength = computeFunctionLength(path); path.node.params = []; // Function is now unsafe (path.node as NodeSymbol)[UNSAFE] = true; // Params changed and using 'arguments' (path.node as NodeSymbol)[PREDICTABLE] = false; me.skip(path); // Update body to point to new function path .get("body") .replaceWith( t.blockStatement([ t.returnStatement( t.callExpression( t.memberExpression( t.memberExpression( t.identifier(rgfArrayName), numericLiteral(index), true ), t.stringLiteral("apply"), true ), [ t.thisExpression(), t.arrayExpression([ t.identifier(rgfArrayName), t.identifier("arguments"), ]), ] ) ), ]) ); path.skip(); me.setFunctionLength(path, originalLength); me.changeData.functions++; }, }, }, }; };