From ef8abce3d6a399ff3cb1cb84d9bb5f287137c6af Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sun, 24 Dec 2023 02:12:51 +0100 Subject: [PATCH 1/2] wip --- packages/webcrack/src/index.ts | 19 +-- .../webcrack/src/unpack/webpack/bundle.ts | 4 +- ...{getDefaultExport.ts => default-export.ts} | 0 packages/webcrack/src/unpack/webpack/esm.ts | 125 +++++++++++++++--- .../{varInjection.ts => var-injection.ts} | 0 5 files changed, 113 insertions(+), 35 deletions(-) rename packages/webcrack/src/unpack/webpack/{getDefaultExport.ts => default-export.ts} (100%) rename packages/webcrack/src/unpack/webpack/{varInjection.ts => var-injection.ts} (100%) diff --git a/packages/webcrack/src/index.ts b/packages/webcrack/src/index.ts index 11e4a497..4e5b13f3 100644 --- a/packages/webcrack/src/index.ts +++ b/packages/webcrack/src/index.ts @@ -151,23 +151,16 @@ export async function webcrack( }), options.mangle && (() => applyTransform(ast, mangle)), // TODO: Also merge unminify visitor (breaks selfDefending/debugProtection atm) - (options.deobfuscate || options.jsx) && + options.deobfuscate && (() => { - return applyTransforms( - ast, - [ - // Have to run this after unminify to properly detect it - options.deobfuscate ? [selfDefending, debugProtection] : [], - options.jsx ? [jsx, jsxNew] : [], - ].flat(), - { noScope: true }, - ); + return applyTransforms(ast, [selfDefending, debugProtection], { + noScope: true, + }); }), options.deobfuscate && (() => applyTransform(ast, mergeObjectAssignments)), - () => (outputCode = generate(ast)), - // Unpacking modifies the same AST and may result in imports not at top level - // so the code has to be generated before options.unpack && (() => (bundle = unpackAST(ast, options.mappings(m)))), + options.jsx && (() => applyTransforms(ast, [jsx, jsxNew])), + () => (outputCode = generate(ast)), ].filter(Boolean) as (() => unknown)[]; for (let i = 0; i < stages.length; i++) { diff --git a/packages/webcrack/src/unpack/webpack/bundle.ts b/packages/webcrack/src/unpack/webpack/bundle.ts index db83d1ed..7f12c90a 100644 --- a/packages/webcrack/src/unpack/webpack/bundle.ts +++ b/packages/webcrack/src/unpack/webpack/bundle.ts @@ -3,10 +3,10 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import { Bundle } from '../bundle'; import { relativePath } from '../path'; +import { convertDefaultRequire } from './default-export'; import { convertESM } from './esm'; -import { convertDefaultRequire } from './getDefaultExport'; import { WebpackModule } from './module'; -import { inlineVarInjections } from './varInjection'; +import { inlineVarInjections } from './var-injection'; export class WebpackBundle extends Bundle { constructor(entryId: string, modules: Map) { diff --git a/packages/webcrack/src/unpack/webpack/getDefaultExport.ts b/packages/webcrack/src/unpack/webpack/default-export.ts similarity index 100% rename from packages/webcrack/src/unpack/webpack/getDefaultExport.ts rename to packages/webcrack/src/unpack/webpack/default-export.ts diff --git a/packages/webcrack/src/unpack/webpack/esm.ts b/packages/webcrack/src/unpack/webpack/esm.ts index f529b685..e96b92c7 100644 --- a/packages/webcrack/src/unpack/webpack/esm.ts +++ b/packages/webcrack/src/unpack/webpack/esm.ts @@ -6,6 +6,13 @@ import { constMemberExpression, findPath, renameFast } from '../../ast-utils'; import { WebpackModule } from './module'; const buildNamespaceImport = statement`import * as NAME from "PATH";`; +const buildNamedImport = (locals: string[], imported: string[], path: string) => + t.importDeclaration( + locals.map((local, i) => + t.importSpecifier(t.identifier(local), t.identifier(imported[i])), + ), + t.stringLiteral(path), + ); const buildNamedExportLet = statement`export let NAME = VALUE;`; /** @@ -27,24 +34,35 @@ export function convertESM(module: WebpackModule): void { m.callExpression(constMemberExpression('require', 'r'), [m.identifier()]), ); - const exportsName = m.capture(m.identifier()); + const exportsObjectName = m.capture(m.identifier()); const exportedName = m.capture(m.anyString()); - const returnedValue = m.capture(m.anyExpression()); + const exportedLocal = m.capture(m.anyExpression()); // E.g. require.d(exports, "counter", function () { return f }); const defineExportMatcher = m.expressionStatement( m.callExpression(constMemberExpression('require', 'd'), [ - exportsName, + exportsObjectName, m.stringLiteral(exportedName), m.functionExpression( undefined, [], - m.blockStatement([m.returnStatement(returnedValue)]), + m.blockStatement([m.returnStatement(exportedLocal)]), ), ]), ); + const exportAssignment = m.expressionStatement( + m.assignmentExpression( + '=', + m.memberExpression( + m.identifier('exports'), + m.identifier(exportedName), + false, + ), + exportedLocal, + ), + ); const emptyObjectVarMatcher = m.variableDeclarator( - m.fromCapture(exportsName), + m.fromCapture(exportsObjectName), m.objectExpression([]), ); @@ -52,21 +70,28 @@ export function convertESM(module: WebpackModule): void { m.arrayOf( m.objectProperty( m.identifier(), - m.arrowFunctionExpression([], m.anyExpression()), + m.or( + m.arrowFunctionExpression([], m.anyExpression()), + m.functionExpression( + null, + [], + m.blockStatement([m.returnStatement()]), + ), + ), ), ), ); // E.g. require.d(exports, { foo: () => a, bar: () => b }); const defineExportsMatcher = m.expressionStatement( m.callExpression(constMemberExpression('require', 'd'), [ - exportsName, + exportsObjectName, m.objectExpression(properties), ]), ); - // E.g. const lib = require("./lib.js"); const requireVariable = m.capture(m.identifier()); const requiredModuleId = m.capture(m.anyNumber()); + // E.g. const lib = require(1); const requireMatcher = m.variableDeclaration(undefined, [ m.variableDeclarator( requireVariable, @@ -76,6 +101,11 @@ export function convertESM(module: WebpackModule): void { ), ]); + const zeroSequenceMatcher = m.sequenceExpression([ + m.numericLiteral(0), + m.identifier(), + ]); + // module = require.hmd(module); const hmdMatcher = m.expressionStatement( m.assignmentExpression( @@ -93,18 +123,62 @@ export function convertESM(module: WebpackModule): void { if (defineEsModuleMatcher.match(path.node)) { module.ast.program.sourceType = 'module'; path.remove(); - } else if ( - module.ast.program.sourceType === 'module' && - requireMatcher.match(path.node) - ) { + } else if (requireMatcher.match(path.node)) { + const binding = path.scope.getBinding(requireVariable.current!.name)!; + const references = binding.referencePaths.map((p) => p.parentPath!); + const validateReferences = ( + references: NodePath[], + ): references is NodePath< + t.MemberExpression & { property: t.Identifier } + >[] => + references.every((p) => + m + .memberExpression( + m.fromCapture(requireVariable), + m.identifier(), + false, + ) + .match(p.node), + ); + if (!validateReferences(references)) { + path.replaceWith( + buildNamespaceImport({ + NAME: requireVariable.current, + PATH: String(requiredModuleId.current), + }), + ); + return; + } + + const importNames = [ + ...new Set(references.map((p) => p.node.property.name)), + ]; + const localNames = importNames.map((name) => { + const binding = path.scope.getBinding(name); + const hasNameConflict = binding?.referencePaths.some( + (p) => p.scope.getBinding(name) !== binding, + ); + return hasNameConflict ? path.scope.generateUid(name) : name; + }); + path.replaceWith( - buildNamespaceImport({ - NAME: requireVariable.current, - PATH: String(requiredModuleId.current), - }), + buildNamedImport( + localNames, + importNames, + String(requiredModuleId.current), + ), ); + + [...references].forEach((ref) => { + ref.replaceWith(ref.node.property); + if (zeroSequenceMatcher.match(ref.parent)) { + ref.parentPath.replaceWith(ref); + } + }); } else if (defineExportsMatcher.match(path.node)) { - const exportsBinding = path.scope.getBinding(exportsName.current!.name); + const exportsBinding = path.scope.getBinding( + exportsObjectName.current!.name, + ); const emptyObject = emptyObjectVarMatcher.match( exportsBinding?.path.node, ) @@ -113,8 +187,12 @@ export function convertESM(module: WebpackModule): void { for (const property of properties.current!) { const exportedKey = property.key as t.Identifier; - const returnedValue = (property.value as t.ArrowFunctionExpression) - .body as t.Expression; + const returnedValue = t.isArrowFunctionExpression(property.value) + ? (property.value.body as t.Expression) + : (( + (property.value as t.FunctionExpression).body + .body[0] as t.ReturnStatement + ).argument as t.Expression); if (emptyObject) { emptyObject.properties.push( t.objectProperty(exportedKey, returnedValue), @@ -125,8 +203,15 @@ export function convertESM(module: WebpackModule): void { } path.remove(); + } else if (exportAssignment.match(path.node)) { + path.replaceWith( + buildNamedExportLet({ + NAME: t.identifier(exportedName.current!), + VALUE: exportedLocal.current!, + }), + ); } else if (defineExportMatcher.match(path.node)) { - exportVariable(path, returnedValue.current!, exportedName.current!); + exportVariable(path, exportedLocal.current!, exportedName.current!); path.remove(); } else if (hmdMatcher.match(path.node)) { path.remove(); diff --git a/packages/webcrack/src/unpack/webpack/varInjection.ts b/packages/webcrack/src/unpack/webpack/var-injection.ts similarity index 100% rename from packages/webcrack/src/unpack/webpack/varInjection.ts rename to packages/webcrack/src/unpack/webpack/var-injection.ts From 50302c2d0ae5fa187945ab7bf4b1987a74c071c9 Mon Sep 17 00:00:00 2001 From: j4k0xb <55899582+j4k0xb@users.noreply.github.com> Date: Sun, 24 Dec 2023 02:12:51 +0100 Subject: [PATCH 2/2] fix: import name conflict --- packages/webcrack/src/unpack/webpack/esm.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/webcrack/src/unpack/webpack/esm.ts b/packages/webcrack/src/unpack/webpack/esm.ts index e96b92c7..3751589b 100644 --- a/packages/webcrack/src/unpack/webpack/esm.ts +++ b/packages/webcrack/src/unpack/webpack/esm.ts @@ -154,23 +154,25 @@ export function convertESM(module: WebpackModule): void { ...new Set(references.map((p) => p.node.property.name)), ]; const localNames = importNames.map((name) => { - const binding = path.scope.getBinding(name); - const hasNameConflict = binding?.referencePaths.some( - (p) => p.scope.getBinding(name) !== binding, + const hasNameConflict = binding.referencePaths.some((ref) => + ref.scope.hasBinding(name), ); return hasNameConflict ? path.scope.generateUid(name) : name; }); - path.replaceWith( + const [importDeclaration] = path.replaceWith( buildNamedImport( localNames, importNames, String(requiredModuleId.current), ), ); + importDeclaration.scope.crawl(); [...references].forEach((ref) => { - ref.replaceWith(ref.node.property); + const localName = + localNames[importNames.indexOf(ref.node.property.name)]; + ref.replaceWith(t.identifier(localName)); if (zeroSequenceMatcher.match(ref.parent)) { ref.parentPath.replaceWith(ref); }