Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance Improvements #131

Merged
merged 22 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
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
488 changes: 49 additions & 439 deletions package-lock.json

Large diffs are not rendered by default.

7 changes: 3 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@
"test": "tests"
},
"dependencies": {
"flast": "^2.1.1",
"isolated-vm": "^5.0.2",
"jsdom": "^25.0.1",
"obfuscation-detector": "^2.0.4"
"flast": "2.2.1",
"isolated-vm": "^5.0.3",
"obfuscation-detector": "^2.0.5"
},
"scripts": {
"test": "node --test --trace-warnings --no-node-snapshot",
Expand Down
4 changes: 3 additions & 1 deletion src/modules/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const defaultMaxIterations = {
valueOf() {return this.value--;},
};

const propertiesThatModifyContent = ['push', 'forEach', 'pop', 'insert', 'add', 'set', 'delete'];
const propertiesThatModifyContent = [
'push', 'forEach', 'pop', 'insert', 'add', 'set', 'delete', 'shift', 'unshift', 'splice'
];

// Builtin functions that shouldn't be resolved in the deobfuscation context.
const skipBuiltinFunctions = [
Expand Down
32 changes: 24 additions & 8 deletions src/modules/safe/rearrangeSwitches.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,32 @@ function rearrangeSwitches(arb, candidateFilter = () => true) {
let counter = 0;
while (currentVal !== undefined && counter < maxRepetition) {
// A matching case or the default case
let currentCase = cases.find(c => c.test?.value === currentVal) || cases.find(c => !c.test);
let currentCase;
for (let j = 0; j < cases.length; j++) {
if (cases[j].test?.value === currentVal || !cases[j].test) {
currentCase = cases[j];
break;
}
}
if (!currentCase) break;
ordered.push(...currentCase.consequent.filter(c => c.type !== 'BreakStatement'));

for (let j = 0; j < currentCase.consequent.length; j++) {
if (currentCase.consequent[j].type !== 'BreakStatement') {
ordered.push(currentCase.consequent[j]);
}
}
let allDescendants = [];
currentCase.consequent.forEach(c => allDescendants.push(...getDescendants(c)));
const assignments2Next = allDescendants.filter(d =>
d.declNode === n.discriminant.declNode &&
d.parentKey === 'left' &&
d.parentNode.type === 'AssignmentExpression');
for (let j = 0; j < currentCase.consequent.length; j++) {
allDescendants.push(...getDescendants(currentCase.consequent[j]));
}
const assignments2Next = [];
for (let j = 0; j < allDescendants.length; j++) {
const d = allDescendants[j];
if (d.declNode === n.discriminant.declNode &&
d.parentKey === 'left' &&
d.parentNode.type === 'AssignmentExpression') {
assignments2Next.push(d);
}
}
if (assignments2Next.length === 1) {
currentVal = assignments2Next[0].parentNode.right.value;
} else {
Expand Down
19 changes: 11 additions & 8 deletions src/modules/safe/resolveFunctionConstructorCalls.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,27 @@ function resolveFunctionConstructorCalls(arb, candidateFilter = () => true) {
const relevantNodes = [
...(arb.ast[0].typeMap.CallExpression || []),
];
for (let i = 0; i < relevantNodes.length; i++) {
nodeLoop: for (let i = 0; i < relevantNodes.length; i++) {
const n = relevantNodes[i];
if (n.callee?.type === 'MemberExpression' &&
(n.callee.property?.name || n.callee.property?.value) === 'constructor' &&
n.arguments.length && n.arguments.slice(-1)[0].type === 'Literal' &&
candidateFilter(n)) {let args = '';
candidateFilter(n)) {
let args = '';
let code = '';
if (n.arguments.length > 1) {
const originalArgs = n.arguments.slice(0, -1);
if (originalArgs.find(n => n.type !== 'Literal')) continue;
args = originalArgs.map(n => n.value).join(', ');
}
for (let j = 0; j < n.arguments.length; j++) {
if (n.arguments[j].type !== 'Literal') continue nodeLoop;
if (code) args += (args.length ? ', ' : '') + code;
code = n.arguments[j].value;
}
} else code = n.arguments[0].value;
// Wrap the code in a valid anonymous function in the same way Function.constructor would.
// Give the anonymous function any arguments it may require.
// Wrap the function in an expression to make it a valid code (since it's anonymous).
// Generate an AST without nodeIds (to avoid duplicates with the rest of the code).
// Extract just the function expression from the AST.
try {
const codeNode = generateFlatAST(`(function (${args}) {${n.arguments.slice(-1)[0].value}})`,
const codeNode = generateFlatAST(`(function (${args}) {${code}})`,
{detailed: false, includeSrc: false})[2];
if (codeNode) arb.markNode(n, codeNode);
} catch {}
Expand Down
20 changes: 13 additions & 7 deletions src/modules/safe/resolveMemberExpressionsWithDirectAssignment.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,20 +13,26 @@ function resolveMemberExpressionsWithDirectAssignment(arb, candidateFilter = ()
const relevantNodes = [
...(arb.ast[0].typeMap.MemberExpression || []),
];
for (let i = 0; i < relevantNodes.length; i++) {
rnLoop: for (let i = 0; i < relevantNodes.length; i++) {
const n = relevantNodes[i];
if (n.object.declNode &&
n.parentNode.type === 'AssignmentExpression' &&
n.parentNode.right.type === 'Literal' &&
candidateFilter(n)) {
const prop = n.property?.value || n.property?.name;
const valueUses = n.object.declNode.references.filter(ref =>
ref.parentNode !== n && ref.parentNode.type === 'MemberExpression' &&
prop === ref.parentNode.property[ref.parentNode.property.computed ? 'value' : 'name']);
const valueUses = [];
for (let j = 0; j < n.object.declNode.references.length; j++) {
/** @type {ASTNode} */
const ref = n.object.declNode.references[j];
if (ref.parentNode !== n && ref.parentNode.type === 'MemberExpression' &&
prop === ref.parentNode.property[ref.parentNode.property.computed ? 'value' : 'name']) {
// Skip if the value is reassigned
if (ref.parentNode.parentNode.type === 'UpdateExpression' ||
(ref.parentNode.parentNode.type === 'AssignmentExpression' && ref.parentNode.parentKey === 'left')) continue rnLoop;
valueUses.push(ref);
}
}
if (valueUses.length) {
// Skip if the value is reassigned
if (valueUses.some(v => v.parentNode.parentNode.type === 'UpdateExpression' ||
(v.parentNode.parentNode.type === 'AssignmentExpression' && v.parentNode.parentKey === 'left'))) continue;
const replacementNode = n.parentNode.right;
for (let j = 0; j < valueUses.length; j++) {
arb.markNode(valueUses[j].parentNode, replacementNode);
Expand Down
4 changes: 2 additions & 2 deletions src/modules/safe/resolveProxyReferences.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {getDescendants} from '../utils/getDescendants.js';
import {areReferencesModified} from '../utils/areReferencesModified.js';
import {doesDescendantMatchCondition} from '../utils/doesDescendantMatchCondition.js';
import {getMainDeclaredObjectOfMemberExpression} from '../utils/getMainDeclaredObjectOfMemberExpression.js';

/**
Expand Down Expand Up @@ -28,7 +28,7 @@ function resolveProxyReferences(arb, candidateFilter = () => true) {
const replacementMainIdentifier = getMainDeclaredObjectOfMemberExpression(n.init)?.declNode;
if (replacementMainIdentifier && replacementMainIdentifier === relevantIdentifier) continue;
// Exclude changes in the identifier's own init
if (getDescendants(n.init).find(n => n.declNode === relevantIdentifier)) continue;
if (doesDescendantMatchCondition(n.init, n => n === relevantIdentifier)) continue;
if (refs.length && !areReferencesModified(arb.ast, refs) && !areReferencesModified(arb.ast, [replacementNode])) {
for (const ref of refs) {
arb.markNode(ref, replacementNode);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {badValue} from '../config.js';
import {Sandbox} from '../utils/sandbox.js';
import {evalInVm} from '../utils/evalInVm.js';
import {getDescendants} from '../utils/getDescendants.js';
import {doesDescendantMatchCondition} from '../utils/doesDescendantMatchCondition.js';

/**
* A special case of function array replacement where the function is wrapped in another function, the array is
Expand All @@ -17,49 +18,60 @@ export default function resolveAugmentedFunctionWrappedArrayReplacements(arb, ca
];
for (let i = 0; i < relevantNodes.length; i++) {
const n = relevantNodes[i];
if (n.id && candidateFilter(n)) {
const descendants = getDescendants(n);
if (descendants.find(d =>
if (n.id &&
doesDescendantMatchCondition(n, d =>
d.type === 'AssignmentExpression' &&
d.left?.name === n.id?.name)) {
const arrDecryptor = n;
const arrCandidates = descendants.filter(c =>
c.type === 'MemberExpression' && c.object.type === 'Identifier')
.map(n => n.object);

for (let j = 0; j < arrCandidates.length; j++) {
const ac = arrCandidates[j];
// If a direct reference to a global variable pointing at an array
let arrRef;
if (!ac.declNode) continue;
if (ac.declNode.scope.type === 'global') {
if (ac.declNode.parentNode?.init?.type === 'ArrayExpression') {
arrRef = ac.declNode.parentNode?.parentNode || ac.declNode.parentNode;
}
} else if (ac.declNode.parentNode?.init?.type === 'CallExpression') {
arrRef = ac.declNode.parentNode.init.callee?.declNode?.parentNode;
d.left?.name === n.id?.name) &&
candidateFilter(n)) {
const descendants = getDescendants(n);
const arrDecryptor = n;
const arrCandidates = [];
for (let q = 0; q < descendants.length; q++) {
const c = descendants[q];
if (c.type === 'MemberExpression' && c.object.type === 'Identifier') arrCandidates.push(c.object);
}
for (let j = 0; j < arrCandidates.length; j++) {
const ac = arrCandidates[j];
// If a direct reference to a global variable pointing at an array
let arrRef;
if (!ac.declNode) continue;
if (ac.declNode.scope.type === 'global') {
if (ac.declNode.parentNode?.init?.type === 'ArrayExpression') {
arrRef = ac.declNode.parentNode?.parentNode || ac.declNode.parentNode;
}
if (arrRef) {
const iife = (arb.ast[0].typeMap.ExpressionStatement || []).find(c =>
c.type === 'ExpressionStatement' &&
c.expression.type === 'CallExpression' &&
c.expression.callee.type === 'FunctionExpression' &&
c.expression.arguments.length &&
c.expression.arguments[0].type === 'Identifier' &&
c.expression.arguments[0].declNode === ac.declNode);
if (iife) {
const context = [arrRef.src, arrDecryptor.src, iife.src].join('\n');
const skipScopes = [arrRef.scope, arrDecryptor.scope, iife.expression.callee.scope];
const replacementCandidates = (arb.ast[0].typeMap.CallExpression || []).filter(c =>
c?.callee?.name === arrDecryptor.id.name &&
!skipScopes.includes(c.scope));
} else if (ac.declNode.parentNode?.init?.type === 'CallExpression') {
arrRef = ac.declNode.parentNode.init.callee?.declNode?.parentNode;
}
if (arrRef) {
const expressionStatements = arb.ast[0].typeMap.ExpressionStatement || [];
for (let k = 0; k < expressionStatements.length; k++) {
const exp = expressionStatements[k];
if (exp.expression.type === 'CallExpression' &&
exp.expression.callee.type === 'FunctionExpression' &&
exp.expression.arguments.length &&
exp.expression.arguments[0].type === 'Identifier' &&
exp.expression.arguments[0].declNode === ac.declNode) {
const context = [arrRef.src, arrDecryptor.src, exp.src].join('\n');
const skipScopes = [arrRef.scope, arrDecryptor.scope, exp.expression.callee.scope];
const callExpressions = arb.ast[0].typeMap.CallExpression || [];
const replacementCandidates = [];
for (let r = 0; r < callExpressions.length; r++) {
const c = callExpressions[r];
if (c.callee?.name === arrDecryptor.id.name &&
!skipScopes.includes(c.scope)) {
replacementCandidates.push(c);
}
}
const sb = new Sandbox();
sb.run(context);
for (let p = 0; p < replacementCandidates.length; p++) {
const rc = replacementCandidates[p];
const replacementNode = evalInVm(`\n${rc.src}`, sb);
if (replacementNode !== badValue) arb.markNode(rc, replacementNode);
if (replacementNode !== badValue) {
arb.markNode(rc, replacementNode);
}
}
break;
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions src/modules/unsafe/resolveEvalCallsOnNonLiterals.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ function resolveEvalCallsOnNonLiterals(arb, candidateFilter = () => true) {
// The code inside the eval might contain references to outside code that should be included.
const contextNodes = getDeclarationWithContext(n, true);
// In case any of the target candidate is included in the context it should be removed.
for (const redundantNode in [n, n?.parentNode, n?.parentNode?.parentNode]) {
if (contextNodes.includes(redundantNode)) contextNodes.splice(contextNodes.indexOf(redundantNode), 1);
const possiblyRedundantNodes = [n, n?.parentNode, n?.parentNode?.parentNode];
for (let i = 0; i < possiblyRedundantNodes.length; i++) {
if (contextNodes.includes(possiblyRedundantNodes[i])) contextNodes.splice(contextNodes.indexOf(possiblyRedundantNodes[i]), 1);
}
const context = contextNodes.length ? createOrderedSrc(contextNodes) : '';
const src = `${context}\n;var __a_ = ${createOrderedSrc([n.arguments[0]])}\n;__a_`;
Expand Down
12 changes: 8 additions & 4 deletions src/modules/unsafe/resolveLocalCalls.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,19 @@ export default function resolveLocalCalls(arb, candidateFilter = () => true) {
candidates.sort(sortByApperanceFrequency);

const modifiedRanges = [];
for (let i = 0; i < candidates.length; i++) {
candidateLoop: for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
if (c.arguments.some(a => badArgumentTypes.includes(a.type)) || isNodeInRanges(c, modifiedRanges)) continue;
if (isNodeInRanges(c, modifiedRanges)) continue;
for (let j = 0; j < c.arguments.length; j++) {
const arg = c.arguments[j];
if (badArgumentTypes.includes(arg.type)) continue candidateLoop;
}
const callee = c.callee?.object || c.callee;
const declNode = c.callee?.declNode || c.callee?.object?.declNode;
if (declNode?.parentNode?.body?.body?.[0]?.type === 'ReturnStatement') {
// Leave this replacement to a safe function
const returnArg = declNode.parentNode.body.body[0].argument;
if (['Literal', 'Identifier'].includes(returnArg.type) || /Function/.test(returnArg.type)) continue; // Unwrap identifier
if (['Literal', 'Identifier'].includes(returnArg.type) || returnArg.type.includes('unction')) continue; // Unwrap identifier
else if (returnArg.type === 'CallExpression' &&
returnArg.callee?.object?.type === 'FunctionExpression' &&
(returnArg.callee.property?.name || returnArg.callee.property?.value) === 'apply') continue; // Unwrap function shells
Expand Down Expand Up @@ -96,7 +100,7 @@ export default function resolveLocalCalls(arb, candidateFilter = () => true) {
// Prevent resolving a function's toString as it might be an anti-debugging mechanism
// which will spring if the code is beautified
if (c.callee.type === 'MemberExpression' && (c.callee.property?.name || c.callee.property?.value) === 'toString' &&
(new RegExp('^function ')).test(replacementNode?.value)) continue;
replacementNode?.value.substring(0, 8) === 'function') continue;
arb.markNode(c, replacementNode);
modifiedRanges.push(c.range);
}
Expand Down
4 changes: 2 additions & 2 deletions src/modules/unsafe/resolveMinimalAlphabet.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {badValue} from '../config.js';
import {evalInVm} from '../utils/evalInVm.js';
import {getDescendants} from '../utils/getDescendants.js';
import {doesDescendantMatchCondition} from '../utils/doesDescendantMatchCondition.js';

/**
* Resolve unary expressions on values which aren't numbers such as +true, +[], +[...], etc,
Expand All @@ -25,7 +25,7 @@ export default function resolveMinimalAlphabet(arb, candidateFilter = () => true
(n.left.type !== 'MemberExpression' && Number.isNaN(parseFloat(n.left?.value))) &&
![n.left?.type, n.right?.type].includes('ThisExpression')) &&
candidateFilter(n)) {
if (getDescendants(n).some(n => n.type === 'ThisExpression')) continue;
if (doesDescendantMatchCondition(n, n => n.type === 'ThisExpression')) continue;
const replacementNode = evalInVm(n.src);
if (replacementNode !== badValue) {
arb.markNode(n, replacementNode);
Expand Down
28 changes: 21 additions & 7 deletions src/modules/utils/areReferencesModified.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,30 @@
import {propertiesThatModifyContent} from '../config.js';

/**
* @param {ASTNode} r
* @param {ASTNode[]} assignmentExpressions
* @return {boolean}
*/
function isMemberExpressionAssignedTo(r, assignmentExpressions) {
for (let i = 0; i < assignmentExpressions.length; i++) {
const n = assignmentExpressions[i];
if (n.left.type === 'MemberExpression' &&
(n.left.object.declNode && (r.object.declNode || r.object) === n.left.object.declNode) &&
((n.left.property?.name || n.left.property?.value) === (r.property?.name || r.property?.value))) return true;
}
return false;
}

/**
* @param {ASTNode[]} ast
* @param {ASTNode[]} refs
* @return {boolean} true if any of the references might modify the original value; false otherwise.
*/
function areReferencesModified(ast, refs) {
// Verify no reference is on the left side of an assignment
return refs.some(r =>
(r.parentKey === 'left' && ['AssignmentExpression', 'ForInStatement', 'ForOfStatement'].includes(r.parentNode.type)) ||
for (let i = 0; i < refs.length; i++) {
const r = refs[i];
if ((r.parentKey === 'left' && ['AssignmentExpression', 'ForInStatement', 'ForOfStatement'].includes(r.parentNode.type)) ||
// Verify no reference is part of an update expression
r.parentNode.type === 'UpdateExpression' ||
// Verify no variable with the same name is declared in a subscope
Expand All @@ -23,11 +39,9 @@ function areReferencesModified(ast, refs) {
r.parentNode.parentKey === 'left')) ||
// Verify there are no member expressions among the references which are being assigned to
(r.type === 'MemberExpression' &&
(ast[0].typeMap.AssignmentExpression || []).some(n =>
n.left.type === 'MemberExpression' &&
n.left.object?.name === r.object?.name &&
(n.left.property?.name || n.left.property?.value === r.property?.name || r.property?.value) &&
(n.left.object.declNode && (r.object.declNode || r.object) === n.left.object.declNode))));
isMemberExpressionAssignedTo(r, ast[0].typeMap.AssignmentExpression || []))) return true;
}
return false;
}

export {areReferencesModified};
10 changes: 8 additions & 2 deletions src/modules/utils/createOrderedSrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,14 @@ function createOrderedSrc(nodes, preserveOrder = false) {
nodes[i] = n.parentNode;
if (!preserveOrder && n.callee.type === 'FunctionExpression') {
// Set nodeId to place IIFE just after its argument's declaration
const argDeclNodeId = n.arguments.find(a => a.nodeId === Math.max(...n.arguments.filter(arg => arg?.declNode?.nodeId).map(arg => arg.nodeId)))?.nodeId;
nodes[i].nodeId = argDeclNodeId ? argDeclNodeId + 1 : nodes[i].nodeId + largeNumber;
let maxArgNodeId = 0;
for (let j = 0; j < n.arguments.length; j++) {
const arg = n.arguments[j];
if (arg?.declNode?.nodeId > maxArgNodeId) {
maxArgNodeId = arg.declNode.nodeId;
}
}
nodes[i].nodeId = maxArgNodeId ? maxArgNodeId + 1 : nodes[i].nodeId + largeNumber;
}
} else if (n.callee.type === 'FunctionExpression') {
if (!preserveOrder) {
Expand Down
Loading
Loading