Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 84 additions & 2 deletions packages/webcrack/src/ast-utils/matcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,8 +155,7 @@ export function isReadonlyObject(
memberAccess: m.Matcher<t.MemberExpression>,
): boolean {
// Workaround because sometimes babel treats the VariableDeclarator/binding itself as a violation
if (!binding.constant && binding.constantViolations[0] !== binding.path)
return false;
if (!isConstantBinding(binding)) return false;

function isPatternAssignment(member: NodePath<t.Node>) {
const { parentPath } = member;
Expand Down Expand Up @@ -195,6 +194,69 @@ export function isReadonlyObject(
);
}

/**
* Returns true if the binding is constant (never re-assigned).
*/
export function isConstantBinding(binding: Binding) {
// Workaround because sometimes babel treats the VariableDeclarator/binding itself as a violation
if (binding.constant || binding.constantViolations[0] === binding.path)
return true;

// if there is only a single assignment to the variable and it is a param
// then consider it a constant (this may not be very safe)
if (binding.kind === 'param' && binding.constantViolations.length === 1) {
const [path] = binding.constantViolations;
if (
path.isAssignmentExpression() &&
!isBindingPossiblyUsedBefore(binding, path)
)
return true;
}

return false;
}

/**
* Checks if the binding could have possibly been used before the specified path.
* We use *possibly* because we don't know for sure (due to FunctionExpressions) if
* the binding was *actually* used, but it *could have* been.
*/
export function isBindingPossiblyUsedBefore(
binding: Binding,
before: NodePath,
includeParent = true,
) {
return binding.referencePaths.some((rp) => {
// this can be a cause for false-positives in FunctionExpressions
// think:
// var x;
// function getX() {
// return x;
// }
// x = 10; // x isn't used before because the function is never called, but we'll return true anyway
// while we could trace referenes to the function and rule out false-positives, it's really not worth it
if (rp.node.start != null && before.node.start != null) {
if (rp.node.start <= before.node.start) return true;
} else {
// if we dont have access to node positioning info, the reference was likely generated by us
// as I'm too lazy to implement an alternative way to check (and afraid of performance issues)
// we'll just pretend it was used
return true;
}

if (includeParent) {
// check if the reference is a child of "before"
// for example:
// x = x + 10;
// this is accessing "x" before the Assignment, so it should count
for (let p = rp.parentPath; p; p = p.parentPath) {
if (p === before) return true;
}
}
return false;
});
}

/**
* Checks if the binding is a temporary variable that is only assigned
* once and has limited references. Often created by transpilers.
Expand Down Expand Up @@ -253,3 +315,23 @@ export function anySubList<T>(
): m.Matcher<Array<T>> {
return new AnySubListMatcher(elements);
}

export function declarationOrAssignment(
name: m.Matcher<t.Identifier>,
matcher: m.Matcher<t.Expression>,
): m.Matcher<t.VariableDeclaration | t.ExpressionStatement> {
return m.or(
m.variableDeclaration(m.anything(), [m.variableDeclarator(name, matcher)]),
m.expressionStatement(m.assignmentExpression('=', name, matcher)),
);
}

export function declaratorOrAssignmentExpression(
name: m.Matcher<t.Identifier>,
matcher: m.Matcher<t.Expression>,
): m.Matcher<t.VariableDeclarator | t.AssignmentExpression> {
return m.or(
m.variableDeclarator(name, matcher),
m.assignmentExpression('=', name, matcher),
);
}
51 changes: 50 additions & 1 deletion packages/webcrack/src/ast-utils/test/matcher.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import * as m from '@codemod/matchers';
import * as t from '@babel/types';
import { parse } from '@babel/parser';
import traverse, { NodePath } from '@babel/traverse';
import { expect, test } from 'vitest';
import { anySubList } from '../matcher.js';
import {
anySubList,
isBindingPossiblyUsedBefore,
isConstantBinding,
} from '../matcher.js';

test('any sub list', () => {
const a = m.capture(m.matcher((x) => x === 2));
Expand All @@ -10,3 +17,45 @@ test('any sub list', () => {
expect(a.currentKeys).toEqual([1]);
expect(b.currentKeys).toEqual([3]);
});

test('isConstantBinding', () => {
const ast = parse('const a = 1; let b = 2, c = 3; c = 4');
traverse(ast, {
Program(path) {
const a = path.scope.getBinding('a')!;
expect(isConstantBinding(a)).toBe(true);

const b = path.scope.getBinding('b')!;
expect(isConstantBinding(b)).toBe(true);

const c = path.scope.getBinding('c')!;
expect(isConstantBinding(c)).toBe(false);
},
});
});

test('isConstantBinding - function', () => {
const ast = parse('function a(b, c) { b = 10; console.log(c); c = 20 }');
traverse(ast, {
Program(path) {
const func = path.get('body.0') as NodePath<t.FunctionDeclaration>;

const b = func.scope.getBinding('b')!;
expect(isConstantBinding(b)).toBe(true);

const c = func.scope.getBinding('c')!;
expect(isConstantBinding(c)).toBe(false);
},
});
});

test('isBindingPossiblyUsedBefore', () => {
const ast = parse('var a = 1; console.log(a); a = 2;');
traverse(ast, {
Program(path) {
const a = path.scope.getBinding('a')!;
const assign = a.constantViolations[0];
expect(isBindingPossiblyUsedBefore(a, assign)).toBe(true);
},
});
});
29 changes: 17 additions & 12 deletions packages/webcrack/src/deobfuscate/control-flow-object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ import {
constKey,
constMemberExpression,
createFunctionMatcher,
declarationOrAssignment,
declaratorOrAssignmentExpression,
findParent,
getPropName,
inlineFunctionCall,
isConstantBinding,
isReadonlyObject,
} from '../ast-utils';
import mergeStrings from '../unminify/transforms/merge-strings';
Expand Down Expand Up @@ -73,9 +76,7 @@ export default {
m.arrayOf(m.objectProperty(propertyKey, propertyValue)),
);
const aliasId = m.capture(m.identifier());
const aliasVar = m.variableDeclaration(m.anything(), [
m.variableDeclarator(aliasId, m.fromCapture(varId)),
]);
const aliasVar = declarationOrAssignment(aliasId, m.fromCapture(varId));
// E.g. "rLxJs"
const assignedKey = m.capture(propertyName);
// E.g. "6|0|4|3|1|5|2"
Expand All @@ -99,7 +100,7 @@ export default {
m.or(m.fromCapture(varId), m.fromCapture(aliasId)),
propertyName,
);
const varMatcher = m.variableDeclarator(
const varMatcher = declaratorOrAssignmentExpression(
varId,
m.objectExpression(objectProperties),
);
Expand All @@ -109,12 +110,7 @@ export default {
propertyName,
);

function isConstantBinding(binding: Binding) {
// Workaround because sometimes babel treats the VariableDeclarator/binding itself as a violation
return binding.constant || binding.constantViolations[0] === binding.path;
}

function transform(path: NodePath<t.VariableDeclarator>) {
function transform(path: NodePath<t.VariableDeclarator | t.Expression>) {
let changes = 0;
if (varMatcher.match(path.node)) {
// Verify all references to make sure they match how the obfuscator
Expand Down Expand Up @@ -177,8 +173,12 @@ export default {
* and others are assigned later.
*/
function transformObjectKeys(objBinding: Binding): boolean {
const container = objBinding.path.parentPath!.container as t.Statement[];
const startIndex = (objBinding.path.parentPath!.key as number) + 1;
const path =
objBinding.kind === 'param'
? objBinding.constantViolations[0].parentPath!
: objBinding.path.parentPath!;
const container = path.container as t.Statement[];
const startIndex = (path.key as number) + 1;
const properties: t.ObjectProperty[] = [];

for (let i = startIndex; i < container.length; i++) {
Expand Down Expand Up @@ -222,6 +222,11 @@ export default {
}

return {
Expression: {
exit(path) {
this.changes += transform(path);
},
},
Comment on lines +225 to +229
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This may be a performance hazard

VariableDeclarator: {
exit(path) {
this.changes += transform(path);
Expand Down
22 changes: 12 additions & 10 deletions packages/webcrack/src/deobfuscate/control-flow-switch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import * as t from '@babel/types';
import * as m from '@codemod/matchers';
import type { Transform } from '../ast-utils';
import { constMemberExpression, infiniteLoop } from '../ast-utils';
import {
constMemberExpression,
declarationOrAssignment,
infiniteLoop,
} from '../ast-utils';

export default {
name: 'control-flow-switch',
Expand All @@ -28,17 +32,15 @@ export default {
const matcher = m.blockStatement(
m.anyList<t.Statement>(
// E.g. const sequence = "2|4|3|0|1".split("|")
m.variableDeclaration(undefined, [
m.variableDeclarator(
sequenceName,
m.callExpression(
constMemberExpression(m.stringLiteral(sequenceString), 'split'),
[m.stringLiteral('|')],
),
declarationOrAssignment(
sequenceName,
m.callExpression(
constMemberExpression(m.stringLiteral(sequenceString), 'split'),
[m.stringLiteral('|')],
),
]),
),
// E.g. let iterator = 0 or -0x1a70 + 0x93d + 0x275 * 0x7
m.variableDeclaration(undefined, [m.variableDeclarator(iterator)]),
declarationOrAssignment(iterator, m.anything()),
infiniteLoop(
m.blockStatement([
m.switchStatement(
Expand Down
11 changes: 5 additions & 6 deletions packages/webcrack/src/deobfuscate/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type * as t from '@babel/types';
import * as m from '@codemod/matchers';
import {
anySubList,
declarationOrAssignment,
findParent,
inlineVariable,
renameFast,
Expand Down Expand Up @@ -112,12 +113,10 @@ export function findDecoders(stringArray: StringArray): Decoder[] {
m.blockStatement(
anySubList(
// var array = getStringArray();
m.variableDeclaration(undefined, [
m.variableDeclarator(
arrayIdentifier,
m.callExpression(m.identifier(stringArray.name)),
),
]),
declarationOrAssignment(
arrayIdentifier,
m.callExpression(m.identifier(stringArray.name)),
),
// var h = array[e]; return h;
// or return array[e -= 254];
m.containerOf(
Expand Down
8 changes: 5 additions & 3 deletions packages/webcrack/src/deobfuscate/string-array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import traverse from '@babel/traverse';
import type * as t from '@babel/types';
import * as m from '@codemod/matchers';
import {
declarationOrAssignment,
inlineArrayElements,
isReadonlyObject,
renameFast,
Expand Down Expand Up @@ -34,9 +35,10 @@ export function findStringArray(ast: t.Node): StringArray | undefined {
m.blockStatement([m.returnStatement(m.fromCapture(arrayIdentifier))]),
),
);
const variableDeclaration = m.variableDeclaration(undefined, [
m.variableDeclarator(arrayIdentifier, arrayExpression),
]);
const variableDeclaration = declarationOrAssignment(
arrayIdentifier,
arrayExpression,
);
// function getStringArray() { ... }
const matcher = m.functionDeclaration(
m.identifier(functionName),
Expand Down