UNPKG

restringer

Version:

Deobfuscate Javascript with emphasis on reconstructing strings

220 lines (184 loc) 7.76 kB
# Restringer [![Node.js CI](https://github.com/PerimeterX/restringer/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/PerimeterX/restringer/actions/workflows/node.js.yml) [![Downloads](https://img.shields.io/npm/dm/restringer.svg?maxAge=43200)](https://www.npmjs.com/package/restringer) Deobfuscate Javascript and reconstruct strings. Simplify cumbersome logic where possible while adhering to scope limitations. Try it online @ [restringer.tech](https://restringer.tech). For comments and suggestions feel free to open an issue or find me on Twitter - [@ctrl__esc](https://twitter.com/ctrl__esc) ## Table of Contents * [Installation](#installation) * [npm](#npm) * [Clone The Repo](#clone-the-repo) * [Usage](#usage) * [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) *** ## Installation ### npm ```shell npm install -g restringer ``` ### Clone The Repo Requires Node 16 or newer. ```shell git clone git@github.com:PerimeterX/restringer.git cd restringer npm install ``` *** ## Usage The [restringer.js](src/restringer.js) uses generic deobfuscation methods that reconstruct and restore obfuscated strings and simplifies redundant logic meant only to encumber. REstringer employs the [Obfuscation Detector](https://github.com/PerimeterX/obfuscation-detector/blob/main/README.md) to identify specific types of obfuscation for which there's a need to apply specific deobfuscation methods in order to circumvent anti-debugging mechanisms or other code traps preventing the script from being deobfuscated. ### Command-Line Usage ``` Usage: restringer input_filename [-h] [-c] [-q | -v] [-m M] [-o [output_filename]] positional arguments: input_filename The obfuscated JS file optional arguments: -h, --help Show this help message and exit. -c, --clean Remove dead nodes from script after deobfuscation is complete (unsafe). -q, --quiet Suppress output to stdout. Output result only to stdout if the -o option is not set. Does not go with the -v option. -m, --max-iterations M Run at most M iterations -v, --verbose Show more debug messages while deobfuscating. Does not go with the -q option. -o, --output [output_filename] Write deobfuscated script to output_filename. <input_filename>-deob.js is used if no filename is provided. ``` Examples: - Print the deobfuscated script to stdout. ```shell restringer [target-file.js] ``` - Save the deobfuscated script to output.js. ```shell restringer [target-file.js] -o output.js ``` - Deobfuscate and print debug info. ```shell restringer [target-file.js] -v ``` - Deobfuscate without printing anything but the deobfuscated output. ```shell restringer [target-file.js] -q ``` ### Use as a Module ```javascript import {REstringer} from 'restringer'; const restringer = new REstringer('"RE" + "stringer"'); if (restringer.deobfuscate()) { console.log(restringer.script); } else { console.log('Nothing was deobfuscated :/'); } // Output: 'REstringer'; ``` *** ## Create Custom Deobfuscators REstringer is highly modularized. It exposes modules that allow creating custom deobfuscators 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 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 import {applyIteratively} from 'flast'; import {safe, unsafe} from 'restringer'; const {normalizeComputed} = safe; const {resolveDefiniteBinaryExpressions, resolveLocalCalls} = unsafe; let script = 'obfuscated JS here'; const deobModules = [ resolveDefiniteBinaryExpressions, resolveLocalCalls, normalizeComputed, ]; 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 import {unsafe} from 'restringer'; const {resolveLocalCalls} = unsafe; import {applyIteratively} from 'flast'; let script = 'obfuscated JS here'; // 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 = applyIteratively(script, [resolveLocalCallsInGlobalScope]); console.log(script); // Deobfuscated script ``` You can also customize any deobfuscation method while still using REstringer without running the loop yourself: ```javascript import fs from 'node:fs'; import {REstringer} from 'restringer'; const inputFilename = process.argv[2]; const code = fs.readFileSync(inputFilename, 'utf-8'); const res = new REstringer(code); // 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'); let changes = 0; // Resolve only the first 5 calls res.safeMethods[res.unsafeMethods.indexOf(targetFunc)] = function customResolveLocalCalls(n) {return targetFunc(n, () => changes++ < 5)} res.deobfuscate(); 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 import {applyIteratively, logger} from 'flast'; // Optional loading from file // import fs from '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(); /** * Replace specific strings with other strings * @param {Arborist} arb * @return {Arborist} */ function replaceSpecificLiterals(arb) { const replacements = { 'Hello': 'General', 'there!': 'Kenobi!', }; // Iterate over only the relevant nodes by targeting specific types using the typeMap property on the root node const relevantNodes = [ ...(arb.ast[0].typeMap.Literal || []), // ...(arb.ast.typeMap.TemplateLiteral || []), // unnecessary for this example, but this is how to add more types ]; for (const n of relevantNodes) { if (replacements[n.value]) { // dynamically define a replacement node by creating an object with a type and value properties // markNode(n) would delete the node, while markNode(n, {...}) would replace the node with the supplied node. arb.markNode(n, {type: 'Literal', value: replacements[n.value]}); } } return arb; } let script = code; script = applyIteratively(script, [ replaceSpecificLiterals, ]); if (code !== script) { console.log(script); // fs.writeFileSync(inputFilename + '-deob.js', script, 'utf-8'); } else console.log(`No changes`); ``` *** ## Read More * [Processors](src/processors/README.md) * [Contribution guide](CONTRIBUTING.md) * [Obfuscation Detector](https://github.com/PerimeterX/obfuscation-detector/blob/main/README.md) * [flAST](https://github.com/PerimeterX/flast/blob/main/README.md)