webcrack
Version:
Deobfuscate, unminify and unpack bundled javascript
1,522 lines (1,495 loc) • 150 kB
JavaScript
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
// src/index.ts
import { parse as parse2 } from "@babel/parser";
import * as m52 from "@codemod/matchers";
import debug5 from "debug";
import { join as join3, normalize as normalize2 } from "node:path";
// src/ast-utils/ast.ts
import * as t from "@babel/types";
function getPropName(node) {
if (t.isIdentifier(node)) {
return node.name;
}
if (t.isStringLiteral(node)) {
return node.value;
}
if (t.isNumericLiteral(node)) {
return node.value.toString();
}
}
// babel-import:@babel/generator
var generator_exports = {};
__export(generator_exports, {
default: () => generator_default
});
__reExport(generator_exports, lib_star);
import module from "@babel/generator/lib/index.js";
import * as lib_star from "@babel/generator/lib/index.js";
var generator_default = module.default ?? module;
// src/ast-utils/generator.ts
var defaultOptions = { jsescOption: { minimal: true } };
function generate(ast, options = defaultOptions) {
return generator_default(ast, options).code;
}
function codePreview(node) {
const code = generate(node, {
minified: true,
shouldPrintComment: () => false,
...defaultOptions
});
if (code.length > 100) {
return code.slice(0, 70) + " \u2026 " + code.slice(-30);
}
return code;
}
// babel-import:@babel/traverse
var traverse_exports = {};
__export(traverse_exports, {
default: () => traverse_default
});
__reExport(traverse_exports, lib_star2);
import module2 from "@babel/traverse/lib/index.js";
import * as lib_star2 from "@babel/traverse/lib/index.js";
var traverse_default = module2.default ?? module2;
// src/ast-utils/inline.ts
import * as t3 from "@babel/types";
import * as m2 from "@codemod/matchers";
// src/ast-utils/matcher.ts
import * as t2 from "@babel/types";
import * as m from "@codemod/matchers";
var safeLiteral = m.matcher(
(node) => t2.isLiteral(node) && (!t2.isTemplateLiteral(node) || node.expressions.length === 0)
);
function infiniteLoop(body) {
return m.or(
m.forStatement(void 0, null, void 0, body),
m.forStatement(void 0, truthyMatcher, void 0, body),
m.whileStatement(truthyMatcher, body)
);
}
function constKey(name) {
return m.or(m.identifier(name), m.stringLiteral(name));
}
function constObjectProperty(value) {
return m.or(
m.objectProperty(m.identifier(), value, false),
m.objectProperty(m.or(m.stringLiteral(), m.numericLiteral()), value)
);
}
function anonymousFunction(params, body) {
return m.or(
m.functionExpression(null, params, body, false),
m.arrowFunctionExpression(params, body)
);
}
function iife(params, body) {
return m.callExpression(anonymousFunction(params, body));
}
function constMemberExpression(object, property) {
if (typeof object === "string") object = m.identifier(object);
return m.or(
m.memberExpression(object, m.identifier(property), false),
m.memberExpression(object, m.stringLiteral(property), true)
);
}
var undefinedMatcher = m.or(
m.identifier("undefined"),
m.unaryExpression("void", m.numericLiteral(0))
);
var trueMatcher = m.or(
m.booleanLiteral(true),
m.unaryExpression("!", m.numericLiteral(0)),
m.unaryExpression("!", m.unaryExpression("!", m.numericLiteral(1))),
m.unaryExpression("!", m.unaryExpression("!", m.arrayExpression([])))
);
var falseMatcher = m.or(
m.booleanLiteral(false),
m.unaryExpression("!", m.arrayExpression([]))
);
var truthyMatcher = m.or(trueMatcher, m.arrayExpression([]));
function findParent(path, matcher16) {
return path.findParent(
(path2) => matcher16.match(path2.node)
);
}
function findPath(path, matcher16) {
return path.find((path2) => matcher16.match(path2.node));
}
function createFunctionMatcher(params, body) {
const captures = Array.from(
{ length: params },
() => m.capture(m.anyString())
);
return m.functionExpression(
void 0,
captures.map(m.identifier),
m.blockStatement(
body(...captures.map((c) => m.identifier(m.fromCapture(c))))
)
);
}
function isReadonlyObject(binding, memberAccess) {
if (!binding.constant && binding.constantViolations[0] !== binding.path)
return false;
function isPatternAssignment(member) {
const { parentPath } = member;
return (
// [obj.property] = [1];
parentPath?.isArrayPattern() || // ({ property: obj.property } = {})
// ({ ...obj.property } = {})
parentPath?.parentPath?.isObjectPattern() && (parentPath.isObjectProperty({ value: member.node }) || parentPath.isRestElement()) || // ([obj.property = 1] = [])
// ({ property: obj.property = 1 } = {})
parentPath?.isAssignmentPattern({ left: member.node })
);
}
return binding.referencePaths.every(
(path) => (
// obj.property
memberAccess.match(path.parent) && // obj.property = 1
!path.parentPath?.parentPath?.isAssignmentExpression({
left: path.parent
}) && // obj.property++
!path.parentPath?.parentPath?.isUpdateExpression({
argument: path.parent
}) && // delete obj.property
!path.parentPath?.parentPath?.isUnaryExpression({
argument: path.parent,
operator: "delete"
}) && !isPatternAssignment(path.parentPath)
)
);
}
function isTemporaryVariable(binding, references, kind = "var") {
return binding !== void 0 && binding.references === references && binding.constantViolations.length === 1 && (kind === "var" ? binding.path.isVariableDeclarator() && binding.path.node.init === null : binding.path.listKey === "params" && binding.path.isIdentifier());
}
var AnySubListMatcher = class extends m.Matcher {
constructor(matchers) {
super();
this.matchers = matchers;
}
matchers;
matchValue(array, keys) {
if (!Array.isArray(array)) return false;
if (this.matchers.length === 0 && array.length === 0) return true;
let j = 0;
for (let i = 0; i < array.length; i++) {
const matches = this.matchers[j].matchValue(array[i], [...keys, i]);
if (matches) {
j++;
if (j === this.matchers.length) {
return true;
}
}
}
return false;
}
};
function anySubList(...elements) {
return new AnySubListMatcher(elements);
}
// src/ast-utils/inline.ts
function inlineVariable(binding, value = m2.anyExpression(), unsafeAssignments = false) {
const varDeclarator = binding.path.node;
const varMatcher = m2.variableDeclarator(
m2.identifier(binding.identifier.name),
value
);
const assignmentMatcher = m2.assignmentExpression(
"=",
m2.identifier(binding.identifier.name),
value
);
if (binding.constant && varMatcher.match(varDeclarator)) {
binding.referencePaths.forEach((ref) => {
ref.replaceWith(varDeclarator.init);
});
binding.path.remove();
} else if (unsafeAssignments && binding.constantViolations.length >= 1) {
let getNearestAssignment2 = function(location) {
return assignments.findLast((assignment) => assignment.start < location);
};
var getNearestAssignment = getNearestAssignment2;
const assignments = binding.constantViolations.map((path) => path.node).filter((node) => assignmentMatcher.match(node));
if (!assignments.length) return;
for (const ref of binding.referencePaths) {
const assignment = getNearestAssignment2(ref.node.start);
if (assignment) ref.replaceWith(assignment.right);
}
for (const path of binding.constantViolations) {
if (path.parentPath?.isExpressionStatement()) {
path.remove();
} else if (path.isAssignmentExpression()) {
path.replaceWith(path.node.right);
}
}
binding.path.remove();
}
}
function inlineArrayElements(array, references) {
for (const reference of references) {
const memberPath = reference.parentPath;
const property = memberPath.node.property;
const index = property.value;
const replacement = array.elements[index];
memberPath.replaceWith(t3.cloneNode(replacement));
}
}
function inlineObjectProperties(binding, property = m2.objectProperty()) {
const varDeclarator = binding.path.node;
const objectProperties = m2.capture(m2.arrayOf(property));
const varMatcher = m2.variableDeclarator(
m2.identifier(binding.identifier.name),
m2.objectExpression(objectProperties)
);
if (!varMatcher.match(varDeclarator)) return;
const propertyMap = new Map(
objectProperties.current.map((p) => [getPropName(p.key), p.value])
);
if (!binding.referencePaths.every((ref) => {
const member = ref.parent;
const propName = getPropName(member.property);
return propertyMap.has(propName);
}))
return;
binding.referencePaths.forEach((ref) => {
const memberPath = ref.parentPath;
const propName = getPropName(memberPath.node.property);
const value = propertyMap.get(propName);
memberPath.replaceWith(value);
});
binding.path.remove();
}
function inlineFunctionCall(fn, caller) {
if (t3.isRestElement(fn.params[1])) {
caller.replaceWith(
t3.callExpression(
caller.node.arguments[0],
caller.node.arguments.slice(1)
)
);
return;
}
const returnedValue = fn.body.body[0].argument;
const clone = t3.cloneNode(returnedValue, true);
traverse_default(clone, {
Identifier(path) {
const paramIndex = fn.params.findIndex(
(p) => p.name === path.node.name
);
if (paramIndex !== -1) {
path.replaceWith(
caller.node.arguments[paramIndex] ?? t3.unaryExpression("void", t3.numericLiteral(0))
);
path.skip();
}
},
noScope: true
});
caller.replaceWith(clone);
}
function inlineFunctionAliases(binding) {
const state = { changes: 0 };
const refs = [...binding.referencePaths];
for (const ref of refs) {
const fn = findParent(ref, m2.functionDeclaration());
const fnName = m2.capture(m2.anyString());
const returnedCall = m2.capture(
m2.callExpression(
m2.identifier(binding.identifier.name),
m2.anyList(m2.slice({ min: 2 }))
)
);
const matcher16 = m2.functionDeclaration(
m2.identifier(fnName),
m2.anyList(m2.slice({ min: 2 })),
m2.blockStatement([m2.returnStatement(returnedCall)])
);
if (fn && matcher16.match(fn.node)) {
const paramUsedInDecodeCall = fn.node.params.some((param) => {
const binding2 = fn.scope.getBinding(param.name);
return binding2?.referencePaths.some(
(ref2) => ref2.findParent((p) => p.node === returnedCall.current)
);
});
if (!paramUsedInDecodeCall) continue;
const fnBinding = fn.scope.parent.getBinding(fnName.current);
if (!fnBinding) continue;
const fnRefs = fnBinding.referencePaths;
refs.push(...fnRefs);
const callRefs = fnRefs.filter(
(ref2) => t3.isCallExpression(ref2.parent) && t3.isIdentifier(ref2.parent.callee, { name: fnName.current })
).map((ref2) => ref2.parentPath);
for (const callRef of callRefs) {
inlineFunctionCall(fn.node, callRef);
state.changes++;
}
fn.remove();
state.changes++;
}
}
binding.scope.crawl();
return state;
}
function inlineVariableAliases(binding, targetName = binding.identifier.name) {
const state = { changes: 0 };
const refs = [...binding.referencePaths];
const varName = m2.capture(m2.anyString());
const matcher16 = m2.or(
m2.variableDeclarator(
m2.identifier(varName),
m2.identifier(binding.identifier.name)
),
m2.assignmentExpression(
"=",
m2.identifier(varName),
m2.identifier(binding.identifier.name)
)
);
for (const ref of refs) {
if (matcher16.match(ref.parent)) {
const varScope = ref.scope;
const varBinding = varScope.getBinding(varName.current);
if (!varBinding) continue;
if (ref.isIdentifier({ name: varBinding.identifier.name })) continue;
state.changes += inlineVariableAliases(varBinding, targetName).changes;
if (ref.parentPath?.isAssignmentExpression()) {
varBinding.path.remove();
if (t3.isExpressionStatement(ref.parentPath.parent)) {
ref.parentPath.remove();
} else {
ref.parentPath.replaceWith(t3.identifier(targetName));
}
} else if (ref.parentPath?.isVariableDeclarator()) {
ref.parentPath.remove();
}
state.changes++;
} else {
ref.replaceWith(t3.identifier(targetName));
state.changes++;
}
}
return state;
}
// src/ast-utils/rename.ts
import * as t4 from "@babel/types";
import * as m3 from "@codemod/matchers";
function renameFast(binding, newName) {
binding.referencePaths.forEach((ref) => {
if (ref.isExportDefaultDeclaration()) return;
if (!ref.isIdentifier()) {
throw new Error(
`Unexpected reference (${ref.type}): ${codePreview(ref.node)}`
);
}
if (ref.scope.hasBinding(newName)) ref.scope.rename(newName);
ref.node.name = newName;
});
const patternMatcher = m3.assignmentExpression(
"=",
m3.or(m3.arrayPattern(), m3.objectPattern())
);
binding.constantViolations.forEach((ref) => {
if (ref.scope.hasBinding(newName)) ref.scope.rename(newName);
if (ref.isAssignmentExpression() && t4.isIdentifier(ref.node.left)) {
ref.node.left.name = newName;
} else if (ref.isUpdateExpression() && t4.isIdentifier(ref.node.argument)) {
ref.node.argument.name = newName;
} else if (ref.isUnaryExpression({ operator: "delete" }) && t4.isIdentifier(ref.node.argument)) {
ref.node.argument.name = newName;
} else if (ref.isVariableDeclarator() && t4.isIdentifier(ref.node.id)) {
ref.node.id.name = newName;
} else if (ref.isVariableDeclarator() && t4.isArrayPattern(ref.node.id)) {
const ids = ref.getBindingIdentifiers();
for (const id in ids) {
if (id === binding.identifier.name) {
ids[id].name = newName;
}
}
} else if (ref.isFor() || patternMatcher.match(ref.node)) {
traverse_default(ref.node, {
Identifier(path) {
if (path.scope !== ref.scope) return path.skip();
if (path.node.name === binding.identifier.name) {
path.node.name = newName;
}
},
noScope: true
});
} else if (ref.isFunctionDeclaration() && t4.isIdentifier(ref.node.id)) {
ref.node.id.name = newName;
} else {
throw new Error(
`Unexpected constant violation (${ref.type}): ${codePreview(ref.node)}`
);
}
});
binding.scope.removeOwnBinding(binding.identifier.name);
binding.scope.bindings[newName] = binding;
binding.identifier.name = newName;
}
function renameParameters(path, newNames) {
const params = path.node.params;
for (let i = 0; i < Math.min(params.length, newNames.length); i++) {
const binding = path.scope.getBinding(params[i].name);
renameFast(binding, newNames[i]);
}
}
// src/ast-utils/transform.ts
import debug from "debug";
var logger = debug("webcrack:transforms");
async function applyTransformAsync(ast, transform, options) {
logger(`${transform.name}: started`);
const state = { changes: 0 };
await transform.run?.(ast, state, options);
if (transform.visitor)
traverse_default(ast, transform.visitor(options), void 0, state);
logger(`${transform.name}: finished with ${state.changes} changes`);
return state;
}
function applyTransform(ast, transform, options) {
logger(`${transform.name}: started`);
const state = { changes: 0 };
transform.run?.(ast, state, options);
if (transform.visitor) {
const visitor = transform.visitor(
options
);
visitor.noScope = !transform.scope;
traverse_default(ast, visitor, void 0, state);
}
logger(`${transform.name}: finished with ${state.changes} changes`);
return state;
}
function applyTransforms(ast, transforms, options = {}) {
options.log ??= true;
const name = options.name ?? transforms.map((t45) => t45.name).join(", ");
if (options.log) logger(`${name}: started`);
const state = { changes: 0 };
for (const transform of transforms) {
transform.run?.(ast, state);
}
const traverseOptions = transforms.flatMap((t45) => t45.visitor?.() ?? []);
if (traverseOptions.length > 0) {
const visitor = traverse_exports.visitors.merge(traverseOptions);
visitor.noScope = options.noScope || transforms.every((t45) => !t45.scope);
traverse_default(ast, visitor, void 0, state);
}
if (options.log) logger(`${name}: finished with ${state.changes} changes`);
return state;
}
function mergeTransforms(options) {
return {
name: options.name,
tags: options.tags,
scope: options.transforms.some((t45) => t45.scope),
visitor() {
return traverse_exports.visitors.merge(
options.transforms.flatMap((t45) => t45.visitor?.() ?? [])
);
}
};
}
// src/deobfuscate/index.ts
import debug3 from "debug";
// src/unminify/transforms/merge-strings.ts
import * as m4 from "@codemod/matchers";
var merge_strings_default = {
name: "merge-strings",
tags: ["safe"],
visitor() {
const left = m4.capture(m4.stringLiteral());
const right = m4.capture(m4.stringLiteral());
const matcher16 = m4.binaryExpression(
"+",
m4.or(left, m4.binaryExpression("+", m4.anything(), left)),
right
);
return {
BinaryExpression: {
exit(path) {
if (!matcher16.match(path.node)) return;
left.current.value += right.current.value;
right.current.value = "";
path.replaceWith(path.node.left);
path.skip();
this.changes++;
}
}
};
}
};
// src/deobfuscate/array-rotator.ts
import * as m5 from "@codemod/matchers";
import { callExpression as callExpression5 } from "@codemod/matchers";
function findArrayRotator(stringArray) {
const arrayIdentifier = m5.capture(m5.identifier());
const pushShift = m5.callExpression(
constMemberExpression(arrayIdentifier, "push"),
[
m5.callExpression(
constMemberExpression(m5.fromCapture(arrayIdentifier), "shift")
)
]
);
const callMatcher = iife(
m5.anything(),
m5.blockStatement(
m5.anyList(
m5.zeroOrMore(),
infiniteLoop(
m5.matcher((node) => {
return m5.containerOf(callExpression5(m5.identifier("parseInt"))).match(node) && m5.blockStatement([
m5.tryStatement(
m5.containerOf(pushShift),
m5.containerOf(pushShift)
)
]).match(node);
})
)
)
)
);
const matcher16 = m5.expressionStatement(
m5.or(callMatcher, m5.unaryExpression("!", callMatcher))
);
for (const ref of stringArray.references) {
const rotator = findParent(ref, matcher16);
if (rotator) {
return rotator;
}
}
}
// src/deobfuscate/control-flow-object.ts
import * as t5 from "@babel/types";
import * as m6 from "@codemod/matchers";
var control_flow_object_default = {
name: "control-flow-object",
tags: ["safe"],
scope: true,
visitor() {
const varId = m6.capture(m6.identifier());
const propertyName = m6.matcher((name) => /^[a-z]{5}$/i.test(name));
const propertyKey = constKey(propertyName);
const propertyValue = m6.or(
// E.g. "6|0|4|3|1|5|2"
m6.stringLiteral(),
// E.g. function (a, b) { return a + b }
createFunctionMatcher(2, (left, right) => [
m6.returnStatement(
m6.or(
m6.binaryExpression(void 0, left, right),
m6.logicalExpression(void 0, left, right),
m6.binaryExpression(void 0, right, left),
m6.logicalExpression(void 0, right, left)
)
)
]),
// E.g. function (a, b, c) { return a(b, c) } with an arbitrary number of arguments
m6.matcher((node) => {
return t5.isFunctionExpression(node) && createFunctionMatcher(node.params.length, (...params) => [
m6.returnStatement(m6.callExpression(params[0], params.slice(1)))
]).match(node);
}),
// E.g. function (a, ...b) { return a(...b) }
(() => {
const fnName = m6.capture(m6.identifier());
const restName = m6.capture(m6.identifier());
return m6.functionExpression(
void 0,
[fnName, m6.restElement(restName)],
m6.blockStatement([
m6.returnStatement(
m6.callExpression(m6.fromCapture(fnName), [
m6.spreadElement(m6.fromCapture(restName))
])
)
])
);
})()
);
const objectProperties = m6.capture(
m6.arrayOf(m6.objectProperty(propertyKey, propertyValue))
);
const aliasId = m6.capture(m6.identifier());
const aliasVar = m6.variableDeclaration(m6.anything(), [
m6.variableDeclarator(aliasId, m6.fromCapture(varId))
]);
const assignedKey = m6.capture(propertyName);
const assignedValue = m6.capture(propertyValue);
const assignment = m6.expressionStatement(
m6.assignmentExpression(
"=",
constMemberExpression(m6.fromCapture(varId), assignedKey),
assignedValue
)
);
const looseAssignment = m6.expressionStatement(
m6.assignmentExpression(
"=",
constMemberExpression(m6.fromCapture(varId), assignedKey)
)
);
const memberAccess = constMemberExpression(
m6.or(m6.fromCapture(varId), m6.fromCapture(aliasId)),
propertyName
);
const varMatcher = m6.variableDeclarator(
varId,
m6.objectExpression(objectProperties)
);
const inlineMatcher = constMemberExpression(
m6.objectExpression(objectProperties),
propertyName
);
function isConstantBinding(binding) {
return binding.constant || binding.constantViolations[0] === binding.path;
}
function transform(path) {
let changes = 0;
if (varMatcher.match(path.node)) {
const binding = path.scope.getBinding(varId.current.name);
if (!binding) return changes;
if (!isConstantBinding(binding)) return changes;
if (!transformObjectKeys(binding)) return changes;
if (!isReadonlyObject(binding, memberAccess)) return changes;
const props = new Map(
objectProperties.current.map((p) => [
getPropName(p.key),
p.value
])
);
if (!props.size) return changes;
const oldRefs = [...binding.referencePaths];
[...binding.referencePaths].reverse().forEach((ref) => {
const memberPath = ref.parentPath;
const propName = getPropName(memberPath.node.property);
const value = props.get(propName);
if (!value) {
ref.addComment("leading", "webcrack:control_flow_missing_prop");
return;
}
if (t5.isStringLiteral(value)) {
memberPath.replaceWith(value);
} else {
inlineFunctionCall(
value,
memberPath.parentPath
);
}
changes++;
});
oldRefs.forEach((ref) => {
const varDeclarator = findParent(ref, m6.variableDeclarator());
if (varDeclarator) changes += transform(varDeclarator);
});
path.remove();
changes++;
}
return changes;
}
function transformObjectKeys(objBinding) {
const container = objBinding.path.parentPath.container;
const startIndex = objBinding.path.parentPath.key + 1;
const properties = [];
for (let i = startIndex; i < container.length; i++) {
const statement5 = container[i];
if (looseAssignment.match(statement5)) {
applyTransform(statement5, merge_strings_default);
}
if (assignment.match(statement5)) {
properties.push(
t5.objectProperty(
t5.identifier(assignedKey.current),
assignedValue.current
)
);
} else {
break;
}
}
const aliasAssignment = container[startIndex + properties.length];
if (!aliasVar.match(aliasAssignment)) return true;
if (objBinding.references !== properties.length + 1) return false;
const aliasBinding = objBinding.scope.getBinding(aliasId.current.name);
if (!isReadonlyObject(aliasBinding, memberAccess)) return false;
objectProperties.current.push(...properties);
container.splice(startIndex, properties.length);
objBinding.referencePaths = aliasBinding.referencePaths;
objBinding.references = aliasBinding.references;
objBinding.identifier.name = aliasBinding.identifier.name;
aliasBinding.path.remove();
return true;
}
return {
VariableDeclarator: {
exit(path) {
this.changes += transform(path);
}
},
MemberExpression: {
exit(path) {
if (!inlineMatcher.match(path.node)) return;
const propName = getPropName(path.node.property);
const value = objectProperties.current.find(
(prop) => getPropName(prop.key) === propName
)?.value;
if (!value) return;
if (t5.isStringLiteral(value)) {
path.replaceWith(value);
} else if (path.parentPath.isCallExpression()) {
inlineFunctionCall(value, path.parentPath);
} else {
path.replaceWith(value);
}
this.changes++;
}
}
};
}
};
// src/deobfuscate/control-flow-switch.ts
import * as t6 from "@babel/types";
import * as m7 from "@codemod/matchers";
var control_flow_switch_default = {
name: "control-flow-switch",
tags: ["safe"],
visitor() {
const sequenceName = m7.capture(m7.identifier());
const sequenceString = m7.capture(
m7.matcher((s) => /^\d+(\|\d+)*$/.test(s))
);
const iterator = m7.capture(m7.identifier());
const cases = m7.capture(
m7.arrayOf(
m7.switchCase(
m7.stringLiteral(m7.matcher((s) => /^\d+$/.test(s))),
m7.anyList(
m7.zeroOrMore(),
m7.or(m7.continueStatement(), m7.returnStatement())
)
)
)
);
const matcher16 = m7.blockStatement(
m7.anyList(
// E.g. const sequence = "2|4|3|0|1".split("|")
m7.variableDeclaration(void 0, [
m7.variableDeclarator(
sequenceName,
m7.callExpression(
constMemberExpression(m7.stringLiteral(sequenceString), "split"),
[m7.stringLiteral("|")]
)
)
]),
// E.g. let iterator = 0 or -0x1a70 + 0x93d + 0x275 * 0x7
m7.variableDeclaration(void 0, [m7.variableDeclarator(iterator)]),
infiniteLoop(
m7.blockStatement([
m7.switchStatement(
// E.g. switch (sequence[iterator++]) {
m7.memberExpression(
m7.fromCapture(sequenceName),
m7.updateExpression("++", m7.fromCapture(iterator)),
true
),
cases
),
m7.breakStatement()
])
),
m7.zeroOrMore()
)
);
return {
BlockStatement: {
exit(path) {
if (!matcher16.match(path.node)) return;
const caseStatements = new Map(
cases.current.map((c) => [
c.test.value,
t6.isContinueStatement(c.consequent.at(-1)) ? c.consequent.slice(0, -1) : c.consequent
])
);
const sequence = sequenceString.current.split("|");
const newStatements = sequence.flatMap((s) => caseStatements.get(s));
path.node.body.splice(0, 3, ...newStatements);
this.changes += newStatements.length + 3;
}
}
};
}
};
// src/deobfuscate/dead-code.ts
import * as t7 from "@babel/types";
import * as m8 from "@codemod/matchers";
var dead_code_default = {
name: "dead-code",
tags: ["unsafe"],
scope: true,
visitor() {
const stringComparison = m8.binaryExpression(
m8.or("===", "==", "!==", "!="),
m8.stringLiteral(),
m8.stringLiteral()
);
const testMatcher = m8.or(
stringComparison,
m8.unaryExpression("!", stringComparison)
);
return {
"IfStatement|ConditionalExpression": {
exit(_path) {
const path = _path;
if (!testMatcher.match(path.node.test)) return;
if (path.get("test").evaluateTruthy()) {
replace(path, path.get("consequent"));
} else if (path.node.alternate) {
replace(path, path.get("alternate"));
} else {
path.remove();
}
this.changes++;
}
}
};
}
};
function replace(path, replacement) {
if (t7.isBlockStatement(replacement.node)) {
const childBindings = replacement.scope.bindings;
for (const name in childBindings) {
const binding = childBindings[name];
if (path.scope.hasOwnBinding(name)) {
renameFast(binding, path.scope.generateUid(name));
}
binding.scope = path.scope;
path.scope.bindings[binding.identifier.name] = binding;
}
path.replaceWithMultiple(replacement.node.body);
} else {
path.replaceWith(replacement);
}
}
// src/deobfuscate/decoder.ts
import { expression } from "@babel/template";
import * as m9 from "@codemod/matchers";
var Decoder = class {
originalName;
name;
path;
constructor(originalName, name, path) {
this.originalName = originalName;
this.name = name;
this.path = path;
}
collectCalls() {
const calls = [];
const literalArgument = m9.or(
m9.binaryExpression(
m9.anything(),
m9.matcher((node) => literalArgument.match(node)),
m9.matcher((node) => literalArgument.match(node))
),
m9.unaryExpression(
"-",
m9.matcher((node) => literalArgument.match(node))
),
m9.numericLiteral(),
m9.stringLiteral()
);
const literalCall = m9.callExpression(
m9.identifier(this.name),
m9.arrayOf(literalArgument)
);
const expressionCall = m9.callExpression(
m9.identifier(this.name),
m9.arrayOf(m9.anyExpression())
);
const conditional = m9.capture(m9.conditionalExpression());
const conditionalCall = m9.callExpression(m9.identifier(this.name), [
conditional
]);
const buildExtractedConditional = expression`TEST ? CALLEE(CONSEQUENT) : CALLEE(ALTERNATE)`;
const binding = this.path.scope.getBinding(this.name);
for (const ref of binding.referencePaths) {
if (conditionalCall.match(ref.parent)) {
const [replacement] = ref.parentPath.replaceWith(
buildExtractedConditional({
TEST: conditional.current.test,
CALLEE: ref.parent.callee,
CONSEQUENT: conditional.current.consequent,
ALTERNATE: conditional.current.alternate
})
);
replacement.scope.crawl();
} else if (literalCall.match(ref.parent)) {
calls.push(ref.parentPath);
} else if (expressionCall.match(ref.parent)) {
ref.parentPath.traverse({
ReferencedIdentifier(path) {
const varBinding = path.scope.getBinding(path.node.name);
if (!varBinding) return;
inlineVariable(varBinding, literalArgument, true);
}
});
if (literalCall.match(ref.parent)) {
calls.push(ref.parentPath);
}
} else if (ref.parentPath?.isExpressionStatement()) {
ref.parentPath.remove();
}
}
return calls;
}
};
function findDecoders(stringArray) {
const decoders = [];
const functionName = m9.capture(m9.anyString());
const arrayIdentifier = m9.capture(m9.identifier());
const matcher16 = m9.functionDeclaration(
m9.identifier(functionName),
m9.anything(),
m9.blockStatement(
anySubList(
// var array = getStringArray();
m9.variableDeclaration(void 0, [
m9.variableDeclarator(
arrayIdentifier,
m9.callExpression(m9.identifier(stringArray.name))
)
]),
// var h = array[e]; return h;
// or return array[e -= 254];
m9.containerOf(
m9.memberExpression(m9.fromCapture(arrayIdentifier), void 0, true)
)
)
)
);
for (const ref of stringArray.references) {
const decoderFn = findParent(ref, matcher16);
if (decoderFn) {
const oldName = functionName.current;
const newName = `__DECODE_${decoders.length}__`;
const binding = decoderFn.scope.getBinding(oldName);
renameFast(binding, newName);
decoders.push(new Decoder(oldName, newName, decoderFn));
}
}
return decoders;
}
// src/deobfuscate/inline-decoded-strings.ts
import * as t8 from "@babel/types";
var inline_decoded_strings_default = {
name: "inline-decoded-strings",
tags: ["unsafe"],
scope: true,
async run(ast, state, options) {
if (!options) return;
const calls = options.vm.decoders.flatMap(
(decoder) => decoder.collectCalls()
);
if (calls.length === 0) return;
const decodedValues = await options.vm.decode(calls);
for (let i = 0; i < calls.length; i++) {
const call = calls[i];
const value = decodedValues[i];
call.replaceWith(t8.valueToNode(value));
if (typeof value !== "string")
call.addComment("leading", "webcrack:decode_error");
}
state.changes += calls.length;
}
};
// src/deobfuscate/inline-decoder-wrappers.ts
var inline_decoder_wrappers_default = {
name: "inline-decoder-wrappers",
tags: ["unsafe"],
scope: true,
run(ast, state, decoder) {
if (!decoder?.node.id) return;
const decoderName = decoder.node.id.name;
const decoderBinding = decoder.parentPath.scope.getBinding(decoderName);
if (decoderBinding) {
state.changes += inlineVariableAliases(decoderBinding).changes;
state.changes += inlineFunctionAliases(decoderBinding).changes;
}
}
};
// src/deobfuscate/inline-object-props.ts
import * as m10 from "@codemod/matchers";
var inline_object_props_default = {
name: "inline-object-props",
tags: ["safe"],
scope: true,
visitor() {
const varId = m10.capture(m10.identifier());
const propertyName = m10.capture(
m10.matcher((name) => /^[\w]+$/i.test(name))
);
const propertyKey = constKey(propertyName);
const objectProperties = m10.capture(
m10.arrayOf(
m10.objectProperty(
propertyKey,
m10.or(m10.stringLiteral(), m10.numericLiteral())
)
)
);
const memberAccess = constMemberExpression(
m10.fromCapture(varId),
propertyName
);
const varMatcher = m10.variableDeclarator(
varId,
m10.objectExpression(objectProperties)
);
const literalMemberAccess = constMemberExpression(
m10.objectExpression(objectProperties),
propertyName
);
return {
MemberExpression(path) {
if (!literalMemberAccess.match(path.node)) return;
const property = objectProperties.current.find(
(p) => getPropName(p.key) === propertyName.current
);
if (!property) return;
path.replaceWith(property.value);
this.changes++;
},
VariableDeclarator(path) {
if (!varMatcher.match(path.node)) return;
if (objectProperties.current.length === 0) return;
const binding = path.scope.getBinding(varId.current.name);
if (!binding || !isReadonlyObject(binding, memberAccess)) return;
inlineObjectProperties(
binding,
m10.objectProperty(
propertyKey,
m10.or(m10.stringLiteral(), m10.numericLiteral())
)
);
this.changes++;
}
};
}
};
// src/deobfuscate/string-array.ts
import * as m11 from "@codemod/matchers";
function findStringArray(ast) {
let result;
const functionName = m11.capture(m11.anyString());
const arrayIdentifier = m11.capture(m11.identifier());
const arrayExpression9 = m11.capture(
m11.arrayExpression(m11.arrayOf(m11.or(m11.stringLiteral(), undefinedMatcher)))
);
const functionAssignment = m11.assignmentExpression(
"=",
m11.identifier(m11.fromCapture(functionName)),
m11.functionExpression(
void 0,
[],
m11.blockStatement([m11.returnStatement(m11.fromCapture(arrayIdentifier))])
)
);
const variableDeclaration16 = m11.variableDeclaration(void 0, [
m11.variableDeclarator(arrayIdentifier, arrayExpression9)
]);
const matcher16 = m11.functionDeclaration(
m11.identifier(functionName),
[],
m11.or(
// var array = ["hello", "world"];
// return (getStringArray = function () { return array; })();
m11.blockStatement([
variableDeclaration16,
m11.returnStatement(m11.callExpression(functionAssignment))
]),
// var array = ["hello", "world"];
// getStringArray = function () { return array; });
// return getStringArray();
m11.blockStatement([
variableDeclaration16,
m11.expressionStatement(functionAssignment),
m11.returnStatement(m11.callExpression(m11.identifier(functionName)))
])
)
);
traverse_default(ast, {
// Wrapped string array from later javascript-obfuscator versions
FunctionDeclaration(path) {
if (matcher16.match(path.node)) {
const length = arrayExpression9.current.elements.length;
const name = functionName.current;
const binding = path.scope.getBinding(name);
renameFast(binding, "__STRING_ARRAY__");
result = {
path,
references: binding.referencePaths,
originalName: name,
name: "__STRING_ARRAY__",
length
};
path.stop();
}
},
// Simple string array inlining (only `array[0]`, `array[1]` etc references, no rotating/decoding).
// May be used by older or different obfuscators
VariableDeclaration(path) {
if (!variableDeclaration16.match(path.node)) return;
const length = arrayExpression9.current.elements.length;
const binding = path.scope.getBinding(arrayIdentifier.current.name);
const memberAccess = m11.memberExpression(
m11.fromCapture(arrayIdentifier),
m11.numericLiteral(m11.matcher((value) => value < length))
);
if (!binding.referenced || !isReadonlyObject(binding, memberAccess))
return;
inlineArrayElements(arrayExpression9.current, binding.referencePaths);
path.remove();
}
});
return result;
}
// src/deobfuscate/vm.ts
import debug2 from "debug";
function createNodeSandbox() {
return async (code) => {
const {
default: { Isolate }
} = await import("isolated-vm");
const isolate = new Isolate();
const context = await isolate.createContext();
const result = await context.eval(code, {
timeout: 1e4,
copy: true,
filename: "file:///obfuscated.js"
});
context.release();
isolate.dispose();
return result;
};
}
function createBrowserSandbox() {
return () => {
throw new Error("Custom Sandbox implementation required.");
};
}
var VMDecoder = class {
decoders;
setupCode;
sandbox;
constructor(sandbox, stringArray, decoders, rotator) {
this.sandbox = sandbox;
this.decoders = decoders;
const generateOptions = {
compact: true,
shouldPrintComment: () => false
};
const stringArrayCode = generate(stringArray.path.node, generateOptions);
const rotatorCode = rotator ? generate(rotator.node, generateOptions) : "";
const decoderCode = decoders.map((decoder) => generate(decoder.path.node, generateOptions)).join(";\n");
this.setupCode = [stringArrayCode, rotatorCode, decoderCode].join(";\n");
}
async decode(calls) {
const code = `(() => {
${this.setupCode}
return [${calls.join(",")}]
})()`;
try {
const result = await this.sandbox(code);
return result;
} catch (error) {
debug2("webcrack:deobfuscate")("vm code:", code);
if (error instanceof Error && (error.message.includes("undefined symbol") || error.message.includes("Segmentation fault"))) {
throw new Error(
"isolated-vm version mismatch. Check https://webcrack.netlify.app/docs/guide/common-errors.html#isolated-vm",
{ cause: error }
);
}
throw error;
}
}
};
// src/deobfuscate/index.ts
var deobfuscate_default = {
name: "deobfuscate",
tags: ["unsafe"],
scope: true,
async run(ast, state, sandbox) {
if (!sandbox) return;
const logger2 = debug3("webcrack:deobfuscate");
const stringArray = findStringArray(ast);
logger2(
stringArray ? `String Array: ${stringArray.originalName}, length ${stringArray.length}` : "String Array: no"
);
if (!stringArray) return;
const rotator = findArrayRotator(stringArray);
logger2(`String Array Rotate: ${rotator ? "yes" : "no"}`);
const decoders = findDecoders(stringArray);
logger2(
`String Array Decoders: ${decoders.map((d) => d.originalName).join(", ")}`
);
state.changes += applyTransform(ast, inline_object_props_default).changes;
for (const decoder of decoders) {
state.changes += applyTransform(
ast,
inline_decoder_wrappers_default,
decoder.path
).changes;
}
const vm = new VMDecoder(sandbox, stringArray, decoders, rotator);
state.changes += (await applyTransformAsync(ast, inline_decoded_strings_default, { vm })).changes;
if (decoders.length > 0) {
stringArray.path.remove();
rotator?.remove();
decoders.forEach((decoder) => decoder.path.remove());
state.changes += 2 + decoders.length;
}
state.changes += applyTransforms(
ast,
[merge_strings_default, dead_code_default, control_flow_object_default, control_flow_switch_default],
{ noScope: true }
).changes;
}
};
// src/deobfuscate/debug-protection.ts
import * as m12 from "@codemod/matchers";
import { ifStatement as ifStatement2 } from "@codemod/matchers";
var debug_protection_default = {
name: "debug-protection",
tags: ["safe"],
scope: true,
visitor() {
const ret = m12.capture(m12.identifier());
const debugProtectionFunctionName = m12.capture(m12.anyString());
const debuggerProtection = m12.capture(m12.identifier());
const counter = m12.capture(m12.identifier());
const debuggerTemplate = m12.ifStatement(
void 0,
void 0,
m12.containerOf(
m12.or(
m12.debuggerStatement(),
m12.callExpression(
constMemberExpression(m12.anyExpression(), "constructor"),
[m12.stringLiteral("debugger")]
)
)
)
);
const intervalCall = m12.callExpression(
constMemberExpression(m12.anyExpression(), "setInterval"),
[
m12.identifier(m12.fromCapture(debugProtectionFunctionName)),
m12.numericLiteral()
]
);
const matcher16 = m12.functionDeclaration(
m12.identifier(debugProtectionFunctionName),
[ret],
m12.blockStatement([
// function debuggerProtection (counter) {
m12.functionDeclaration(
debuggerProtection,
[counter],
m12.blockStatement([
debuggerTemplate,
// debuggerProtection(++counter);
m12.expressionStatement(
m12.callExpression(m12.fromCapture(debuggerProtection), [
m12.updateExpression("++", m12.fromCapture(counter), true)
])
)
])
),
m12.tryStatement(
m12.blockStatement([
// if (ret) {
ifStatement2(
m12.fromCapture(ret),
// return debuggerProtection;
m12.blockStatement([
m12.returnStatement(m12.fromCapture(debuggerProtection))
]),
// } else { debuggerProtection(0); }
m12.blockStatement([
m12.expressionStatement(
m12.callExpression(m12.fromCapture(debuggerProtection), [
m12.numericLiteral(0)
])
)
])
)
])
)
])
);
return {
FunctionDeclaration(path) {
if (!matcher16.match(path.node)) return;
const binding = path.scope.getBinding(
debugProtectionFunctionName.current
);
binding?.referencePaths.forEach((ref) => {
if (intervalCall.match(ref.parent)) {
findParent(ref, iife())?.remove();
}
});
path.remove();
}
};
}
};
// src/deobfuscate/evaluate-globals.ts
import * as t9 from "@babel/types";
import * as m13 from "@codemod/matchers";
var FUNCTIONS = {
atob,
unescape,
decodeURI,
decodeURIComponent
};
var evaluate_globals_default = {
name: "evaluate-globals",
tags: ["safe"],
scope: true,
visitor() {
const name = m13.capture(
m13.or(...Object.keys(FUNCTIONS))
);
const arg = m13.capture(m13.anyString());
const matcher16 = m13.callExpression(m13.identifier(name), [
m13.stringLiteral(arg)
]);
return {
CallExpression: {
exit(path) {
if (!matcher16.match(path.node)) return;
if (path.scope.hasBinding(name.current, { noGlobals: true })) return;
try {
const value = FUNCTIONS[name.current].call(
globalThis,
arg.current
);
path.replaceWith(t9.stringLiteral(value));
this.changes++;
} catch {
}
}
}
};
}
};
// src/deobfuscate/merge-object-assignments.ts
import * as t10 from "@babel/types";
import * as m14 from "@codemod/matchers";
var merge_object_assignments_default = {
name: "merge-object-assignments",
tags: ["safe"],
scope: true,
visitor: () => {
const id = m14.capture(m14.identifier());
const object = m14.capture(m14.objectExpression([]));
const varMatcher = m14.variableDeclaration(void 0, [
m14.variableDeclarator(id, object)
]);
const key = m14.capture(m14.anyExpression());
const computed = m14.capture(m14.anything());
const value = m14.capture(m14.anyExpression());
const assignmentMatcher = m14.expressionStatement(
m14.assignmentExpression(
"=",
m14.memberExpression(m14.fromCapture(id), key, computed),
value
)
);
return {
Program(path) {
path.scope.crawl();
},
VariableDeclaration: {
exit(path) {
if (!path.inList || !varMatcher.match(path.node)) return;
const binding = path.scope.getBinding(id.current.name);
const container = path.container;
const siblingIndex = path.key + 1;
while (siblingIndex < container.length) {
const sibling = path.getSibling(siblingIndex);
if (!assignmentMatcher.match(sibling.node) || hasCircularReference(value.current, binding))
return;
const isComputed = computed.current && key.current.type !== "NumericLiteral" && key.current.type !== "StringLiteral";
object.current.properties.push(
t10.objectProperty(key.current, value.current, isComputed)
);
sibling.remove();
binding.dereference();
binding.referencePaths.shift();
if (binding.references === 1 && inlineableObject.match(object.current) && !isRepeatedCallReference(binding, binding.referencePaths[0])) {
binding.referencePaths[0].replaceWith(object.current);
path.remove();
this.changes++;
}
}
}
}
};
}
};
function hasCircularReference(node, binding) {
return (
// obj.foo = obj;
binding.referencePaths.some((path) => path.find((p) => p.node === node)) || // obj.foo = fn(); where fn could reference the binding or not, for simplicity we assume it does.
m14.containerOf(m14.callExpression()).match(node)
);
}
var repeatedCallMatcher = m14.or(
m14.forStatement(),
m14.forOfStatement(),
m14.forInStatement(),
m14.whileStatement(),
m14.doWhileStatement(),
m14.function(),
m14.objectMethod(),
m14.classBody()
);
function isRepeatedCallReference(binding, reference) {
const block = binding.scope.getBlockParent().path;
const repeatable = findParent(reference, repeatedCallMatcher);
return repeatable?.isDescendant(block);
}
var inlineableObject = m14.matcher(
(node) => m14.or(
safeLiteral,
m14.arrayExpression(m14.arrayOf(inlineableObject)),
m14.objectExpression(m14.arrayOf(constObjectProperty(inlineableObject)))
).match(node)
);
// src/deobfuscate/self-defending.ts
import * as m15 from "@codemod/matchers";
var self_defending_default = {
name: "self-defending",
tags: ["safe"],
scope: true,
visitor() {
const callController = m15.capture(m15.anyString());
const firstCall = m15.capture(m15.identifier());
const rfn