diff --git a/apps/docs/src/guide/api.md b/apps/docs/src/guide/api.md index d863985d..bd56101f 100644 --- a/apps/docs/src/guide/api.md +++ b/apps/docs/src/guide/api.md @@ -18,7 +18,7 @@ const result = await webcrack('const a = 1+1;'); console.log(result.code); // 'const a = 2;' ``` -Save the deobufscated code and the unpacked bundle to the given directory: +Save the deobfuscated code and the unpacked bundle to the given directory: ```js import fs from 'fs'; @@ -57,6 +57,14 @@ await webcrack(code, { }); ``` +Only mangle variable names that match a filter: + +```js +await webcrack(code, { + mangle: (id) => id.startsWith('_0x'), +}); +``` + ## Customize Paths Useful for reverse-engineering and tracking changes across multiple versions of a bundle. diff --git a/apps/playground/src/App.tsx b/apps/playground/src/App.tsx index 85e6ff85..ab40e9c4 100644 --- a/apps/playground/src/App.tsx +++ b/apps/playground/src/App.tsx @@ -22,12 +22,14 @@ import { debounce } from './utils/debounce'; import { downloadFile } from './utils/files'; import type { DeobfuscateResult } from './webcrack.worker'; +export type MangleMode = 'off' | 'all' | 'hex' | 'short'; + export const [config, setConfig] = createStore({ deobfuscate: true, unminify: true, unpack: true, jsx: true, - mangle: false, + mangleMode: 'off' as MangleMode, }); function App() { diff --git a/apps/playground/src/components/Sidebar.tsx b/apps/playground/src/components/Sidebar.tsx index 9f442a05..5a47fe63 100644 --- a/apps/playground/src/components/Sidebar.tsx +++ b/apps/playground/src/components/Sidebar.tsx @@ -1,5 +1,5 @@ import { Show } from 'solid-js'; -import { config, setConfig } from '../App'; +import { config, setConfig, type MangleMode } from '../App'; import { useDeobfuscateContext } from '../context/DeobfuscateContext'; import FileTree from './FileTree'; @@ -205,15 +205,19 @@ export default function Sidebar(props: Props) { - - setConfig('mangle', e.currentTarget.checked)} - /> + + worker.postMessage(message); interface Props { code: string | undefined; - options: Options; + options: Options & { mangleMode: MangleMode }; onResult: (result: DeobfuscateResult) => void; } diff --git a/apps/playground/src/webcrack.worker.ts b/apps/playground/src/webcrack.worker.ts index 74e04f5b..756606af 100644 --- a/apps/playground/src/webcrack.worker.ts +++ b/apps/playground/src/webcrack.worker.ts @@ -1,8 +1,13 @@ import type { Options, Sandbox } from 'webcrack'; import { webcrack } from 'webcrack'; +import type { MangleMode } from './App'; export type WorkerRequest = - | { type: 'deobfuscate'; code: string; options: Options } + | { + type: 'deobfuscate'; + code: string; + options: Options & { mangleMode: MangleMode }; + } | { type: 'sandbox'; result: unknown }; export type WorkerResponse = @@ -45,6 +50,7 @@ self.onmessage = async ({ data }: MessageEvent) => { sandbox, onProgress, ...data.options, + mangle: convertMangleMode(data.options.mangleMode), }); const files = Array.from(result.bundle?.modules ?? [], ([, module]) => ({ code: module.code, @@ -56,3 +62,18 @@ self.onmessage = async ({ data }: MessageEvent) => { postMessage({ type: 'error', error: error as Error }); } }; + +function convertMangleMode(mode: MangleMode) { + const HEX_IDENTIFIER = /_0x[a-f\d]+/i; + + switch (mode) { + case 'off': + return false; + case 'all': + return true; + case 'hex': + return (id: string) => HEX_IDENTIFIER.test(id); + case 'short': + return (id: string) => id.length <= 2; + } +} diff --git a/packages/webcrack/package.json b/packages/webcrack/package.json index fab6ee86..c04c472e 100644 --- a/packages/webcrack/package.json +++ b/packages/webcrack/package.json @@ -47,7 +47,6 @@ "@babel/traverse": "^7.24.7", "@babel/types": "^7.24.7", "@codemod/matchers": "^1.7.1", - "babel-plugin-minify-mangle-names": "^0.5.1", "commander": "^12.1.0", "debug": "^4.3.5", "isolated-vm": "^5.0.0" diff --git a/packages/webcrack/src/ast-utils/scope.ts b/packages/webcrack/src/ast-utils/scope.ts new file mode 100644 index 00000000..bb322e6a --- /dev/null +++ b/packages/webcrack/src/ast-utils/scope.ts @@ -0,0 +1,24 @@ +import type { Scope } from '@babel/traverse'; +import { toIdentifier } from '@babel/types'; + +/** + * Like scope.generateUid from babel, but without the underscore prefix and name filters + */ +export function generateUid(scope: Scope, name: string = 'temp'): string { + let uid = ''; + let i = 1; + do { + uid = toIdentifier(i > 1 ? `${name}${i}` : name); + i++; + } while ( + scope.hasLabel(uid) || + scope.hasBinding(uid) || + scope.hasGlobal(uid) || + scope.hasReference(uid) + ); + + const program = scope.getProgramParent(); + program.references[uid] = true; + program.uids[uid] = true; + return uid; +} diff --git a/packages/webcrack/src/index.ts b/packages/webcrack/src/index.ts index 694bf126..d04411ff 100644 --- a/packages/webcrack/src/index.ts +++ b/packages/webcrack/src/index.ts @@ -42,7 +42,7 @@ export interface WebcrackResult { code: string; bundle: Bundle | undefined; /** - * Save the deobufscated code and the extracted bundle to the given directory. + * Save the deobfuscated code and the extracted bundle to the given directory. * @param path Output directory */ save(path: string): Promise; @@ -73,7 +73,7 @@ export interface Options { * Mangle variable names. * @default false */ - mangle?: boolean; + mangle?: boolean | ((id: string) => boolean); /** * Assigns paths to modules based on the given matchers. * This will also rewrite `require()` calls to use the new paths. @@ -156,7 +156,13 @@ export async function webcrack( (() => { applyTransforms(ast, [transpile, unminify]); }), - options.mangle && (() => applyTransform(ast, mangle)), + options.mangle && + (() => + applyTransform( + ast, + mangle, + typeof options.mangle === 'boolean' ? () => true : options.mangle, + )), // TODO: Also merge unminify visitor (breaks selfDefending/debugProtection atm) (options.deobfuscate || options.jsx) && (() => { diff --git a/packages/webcrack/src/transforms/babel-plugin-minify-mangle-names.d.ts b/packages/webcrack/src/transforms/babel-plugin-minify-mangle-names.d.ts deleted file mode 100644 index 439278b9..00000000 --- a/packages/webcrack/src/transforms/babel-plugin-minify-mangle-names.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -declare module 'babel-plugin-minify-mangle-names' { - import type { Visitor, traverse } from '@babel/traverse'; - import type * as t from '@babel/types'; - - export default function mangle(babel: Babel): { - visitor: Visitor; - }; - - interface Babel { - types: typeof t; - traverse: typeof traverse; - } -} diff --git a/packages/webcrack/src/transforms/jsx-new.ts b/packages/webcrack/src/transforms/jsx-new.ts index 78993ae6..8c251412 100644 --- a/packages/webcrack/src/transforms/jsx-new.ts +++ b/packages/webcrack/src/transforms/jsx-new.ts @@ -2,6 +2,7 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import type { Transform } from '../ast-utils'; import { codePreview, constMemberExpression } from '../ast-utils'; +import { generateUid } from '../ast-utils/scope'; const DEFAULT_PRAGMA_CANDIDATES = [ 'jsx', @@ -55,7 +56,7 @@ export default { if (convertibleName.match(type.current!)) { name = convertType(type.current); } else { - name = t.jsxIdentifier(path.scope.generateUid('Component')); + name = t.jsxIdentifier(generateUid(path.scope, 'Component')); const componentVar = t.variableDeclaration('const', [ t.variableDeclarator(t.identifier(name.name), type.current), ]); diff --git a/packages/webcrack/src/transforms/jsx.ts b/packages/webcrack/src/transforms/jsx.ts index f1a7fd7c..78744758 100644 --- a/packages/webcrack/src/transforms/jsx.ts +++ b/packages/webcrack/src/transforms/jsx.ts @@ -2,6 +2,7 @@ import * as t from '@babel/types'; import * as m from '@codemod/matchers'; import type { Transform } from '../ast-utils'; import { codePreview, constMemberExpression } from '../ast-utils'; +import { generateUid } from '../ast-utils/scope'; export default { name: 'jsx', @@ -72,7 +73,7 @@ export default { ) { const binding = path.scope.getBinding(type.current.name); if (!binding) return; - name = t.jsxIdentifier(path.scope.generateUid('Component')); + name = t.jsxIdentifier(generateUid(path.scope, 'Component')); path.scope.rename(type.current.name, name.name); } diff --git a/packages/webcrack/src/transforms/mangle.ts b/packages/webcrack/src/transforms/mangle.ts index bdeadb66..aa816e0c 100644 --- a/packages/webcrack/src/transforms/mangle.ts +++ b/packages/webcrack/src/transforms/mangle.ts @@ -1,66 +1,91 @@ -import { statement } from '@babel/template'; -import type { Visitor } from '@babel/traverse'; -import traverse, { NodePath, visitors } from '@babel/traverse'; -import * as t from '@babel/types'; -import mangle from 'babel-plugin-minify-mangle-names'; -import type { Transform } from '../ast-utils'; -import { safeLiteral } from '../ast-utils'; - -// See https://github.com/j4k0xb/webcrack/issues/41 and https://github.com/babel/minify/issues/1023 -const fixDefaultParamError: Visitor = { - Function(path) { - const { params } = path.node; - - for (let i = params.length - 1; i >= 0; i--) { - const param = params[i]; - if (!t.isAssignmentPattern(param) || safeLiteral.match(param.right)) - continue; - - if (!t.isBlockStatement(path.node.body)) { - path.node.body = t.blockStatement([t.returnStatement(path.node.body)]); - } - - const body = path.get('body') as NodePath; - if (t.isIdentifier(param.left)) { - body.unshiftContainer( - 'body', - statement`if (${param.left} === undefined) ${param.left} = ${param.right}`(), - ); - } else { - const tempId = path.scope.generateUidIdentifier(); - body.unshiftContainer( - 'body', - statement`var ${param.left} = ${tempId} === undefined ? ${param.right} : ${tempId}`(), - ); - param.left = tempId; - } - param.right = t.identifier('undefined'); - } - }, -}; +import type { NodePath } from '@babel/traverse'; +import type * as t from '@babel/types'; +import * as m from '@codemod/matchers'; +import { renameFast, type Transform } from '../ast-utils'; +import { generateUid } from '../ast-utils/scope'; export default { name: 'mangle', tags: ['safe'], scope: true, - run(ast) { - // path.hub is undefined for some reason, monkey-patch to avoid error... - // eslint-disable-next-line @typescript-eslint/unbound-method - const { getSource } = NodePath.prototype; - NodePath.prototype.getSource = () => ''; - const visitor = visitors.merge([ - fixDefaultParamError, - mangle({ types: t, traverse }).visitor, - ]); + visitor(match = () => true) { + return { + BindingIdentifier: { + exit(path) { + if (!path.isBindingIdentifier()) return; + if (path.parentPath.isImportSpecifier()) return; + if (path.parentPath.isObjectProperty()) return; + if (!match(path.node.name)) return; - traverse(ast, visitor, undefined, { - opts: { - eval: true, - topLevel: true, - exclude: { React: true }, - }, - }); + const binding = path.scope.getBinding(path.node.name); + if (!binding) return; + if ( + binding.referencePaths.some((ref) => ref.isExportNamedDeclaration()) + ) + return; - NodePath.prototype.getSource = getSource; + renameFast(binding, inferName(path)); + }, + }, + }; }, -} satisfies Transform; +} satisfies Transform<(id: string) => boolean>; + +const requireMatcher = m.variableDeclarator( + m.identifier(), + m.callExpression(m.identifier('require'), [m.stringLiteral()]), +); + +function inferName(path: NodePath): string { + if (path.parentPath.isClass({ id: path.node })) { + return generateUid(path.scope, 'C'); + } else if (path.parentPath.isFunction({ id: path.node })) { + return generateUid(path.scope, 'f'); + } else if ( + path.listKey === 'params' || + (path.parentPath.isAssignmentPattern({ left: path.node }) && + path.parentPath.listKey === 'params') + ) { + return generateUid(path.scope, 'p'); + } else if (requireMatcher.match(path.parent)) { + return generateUid( + path.scope, + (path.parentPath.get('init.arguments.0') as NodePath) + .node.value, + ); + } else if (path.parentPath.isVariableDeclarator({ id: path.node })) { + const init = path.parentPath.get('init'); + const suffix = (init.isExpression() && generateExpressionName(init)) || ''; + return generateUid(path.scope, 'v' + titleCase(suffix)); + } else if (path.parentPath.isArrayPattern()) { + return generateUid(path.scope, 'v'); + } else { + return path.node.name; + } +} + +function generateExpressionName( + expression: NodePath, +): string | undefined { + if (expression.isIdentifier()) { + return expression.node.name; + } else if (expression.isFunctionExpression()) { + return expression.node.id?.name ?? 'f'; + } else if (expression.isArrowFunctionExpression()) { + return 'f'; + } else if (expression.isClassExpression()) { + return expression.node.id?.name ?? 'C'; + } else if (expression.isCallExpression()) { + return generateExpressionName( + expression.get('callee') as NodePath, + ); + } else if (expression.isThisExpression()) { + return 'this'; + } else { + return undefined; + } +} + +function titleCase(str: string) { + return str.length > 0 ? str[0].toUpperCase() + str.slice(1) : str; +} diff --git a/packages/webcrack/test/jsx-new.test.ts b/packages/webcrack/test/jsx-new.test.ts index 38852bf9..1ffdb64c 100644 --- a/packages/webcrack/test/jsx-new.test.ts +++ b/packages/webcrack/test/jsx-new.test.ts @@ -17,8 +17,8 @@ test('deeply nested member expression type', () => test('any other expression type', () => expectJS('jsx(r ? "a" : "div", {});').toMatchInlineSnapshot(` - const _Component = r ? "a" : "div"; - <_Component />; + const Component = r ? "a" : "div"; + ; `)); test('rename component with conflicting name', () => diff --git a/packages/webcrack/test/jsx.test.ts b/packages/webcrack/test/jsx.test.ts index b76065fb..57c73b63 100644 --- a/packages/webcrack/test/jsx.test.ts +++ b/packages/webcrack/test/jsx.test.ts @@ -22,9 +22,9 @@ test('deeply nested member expression type', () => test('rename component with conflicting name', () => expectJS('function a(){} React.createElement(a, null);') .toMatchInlineSnapshot(` - function _Component() {} - <_Component />; - `)); + function Component() {} + ; + `)); test('attributes', () => expectJS( diff --git a/packages/webcrack/test/mangle.test.ts b/packages/webcrack/test/mangle.test.ts index e18df845..a0972528 100644 --- a/packages/webcrack/test/mangle.test.ts +++ b/packages/webcrack/test/mangle.test.ts @@ -4,42 +4,56 @@ import mangle from '../src/transforms/mangle'; const expectJS = testTransform(mangle); -// https://github.com/j4k0xb/webcrack/issues/41 -test('rename default parameters of function', () => { +test('variable', () => { + expectJS('let x = 1;').toMatchInlineSnapshot('let v = 1;'); + expectJS('let x = exports;').toMatchInlineSnapshot(`let vExports = exports;`); + expectJS('let x = () => {};').toMatchInlineSnapshot(`let vF = () => {};`); + expectJS('let x = class {};').toMatchInlineSnapshot(`let vC = class {};`); + expectJS('let x = Array(100);').toMatchInlineSnapshot( + `let vArray = Array(100);`, + ); + expectJS('let [x] = 1;').toMatchInlineSnapshot(`let [v] = 1;`); + expectJS('const x = require("fs");').toMatchInlineSnapshot( + `const fs = require("fs");`, + ); expectJS(` - function func(arg1, arg2 = 0, arg3 = arg1, arg4 = arg1) { - return arg1; - } + const x = require("node:fs"); + const y = require("node:fs"); `).toMatchInlineSnapshot(` - function a(a, b = 0, c = undefined, d = undefined) { - if (c === undefined) c = a; - if (d === undefined) d = a; - return a; - } + const nodeFs = require("node:fs"); + const nodeFs2 = require("node:fs"); `); }); -test('rename default parameters of arrow function', () => { - expectJS(` - const func = (arg1, arg2 = 0, arg3 = arg1, arg4 = arg1) => arg1; - `).toMatchInlineSnapshot(` - const a = (a, b = 0, c = undefined, d = undefined) => { - if (c === undefined) c = a; - if (d === undefined) d = a; - return a; - }; +test('ignore exports', () => { + expectJS('export const x = 1;').toMatchInlineSnapshot('export const x = 1;'); + expectJS('export class X {}').toMatchInlineSnapshot(`export class X {}`); +}); + +test('only rename _0x variable', () => { + expectJS( + ` + let _0x4c3e = 1; + let foo = 2; + `, + (id) => id.startsWith('_0x'), + ).toMatchInlineSnapshot(` + let v = 1; + let foo = 2; `); }); -test('rename default destructuring parameters', () => { +test('class', () => { + expectJS('class abc {}').toMatchInlineSnapshot('class C {}'); +}); + +test('function', () => { + expectJS('function abc() {}').toMatchInlineSnapshot('function f() {}'); +}); + +test('parameters', () => { expectJS(` - function func(arg1, [arg2] = arg1) { - return arg2; - } - `).toMatchInlineSnapshot(` - function a(a, b = undefined) { - var [c] = b === undefined ? a : b; - return c; - } - `); + (x, y, z) => x + y + z; + `).toMatchInlineSnapshot(`(p, p2, p3) => p + p2 + p3;`); + expectJS('(x = 1) => x;').toMatchInlineSnapshot(`(p = 1) => p;`); }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f233e4c..8b490dde 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -157,9 +157,6 @@ importers: '@codemod/matchers': specifier: ^1.7.1 version: 1.7.1 - babel-plugin-minify-mangle-names: - specifier: ^0.5.1 - version: 0.5.1 commander: specifier: ^12.1.0 version: 12.1.0 @@ -1161,17 +1158,11 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - babel-helper-mark-eval-scopes@0.4.3: - resolution: {integrity: sha512-+d/mXPP33bhgHkdVOiPkmYoeXJ+rXRWi7OdhwpyseIqOS8CmzHQXHUp/+/Qr8baXsT0kjGpMHHofHs6C3cskdA==} - babel-plugin-jsx-dom-expressions@0.37.21: resolution: {integrity: sha512-WbQo1NQ241oki8bYasVzkMXOTSIri5GO/K47rYJb2ZBh8GaPUEWiWbMV3KwXz+96eU2i54N6ThzjQG/f5n8Azw==} peerDependencies: '@babel/core': ^7.20.12 - babel-plugin-minify-mangle-names@0.5.1: - resolution: {integrity: sha512-8KMichAOae2FHlipjNDTo2wz97MdEb2Q0jrn4NIRXzHH7SJ3c5TaNNBkeTHbk9WUsMnqpNUx949ugM9NFWewzw==} - babel-preset-solid@1.8.17: resolution: {integrity: sha512-s/FfTZOeds0hYxYqce90Jb+0ycN2lrzC7VP1k1JIn3wBqcaexDKdYi6xjB+hMNkL+Q6HobKbwsriqPloasR9LA==} peerDependencies: @@ -3796,8 +3787,6 @@ snapshots: dependencies: possible-typed-array-names: 1.0.0 - babel-helper-mark-eval-scopes@0.4.3: {} - babel-plugin-jsx-dom-expressions@0.37.21(@babel/core@7.24.7): dependencies: '@babel/core': 7.24.7 @@ -3807,10 +3796,6 @@ snapshots: html-entities: 2.3.3 validate-html-nesting: 1.2.2 - babel-plugin-minify-mangle-names@0.5.1: - dependencies: - babel-helper-mark-eval-scopes: 0.4.3 - babel-preset-solid@1.8.17(@babel/core@7.24.7): dependencies: '@babel/core': 7.24.7