diff --git a/README.md b/README.md index ef158f3..7338286 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ For comments and suggestions feel free to open an issue or find me on Twitter - * [Command-Line Usage](#command-line-usage) * [Use as a Module](#use-as-a-module) * [Create Custom Deobfuscators](#create-custom-deobfuscators) + * [Boilerplate Code for Starting from Scratch](#boilerplate-code-for-starting-from-scratch) * [Read More](#read-more) *** @@ -80,7 +81,7 @@ REstringer is highly modularized. It exposes modules that allow creating custom that can solve specific problems. The basic structure of such a deobfuscator would be an array of deobfuscation modules -(either [safe](src/modules/safe) or [unsafe](src/modules/unsafe)), run via the [runLoop](src/modules/utils/runLoop.js) util function. +(either [safe](src/modules/safe) or [unsafe](src/modules/unsafe)), run via flAST's applyIteratively utility function. Unsafe modules run code through `eval` (using [isolated-vm](https://www.npmjs.com/package/isolated-vm) to be on the safe side) while safe modules do not. @@ -88,15 +89,15 @@ Unsafe modules run code through `eval` (using [isolated-vm](https://www.npmjs.co const { safe: {normalizeComputed}, unsafe: {resolveDefiniteBinaryExpressions, resolveLocalCalls}, - utils: {runLoop} } = require('restringer').deobModules; +const {applyIteratively} = require('flast').utils; let script = 'obfuscated JS here'; const deobModules = [ resolveDefiniteBinaryExpressions, resolveLocalCalls, normalizeComputed, ]; -script = runLoop(script, deobModules); +script = applyIteratively(script, deobModules); console.log(script); // Deobfuscated script ``` @@ -104,15 +105,15 @@ With the additional `candidateFilter` function argument, it's possible to narrow ```javascript const { unsafe: {resolveLocalCalls}, - utils: {runLoop} } = require('restringer').deobModules; +const {applyIteratively} = require('flast').utils; let script = 'obfuscated JS here'; -// It's better to define a function with a name that can show up in the log (otherwise you'll get 'undefined') +// It's better to define a function with a meaningful name that can show up in the log function resolveLocalCallsInGlobalScope(arb) { return resolveLocalCalls(arb, n => n.parentNode?.type === 'Program'); } -script = runLoop(script, [resolveLocalCallsInGlobalScope]); +script = applyIteratively(script, [resolveLocalCallsInGlobalScope]); console.log(script); // Deobfuscated script ``` @@ -125,7 +126,7 @@ const inputFilename = process.argv[2]; const code = fs.readFileSync(inputFilename, 'utf-8'); const res = new REstringer(code); -// res.logger.setLogLevel(res.logger.logLevels.DEBUG); +// res.logger.setLogLevelDebug(); res.detectObfuscationType = false; // Skip obfuscation type detection, including any pre and post processors const targetFunc = res.unsafeMethods.find(m => m.name === 'resolveLocalCalls'); @@ -138,6 +139,40 @@ if (res.script !== code) { console.log('[+] Deob successful'); fs.writeFileSync(`${inputFilename}-deob.js`, res.script, 'utf-8'); } else console.log('[-] Nothing deobfuscated :/'); +``` + +*** + +### Boilerplate code for starting from scratch +```javascript +const {logger, applyIteratively, treeModifier} = require('flast').utils; +// Optional loading from file +// const fs = require('node:fs'); +// const inputFilename = process.argv[2] || 'target.js'; +// const code = fs.readFileSync(inputFilename, 'utf-8'); +const code = `(function() { + function createMessage() {return 'Hello' + ' ' + 'there!';} + function print(msg) {console.log(msg);} + print(createMessage()); +})();`; + +logger.setLogLevelDebug(); +let script = code; +// Use this function to target the relevant nodes +const f = n => n.type === 'Literal' && replacements[n.value]; +// Use this function to modify the nodes according to your needs. +// markNode(n) would delete the node, while markNode(n, {...}) would replace the node with the supplied node. +const m = (n, arb) => arb.markNode(n, { + type: 'Literal', + value: replacements[n.value], +}); +const swc = treeModifier(f, m, 'StarWarsChanger'); +script = applyIteratively(script, [swc]); +if (code !== script) { + console.log(script); + // fs.writeFileSync(inputFilename + '-deob.js', script, 'utf-8'); +} else console.log(`No changes`); + ``` *** diff --git a/package-lock.json b/package-lock.json index 3670ddb..9243efb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "restringer", - "version": "1.10.3", + "version": "1.10.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "restringer", - "version": "1.10.3", + "version": "1.10.4", "license": "MIT", "dependencies": { - "flast": "^1.6.0", + "flast": "^1.7.1", "isolated-vm": "^5.0.1", "jsdom": "^24.1.0", "obfuscation-detector": "^1.1.7" @@ -755,9 +755,10 @@ } }, "node_modules/flast": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/flast/-/flast-1.6.0.tgz", - "integrity": "sha512-dT2tCPWxW6kvcEsEW6iGObXaaBipxQC+U7TZf4xVC3Qfks4MrBlV+/J9zrE1s/Mk0YmHrXKC8eKthoL04x6Ouw==", + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/flast/-/flast-1.7.1.tgz", + "integrity": "sha512-gBOTJxdR8T51/5EO6YG7F2OpR2ff3tjVmKBM6jJZvZjTkMCiLnfMWU7jcfeP+LHIPm2PvLVWoyBnaGub0n9ySg==", + "license": "MIT", "dependencies": { "escodegen": "^2.1.0", "eslint-scope": "^8.0.1", diff --git a/package.json b/package.json index 94a2f4e..0297956 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "restringer", - "version": "1.10.3", + "version": "1.10.4", "description": "Deobfuscate Javascript with emphasis on reconstructing strings", "main": "index.js", "bin": { @@ -11,7 +11,7 @@ "test": "tests" }, "dependencies": { - "flast": "^1.6.0", + "flast": "^1.7.1", "isolated-vm": "^5.0.1", "jsdom": "^24.1.0", "obfuscation-detector": "^1.1.7" diff --git a/src/modules/safe/replaceEvalCallsWithLiteralContent.js b/src/modules/safe/replaceEvalCallsWithLiteralContent.js index ead44be..6094aaa 100644 --- a/src/modules/safe/replaceEvalCallsWithLiteralContent.js +++ b/src/modules/safe/replaceEvalCallsWithLiteralContent.js @@ -1,7 +1,6 @@ -const {generateFlatAST} = require('flast'); -const logger = require(__dirname + '/../utils/logger'); const getCache = require(__dirname + '/../utils/getCache'); const generateHash = require(__dirname + '/../utils/generateHash'); +const {generateFlatAST, utils: {logger}} = require('flast'); /** * Extract string values of eval call expressions, and replace calls with the actual code, without running it through eval. diff --git a/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js b/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js index e7c4774..8f93cc7 100644 --- a/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js +++ b/src/modules/safe/replaceNewFuncCallsWithLiteralContent.js @@ -1,7 +1,6 @@ -const {generateFlatAST} = require('flast'); -const logger = require(__dirname + '/../utils/logger'); const getCache = require(__dirname + '/../utils/getCache'); const generateHash = require(__dirname + '/../utils/generateHash'); +const {generateFlatAST, utils: {logger}} = require('flast'); /** * Extract string values of eval call expressions, and replace calls with the actual code, without running it through eval. diff --git a/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js b/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js index f604890..fbc5aaa 100644 --- a/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js +++ b/src/modules/safe/resolveMemberExpressionReferencesToArrayIndex.js @@ -1,4 +1,4 @@ -const logger = require(__dirname + '/../utils/logger'); +const {logger} = require('flast').utils; const minArrayLength = 20; diff --git a/src/modules/unsafe/resolveBuiltinCalls.js b/src/modules/unsafe/resolveBuiltinCalls.js index 814a36d..0fd0dab 100644 --- a/src/modules/unsafe/resolveBuiltinCalls.js +++ b/src/modules/unsafe/resolveBuiltinCalls.js @@ -1,5 +1,5 @@ +const {logger} = require('flast').utils; const {badValue} = require(__dirname + '/../config'); -const logger = require(__dirname + '/../utils/logger'); const Sandbox = require(__dirname + '/../utils/sandbox'); const evalInVm = require(__dirname + '/../utils/evalInVm'); const createNewNode = require(__dirname + '/../utils/createNewNode'); diff --git a/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js b/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js index c1640c1..f331e84 100644 --- a/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js +++ b/src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js @@ -1,5 +1,5 @@ +const {logger} = require('flast').utils; const {badValue} = require(__dirname + '/../config'); -const logger = require(__dirname + '/../utils/logger'); const Sandbox = require(__dirname + '/../utils/sandbox'); const evalInVm = require(__dirname + '/../utils/evalInVm'); const createOrderedSrc = require(__dirname + '/../utils/createOrderedSrc'); diff --git a/src/modules/utils/createNewNode.js b/src/modules/utils/createNewNode.js index fe578ff..b117c95 100644 --- a/src/modules/utils/createNewNode.js +++ b/src/modules/utils/createNewNode.js @@ -1,7 +1,6 @@ -const logger = require(__dirname + '/logger'); -const {generateCode, parseCode} = require('flast'); const {badValue} = require(__dirname + '/../config'); const getObjType = require(__dirname + '/getObjType'); +const {generateCode, parseCode, utils: {logger}} = require('flast'); /** * Create a node from a value by its type. diff --git a/src/modules/utils/evalInVm.js b/src/modules/utils/evalInVm.js index 75f3164..60d8785 100644 --- a/src/modules/utils/evalInVm.js +++ b/src/modules/utils/evalInVm.js @@ -1,7 +1,7 @@ +const {logger} = require('flast').utils; const Sandbox = require(__dirname + '/sandbox'); const assert = require('node:assert'); const {badValue} = require(__dirname + '/../config'); -const logger = require(__dirname + '/../utils/logger'); const getObjType = require(__dirname + '/../utils/getObjType'); const generateHash = require(__dirname + '/../utils/generateHash'); const createNewNode = require(__dirname + '/../utils/createNewNode'); diff --git a/src/modules/utils/evalWithDom.js b/src/modules/utils/evalWithDom.js index 29b2a48..225528b 100644 --- a/src/modules/utils/evalWithDom.js +++ b/src/modules/utils/evalWithDom.js @@ -2,8 +2,9 @@ const fs = require('node:fs'); const Sandbox = require(__dirname + '/sandbox'); +// eslint-disable-next-line no-unused-vars const {JSDOM} = require('jsdom'); -const logger = require(__dirname + '/../utils/logger'); +const {logger} = require('flast').utils; const generateHash = require(__dirname + '/../utils/generateHash'); let jQuerySrc = ''; diff --git a/src/modules/utils/index.js b/src/modules/utils/index.js index 6f16edb..4efdec9 100644 --- a/src/modules/utils/index.js +++ b/src/modules/utils/index.js @@ -15,9 +15,7 @@ module.exports = { getObjType: require(__dirname + '/getObjType'), isNodeInRanges: require(__dirname + '/isNodeInRanges'), isNodeMarked: require(__dirname + '/isNodeMarked'), - logger: require(__dirname + '/logger'), normalizeScript: require(__dirname + '/normalizeScript'), - runLoop: require(__dirname + '/runLoop'), safeImplementations: require(__dirname + '/safeImplementations'), sandbox: require(__dirname + '/sandbox'), }; \ No newline at end of file diff --git a/src/modules/utils/normalizeScript.js b/src/modules/utils/normalizeScript.js index 9842cbd..740221b 100644 --- a/src/modules/utils/normalizeScript.js +++ b/src/modules/utils/normalizeScript.js @@ -1,4 +1,4 @@ -const runLoop = require(__dirname + '/runLoop'); +const {applyIteratively} = require('flast').utils; const normalizeComputed = require(__dirname + '/../safe/normalizeComputed'); const normalizeEmptyStatements = require(__dirname + '/../safe/normalizeEmptyStatements'); const normalizeRedundantNotOperator = require(__dirname + '/../unsafe/normalizeRedundantNotOperator'); @@ -9,7 +9,7 @@ const normalizeRedundantNotOperator = require(__dirname + '/../unsafe/normalizeR * @return {string} The normalized script. */ function normalizeScript(script) { - return runLoop(script, [ + return applyIteratively(script, [ normalizeComputed, normalizeRedundantNotOperator, normalizeEmptyStatements, diff --git a/src/modules/utils/runLoop.js b/src/modules/utils/runLoop.js index ab6d79b..c0a6734 100644 --- a/src/modules/utils/runLoop.js +++ b/src/modules/utils/runLoop.js @@ -1,6 +1,5 @@ -const {Arborist} = require('flast'); +const {Arborist, utils: {logger}} = require('flast'); const generateHash = require(__dirname + '/generateHash'); -const logger = require(__dirname + '/../utils/logger'); const {defaultMaxIterations, getGlobalMaxIterations} = require(__dirname + '/../config'); let globalIterationsCounter = 0; diff --git a/src/restringer.js b/src/restringer.js index 2562e01..ecfad3e 100755 --- a/src/restringer.js +++ b/src/restringer.js @@ -1,12 +1,11 @@ #!/usr/bin/env node +const {logger, applyIteratively} = require('flast').utils; const processors = require(__dirname + '/processors'); const detectObfuscation = require('obfuscation-detector'); const version = require(__dirname + '/../package').version; const { utils: { - runLoop, normalizeScript, - logger, }, safe, unsafe, @@ -33,7 +32,7 @@ class REstringer { this._preprocessors = []; this._postprocessors = []; this.logger = logger; - this.logger.setLogLevel(logger.logLevels.LOG); // Default log level + this.logger.setLogLevelLog(); this.detectObfuscationType = true; // Deobfuscation methods that don't use eval this.safeMethods = [ @@ -106,7 +105,7 @@ class REstringer { let modified, script; do { this.modified = false; - script = runLoop(this.script, this.safeMethods.concat(this.unsafeMethods)); + script = applyIteratively(this.script, this.safeMethods.concat(this.unsafeMethods)); if (this.script !== script) { this.modified = true; this.script = script; @@ -130,7 +129,7 @@ class REstringer { this._loopSafeAndUnsafeDeobfuscationMethods(); this._runProcessors(this._postprocessors); if (this.modified && this.normalize) this.script = normalizeScript(this.script); - if (clean) this.script = runLoop(this.script, [unsafe.removeDeadNodes]); + if (clean) this.script = applyIteratively(this.script, [unsafe.removeDeadNodes]); return this.modified; } @@ -142,7 +141,7 @@ class REstringer { _runProcessors(processors) { for (let i = 0; i < processors.length; i++) { const processor = processors[i]; - this.script = runLoop(this.script, [processor], 1); + this.script = applyIteratively(this.script, [processor], 1); } } } @@ -158,8 +157,8 @@ if (require.main === module) { const startTime = Date.now(); const restringer = new REstringer(content); - if (args.quiet) restringer.logger.setLogLevel(logger.logLevels.NONE); - else if (args.verbose) restringer.logger.setLogLevel(logger.logLevels.DEBUG); + if (args.quiet) restringer.logger.setLogLevelNone(); + else if (args.verbose) restringer.logger.setLogLevelDebug(); logger.log(`[!] REstringer v${REstringer.__version__}`); logger.log(`[!] Deobfuscating ${args.inputFilename}...`); if (args.maxIterations) { diff --git a/tests/testDeobfuscations.js b/tests/testDeobfuscations.js index 4d16ce7..489fa2d 100644 --- a/tests/testDeobfuscations.js +++ b/tests/testDeobfuscations.js @@ -12,7 +12,7 @@ const tests = { * @param expected {string} - The expected output */ function testCodeSample(testName, source, expected) { - process.stdout.write(`Testing ${testName}... `); + process.stdout.write(`${testName}... `); console.time('PASS'); const restringer = new REstringer(source); restringer.logger.setLogLevel(restringer.logger.logLevels.NONE); @@ -32,7 +32,7 @@ for (const [moduleName, moduleTests] of Object.entries(tests)) { testCodeSample(`[${moduleName}] ${test.name}`.padEnd(90, '.'), test.source, test.expected); } else { skippedTests++; - console.log(`Testing [${moduleName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); + console.log(`[${moduleName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); } } } diff --git a/tests/testModules.js b/tests/testModules.js index 4c13a01..88a4f23 100644 --- a/tests/testModules.js +++ b/tests/testModules.js @@ -1,6 +1,6 @@ const assert = require('node:assert'); const {Arborist} = require('flast'); -const {runLoop, logger} = require(__dirname + '/../src/modules').utils; +const {logger, applyIteratively} = require('flast').utils; const tests = { modulesTests: __dirname + '/modules-tests', @@ -19,7 +19,7 @@ const defaultPrepRes = arb => {arb.applyChanges(); return arb.script;}; * @param prepRes {function} - (optional) Function for parsing the test output. */ function testModuleOnce(testName, testFunc, source, expected, prepTest = defaultPrepTest, prepRes = defaultPrepRes) { - process.stdout.write(`Testing ${testName}... `); + process.stdout.write(`${testName}... `); console.time('PASS'); const testInput = prepTest(source); const rawRes = testFunc(...testInput); @@ -38,10 +38,10 @@ function testModuleOnce(testName, testFunc, source, expected, prepTest = default * @param prepRes {function} - (optional) Function for parsing the test output. */ function testModuleInLoop(testName, testFunc, source, expected, prepTest = null, prepRes = null) { - process.stdout.write(`Testing ${testName}... `); + process.stdout.write(`${testName}... `); console.time('PASS'); const testInput = prepTest ? prepTest(source) : source; - const rawResult = runLoop(testInput, [testFunc]); + const rawResult = applyIteratively(testInput, [testFunc]); const result = prepRes ? prepRes(rawResult) : rawResult; assert.deepEqual(result, expected); console.timeEnd('PASS'); @@ -58,11 +58,11 @@ for (const [moduleName, moduleTests] of Object.entries(tests)) { if (test.enabled) { // Tests will have the `looped` flag if they only produce the desired result after consecutive runs if (!test.looped) testModuleOnce(`[${moduleName}] ${test.name}`.padEnd(90, '.'), require(test.func), test.source, test.expected, test.prepareTest, test.prepareResult); - // Tests will have the `isUtil` flag if they do not return an Arborist instance (i.e. can't use runLoop) + // Tests will have the `isUtil` flag if they do not return an Arborist instance (i.e. can't use applyIteratively) if (!test.isUtil) testModuleInLoop(`[${moduleName}] ${test.name} (looped)`.padEnd(90, '.'), require(test.func), test.source, test.expected, test.prepareTest, test.prepareResult); } else { skippedTests++; - console.log(`Testing [${moduleName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); + console.log(`[${moduleName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); } } } diff --git a/tests/testObfuscatedSamples.js b/tests/testObfuscatedSamples.js index fb339b6..b8b41a0 100644 --- a/tests/testObfuscatedSamples.js +++ b/tests/testObfuscatedSamples.js @@ -17,7 +17,7 @@ function normalizeCode(code) { } function testSampleDeobfuscation(testSampleName, testSampleFilename) { - process.stdout.write(`Testing '${testSampleName}' obfuscated sample...`.padEnd(60, '.')); + process.stdout.write(`'${testSampleName}' obfuscated sample...`.padEnd(60, '.')); console.time(' PASS'); const obfuscatedSource = fs.readFileSync(testSampleFilename, 'utf-8'); const deobfuscatedTarget = normalizeCode(fs.readFileSync(`${testSampleFilename}-deob.js`, 'utf-8')); diff --git a/tests/testProcessors.js b/tests/testProcessors.js index a546d4d..6c5d051 100644 --- a/tests/testProcessors.js +++ b/tests/testProcessors.js @@ -18,7 +18,7 @@ const defaultPrepRes = arb => {arb.applyChanges(); return arb.script;}; * @param {function} prepRes - (optional) Function for parsing the test output. */ function testProcessor(testName, testProcs, source, expected, prepTest = defaultPrepTest, prepRes = defaultPrepRes) { - process.stdout.write(`Testing ${testName}... `); + process.stdout.write(`${testName}... `); console.time('PASS'); let rawRes = prepTest(source); testProcs.preprocessors.forEach(proc => rawRes = proc(...(Array.isArray(rawRes) ? rawRes : [rawRes]))); @@ -39,7 +39,7 @@ for (const [processorName, procTests] of Object.entries(tests)) { testProcessor(`[${processorName}] ${test.name}`.padEnd(90, '.'), require(test.processors), test.source, test.expected, test.prepareTest, test.prepareResult); } else { skippedTests++; - console.log(`Testing [${processorName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); + console.log(`[${processorName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); } } } diff --git a/tests/testUtils.js b/tests/testUtils.js index b74365b..0d92f77 100644 --- a/tests/testUtils.js +++ b/tests/testUtils.js @@ -11,7 +11,7 @@ const tests = { * @param verifyFunc {function} - The expected output */ function testCodeSample(testName, testFunc, verifyFunc) { - process.stdout.write(`Testing ${testName}... `); + process.stdout.write(`${testName}... `); console.time('PASS'); const results = testFunc(); const expected = verifyFunc(); @@ -30,7 +30,7 @@ for (const [moduleName, moduleTests] of Object.entries(tests)) { testCodeSample(`[${moduleName}] ${test.name}`.padEnd(90, '.'), test.testFunc, test.verifyFunc); } else { skippedTests++; - console.log(`Testing [${moduleName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); + console.log(`[${moduleName}] ${test.name}...`.padEnd(101, '.') + ` SKIPPED: ${test.reason}`); } } }