Skip to content

Commit

Permalink
Upgrade flast 1.7.1 (#114)
Browse files Browse the repository at this point in the history
* Remove the redundant word Testing from the log

* Upgrade flAST to v1.7.1

- Use logger and applyIteratively from flast.utils instead of the current modules

* Add boilerplate code for starting from scratch

* 1.10.4
  • Loading branch information
ctrl-escp authored Jul 5, 2024
1 parent f235972 commit 21e0078
Show file tree
Hide file tree
Showing 20 changed files with 83 additions and 53 deletions.
49 changes: 42 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
***

Expand Down Expand Up @@ -80,39 +81,39 @@ 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.

```javascript
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
```

With the additional `candidateFilter` function argument, it's possible to narrow down the targeted nodes:
```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
```
Expand All @@ -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');
Expand All @@ -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`);

```
***
Expand Down
13 changes: 7 additions & 6 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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"
Expand Down
3 changes: 1 addition & 2 deletions src/modules/safe/replaceEvalCallsWithLiteralContent.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
3 changes: 1 addition & 2 deletions src/modules/safe/replaceNewFuncCallsWithLiteralContent.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const logger = require(__dirname + '/../utils/logger');
const {logger} = require('flast').utils;

const minArrayLength = 20;

Expand Down
2 changes: 1 addition & 1 deletion src/modules/unsafe/resolveBuiltinCalls.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
2 changes: 1 addition & 1 deletion src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
3 changes: 1 addition & 2 deletions src/modules/utils/createNewNode.js
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/modules/utils/evalInVm.js
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
3 changes: 2 additions & 1 deletion src/modules/utils/evalWithDom.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand Down
2 changes: 0 additions & 2 deletions src/modules/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
};
4 changes: 2 additions & 2 deletions src/modules/utils/normalizeScript.js
Original file line number Diff line number Diff line change
@@ -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');
Expand All @@ -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,
Expand Down
3 changes: 1 addition & 2 deletions src/modules/utils/runLoop.js
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
15 changes: 7 additions & 8 deletions src/restringer.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = [
Expand Down Expand Up @@ -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;
Expand All @@ -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;
}

Expand All @@ -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);
}
}
}
Expand All @@ -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) {
Expand Down
4 changes: 2 additions & 2 deletions tests/testDeobfuscations.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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}`);
}
}
}
Expand Down
12 changes: 6 additions & 6 deletions tests/testModules.js
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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);
Expand All @@ -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');
Expand All @@ -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}`);
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion tests/testObfuscatedSamples.js
Original file line number Diff line number Diff line change
Expand Up @@ -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'));
Expand Down
Loading

0 comments on commit 21e0078

Please sign in to comment.