Skip to content

Commit

Permalink
Improve wrapped functions handling (#73)
Browse files Browse the repository at this point in the history
* Add replaceNewFuncCallsWithLiteralContent with tests

* Expand unwrapIIFEs to include unwrapping of IIFEs with multiple statements or expressions.
Add a relevant test case.

* 1.7.0

* Skip cases where IIFE is used to initialize or set a value.
Add TN tests

* Add obfuscated sample for new Function and IIFEs unwrapping
  • Loading branch information
BenBaryoPX authored Apr 4, 2023
1 parent f503dd1 commit 2515f3e
Show file tree
Hide file tree
Showing 10 changed files with 312 additions and 9 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.6.6",
"version": "1.7.0",
"description": "Deobfuscate Javascript with emphasis on reconstructing strings",
"main": "index.js",
"bin": {
Expand Down
1 change: 1 addition & 0 deletions src/modules/safe/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
replaceFunctionShellsWithWrappedValueIIFE: require(__dirname + '/replaceFunctionShellsWithWrappedValueIIFE'),
replaceIdentifierWithFixedAssignedValue: require(__dirname + '/replaceIdentifierWithFixedAssignedValue'),
replaceIdentifierWithFixedValueNotAssignedAtDeclaration: require(__dirname + '/replaceIdentifierWithFixedValueNotAssignedAtDeclaration'),
replaceNewFuncCallsWithLiteralContent: require(__dirname + '/replaceNewFuncCallsWithLiteralContent'),
replaceSequencesWithExpressions: require(__dirname + '/replaceSequencesWithExpressions'),
resolveDeterministicIfStatements: require(__dirname + '/resolveDeterministicIfStatements'),
resolveFunctionConstructorCalls: require(__dirname + '/resolveFunctionConstructorCalls'),
Expand Down
63 changes: 63 additions & 0 deletions src/modules/safe/replaceNewFuncCallsWithLiteralContent.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const {generateFlatAST} = require('flast');
const logger = require(__dirname + '/../utils/logger');
const getCache = require(__dirname + '/../utils/getCache');
const generateHash = require(__dirname + '/../utils/generateHash');

/**
* Extract string values of eval call expressions, and replace calls with the actual code, without running it through eval.
* E.g.
* new Function('!function() {console.log("hello world")}()')();
* will be replaced with
* !function () {console.log("hello world")}();
* @param {Arborist} arb
* @param {Function} candidateFilter (optional) a filter to apply on the candidates list
* @return {Arborist}
*/
function replaceNewFuncCallsWithLiteralContent(arb, candidateFilter = () => true) {
const cache = getCache(arb.ast[0].scriptHash);
const candidates = arb.ast.filter(n =>
n.type === 'NewExpression' &&
n.parentKey === 'callee' &&
n.parentNode?.arguments?.length === 0 &&
n.callee?.name === 'Function' &&
n.arguments?.length === 1 &&
n.arguments[0].type === 'Literal' &&
candidateFilter(n));

for (const c of candidates) {
const targetCodeStr = c.arguments[0].value;
const cacheName = `replaceEval-${generateHash(targetCodeStr)}`;
try {
if (!cache[cacheName]) {
let body;
if (targetCodeStr) {
body = generateFlatAST(targetCodeStr, {detailed: false})[0].body;
if (body.length > 1) {
body = {
type: 'BlockStatement',
body,
};
} else {
body = body[0];
if (body.type === 'ExpressionStatement') body = body.expression;
}
} else body = {
type: 'Literal',
value: targetCodeStr,
};
cache[cacheName] = body;
}
let replacementNode = cache[cacheName];
let targetNode = c.parentNode;
if (targetNode.parentNode.type === 'ExpressionStatement' && replacementNode.type === 'BlockStatement') {
targetNode = targetNode.parentNode;
}
arb.markNode(targetNode, replacementNode);
} catch (e) {
logger.debug(`[-] Unable to replace new function's body with call expression: ${e}`);
}
}
return arb;
}

module.exports = replaceNewFuncCallsWithLiteralContent;
36 changes: 31 additions & 5 deletions src/modules/safe/unwrapIIFEs.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,44 @@ function unwrapIIFEs(arb, candidateFilter = () => true) {
!n.arguments.length &&
['ArrowFunctionExpression', 'FunctionExpression'].includes(n.callee.type) &&
!n.callee.id &&
(
// IIFEs with a single return statement
(((
n.callee.body.type !== 'BlockStatement' ||
(
n.callee.body.body.length === 1 &&
n.callee.body.body[0].type === 'ReturnStatement')
) &&
n.parentKey === 'init' &&
n.parentKey === 'init') ||
// Generic IIFE wrappers
(n.parentKey === 'ExpressionStatement' ||
n.parentKey === 'argument' &&
n.parentNode.type === 'UnaryExpression')) &&
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);
candidatesLoop: for (const c of candidates) {
let targetNode = c;
let replacementNode = c.callee.body;
if (replacementNode.type === 'BlockStatement') {
let targetChild = replacementNode;
// IIFEs with a single return statement
if (replacementNode.body?.length === 1 && replacementNode.body[0].argument) replacementNode = replacementNode.body[0].argument;
// IIFEs with multiple statements or expressions
else while (targetNode && !targetNode.body) {
// Skip cases where IIFE is used to initialize or set a value
if (targetNode.parentKey === 'init' || targetNode.type === 'AssignmentExpression' ) continue candidatesLoop;
targetChild = targetNode;
targetNode = targetNode.parentNode;
}
if (!targetNode || !targetNode.body) targetNode = c;
else {
// Place the wrapped code instead of the wrapper node
replacementNode = {
...targetNode,
body: [...targetNode.body.filter(n => n !== targetChild), ...replacementNode.body],
};
}
}
arb.markNode(targetNode, replacementNode);
}
return arb;
}
Expand Down
2 changes: 2 additions & 0 deletions src/restringer.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const {
replaceEvalCallsWithLiteralContent,
replaceIdentifierWithFixedAssignedValue,
replaceIdentifierWithFixedValueNotAssignedAtDeclaration,
replaceNewFuncCallsWithLiteralContent,
replaceBooleanExpressionsWithIf,
replaceSequencesWithExpressions,
resolveFunctionConstructorCalls,
Expand Down Expand Up @@ -113,6 +114,7 @@ class REstringer {
replaceEvalCallsWithLiteralContent,
replaceIdentifierWithFixedAssignedValue,
replaceIdentifierWithFixedValueNotAssignedAtDeclaration,
replaceNewFuncCallsWithLiteralContent,
replaceBooleanExpressionsWithIf,
replaceSequencesWithExpressions,
resolveFunctionConstructorCalls,
Expand Down
37 changes: 37 additions & 0 deletions tests/modules-tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,13 @@ case 1: console.log(1); a = 2; break;}}})();`,
source: `let a; a = 3; const b = a * 2; console.log(b + a);`,
expected: `let a;\na = 3;\nconst b = 3 * 2;\nconsole.log(b + 3);`,
},
{
enabled: true,
name: 'replaceNewFuncCallsWithLiteralContent - TP-1',
func: __dirname + '/../src/modules/safe/replaceNewFuncCallsWithLiteralContent',
source: `new Function("!function() {console.log('hello world')}()")();`,
expected: `!function () {\n console.log('hello world');\n}();`,
},
{
enabled: true,
name: 'replaceBooleanExpressionsWithIf - TP-1',
Expand Down Expand Up @@ -389,6 +396,36 @@ case 1: console.log(1); a = 2; break;}}})();`,
})();`,
expected: `var a = b => c(b - 40);`,
},
{
enabled: true,
name: 'unwrapIIFEs - TP-3 (inline unwrapping)',
func: __dirname + '/../src/modules/safe/unwrapIIFEs',
source: `!function() {
var a = 'message';
console.log(a);
}();`,
expected: `var a = 'message';\nconsole.log(a);`,
},
{
enabled: true,
name: 'unwrapIIFEs - TN-1 (unary declarator init)',
func: __dirname + '/../src/modules/safe/unwrapIIFEs',
source: `var b = !function() {
var a = 'message';
console.log(a);
}();`,
expected: `var b = !function() {\n\tvar a = 'message';\n\tconsole.log(a);\n}();`,
},
{
enabled: true,
name: 'unwrapIIFEs - TN-2 (unary assignment right)',
func: __dirname + '/../src/modules/safe/unwrapIIFEs',
source: `b = !function() {
var a = 'message';
console.log(a);
}();`,
expected: `b = !function() {\n\tvar a = 'message';\n\tconsole.log(a);\n}();`,
},
{
enabled: true,
name: 'unwrapSimpleOperations - TP-1',
Expand Down
3 changes: 2 additions & 1 deletion tests/obfuscated-samples.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ module.exports = {
// Order by approximate deobfuscation time, ascending
'JSFuck': 'jsfuck.js',
'Ant & Cockroach': 'ant.js',
'New Function IIFE': 'newFunc.js',
'Hunter': 'hunter.js',
'_$_': 'udu.js',
'Prototype Calls': 'prototypeCalls.js',
'Obfuscator.io': 'obfuscatorIo.js',
'Caesar+': 'caesar.js',
'eval(Ox$': 'evalOxd.js',
'Obfuscator.io': 'obfuscatorIo.js',
'$s': 'ds.js',
'Local Proxies': 'localProxies.js',
};
1 change: 1 addition & 0 deletions tests/resources/newFunc.js

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

Loading

0 comments on commit 2515f3e

Please sign in to comment.