UNPKG

webcrack

Version:

Deobfuscate, unminify and unpack bundled javascript

1,522 lines (1,495 loc) 150 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default")); // src/index.ts import { parse as parse2 } from "@babel/parser"; import * as m52 from "@codemod/matchers"; import debug5 from "debug"; import { join as join3, normalize as normalize2 } from "node:path"; // src/ast-utils/ast.ts import * as t from "@babel/types"; function getPropName(node) { if (t.isIdentifier(node)) { return node.name; } if (t.isStringLiteral(node)) { return node.value; } if (t.isNumericLiteral(node)) { return node.value.toString(); } } // babel-import:@babel/generator var generator_exports = {}; __export(generator_exports, { default: () => generator_default }); __reExport(generator_exports, lib_star); import module from "@babel/generator/lib/index.js"; import * as lib_star from "@babel/generator/lib/index.js"; var generator_default = module.default ?? module; // src/ast-utils/generator.ts var defaultOptions = { jsescOption: { minimal: true } }; function generate(ast, options = defaultOptions) { return generator_default(ast, options).code; } function codePreview(node) { const code = generate(node, { minified: true, shouldPrintComment: () => false, ...defaultOptions }); if (code.length > 100) { return code.slice(0, 70) + " \u2026 " + code.slice(-30); } return code; } // babel-import:@babel/traverse var traverse_exports = {}; __export(traverse_exports, { default: () => traverse_default }); __reExport(traverse_exports, lib_star2); import module2 from "@babel/traverse/lib/index.js"; import * as lib_star2 from "@babel/traverse/lib/index.js"; var traverse_default = module2.default ?? module2; // src/ast-utils/inline.ts import * as t3 from "@babel/types"; import * as m2 from "@codemod/matchers"; // src/ast-utils/matcher.ts import * as t2 from "@babel/types"; import * as m from "@codemod/matchers"; var safeLiteral = m.matcher( (node) => t2.isLiteral(node) && (!t2.isTemplateLiteral(node) || node.expressions.length === 0) ); function infiniteLoop(body) { return m.or( m.forStatement(void 0, null, void 0, body), m.forStatement(void 0, truthyMatcher, void 0, body), m.whileStatement(truthyMatcher, body) ); } function constKey(name) { return m.or(m.identifier(name), m.stringLiteral(name)); } function constObjectProperty(value) { return m.or( m.objectProperty(m.identifier(), value, false), m.objectProperty(m.or(m.stringLiteral(), m.numericLiteral()), value) ); } function anonymousFunction(params, body) { return m.or( m.functionExpression(null, params, body, false), m.arrowFunctionExpression(params, body) ); } function iife(params, body) { return m.callExpression(anonymousFunction(params, body)); } function constMemberExpression(object, property) { if (typeof object === "string") object = m.identifier(object); return m.or( m.memberExpression(object, m.identifier(property), false), m.memberExpression(object, m.stringLiteral(property), true) ); } var undefinedMatcher = m.or( m.identifier("undefined"), m.unaryExpression("void", m.numericLiteral(0)) ); var trueMatcher = m.or( m.booleanLiteral(true), m.unaryExpression("!", m.numericLiteral(0)), m.unaryExpression("!", m.unaryExpression("!", m.numericLiteral(1))), m.unaryExpression("!", m.unaryExpression("!", m.arrayExpression([]))) ); var falseMatcher = m.or( m.booleanLiteral(false), m.unaryExpression("!", m.arrayExpression([])) ); var truthyMatcher = m.or(trueMatcher, m.arrayExpression([])); function findParent(path, matcher16) { return path.findParent( (path2) => matcher16.match(path2.node) ); } function findPath(path, matcher16) { return path.find((path2) => matcher16.match(path2.node)); } function createFunctionMatcher(params, body) { const captures = Array.from( { length: params }, () => m.capture(m.anyString()) ); return m.functionExpression( void 0, captures.map(m.identifier), m.blockStatement( body(...captures.map((c) => m.identifier(m.fromCapture(c)))) ) ); } function isReadonlyObject(binding, memberAccess) { if (!binding.constant && binding.constantViolations[0] !== binding.path) return false; function isPatternAssignment(member) { const { parentPath } = member; return ( // [obj.property] = [1]; parentPath?.isArrayPattern() || // ({ property: obj.property } = {}) // ({ ...obj.property } = {}) parentPath?.parentPath?.isObjectPattern() && (parentPath.isObjectProperty({ value: member.node }) || parentPath.isRestElement()) || // ([obj.property = 1] = []) // ({ property: obj.property = 1 } = {}) parentPath?.isAssignmentPattern({ left: member.node }) ); } return binding.referencePaths.every( (path) => ( // obj.property memberAccess.match(path.parent) && // obj.property = 1 !path.parentPath?.parentPath?.isAssignmentExpression({ left: path.parent }) && // obj.property++ !path.parentPath?.parentPath?.isUpdateExpression({ argument: path.parent }) && // delete obj.property !path.parentPath?.parentPath?.isUnaryExpression({ argument: path.parent, operator: "delete" }) && !isPatternAssignment(path.parentPath) ) ); } function isTemporaryVariable(binding, references, kind = "var") { return binding !== void 0 && binding.references === references && binding.constantViolations.length === 1 && (kind === "var" ? binding.path.isVariableDeclarator() && binding.path.node.init === null : binding.path.listKey === "params" && binding.path.isIdentifier()); } var AnySubListMatcher = class extends m.Matcher { constructor(matchers) { super(); this.matchers = matchers; } matchers; matchValue(array, keys) { if (!Array.isArray(array)) return false; if (this.matchers.length === 0 && array.length === 0) return true; let j = 0; for (let i = 0; i < array.length; i++) { const matches = this.matchers[j].matchValue(array[i], [...keys, i]); if (matches) { j++; if (j === this.matchers.length) { return true; } } } return false; } }; function anySubList(...elements) { return new AnySubListMatcher(elements); } // src/ast-utils/inline.ts function inlineVariable(binding, value = m2.anyExpression(), unsafeAssignments = false) { const varDeclarator = binding.path.node; const varMatcher = m2.variableDeclarator( m2.identifier(binding.identifier.name), value ); const assignmentMatcher = m2.assignmentExpression( "=", m2.identifier(binding.identifier.name), value ); if (binding.constant && varMatcher.match(varDeclarator)) { binding.referencePaths.forEach((ref) => { ref.replaceWith(varDeclarator.init); }); binding.path.remove(); } else if (unsafeAssignments && binding.constantViolations.length >= 1) { let getNearestAssignment2 = function(location) { return assignments.findLast((assignment) => assignment.start < location); }; var getNearestAssignment = getNearestAssignment2; const assignments = binding.constantViolations.map((path) => path.node).filter((node) => assignmentMatcher.match(node)); if (!assignments.length) return; for (const ref of binding.referencePaths) { const assignment = getNearestAssignment2(ref.node.start); if (assignment) ref.replaceWith(assignment.right); } for (const path of binding.constantViolations) { if (path.parentPath?.isExpressionStatement()) { path.remove(); } else if (path.isAssignmentExpression()) { path.replaceWith(path.node.right); } } binding.path.remove(); } } function inlineArrayElements(array, references) { for (const reference of references) { const memberPath = reference.parentPath; const property = memberPath.node.property; const index = property.value; const replacement = array.elements[index]; memberPath.replaceWith(t3.cloneNode(replacement)); } } function inlineObjectProperties(binding, property = m2.objectProperty()) { const varDeclarator = binding.path.node; const objectProperties = m2.capture(m2.arrayOf(property)); const varMatcher = m2.variableDeclarator( m2.identifier(binding.identifier.name), m2.objectExpression(objectProperties) ); if (!varMatcher.match(varDeclarator)) return; const propertyMap = new Map( objectProperties.current.map((p) => [getPropName(p.key), p.value]) ); if (!binding.referencePaths.every((ref) => { const member = ref.parent; const propName = getPropName(member.property); return propertyMap.has(propName); })) return; binding.referencePaths.forEach((ref) => { const memberPath = ref.parentPath; const propName = getPropName(memberPath.node.property); const value = propertyMap.get(propName); memberPath.replaceWith(value); }); binding.path.remove(); } function inlineFunctionCall(fn, caller) { if (t3.isRestElement(fn.params[1])) { caller.replaceWith( t3.callExpression( caller.node.arguments[0], caller.node.arguments.slice(1) ) ); return; } const returnedValue = fn.body.body[0].argument; const clone = t3.cloneNode(returnedValue, true); traverse_default(clone, { Identifier(path) { const paramIndex = fn.params.findIndex( (p) => p.name === path.node.name ); if (paramIndex !== -1) { path.replaceWith( caller.node.arguments[paramIndex] ?? t3.unaryExpression("void", t3.numericLiteral(0)) ); path.skip(); } }, noScope: true }); caller.replaceWith(clone); } function inlineFunctionAliases(binding) { const state = { changes: 0 }; const refs = [...binding.referencePaths]; for (const ref of refs) { const fn = findParent(ref, m2.functionDeclaration()); const fnName = m2.capture(m2.anyString()); const returnedCall = m2.capture( m2.callExpression( m2.identifier(binding.identifier.name), m2.anyList(m2.slice({ min: 2 })) ) ); const matcher16 = m2.functionDeclaration( m2.identifier(fnName), m2.anyList(m2.slice({ min: 2 })), m2.blockStatement([m2.returnStatement(returnedCall)]) ); if (fn && matcher16.match(fn.node)) { const paramUsedInDecodeCall = fn.node.params.some((param) => { const binding2 = fn.scope.getBinding(param.name); return binding2?.referencePaths.some( (ref2) => ref2.findParent((p) => p.node === returnedCall.current) ); }); if (!paramUsedInDecodeCall) continue; const fnBinding = fn.scope.parent.getBinding(fnName.current); if (!fnBinding) continue; const fnRefs = fnBinding.referencePaths; refs.push(...fnRefs); const callRefs = fnRefs.filter( (ref2) => t3.isCallExpression(ref2.parent) && t3.isIdentifier(ref2.parent.callee, { name: fnName.current }) ).map((ref2) => ref2.parentPath); for (const callRef of callRefs) { inlineFunctionCall(fn.node, callRef); state.changes++; } fn.remove(); state.changes++; } } binding.scope.crawl(); return state; } function inlineVariableAliases(binding, targetName = binding.identifier.name) { const state = { changes: 0 }; const refs = [...binding.referencePaths]; const varName = m2.capture(m2.anyString()); const matcher16 = m2.or( m2.variableDeclarator( m2.identifier(varName), m2.identifier(binding.identifier.name) ), m2.assignmentExpression( "=", m2.identifier(varName), m2.identifier(binding.identifier.name) ) ); for (const ref of refs) { if (matcher16.match(ref.parent)) { const varScope = ref.scope; const varBinding = varScope.getBinding(varName.current); if (!varBinding) continue; if (ref.isIdentifier({ name: varBinding.identifier.name })) continue; state.changes += inlineVariableAliases(varBinding, targetName).changes; if (ref.parentPath?.isAssignmentExpression()) { varBinding.path.remove(); if (t3.isExpressionStatement(ref.parentPath.parent)) { ref.parentPath.remove(); } else { ref.parentPath.replaceWith(t3.identifier(targetName)); } } else if (ref.parentPath?.isVariableDeclarator()) { ref.parentPath.remove(); } state.changes++; } else { ref.replaceWith(t3.identifier(targetName)); state.changes++; } } return state; } // src/ast-utils/rename.ts import * as t4 from "@babel/types"; import * as m3 from "@codemod/matchers"; function renameFast(binding, newName) { binding.referencePaths.forEach((ref) => { if (ref.isExportDefaultDeclaration()) return; if (!ref.isIdentifier()) { throw new Error( `Unexpected reference (${ref.type}): ${codePreview(ref.node)}` ); } if (ref.scope.hasBinding(newName)) ref.scope.rename(newName); ref.node.name = newName; }); const patternMatcher = m3.assignmentExpression( "=", m3.or(m3.arrayPattern(), m3.objectPattern()) ); binding.constantViolations.forEach((ref) => { if (ref.scope.hasBinding(newName)) ref.scope.rename(newName); if (ref.isAssignmentExpression() && t4.isIdentifier(ref.node.left)) { ref.node.left.name = newName; } else if (ref.isUpdateExpression() && t4.isIdentifier(ref.node.argument)) { ref.node.argument.name = newName; } else if (ref.isUnaryExpression({ operator: "delete" }) && t4.isIdentifier(ref.node.argument)) { ref.node.argument.name = newName; } else if (ref.isVariableDeclarator() && t4.isIdentifier(ref.node.id)) { ref.node.id.name = newName; } else if (ref.isVariableDeclarator() && t4.isArrayPattern(ref.node.id)) { const ids = ref.getBindingIdentifiers(); for (const id in ids) { if (id === binding.identifier.name) { ids[id].name = newName; } } } else if (ref.isFor() || patternMatcher.match(ref.node)) { traverse_default(ref.node, { Identifier(path) { if (path.scope !== ref.scope) return path.skip(); if (path.node.name === binding.identifier.name) { path.node.name = newName; } }, noScope: true }); } else if (ref.isFunctionDeclaration() && t4.isIdentifier(ref.node.id)) { ref.node.id.name = newName; } else { throw new Error( `Unexpected constant violation (${ref.type}): ${codePreview(ref.node)}` ); } }); binding.scope.removeOwnBinding(binding.identifier.name); binding.scope.bindings[newName] = binding; binding.identifier.name = newName; } function renameParameters(path, newNames) { const params = path.node.params; for (let i = 0; i < Math.min(params.length, newNames.length); i++) { const binding = path.scope.getBinding(params[i].name); renameFast(binding, newNames[i]); } } // src/ast-utils/transform.ts import debug from "debug"; var logger = debug("webcrack:transforms"); async function applyTransformAsync(ast, transform, options) { logger(`${transform.name}: started`); const state = { changes: 0 }; await transform.run?.(ast, state, options); if (transform.visitor) traverse_default(ast, transform.visitor(options), void 0, state); logger(`${transform.name}: finished with ${state.changes} changes`); return state; } function applyTransform(ast, transform, options) { logger(`${transform.name}: started`); const state = { changes: 0 }; transform.run?.(ast, state, options); if (transform.visitor) { const visitor = transform.visitor( options ); visitor.noScope = !transform.scope; traverse_default(ast, visitor, void 0, state); } logger(`${transform.name}: finished with ${state.changes} changes`); return state; } function applyTransforms(ast, transforms, options = {}) { options.log ??= true; const name = options.name ?? transforms.map((t45) => t45.name).join(", "); if (options.log) logger(`${name}: started`); const state = { changes: 0 }; for (const transform of transforms) { transform.run?.(ast, state); } const traverseOptions = transforms.flatMap((t45) => t45.visitor?.() ?? []); if (traverseOptions.length > 0) { const visitor = traverse_exports.visitors.merge(traverseOptions); visitor.noScope = options.noScope || transforms.every((t45) => !t45.scope); traverse_default(ast, visitor, void 0, state); } if (options.log) logger(`${name}: finished with ${state.changes} changes`); return state; } function mergeTransforms(options) { return { name: options.name, tags: options.tags, scope: options.transforms.some((t45) => t45.scope), visitor() { return traverse_exports.visitors.merge( options.transforms.flatMap((t45) => t45.visitor?.() ?? []) ); } }; } // src/deobfuscate/index.ts import debug3 from "debug"; // src/unminify/transforms/merge-strings.ts import * as m4 from "@codemod/matchers"; var merge_strings_default = { name: "merge-strings", tags: ["safe"], visitor() { const left = m4.capture(m4.stringLiteral()); const right = m4.capture(m4.stringLiteral()); const matcher16 = m4.binaryExpression( "+", m4.or(left, m4.binaryExpression("+", m4.anything(), left)), right ); return { BinaryExpression: { exit(path) { if (!matcher16.match(path.node)) return; left.current.value += right.current.value; right.current.value = ""; path.replaceWith(path.node.left); path.skip(); this.changes++; } } }; } }; // src/deobfuscate/array-rotator.ts import * as m5 from "@codemod/matchers"; import { callExpression as callExpression5 } from "@codemod/matchers"; function findArrayRotator(stringArray) { const arrayIdentifier = m5.capture(m5.identifier()); const pushShift = m5.callExpression( constMemberExpression(arrayIdentifier, "push"), [ m5.callExpression( constMemberExpression(m5.fromCapture(arrayIdentifier), "shift") ) ] ); const callMatcher = iife( m5.anything(), m5.blockStatement( m5.anyList( m5.zeroOrMore(), infiniteLoop( m5.matcher((node) => { return m5.containerOf(callExpression5(m5.identifier("parseInt"))).match(node) && m5.blockStatement([ m5.tryStatement( m5.containerOf(pushShift), m5.containerOf(pushShift) ) ]).match(node); }) ) ) ) ); const matcher16 = m5.expressionStatement( m5.or(callMatcher, m5.unaryExpression("!", callMatcher)) ); for (const ref of stringArray.references) { const rotator = findParent(ref, matcher16); if (rotator) { return rotator; } } } // src/deobfuscate/control-flow-object.ts import * as t5 from "@babel/types"; import * as m6 from "@codemod/matchers"; var control_flow_object_default = { name: "control-flow-object", tags: ["safe"], scope: true, visitor() { const varId = m6.capture(m6.identifier()); const propertyName = m6.matcher((name) => /^[a-z]{5}$/i.test(name)); const propertyKey = constKey(propertyName); const propertyValue = m6.or( // E.g. "6|0|4|3|1|5|2" m6.stringLiteral(), // E.g. function (a, b) { return a + b } createFunctionMatcher(2, (left, right) => [ m6.returnStatement( m6.or( m6.binaryExpression(void 0, left, right), m6.logicalExpression(void 0, left, right), m6.binaryExpression(void 0, right, left), m6.logicalExpression(void 0, right, left) ) ) ]), // E.g. function (a, b, c) { return a(b, c) } with an arbitrary number of arguments m6.matcher((node) => { return t5.isFunctionExpression(node) && createFunctionMatcher(node.params.length, (...params) => [ m6.returnStatement(m6.callExpression(params[0], params.slice(1))) ]).match(node); }), // E.g. function (a, ...b) { return a(...b) } (() => { const fnName = m6.capture(m6.identifier()); const restName = m6.capture(m6.identifier()); return m6.functionExpression( void 0, [fnName, m6.restElement(restName)], m6.blockStatement([ m6.returnStatement( m6.callExpression(m6.fromCapture(fnName), [ m6.spreadElement(m6.fromCapture(restName)) ]) ) ]) ); })() ); const objectProperties = m6.capture( m6.arrayOf(m6.objectProperty(propertyKey, propertyValue)) ); const aliasId = m6.capture(m6.identifier()); const aliasVar = m6.variableDeclaration(m6.anything(), [ m6.variableDeclarator(aliasId, m6.fromCapture(varId)) ]); const assignedKey = m6.capture(propertyName); const assignedValue = m6.capture(propertyValue); const assignment = m6.expressionStatement( m6.assignmentExpression( "=", constMemberExpression(m6.fromCapture(varId), assignedKey), assignedValue ) ); const looseAssignment = m6.expressionStatement( m6.assignmentExpression( "=", constMemberExpression(m6.fromCapture(varId), assignedKey) ) ); const memberAccess = constMemberExpression( m6.or(m6.fromCapture(varId), m6.fromCapture(aliasId)), propertyName ); const varMatcher = m6.variableDeclarator( varId, m6.objectExpression(objectProperties) ); const inlineMatcher = constMemberExpression( m6.objectExpression(objectProperties), propertyName ); function isConstantBinding(binding) { return binding.constant || binding.constantViolations[0] === binding.path; } function transform(path) { let changes = 0; if (varMatcher.match(path.node)) { const binding = path.scope.getBinding(varId.current.name); if (!binding) return changes; if (!isConstantBinding(binding)) return changes; if (!transformObjectKeys(binding)) return changes; if (!isReadonlyObject(binding, memberAccess)) return changes; const props = new Map( objectProperties.current.map((p) => [ getPropName(p.key), p.value ]) ); if (!props.size) return changes; const oldRefs = [...binding.referencePaths]; [...binding.referencePaths].reverse().forEach((ref) => { const memberPath = ref.parentPath; const propName = getPropName(memberPath.node.property); const value = props.get(propName); if (!value) { ref.addComment("leading", "webcrack:control_flow_missing_prop"); return; } if (t5.isStringLiteral(value)) { memberPath.replaceWith(value); } else { inlineFunctionCall( value, memberPath.parentPath ); } changes++; }); oldRefs.forEach((ref) => { const varDeclarator = findParent(ref, m6.variableDeclarator()); if (varDeclarator) changes += transform(varDeclarator); }); path.remove(); changes++; } return changes; } function transformObjectKeys(objBinding) { const container = objBinding.path.parentPath.container; const startIndex = objBinding.path.parentPath.key + 1; const properties = []; for (let i = startIndex; i < container.length; i++) { const statement5 = container[i]; if (looseAssignment.match(statement5)) { applyTransform(statement5, merge_strings_default); } if (assignment.match(statement5)) { properties.push( t5.objectProperty( t5.identifier(assignedKey.current), assignedValue.current ) ); } else { break; } } const aliasAssignment = container[startIndex + properties.length]; if (!aliasVar.match(aliasAssignment)) return true; if (objBinding.references !== properties.length + 1) return false; const aliasBinding = objBinding.scope.getBinding(aliasId.current.name); if (!isReadonlyObject(aliasBinding, memberAccess)) return false; objectProperties.current.push(...properties); container.splice(startIndex, properties.length); objBinding.referencePaths = aliasBinding.referencePaths; objBinding.references = aliasBinding.references; objBinding.identifier.name = aliasBinding.identifier.name; aliasBinding.path.remove(); return true; } return { VariableDeclarator: { exit(path) { this.changes += transform(path); } }, MemberExpression: { exit(path) { if (!inlineMatcher.match(path.node)) return; const propName = getPropName(path.node.property); const value = objectProperties.current.find( (prop) => getPropName(prop.key) === propName )?.value; if (!value) return; if (t5.isStringLiteral(value)) { path.replaceWith(value); } else if (path.parentPath.isCallExpression()) { inlineFunctionCall(value, path.parentPath); } else { path.replaceWith(value); } this.changes++; } } }; } }; // src/deobfuscate/control-flow-switch.ts import * as t6 from "@babel/types"; import * as m7 from "@codemod/matchers"; var control_flow_switch_default = { name: "control-flow-switch", tags: ["safe"], visitor() { const sequenceName = m7.capture(m7.identifier()); const sequenceString = m7.capture( m7.matcher((s) => /^\d+(\|\d+)*$/.test(s)) ); const iterator = m7.capture(m7.identifier()); const cases = m7.capture( m7.arrayOf( m7.switchCase( m7.stringLiteral(m7.matcher((s) => /^\d+$/.test(s))), m7.anyList( m7.zeroOrMore(), m7.or(m7.continueStatement(), m7.returnStatement()) ) ) ) ); const matcher16 = m7.blockStatement( m7.anyList( // E.g. const sequence = "2|4|3|0|1".split("|") m7.variableDeclaration(void 0, [ m7.variableDeclarator( sequenceName, m7.callExpression( constMemberExpression(m7.stringLiteral(sequenceString), "split"), [m7.stringLiteral("|")] ) ) ]), // E.g. let iterator = 0 or -0x1a70 + 0x93d + 0x275 * 0x7 m7.variableDeclaration(void 0, [m7.variableDeclarator(iterator)]), infiniteLoop( m7.blockStatement([ m7.switchStatement( // E.g. switch (sequence[iterator++]) { m7.memberExpression( m7.fromCapture(sequenceName), m7.updateExpression("++", m7.fromCapture(iterator)), true ), cases ), m7.breakStatement() ]) ), m7.zeroOrMore() ) ); return { BlockStatement: { exit(path) { if (!matcher16.match(path.node)) return; const caseStatements = new Map( cases.current.map((c) => [ c.test.value, t6.isContinueStatement(c.consequent.at(-1)) ? c.consequent.slice(0, -1) : c.consequent ]) ); const sequence = sequenceString.current.split("|"); const newStatements = sequence.flatMap((s) => caseStatements.get(s)); path.node.body.splice(0, 3, ...newStatements); this.changes += newStatements.length + 3; } } }; } }; // src/deobfuscate/dead-code.ts import * as t7 from "@babel/types"; import * as m8 from "@codemod/matchers"; var dead_code_default = { name: "dead-code", tags: ["unsafe"], scope: true, visitor() { const stringComparison = m8.binaryExpression( m8.or("===", "==", "!==", "!="), m8.stringLiteral(), m8.stringLiteral() ); const testMatcher = m8.or( stringComparison, m8.unaryExpression("!", stringComparison) ); return { "IfStatement|ConditionalExpression": { exit(_path) { const path = _path; if (!testMatcher.match(path.node.test)) return; if (path.get("test").evaluateTruthy()) { replace(path, path.get("consequent")); } else if (path.node.alternate) { replace(path, path.get("alternate")); } else { path.remove(); } this.changes++; } } }; } }; function replace(path, replacement) { if (t7.isBlockStatement(replacement.node)) { const childBindings = replacement.scope.bindings; for (const name in childBindings) { const binding = childBindings[name]; if (path.scope.hasOwnBinding(name)) { renameFast(binding, path.scope.generateUid(name)); } binding.scope = path.scope; path.scope.bindings[binding.identifier.name] = binding; } path.replaceWithMultiple(replacement.node.body); } else { path.replaceWith(replacement); } } // src/deobfuscate/decoder.ts import { expression } from "@babel/template"; import * as m9 from "@codemod/matchers"; var Decoder = class { originalName; name; path; constructor(originalName, name, path) { this.originalName = originalName; this.name = name; this.path = path; } collectCalls() { const calls = []; const literalArgument = m9.or( m9.binaryExpression( m9.anything(), m9.matcher((node) => literalArgument.match(node)), m9.matcher((node) => literalArgument.match(node)) ), m9.unaryExpression( "-", m9.matcher((node) => literalArgument.match(node)) ), m9.numericLiteral(), m9.stringLiteral() ); const literalCall = m9.callExpression( m9.identifier(this.name), m9.arrayOf(literalArgument) ); const expressionCall = m9.callExpression( m9.identifier(this.name), m9.arrayOf(m9.anyExpression()) ); const conditional = m9.capture(m9.conditionalExpression()); const conditionalCall = m9.callExpression(m9.identifier(this.name), [ conditional ]); const buildExtractedConditional = expression`TEST ? CALLEE(CONSEQUENT) : CALLEE(ALTERNATE)`; const binding = this.path.scope.getBinding(this.name); for (const ref of binding.referencePaths) { if (conditionalCall.match(ref.parent)) { const [replacement] = ref.parentPath.replaceWith( buildExtractedConditional({ TEST: conditional.current.test, CALLEE: ref.parent.callee, CONSEQUENT: conditional.current.consequent, ALTERNATE: conditional.current.alternate }) ); replacement.scope.crawl(); } else if (literalCall.match(ref.parent)) { calls.push(ref.parentPath); } else if (expressionCall.match(ref.parent)) { ref.parentPath.traverse({ ReferencedIdentifier(path) { const varBinding = path.scope.getBinding(path.node.name); if (!varBinding) return; inlineVariable(varBinding, literalArgument, true); } }); if (literalCall.match(ref.parent)) { calls.push(ref.parentPath); } } else if (ref.parentPath?.isExpressionStatement()) { ref.parentPath.remove(); } } return calls; } }; function findDecoders(stringArray) { const decoders = []; const functionName = m9.capture(m9.anyString()); const arrayIdentifier = m9.capture(m9.identifier()); const matcher16 = m9.functionDeclaration( m9.identifier(functionName), m9.anything(), m9.blockStatement( anySubList( // var array = getStringArray(); m9.variableDeclaration(void 0, [ m9.variableDeclarator( arrayIdentifier, m9.callExpression(m9.identifier(stringArray.name)) ) ]), // var h = array[e]; return h; // or return array[e -= 254]; m9.containerOf( m9.memberExpression(m9.fromCapture(arrayIdentifier), void 0, true) ) ) ) ); for (const ref of stringArray.references) { const decoderFn = findParent(ref, matcher16); if (decoderFn) { const oldName = functionName.current; const newName = `__DECODE_${decoders.length}__`; const binding = decoderFn.scope.getBinding(oldName); renameFast(binding, newName); decoders.push(new Decoder(oldName, newName, decoderFn)); } } return decoders; } // src/deobfuscate/inline-decoded-strings.ts import * as t8 from "@babel/types"; var inline_decoded_strings_default = { name: "inline-decoded-strings", tags: ["unsafe"], scope: true, async run(ast, state, options) { if (!options) return; const calls = options.vm.decoders.flatMap( (decoder) => decoder.collectCalls() ); if (calls.length === 0) return; const decodedValues = await options.vm.decode(calls); for (let i = 0; i < calls.length; i++) { const call = calls[i]; const value = decodedValues[i]; call.replaceWith(t8.valueToNode(value)); if (typeof value !== "string") call.addComment("leading", "webcrack:decode_error"); } state.changes += calls.length; } }; // src/deobfuscate/inline-decoder-wrappers.ts var inline_decoder_wrappers_default = { name: "inline-decoder-wrappers", tags: ["unsafe"], scope: true, run(ast, state, decoder) { if (!decoder?.node.id) return; const decoderName = decoder.node.id.name; const decoderBinding = decoder.parentPath.scope.getBinding(decoderName); if (decoderBinding) { state.changes += inlineVariableAliases(decoderBinding).changes; state.changes += inlineFunctionAliases(decoderBinding).changes; } } }; // src/deobfuscate/inline-object-props.ts import * as m10 from "@codemod/matchers"; var inline_object_props_default = { name: "inline-object-props", tags: ["safe"], scope: true, visitor() { const varId = m10.capture(m10.identifier()); const propertyName = m10.capture( m10.matcher((name) => /^[\w]+$/i.test(name)) ); const propertyKey = constKey(propertyName); const objectProperties = m10.capture( m10.arrayOf( m10.objectProperty( propertyKey, m10.or(m10.stringLiteral(), m10.numericLiteral()) ) ) ); const memberAccess = constMemberExpression( m10.fromCapture(varId), propertyName ); const varMatcher = m10.variableDeclarator( varId, m10.objectExpression(objectProperties) ); const literalMemberAccess = constMemberExpression( m10.objectExpression(objectProperties), propertyName ); return { MemberExpression(path) { if (!literalMemberAccess.match(path.node)) return; const property = objectProperties.current.find( (p) => getPropName(p.key) === propertyName.current ); if (!property) return; path.replaceWith(property.value); this.changes++; }, VariableDeclarator(path) { if (!varMatcher.match(path.node)) return; if (objectProperties.current.length === 0) return; const binding = path.scope.getBinding(varId.current.name); if (!binding || !isReadonlyObject(binding, memberAccess)) return; inlineObjectProperties( binding, m10.objectProperty( propertyKey, m10.or(m10.stringLiteral(), m10.numericLiteral()) ) ); this.changes++; } }; } }; // src/deobfuscate/string-array.ts import * as m11 from "@codemod/matchers"; function findStringArray(ast) { let result; const functionName = m11.capture(m11.anyString()); const arrayIdentifier = m11.capture(m11.identifier()); const arrayExpression9 = m11.capture( m11.arrayExpression(m11.arrayOf(m11.or(m11.stringLiteral(), undefinedMatcher))) ); const functionAssignment = m11.assignmentExpression( "=", m11.identifier(m11.fromCapture(functionName)), m11.functionExpression( void 0, [], m11.blockStatement([m11.returnStatement(m11.fromCapture(arrayIdentifier))]) ) ); const variableDeclaration16 = m11.variableDeclaration(void 0, [ m11.variableDeclarator(arrayIdentifier, arrayExpression9) ]); const matcher16 = m11.functionDeclaration( m11.identifier(functionName), [], m11.or( // var array = ["hello", "world"]; // return (getStringArray = function () { return array; })(); m11.blockStatement([ variableDeclaration16, m11.returnStatement(m11.callExpression(functionAssignment)) ]), // var array = ["hello", "world"]; // getStringArray = function () { return array; }); // return getStringArray(); m11.blockStatement([ variableDeclaration16, m11.expressionStatement(functionAssignment), m11.returnStatement(m11.callExpression(m11.identifier(functionName))) ]) ) ); traverse_default(ast, { // Wrapped string array from later javascript-obfuscator versions FunctionDeclaration(path) { if (matcher16.match(path.node)) { const length = arrayExpression9.current.elements.length; const name = functionName.current; const binding = path.scope.getBinding(name); renameFast(binding, "__STRING_ARRAY__"); result = { path, references: binding.referencePaths, originalName: name, name: "__STRING_ARRAY__", length }; path.stop(); } }, // Simple string array inlining (only `array[0]`, `array[1]` etc references, no rotating/decoding). // May be used by older or different obfuscators VariableDeclaration(path) { if (!variableDeclaration16.match(path.node)) return; const length = arrayExpression9.current.elements.length; const binding = path.scope.getBinding(arrayIdentifier.current.name); const memberAccess = m11.memberExpression( m11.fromCapture(arrayIdentifier), m11.numericLiteral(m11.matcher((value) => value < length)) ); if (!binding.referenced || !isReadonlyObject(binding, memberAccess)) return; inlineArrayElements(arrayExpression9.current, binding.referencePaths); path.remove(); } }); return result; } // src/deobfuscate/vm.ts import debug2 from "debug"; function createNodeSandbox() { return async (code) => { const { default: { Isolate } } = await import("isolated-vm"); const isolate = new Isolate(); const context = await isolate.createContext(); const result = await context.eval(code, { timeout: 1e4, copy: true, filename: "file:///obfuscated.js" }); context.release(); isolate.dispose(); return result; }; } function createBrowserSandbox() { return () => { throw new Error("Custom Sandbox implementation required."); }; } var VMDecoder = class { decoders; setupCode; sandbox; constructor(sandbox, stringArray, decoders, rotator) { this.sandbox = sandbox; this.decoders = decoders; const generateOptions = { compact: true, shouldPrintComment: () => false }; const stringArrayCode = generate(stringArray.path.node, generateOptions); const rotatorCode = rotator ? generate(rotator.node, generateOptions) : ""; const decoderCode = decoders.map((decoder) => generate(decoder.path.node, generateOptions)).join(";\n"); this.setupCode = [stringArrayCode, rotatorCode, decoderCode].join(";\n"); } async decode(calls) { const code = `(() => { ${this.setupCode} return [${calls.join(",")}] })()`; try { const result = await this.sandbox(code); return result; } catch (error) { debug2("webcrack:deobfuscate")("vm code:", code); if (error instanceof Error && (error.message.includes("undefined symbol") || error.message.includes("Segmentation fault"))) { throw new Error( "isolated-vm version mismatch. Check https://webcrack.netlify.app/docs/guide/common-errors.html#isolated-vm", { cause: error } ); } throw error; } } }; // src/deobfuscate/index.ts var deobfuscate_default = { name: "deobfuscate", tags: ["unsafe"], scope: true, async run(ast, state, sandbox) { if (!sandbox) return; const logger2 = debug3("webcrack:deobfuscate"); const stringArray = findStringArray(ast); logger2( stringArray ? `String Array: ${stringArray.originalName}, length ${stringArray.length}` : "String Array: no" ); if (!stringArray) return; const rotator = findArrayRotator(stringArray); logger2(`String Array Rotate: ${rotator ? "yes" : "no"}`); const decoders = findDecoders(stringArray); logger2( `String Array Decoders: ${decoders.map((d) => d.originalName).join(", ")}` ); state.changes += applyTransform(ast, inline_object_props_default).changes; for (const decoder of decoders) { state.changes += applyTransform( ast, inline_decoder_wrappers_default, decoder.path ).changes; } const vm = new VMDecoder(sandbox, stringArray, decoders, rotator); state.changes += (await applyTransformAsync(ast, inline_decoded_strings_default, { vm })).changes; if (decoders.length > 0) { stringArray.path.remove(); rotator?.remove(); decoders.forEach((decoder) => decoder.path.remove()); state.changes += 2 + decoders.length; } state.changes += applyTransforms( ast, [merge_strings_default, dead_code_default, control_flow_object_default, control_flow_switch_default], { noScope: true } ).changes; } }; // src/deobfuscate/debug-protection.ts import * as m12 from "@codemod/matchers"; import { ifStatement as ifStatement2 } from "@codemod/matchers"; var debug_protection_default = { name: "debug-protection", tags: ["safe"], scope: true, visitor() { const ret = m12.capture(m12.identifier()); const debugProtectionFunctionName = m12.capture(m12.anyString()); const debuggerProtection = m12.capture(m12.identifier()); const counter = m12.capture(m12.identifier()); const debuggerTemplate = m12.ifStatement( void 0, void 0, m12.containerOf( m12.or( m12.debuggerStatement(), m12.callExpression( constMemberExpression(m12.anyExpression(), "constructor"), [m12.stringLiteral("debugger")] ) ) ) ); const intervalCall = m12.callExpression( constMemberExpression(m12.anyExpression(), "setInterval"), [ m12.identifier(m12.fromCapture(debugProtectionFunctionName)), m12.numericLiteral() ] ); const matcher16 = m12.functionDeclaration( m12.identifier(debugProtectionFunctionName), [ret], m12.blockStatement([ // function debuggerProtection (counter) { m12.functionDeclaration( debuggerProtection, [counter], m12.blockStatement([ debuggerTemplate, // debuggerProtection(++counter); m12.expressionStatement( m12.callExpression(m12.fromCapture(debuggerProtection), [ m12.updateExpression("++", m12.fromCapture(counter), true) ]) ) ]) ), m12.tryStatement( m12.blockStatement([ // if (ret) { ifStatement2( m12.fromCapture(ret), // return debuggerProtection; m12.blockStatement([ m12.returnStatement(m12.fromCapture(debuggerProtection)) ]), // } else { debuggerProtection(0); } m12.blockStatement([ m12.expressionStatement( m12.callExpression(m12.fromCapture(debuggerProtection), [ m12.numericLiteral(0) ]) ) ]) ) ]) ) ]) ); return { FunctionDeclaration(path) { if (!matcher16.match(path.node)) return; const binding = path.scope.getBinding( debugProtectionFunctionName.current ); binding?.referencePaths.forEach((ref) => { if (intervalCall.match(ref.parent)) { findParent(ref, iife())?.remove(); } }); path.remove(); } }; } }; // src/deobfuscate/evaluate-globals.ts import * as t9 from "@babel/types"; import * as m13 from "@codemod/matchers"; var FUNCTIONS = { atob, unescape, decodeURI, decodeURIComponent }; var evaluate_globals_default = { name: "evaluate-globals", tags: ["safe"], scope: true, visitor() { const name = m13.capture( m13.or(...Object.keys(FUNCTIONS)) ); const arg = m13.capture(m13.anyString()); const matcher16 = m13.callExpression(m13.identifier(name), [ m13.stringLiteral(arg) ]); return { CallExpression: { exit(path) { if (!matcher16.match(path.node)) return; if (path.scope.hasBinding(name.current, { noGlobals: true })) return; try { const value = FUNCTIONS[name.current].call( globalThis, arg.current ); path.replaceWith(t9.stringLiteral(value)); this.changes++; } catch { } } } }; } }; // src/deobfuscate/merge-object-assignments.ts import * as t10 from "@babel/types"; import * as m14 from "@codemod/matchers"; var merge_object_assignments_default = { name: "merge-object-assignments", tags: ["safe"], scope: true, visitor: () => { const id = m14.capture(m14.identifier()); const object = m14.capture(m14.objectExpression([])); const varMatcher = m14.variableDeclaration(void 0, [ m14.variableDeclarator(id, object) ]); const key = m14.capture(m14.anyExpression()); const computed = m14.capture(m14.anything()); const value = m14.capture(m14.anyExpression()); const assignmentMatcher = m14.expressionStatement( m14.assignmentExpression( "=", m14.memberExpression(m14.fromCapture(id), key, computed), value ) ); return { Program(path) { path.scope.crawl(); }, VariableDeclaration: { exit(path) { if (!path.inList || !varMatcher.match(path.node)) return; const binding = path.scope.getBinding(id.current.name); const container = path.container; const siblingIndex = path.key + 1; while (siblingIndex < container.length) { const sibling = path.getSibling(siblingIndex); if (!assignmentMatcher.match(sibling.node) || hasCircularReference(value.current, binding)) return; const isComputed = computed.current && key.current.type !== "NumericLiteral" && key.current.type !== "StringLiteral"; object.current.properties.push( t10.objectProperty(key.current, value.current, isComputed) ); sibling.remove(); binding.dereference(); binding.referencePaths.shift(); if (binding.references === 1 && inlineableObject.match(object.current) && !isRepeatedCallReference(binding, binding.referencePaths[0])) { binding.referencePaths[0].replaceWith(object.current); path.remove(); this.changes++; } } } } }; } }; function hasCircularReference(node, binding) { return ( // obj.foo = obj; binding.referencePaths.some((path) => path.find((p) => p.node === node)) || // obj.foo = fn(); where fn could reference the binding or not, for simplicity we assume it does. m14.containerOf(m14.callExpression()).match(node) ); } var repeatedCallMatcher = m14.or( m14.forStatement(), m14.forOfStatement(), m14.forInStatement(), m14.whileStatement(), m14.doWhileStatement(), m14.function(), m14.objectMethod(), m14.classBody() ); function isRepeatedCallReference(binding, reference) { const block = binding.scope.getBlockParent().path; const repeatable = findParent(reference, repeatedCallMatcher); return repeatable?.isDescendant(block); } var inlineableObject = m14.matcher( (node) => m14.or( safeLiteral, m14.arrayExpression(m14.arrayOf(inlineableObject)), m14.objectExpression(m14.arrayOf(constObjectProperty(inlineableObject))) ).match(node) ); // src/deobfuscate/self-defending.ts import * as m15 from "@codemod/matchers"; var self_defending_default = { name: "self-defending", tags: ["safe"], scope: true, visitor() { const callController = m15.capture(m15.anyString()); const firstCall = m15.capture(m15.identifier()); const rfn