Skip to content

Commit

Permalink
feat: configurable smart rename (#100)
Browse files Browse the repository at this point in the history
  • Loading branch information
j4k0xb authored Aug 2, 2024
1 parent 2b311e0 commit 0b619e5
Show file tree
Hide file tree
Showing 16 changed files with 218 additions and 140 deletions.
10 changes: 9 additions & 1 deletion apps/docs/src/guide/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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.
Expand Down
4 changes: 3 additions & 1 deletion apps/playground/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
24 changes: 14 additions & 10 deletions apps/playground/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -205,15 +205,19 @@ export default function Sidebar(props: Props) {
<path d="M10 8v6a2 2 0 1 0 4 0v-1a2 2 0 1 0 -4 0v1" />
<path d="M20.732 12a2 2 0 0 0 -3.732 1v1a2 2 0 0 0 3.726 1.01" />
</svg>
<span class="label-text ml-4 mr-auto hidden sm:inline">
Mangle Variables
</span>
<input
type="checkbox"
class="checkbox checkbox-sm hidden sm:inline"
checked={config.mangle}
onClick={(e) => setConfig('mangle', e.currentTarget.checked)}
/>
<span class="label-text ml-4 mr-auto hidden sm:inline">Mangle</span>
<select
class="select select-sm select-bordered ml-4 flex-1 w-full"
value={config.mangleMode}
onChange={(e) =>
setConfig('mangleMode', e.currentTarget.value as MangleMode)
}
>
<option value="off">Off</option>
<option value="hex">Hex (_0x)</option>
<option value="short">Short Names</option>
<option value="all">All Names</option>
</select>
</label>

<FileTree
Expand Down
3 changes: 2 additions & 1 deletion apps/playground/src/context/DeobfuscateContext.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { ParentProps } from 'solid-js';
import { createContext, createSignal, useContext } from 'solid-js';
import type { Options } from 'webcrack';
import type { MangleMode } from '../App';
import { evalCode } from '../sandbox';
import type {
DeobfuscateResult,
Expand All @@ -15,7 +16,7 @@ const postMessage = (message: WorkerRequest) => worker.postMessage(message);

interface Props {
code: string | undefined;
options: Options;
options: Options & { mangleMode: MangleMode };
onResult: (result: DeobfuscateResult) => void;
}

Expand Down
23 changes: 22 additions & 1 deletion apps/playground/src/webcrack.worker.ts
Original file line number Diff line number Diff line change
@@ -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 =
Expand Down Expand Up @@ -45,6 +50,7 @@ self.onmessage = async ({ data }: MessageEvent<WorkerRequest>) => {
sandbox,
onProgress,
...data.options,
mangle: convertMangleMode(data.options.mangleMode),
});
const files = Array.from(result.bundle?.modules ?? [], ([, module]) => ({
code: module.code,
Expand All @@ -56,3 +62,18 @@ self.onmessage = async ({ data }: MessageEvent<WorkerRequest>) => {
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;
}
}
1 change: 0 additions & 1 deletion packages/webcrack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 24 additions & 0 deletions packages/webcrack/src/ast-utils/scope.ts
Original file line number Diff line number Diff line change
@@ -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;
}
12 changes: 9 additions & 3 deletions packages/webcrack/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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) &&
(() => {
Expand Down

This file was deleted.

3 changes: 2 additions & 1 deletion packages/webcrack/src/transforms/jsx-new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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),
]);
Expand Down
3 changes: 2 additions & 1 deletion packages/webcrack/src/transforms/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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);
}

Expand Down
141 changes: 83 additions & 58 deletions packages/webcrack/src/transforms/mangle.ts
Original file line number Diff line number Diff line change
@@ -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<t.BlockStatement>;
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<t.Identifier>): 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<t.StringLiteral>)
.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<t.Expression>,
): 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<t.Expression>,
);
} else if (expression.isThisExpression()) {
return 'this';
} else {
return undefined;
}
}

function titleCase(str: string) {
return str.length > 0 ? str[0].toUpperCase() + str.slice(1) : str;
}
Loading

0 comments on commit 0b619e5

Please sign in to comment.