UNPKG

js-confuser

Version:

JavaScript Obfuscation Tool.

675 lines (575 loc) 16.8 kB
import * as t from "@babel/types"; import { NodePath } from "@babel/traverse"; import { ok } from "assert"; import { deepClone } from "./node"; export function getPatternIdentifierNames( path: NodePath | NodePath[] ): Set<string> { if (Array.isArray(path)) { var allNames = new Set<string>(); for (var p of path) { var names = getPatternIdentifierNames(p); for (var name of names) { allNames.add(name); } } return allNames; } var names = new Set<string>(); var functionParent = path.find((parent) => parent.isFunction()); path.traverse({ BindingIdentifier: (bindingPath) => { var bindingFunctionParent = bindingPath.find((parent) => parent.isFunction() ); if (functionParent === bindingFunctionParent) { names.add(bindingPath.node.name); } }, }); // Check if the path itself is a binding identifier if (path.isBindingIdentifier()) { names.add(path.node.name); } return names; } /** * Ensures a `String Literal` is 'computed' before replacing it with a more complex expression. * * ```js * // Input * { * "myToBeEncodedString": "value" * } * * // Output * { * ["myToBeEncodedString"]: "value" * } * ``` * @param path */ export function ensureComputedExpression(path: NodePath<t.Node>) { if ( (t.isObjectMember(path.parent) || t.isClassMethod(path.parent) || t.isClassProperty(path.parent)) && path.parent.key === path.node && !path.parent.computed ) { path.parent.computed = true; } } /** * Retrieves a function name from debugging purposes. * - Function Declaration / Expression * - Variable Declaration * - Object property / method * - Class property / method * - Program returns "[Program]" * - Default returns "anonymous" * @param path * @returns */ export function getFunctionName(path: NodePath<t.Function>): string { if (!path) return "null"; if (path.isProgram()) return "[Program]"; // Check function declaration/expression ID if ( (t.isFunctionDeclaration(path.node) || t.isFunctionExpression(path.node)) && path.node.id ) { return path.node.id.name; } // Check for containing variable declaration if ( path.parentPath?.isVariableDeclarator() && t.isIdentifier(path.parentPath.node.id) ) { return path.parentPath.node.id.name; } if (path.isObjectMethod() || path.isClassMethod()) { var property = getObjectPropertyAsString(path.node); if (property) return property; } // Check for containing property in an object if ( path.parentPath?.isObjectProperty() || path.parentPath?.isClassProperty() ) { var property = getObjectPropertyAsString(path.parentPath.node); if (property) return property; } var output = "anonymous"; if (path.isFunction()) { if (path.node.generator) { output += "*"; } else if (path.node.async) { output = "async " + output; } } return output; } export function isModuleImport(path: NodePath<t.StringLiteral>) { // Import Declaration if (path.parentPath.isImportDeclaration()) { return true; } // Dynamic Import / require() call if ( t.isCallExpression(path.parent) && (t.isIdentifier(path.parent.callee, { name: "require" }) || t.isImport(path.parent.callee)) && path.node === path.parent.arguments[0] ) { return true; } return false; } export function getBlock(path: NodePath) { return path.find((p) => p.isBlock()) as NodePath<t.Block>; } export function getParentFunctionOrProgram( path: NodePath ): NodePath<t.Function | t.Program> { if (path.isProgram()) return path; // Find the nearest function-like parent const functionOrProgramPath = path.findParent( (parentPath) => parentPath.isFunction() || parentPath.isProgram() ) as NodePath<t.Function | t.Program>; ok(functionOrProgramPath); return functionOrProgramPath; } export function getObjectPropertyAsString( property: t.ObjectMember | t.ClassProperty | t.ClassMethod ): string { ok( t.isObjectMember(property) || t.isClassProperty(property) || t.isClassMethod(property) ); if (!property.computed && t.isIdentifier(property.key)) { return property.key.name; } if (t.isStringLiteral(property.key)) { return property.key.value; } if (t.isNumericLiteral(property.key)) { return property.key.value.toString(); } return null; } /** * Gets the property of a MemberExpression as a string. * * @param memberPath - The path of the MemberExpression node. * @returns The property as a string or null if it cannot be determined. */ export function getMemberExpressionPropertyAsString( member: t.MemberExpression ): string | null { t.assertMemberExpression(member); const property = member.property; if (!member.computed && t.isIdentifier(property)) { return property.name; } if (t.isStringLiteral(property)) { return property.value; } if (t.isNumericLiteral(property)) { return property.value.toString(); } return null; // If the property cannot be determined } function registerPaths(paths: NodePath[]) { for (var path of paths) { if (path.isVariableDeclaration() && path.node.kind === "var") { getParentFunctionOrProgram(path).scope.registerDeclaration(path); } path.scope.registerDeclaration(path); } return paths; } function nodeListToNodes(nodesIn: (t.Statement | t.Statement[])[]) { var nodes: t.Statement[] = []; if (Array.isArray(nodesIn[0])) { ok(nodesIn.length === 1); nodes = nodesIn[0]; } else { nodes = nodesIn as t.Statement[]; } return nodes; } /** * Appends to the bottom of a block. Preserving last expression for the top level. */ export function append( path: NodePath, ...nodesIn: (t.Statement | t.Statement[])[] ) { var nodes = nodeListToNodes(nodesIn); var listParent = path.find( (p) => p.isFunction() || p.isBlock() || p.isSwitchCase() ); if (!listParent) { throw new Error("Could not find a suitable parent to prepend to"); } if (listParent.isProgram()) { var lastExpression = listParent.get("body").at(-1); if (lastExpression.isExpressionStatement()) { return registerPaths(lastExpression.insertBefore(nodes)); } } if (listParent.isSwitchCase()) { return registerPaths(listParent.pushContainer("consequent", nodes)); } if (listParent.isFunction()) { var body = listParent.get("body"); if (listParent.isArrowFunctionExpression() && listParent.node.expression) { if (!body.isBlockStatement()) { body.replaceWith( t.blockStatement([t.returnStatement(body.node as t.Expression)]) ); } } ok(body.isBlockStatement()); return registerPaths(body.pushContainer("body", nodes)); } ok(listParent.isBlock()); return registerPaths(listParent.pushContainer("body", nodes)); } /** * Prepends and registers a list of nodes to the beginning of a block. * * - Preserves import declarations by inserting after the last import declaration. * - Handles arrow functions * - Handles switch cases * @param path * @param nodes * @returns */ export function prepend( path: NodePath, ...nodesIn: (t.Statement | t.Statement[])[] ): NodePath[] { var nodes = nodeListToNodes(nodesIn); var listParent = path.find( (p) => p.isFunction() || p.isBlock() || p.isSwitchCase() ); if (!listParent) { throw new Error("Could not find a suitable parent to prepend to"); } if (listParent.isProgram()) { // Preserve import declarations // Filter out import declarations const body = listParent.get("body"); let afterImport = 0; for (var stmt of body) { if (!stmt.isImportDeclaration()) { break; } afterImport++; } if (afterImport === 0) { // No import declarations, so we can safely unshift everything return registerPaths(listParent.unshiftContainer("body", nodes)); } // Insert the nodes after the last import declaration return registerPaths(body[afterImport - 1].insertAfter(nodes)); } if (listParent.isFunction()) { var body = listParent.get("body"); if (listParent.isArrowFunctionExpression() && listParent.node.expression) { if (!body.isBlockStatement()) { body = body.replaceWith( t.blockStatement([t.returnStatement(body.node as t.Expression)]) )[0]; } } ok(body.isBlockStatement()); return registerPaths(body.unshiftContainer("body", nodes)); } if (listParent.isBlock()) { return registerPaths(listParent.unshiftContainer("body", nodes)); } if (listParent.isSwitchCase()) { return registerPaths(listParent.unshiftContainer("consequent", nodes)); } ok(false); } export function prependProgram( path: NodePath, ...nodes: (t.Statement | t.Statement[])[] ) { var program = path.find((p) => p.isProgram()); ok(program); ok(program.isProgram()); return prepend(program, ...nodes); } /** * A referenced or binding identifier, only names that reflect variables. * * - Excludes labels * * @param path * @returns */ export function isVariableIdentifier(path: NodePath<t.Identifier>) { if ( !path.isReferencedIdentifier() && !(path as NodePath).isBindingIdentifier() ) return false; // abc: {} // not a variable identifier if (path.key === "label" && path.parentPath?.isLabeledStatement()) return false; return true; } /** * Subset of BindingIdentifier, excluding non-defined assignment expressions. * * @example * var a = 1; // true * var {c} = {} // true * function b() {} // true * function d([e] = [], ...f) {} // true * * f = 0; // false * f(); // false * @param path * @returns */ export function isDefiningIdentifier(path: NodePath<t.Identifier>) { if (path.key === "id" && path.parentPath.isFunction()) return true; if (path.key === "id" && path.parentPath.isClassDeclaration) return true; if ( path.key === "local" && (path.parentPath.isImportSpecifier() || path.parentPath.isImportDefaultSpecifier() || path.parentPath.isImportNamespaceSpecifier()) ) return true; var maxTraversalPath = path.find( (p) => (p.key === "id" && p.parentPath?.isVariableDeclarator()) || (p.listKey === "params" && p.parentPath?.isFunction()) || (p.key === "param" && p.parentPath?.isCatchClause()) ); if (!maxTraversalPath) return false; var cursor: NodePath = path; while (cursor && cursor !== maxTraversalPath) { if ( cursor.parentPath.isObjectProperty() && cursor.parentPath.parentPath?.isObjectPattern() ) { if (cursor.key !== "value") { return false; } } else if (cursor.parentPath.isArrayPattern()) { if (cursor.listKey !== "elements") { return false; } } else if (cursor.parentPath.isRestElement()) { if (cursor.key !== "argument") { return false; } } else if (cursor.parentPath.isAssignmentPattern()) { if (cursor.key !== "left") { return false; } } else if (cursor.parentPath.isObjectPattern()) { } else return false; cursor = cursor.parentPath; } return true; } /** * @example * function id() {} // true * class id {} // true * var id; // false * @param path * @returns */ export function isStrictIdentifier(path: NodePath): boolean { if ( path.key === "id" && (path.parentPath.isFunction() || path.parentPath.isClass()) ) return true; return false; } export function isExportedIdentifier(path: NodePath<t.Identifier>) { // Check if the identifier is directly inside an ExportNamedDeclaration if (path.parentPath.isExportNamedDeclaration()) { return true; } // Check if the identifier is in an ExportDefaultDeclaration if (path.parentPath.isExportDefaultDeclaration()) { return true; } // Check if the identifier is within an ExportSpecifier if ( path.parentPath.isExportSpecifier() && path.parentPath.parentPath.isExportNamedDeclaration() ) { return true; } // Check if it's part of an exported variable declaration (e.g., export const a = 1;) if ( path.parentPath.isVariableDeclarator() && path.parentPath.parentPath.parentPath.isExportNamedDeclaration() ) { return true; } // Check if it's part of an exported function declaration (e.g., export function abc() {}) if ( (path.parentPath.isFunctionDeclaration() || path.parentPath.isClassDeclaration()) && path.parentPath.parentPath.isExportNamedDeclaration() ) { return true; } return false; } /** * @example * function abc() { * "use strict"; * } // true * @param path * @returns */ export function isStrictMode(path: NodePath) { // Classes are always in strict mode if (path.isClass()) return true; if (path.isBlock()) { if (path.isTSModuleBlock()) return false; return (path.node as t.BlockStatement | t.Program).directives.some( (directive) => directive.value.value === "use strict" ); } if (path.isFunction()) { const fnBody = path.get("body"); if (fnBody.isBlock()) { return isStrictMode(fnBody); } } return false; } /** * A modified identifier is an identifier that is assigned to or updated. * * - Assignment Expression * - Update Expression * * @param identifierPath */ export function isModifiedIdentifier(identifierPath: NodePath<t.Identifier>) { var isModification = false; if (identifierPath.parentPath.isUpdateExpression()) { isModification = true; } if ( identifierPath.find( (p) => p.key === "left" && p.parentPath?.isAssignmentExpression() ) ) { isModification = true; } return isModification; } export function replaceDefiningIdentifierToMemberExpression( path: NodePath<t.Identifier>, memberExpression: t.MemberExpression ) { // function id(){} -> var id = function() {} if (path.key === "id" && path.parentPath.isFunctionDeclaration()) { var asFunctionExpression = deepClone( path.parentPath.node ) as t.Node as t.FunctionExpression; asFunctionExpression.type = "FunctionExpression"; path.parentPath.replaceWith( t.expressionStatement( t.assignmentExpression("=", memberExpression, asFunctionExpression) ) ); return; } // class id{} -> var id = class {} if (path.key === "id" && path.parentPath.isClassDeclaration()) { var asClassExpression = deepClone( path.parentPath.node ) as t.Node as t.ClassExpression; asClassExpression.type = "ClassExpression"; path.parentPath.replaceWith( t.expressionStatement( t.assignmentExpression("=", memberExpression, asClassExpression) ) ); return; } // var id = 1 -> id = 1 var variableDeclaratorChild = path.find( (p) => p.key === "id" && p.parentPath?.isVariableDeclarator() && p.parentPath?.parentPath?.isVariableDeclaration() ) as NodePath<t.VariableDeclarator["id"]>; if (variableDeclaratorChild) { var variableDeclarator = variableDeclaratorChild.parentPath as NodePath<t.VariableDeclarator>; var variableDeclaration = variableDeclarator.parentPath as NodePath<t.VariableDeclaration>; if (variableDeclaration.type === "VariableDeclaration") { ok( variableDeclaration.node.declarations.length === 1, "Multiple declarations not supported" ); } const id = variableDeclarator.get("id"); const init = variableDeclarator.get("init"); var newExpression: t.Node = id.node; var isForInitializer = (variableDeclaration.key === "init" || variableDeclaration.key === "left") && variableDeclaration.parentPath.isFor(); if (init.node || !isForInitializer) { newExpression = t.assignmentExpression( "=", id.node, init.node || t.identifier("undefined") ); } if (!isForInitializer) { newExpression = t.expressionStatement(newExpression as t.Expression); } path.replaceWith(memberExpression); if (variableDeclaration.isVariableDeclaration()) { variableDeclaration.replaceWith(newExpression); } return; } // Safely replace the identifier with the member expression // ensureComputedExpression(path); // path.replaceWith(memberExpression); } /** * @example * undefined // true * void 0 // true */ export function isUndefined(path: NodePath) { if (path.isIdentifier() && path.node.name === "undefined") { return true; } if ( path.isUnaryExpression() && path.node.operator === "void" && path.node.argument.type === "NumericLiteral" && path.node.argument.value === 0 ) { return true; } return false; }