Skip to content

Commit

Permalink
Apply Augmented Array Replacement to Func2Arr cases, Unwrappers and S…
Browse files Browse the repository at this point in the history
…implifications (#56)

* Fix issue where a number below zero would be replaced with a string

* Improve failure messages

* Improve context collection to include missing member expression objects and call expressions' callees

* Create simplifyCalls to remove unnecessary usage of '.call(this' or '.apply(this' when calling a function

* Create unwrappers for IIFEs and simple operations

* Create resolveFunctionToArray module

* Add tests for new modules

* Adjust test results

* Remove redundant code

* Apply augmented array replacement for function2array cases

* Add augmented function2array replacement test

* Use assert.equal() instead of assert()

* Add test for resolveFunctionToArray

* 1.5.0
  • Loading branch information
ctrl-escp authored Dec 24, 2022
1 parent 08b557b commit 3fb9e15
Show file tree
Hide file tree
Showing 20 changed files with 365 additions and 83 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "restringer",
"version": "1.4.6",
"version": "1.5.0",
"description": "Deobfuscate Javascript with emphasis on reconstructing strings",
"main": "index.js",
"bin": {
Expand Down
3 changes: 3 additions & 0 deletions src/modules/safe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,8 @@ module.exports = {
resolveProxyReferences: require(__dirname + '/resolveProxyReferences'),
resolveProxyVariables: require(__dirname + '/resolveProxyVariables'),
resolveRedundantLogicalExpressions: require(__dirname + '/resolveRedundantLogicalExpressions'),
simplifyCalls: require(__dirname + '/simplifyCalls'),
unwrapFunctionShells: require(__dirname + '/unwrapFunctionShells'),
unwrapIIFEs: require(__dirname + '/unwrapIIFEs'),
unwrapSimpleOperations: require(__dirname + '/unwrapSimpleOperations'),
};
28 changes: 28 additions & 0 deletions src/modules/safe/simplifyCalls.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* Remove unnecessary usage of '.call(this' or '.apply(this' when calling a function
* @param {Arborist} arb
* @param {Function} candidateFilter (optional) a filter to apply on the candidates list
* @return {Arborist}
*/
function simplifyCalls(arb, candidateFilter = () => true) {
const candidates = arb.ast.filter(n =>
n.type === 'CallExpression' &&
n.arguments.length &&
n.arguments[0].type === 'ThisExpression' &&
n.callee.type === 'MemberExpression' &&
['apply', 'call'].includes(n.callee.property?.name || n.callee.property?.value) &&
(n.callee.object?.name || n.callee?.value) !== 'Function' &&
!/function/i.test(n.callee.object.type) &&
candidateFilter(n));

for (const c of candidates) {
arb.markNode(c, {
type: 'CallExpression',
callee: c.callee.object,
arguments: (c.callee.property?.name || c.callee.property?.value) === 'apply' ? c.arguments[1].elements : c.arguments.slice(1),
});
}
return arb;
}

module.exports = simplifyCalls;
29 changes: 29 additions & 0 deletions src/modules/safe/unwrapIIFEs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* Replace IIFEs that are unwrapping a function with the unwraped function.
* @param {Arborist} arb
* @param {Function} candidateFilter (optional) a filter to apply on the candidates list
* @return {Arborist}
*/
function unwrapIIFEs(arb, candidateFilter = () => true) {
const candidates = arb.ast.filter(n =>
n.type === 'CallExpression' &&
!n.arguments.length &&
['ArrowFunctionExpression', 'FunctionExpression'].includes(n.callee.type) &&
!n.callee.id &&
(
n.callee.body.type !== 'BlockStatement' ||
(
n.callee.body.body.length === 1 &&
n.callee.body.body[0].type === 'ReturnStatement')
) &&
n.parentKey === 'init' &&
candidateFilter(n));

for (const c of candidates) {
const replacementNode = c.callee.body.type !== 'BlockStatement' ? c.callee.body : c.callee.body.body[0].argument;
arb.markNode(c, replacementNode);
}
return arb;
}

module.exports = unwrapIIFEs;
94 changes: 94 additions & 0 deletions src/modules/safe/unwrapSimpleOperations.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
const operators = ['+', '-', '*', '/', '%', '&', '|', '&&', '||', '**', '^'];
const fixes = ['!', '~', '-', '+', '--', '++'];

/**
*
* @param {ASTNode} n
* @return {boolean}
*/
function matchBinaryOrLogical(n) {
// noinspection JSUnresolvedVariable
return ['LogicalExpression', 'BinaryExpression'].includes(n.type) &&
operators.includes(n.operator) &&
n.parentNode.type === 'ReturnStatement' &&
n.parentNode.parentNode?.body?.length === 1 &&
n.left?.declNode?.parentKey === 'params' &&
n.right?.declNode?.parentKey === 'params';
}

/**
*
* @param {ASTNode} c
* @param {Arborist} arb
*/
function handleBinaryOrLogical(c, arb) {
// noinspection JSUnresolvedVariable
const refs = (c.scope.block?.id?.references || []).map(r => r.parentNode);
for (const ref of refs) {
if (ref.arguments.length === 2) arb.markNode(ref, {
type: c.type,
operator: c.operator,
left: ref.arguments[0],
right: ref.arguments[1],
});
}
}

/**
*
* @param {ASTNode} n
* @return {boolean}
*/
function matchUnary(n) {
// noinspection JSUnresolvedVariable
return n.type === 'UnaryExpression' &&
fixes.includes(n.operator) &&
n.parentNode.type === 'ReturnStatement' &&
n.parentNode.parentNode?.body?.length === 1 &&
n.argument?.declNode?.parentKey === 'params';
}

/**
*
* @param {ASTNode} c
* @param {Arborist} arb
*/
function handleUnary(c, arb) {
// noinspection JSUnresolvedVariable
const refs = (c.scope.block?.id?.references || []).map(r => r.parentNode);
for (const ref of refs) {
if (ref.arguments.length === 1) arb.markNode(ref, {
type: c.type,
operator: c.operator,
prefix: c.prefix,
argument: ref.arguments[0],
});
}
}

/**
* Replace calls to functions that wrap simple operations with the actual operations
* @param {Arborist} arb
* @param {Function} candidateFilter (optional) a filter to apply on the candidates list
* @return {Arborist}
*/
function unwrapSimpleOperations(arb, candidateFilter = () => true) {
const candidates = arb.ast.filter(n =>
(matchBinaryOrLogical(n) || matchUnary(n)) &&
candidateFilter(n));

for (const c of candidates) {
switch (c.type) {
case 'BinaryExpression':
case 'LogicalExpression':
handleBinaryOrLogical(c, arb);
break;
case 'UnaryExpression':
handleUnary(c, arb);
break;
}
}
return arb;
}

module.exports = unwrapSimpleOperations;
1 change: 1 addition & 0 deletions src/modules/unsafe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module.exports = {
resolveDefiniteMemberExpressions: require(__dirname + '/resolveDefiniteMemberExpressions'),
resolveDeterministicConditionalExpressions: require(__dirname + '/resolveDeterministicConditionalExpressions'),
resolveEvalCallsOnNonLiterals: require(__dirname + '/resolveEvalCallsOnNonLiterals'),
resolveFunctionToArray: require(__dirname + '/resolveFunctionToArray'),
resolveInjectedPrototypeMethodCalls: require(__dirname + '/resolveInjectedPrototypeMethodCalls'),
resolveLocalCalls: require(__dirname + '/resolveLocalCalls'),
resolveMemberExpressionsLocalReferences: require(__dirname + '/resolveMemberExpressionsLocalReferences'),
Expand Down
11 changes: 10 additions & 1 deletion src/modules/unsafe/resolveDefiniteBinaryExpressions.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,16 @@ function resolveDefiniteBinaryExpressions(arb, candidateFilter = () => true) {

for (const c of candidates) {
const newNode = evalInVm(c.src);
if (newNode !== badValue) arb.markNode(c, newNode);
if (newNode !== badValue) {
// Fix issue where a number below zero would be replaced with a string
if (newNode.type === 'UnaryExpression' && typeof c?.left?.value === 'number' && typeof c?.right?.value === 'number') {
// noinspection JSCheckFunctionSignatures
const v = parseInt(newNode.argument.value);
newNode.argument.value = v;
newNode.argument.raw = `${v}`;
}
arb.markNode(c, newNode);
}
}
return arb;
}
Expand Down
41 changes: 41 additions & 0 deletions src/modules/unsafe/resolveFunctionToArray.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Function To Array Replacements
* The obfuscated script dynamically generates an array which is referenced throughout the script.
*/
const evalInVm = require(__dirname + '/evalInVm');
const {
createOrderedSrc,
getDeclarationWithContext,
} = require(__dirname + '/../utils');
const {badValue} = require(__dirname + '/../config');

/**
* Run the generating function and replace it with the actual array.
* Candidates are variables which are assigned a call expression, and every reference to them is a member expression.
* E.g.
* function getArr() {return ['One', 'Two', 'Three']};
* const a = getArr();
* console.log(`${a[0]} + ${a[1]} = ${a[2]}`);
* @param {Arborist} arb
* @return {Arborist}
*/
function resolveFunctionToArray(arb) {
// noinspection DuplicatedCode
const candidates = arb.ast.filter(n =>
n.type === 'VariableDeclarator' &&
n.init?.type === 'CallExpression' &&
n.id?.references &&
!n.id.references.find(r => r.parentNode.type !== 'MemberExpression'));

for (const c of candidates) {
const targetNode = c.init.callee?.declNode?.parentNode || c.init;
const src = createOrderedSrc(getDeclarationWithContext(targetNode).concat(c.init));
const newNode = evalInVm(src);
if (newNode !== badValue) {
arb.markNode(c.init, newNode);
}
}
return arb;
}

module.exports = resolveFunctionToArray;
4 changes: 3 additions & 1 deletion src/modules/utils/getDeclarationWithContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,16 @@ function getDeclarationWithContext(originNode) {
case 'AssignmentExpression':
relevantScope = relevantNode.right?.scope;
examineStack.push(relevantNode.right);
if (relevantNode.init) examineStack.push(relevantNode.init);
break;
case 'CallExpression':
relevantScope = relevantNode.callee.scope;
references.push(...relevantNode.arguments.filter(a => a.type === 'Identifier'));
examineStack.push(relevantNode.callee);
break;
case 'MemberExpression':
relevantScope = relevantNode.object.scope;
examineStack.push(relevantNode.property);
examineStack.push(relevantNode.object, relevantNode.property);
break;
case 'Identifier':
if (relevantNode.declNode) {
Expand Down
21 changes: 16 additions & 5 deletions src/processors/augmentedArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
*/
const {
unsafe: {
evalInVm
evalInVm,
resolveFunctionToArray,
},
config: {
badValue
Expand All @@ -43,24 +44,34 @@ function replaceArrayWithStaticAugmentedVersion(arb) {

for (const candidate of candidates) {
const relevantArrayIdentifier = candidate.arguments.find(n => n.type === 'Identifier');
const declKind = /function/i.test(relevantArrayIdentifier.declNode.parentNode.type) ? '' : 'var ';
const ref = !declKind ? `${relevantArrayIdentifier.name}()` : relevantArrayIdentifier.name;
// The context for this eval is the relevant array and the IIFE augmenting it (the candidate).
const context = `var ${relevantArrayIdentifier.declNode.parentNode.src}\n!${createOrderedSrc(getDeclarationWithContext(candidate))}`;
const context = `${declKind}${relevantArrayIdentifier.declNode.parentNode.src}\n!${createOrderedSrc(getDeclarationWithContext(candidate))}`;
// By adding the name of the array after the context, the un-shuffled array is procured.
const src = `${context};\n${relevantArrayIdentifier.name};`;
const src = `${context};\n${ref};`;
const newNode = evalInVm(src); // The new node will hold the un-shuffled array's assignment
if (newNode !== badValue) {
let candidateExpression = candidate;
while (candidateExpression && candidateExpression.type !== 'ExpressionStatement') {
candidateExpression = candidateExpression?.parentNode;
}
arb.markNode(candidateExpression ? candidateExpression : candidate);
arb.markNode(relevantArrayIdentifier.declNode.parentNode.init, newNode);
if (relevantArrayIdentifier.declNode.parentNode.type === 'FunctionDeclaration') {
arb.markNode(relevantArrayIdentifier.declNode.parentNode.body, {
type: 'BlockStatement',
body: [{
type: 'ReturnStatement',
argument: newNode,
}],
});
} else arb.markNode(relevantArrayIdentifier.declNode.parentNode.init, newNode);
}
}
return arb;
}

module.exports = {
preprocessors: [replaceArrayWithStaticAugmentedVersion],
preprocessors: [replaceArrayWithStaticAugmentedVersion, resolveFunctionToArray],
postprocessors: [],
};
38 changes: 2 additions & 36 deletions src/processors/functionToArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,12 @@
*/
const {
unsafe: {
evalInVm,
resolveFunctionToArray,
},
utils: {
createOrderedSrc,
getDeclarationWithContext,
},
config: {
badValue,
}
} = require(__dirname + '/../modules');

/**
* Run the generating function and replace it with the actual array.
* Candidates are variables which are assigned a call expression, and every reference to them is a member expression.
* E.g.
* function getArr() {return ['One', 'Two', 'Three']};
* const a = getArr();
* console.log(`${a[0]} + ${a[1]} = ${a[2]}`);
* @param {Arborist} arb
* @return {Arborist}
*/
function replaceFunctionWithArray(arb) {
const candidates = arb.ast.filter(n =>
n.type === 'VariableDeclarator' &&
n.init?.type === 'CallExpression' &&
n.id?.references &&
!n.id.references.find(r => r.parentNode.type !== 'MemberExpression'));

for (const c of candidates) {
const targetNode = c.init.callee?.declNode?.parentNode || c.init;
const src = createOrderedSrc(getDeclarationWithContext(targetNode).concat(c.init));
const newNode = evalInVm(src);
if (newNode !== badValue) {
arb.markNode(c.init, newNode);
}
}
return arb;
}

module.exports = {
preprocessors: [replaceFunctionWithArray],
preprocessors: [resolveFunctionToArray],
postprocessors: [],
};
Loading

0 comments on commit 3fb9e15

Please sign in to comment.