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) {
-
- Mangle Variables
-
- setConfig('mangle', e.currentTarget.checked)}
- />
+ Mangle
+
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