Skip to content

Commit

Permalink
v1.9.0 Replace vm2 with isolated-vm (#82)
Browse files Browse the repository at this point in the history
* Update dependencies

* Replace the vulnerable vm2 library with isolated-vm.

* Disable Caesar+ support as evalWithDom is yet unsupported.

* Adjust tests to new sandbox specs.

* 1.9.0
  • Loading branch information
BenBaryoPX authored Jul 24, 2023
1 parent 579d41b commit 26541a5
Show file tree
Hide file tree
Showing 20 changed files with 237 additions and 313 deletions.
345 changes: 119 additions & 226 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "restringer",
"version": "1.8.0",
"version": "1.9.0",
"description": "Deobfuscate Javascript with emphasis on reconstructing strings",
"main": "index.js",
"bin": {
Expand All @@ -12,9 +12,9 @@
},
"dependencies": {
"flast": "^1.5.0",
"isolated-vm": "^4.5.0",
"jsdom": "^22.1.0",
"obfuscation-detector": "^1.1.4",
"vm2": "^3.9.19"
"obfuscation-detector": "^1.1.4"
},
"scripts": {
"test": "node --trace-warnings tests/testRestringer.js"
Expand All @@ -38,7 +38,7 @@
},
"homepage": "https://github.com/PerimeterX/Restringer#readme",
"devDependencies": {
"eslint": "^8.42.0",
"eslint": "^8.45.0",
"husky": "^8.0.3"
}
}
8 changes: 4 additions & 4 deletions src/modules/unsafe/normalizeRedundantNotOperator.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const canUnaryExpressionBeResolved = require(__dirname + '/../utils/canUnaryExpressionBeResolved');

Expand All @@ -12,16 +12,16 @@ const relevantNodeTypes = ['Literal', 'ArrayExpression', 'ObjectExpression', 'Un
* @return {Arborist}
*/
function normalizeRedundantNotOperator(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSB;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.operator === '!' &&
n.type === 'UnaryExpression' &&
relevantNodeTypes.includes(n.argument.type) &&
candidateFilter(n)) {
if (canUnaryExpressionBeResolved(n.argument)) {
sharedVM = sharedVM || getVM();
const replacementNode = evalInVm(n.src, sharedVM);
sharedSB = sharedSB || new Sandbox();
const replacementNode = evalInVm(n.src, sharedSB);
if (replacementNode !== badValue) arb.markNode(n, replacementNode);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const getDescendants = require(__dirname + '/../utils/getDescendants');

Expand Down Expand Up @@ -51,11 +51,11 @@ function resolveAugmentedFunctionWrappedArrayReplacements(arb, candidateFilter =
const replacementCandidates = arb.ast.filter(c =>
c?.callee?.name === arrDecryptor.id.name &&
!skipScopes.includes(c.scope));
const contextVM = getVM();
contextVM.run(context);
const sb = new Sandbox();
sb.run(context);
for (let p = 0; p < replacementCandidates.length; p++) {
const rc = replacementCandidates[p];
const replacementNode = evalInVm(`\n${rc.src}`, contextVM);
const replacementNode = evalInVm(`\n${rc.src}`, sb);
if (replacementNode !== badValue) arb.markNode(rc, replacementNode);
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveBuiltinCalls.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const logger = require(__dirname + '/../utils/logger');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const createNewNode = require(__dirname + '/../utils/createNewNode');
const safeImplementations = require(__dirname + '/../utils/safeImplementations');
Expand Down Expand Up @@ -42,7 +42,7 @@ function isUnwantedNode(node) {
* @return {Arborist}
*/
function resolveBuiltinCalls(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSb;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (!isUnwantedNode(n) && candidateFilter(n) && (isSafeCall(n) ||
Expand All @@ -57,8 +57,8 @@ function resolveBuiltinCalls(arb, candidateFilter = () => true) {
arb.markNode(n, createNewNode(tempValue));
}
} else {
sharedVM = sharedVM || getVM();
const replacementNode = evalInVm(n.src, sharedVM);
sharedSb = sharedSb || new Sandbox();
const replacementNode = evalInVm(n.src, sharedSb);
if (replacementNode !== badValue) arb.markNode(n, replacementNode);
}
} catch (e) {
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveDefiniteBinaryExpressions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const doesBinaryExpressionContainOnlyLiterals = require(__dirname + '/../utils/doesBinaryExpressionContainOnlyLiterals');

Expand All @@ -13,12 +13,12 @@ const doesBinaryExpressionContainOnlyLiterals = require(__dirname + '/../utils/d
* @return {Arborist}
*/
function resolveDefiniteBinaryExpressions(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSb;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.type === 'BinaryExpression' && doesBinaryExpressionContainOnlyLiterals(n) && candidateFilter(n)) {
sharedVM = sharedVM || getVM();
const replacementNode = evalInVm(n.src, sharedVM);
sharedSb = sharedSb || new Sandbox();
const replacementNode = evalInVm(n.src, sharedSb);
if (replacementNode !== badValue) {
// Fix issue where a number below zero would be replaced with a string
if (replacementNode.type === 'UnaryExpression' && typeof n?.left?.value === 'number' && typeof n?.right?.value === 'number') {
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveDefiniteMemberExpressions.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');

/**
Expand All @@ -12,7 +12,7 @@ const evalInVm = require(__dirname + '/../utils/evalInVm');
* @return {Arborist}
*/
function resolveDefiniteMemberExpressions(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSb;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.type === 'MemberExpression' &&
Expand All @@ -23,8 +23,8 @@ function resolveDefiniteMemberExpressions(arb, candidateFilter = () => true) {
['ArrayExpression', 'Literal'].includes(n.object.type) &&
(n.object?.value?.length || n.object?.elements?.length) &&
candidateFilter(n)) {
sharedVM = sharedVM || getVM();
const replacementNode = evalInVm(n.src, sharedVM);
sharedSb = sharedSb || new Sandbox();
const replacementNode = evalInVm(n.src, sharedSb);
if (replacementNode !== badValue) arb.markNode(n, replacementNode);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');

/**
Expand All @@ -10,14 +10,14 @@ const evalInVm = require(__dirname + '/../utils/evalInVm');
* @return {Arborist}
*/
function resolveDeterministicConditionalExpressions(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSb;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.type === 'ConditionalExpression' &&
n.test.type === 'Literal' &&
candidateFilter(n)) {
sharedVM = sharedVM || getVM();
const replacementNode = evalInVm(`Boolean(${n.test.src});`, sharedVM);
sharedSb = sharedSb || new Sandbox();
const replacementNode = evalInVm(`Boolean(${n.test.src});`, sharedSb);
if (replacementNode.type === 'Literal') {
arb.markNode(n, replacementNode.value ? n.consequent : n.alternate);
}
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveEvalCallsOnNonLiterals.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const {parseCode} = require('flast');
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const createOrderedSrc = require(__dirname + '/../utils/createOrderedSrc');
const getDeclarationWithContext = require(__dirname + '/../utils/getDeclarationWithContext');
Expand All @@ -14,7 +14,7 @@ const getDeclarationWithContext = require(__dirname + '/../utils/getDeclarationW
* @return {Arborist}
*/
function resolveEvalCallsOnNonLiterals(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSb;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.type === 'CallExpression' &&
Expand All @@ -30,8 +30,8 @@ function resolveEvalCallsOnNonLiterals(arb, candidateFilter = () => true) {
}
const context = contextNodes.length ? createOrderedSrc(contextNodes) : '';
const src = `${context}\n;var __a_ = ${createOrderedSrc([n.arguments[0]])}\n;__a_`;
sharedVM = sharedVM || getVM();
const newNode = evalInVm(src, sharedVM);
sharedSb = sharedSb || new Sandbox();
const newNode = evalInVm(src, sharedSb);
const targetNode = n.parentNode.type === 'ExpressionStatement' ? n.parentNode : n;
let replacementNode = newNode;
try {
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveFunctionToArray.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Function To Array Replacements
* The obfuscated script dynamically generates an array which is referenced throughout the script.
*/
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const {
createOrderedSrc,
Expand All @@ -22,7 +22,7 @@ const {badValue} = require(__dirname + '/../config');
* @return {Arborist}
*/
function resolveFunctionToArray(arb, candidateFilter = () => true) {
let sharedVM;
let sharedSb;
for (let i = 0; i < arb.ast.length; i++) {
const n = arb.ast[i];
if (n.type === 'VariableDeclarator' && n.init?.type === 'CallExpression' && n.id?.references &&
Expand All @@ -32,8 +32,8 @@ function resolveFunctionToArray(arb, candidateFilter = () => true) {
let src = '';
if (![n.init, n.init?.parentNode].includes(targetNode)) src += createOrderedSrc(getDeclarationWithContext(targetNode));
src += `\n${createOrderedSrc([n.init])}`;
sharedVM = sharedVM || getVM();
const replacementNode = evalInVm(src, sharedVM);
sharedSb = sharedSb || new Sandbox();
const replacementNode = evalInVm(src, sharedSb);
if (replacementNode !== badValue) {
arb.markNode(n.init, replacementNode);
}
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveInjectedPrototypeMethodCalls.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const {badValue} = require(__dirname + '/../config');
const getVM = require(__dirname + '/../utils/getVM');
const logger = require(__dirname + '/../utils/logger');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const createOrderedSrc = require(__dirname + '/../utils/createOrderedSrc');
const getDeclarationWithContext = require(__dirname + '/../utils/getDeclarationWithContext');
Expand All @@ -26,14 +26,14 @@ function resolveInjectedPrototypeMethodCalls(arb, candidateFilter = () => true)
try {
const methodName = n.left.property?.name || n.left.property?.value;
const context = getDeclarationWithContext(n);
const contextVM = getVM();
contextVM.run(createOrderedSrc(context));
const contextSb = new Sandbox();
contextSb.run(createOrderedSrc(context));
for (let j = 0; j < arb.ast.length; j++) {
const ref = arb.ast[j];
if (ref.type === 'CallExpression' &&
ref.callee.type === 'MemberExpression' &&
(ref.callee.property?.name || ref.callee.property?.value) === methodName) {
const replacementNode = evalInVm(`\n${createOrderedSrc([ref])}`, contextVM);
const replacementNode = evalInVm(`\n${createOrderedSrc([ref])}`, contextSb);
if (replacementNode !== badValue) arb.markNode(ref, replacementNode);
}
}
Expand Down
8 changes: 4 additions & 4 deletions src/modules/unsafe/resolveLocalCalls.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const getVM = require(__dirname + '/../utils/getVM');
const Sandbox = require(__dirname + '/../utils/sandbox');
const evalInVm = require(__dirname + '/../utils/evalInVm');
const getCache = require(__dirname + '/../utils/getCache');
const getCalleeName = require(__dirname + '/../utils/getCalleeName');
Expand Down Expand Up @@ -81,11 +81,11 @@ function resolveLocalCalls(arb, candidateFilter = () => true) {
if (declNode.parentNode.type === 'FunctionDeclaration' &&
declNode.parentNode?.body?.body?.length &&
['Identifier', 'Literal'].includes(declNode.parentNode.body.body[0]?.argument?.type)) continue;
const contextVM = getVM();
const contextSb = new Sandbox();
try {
contextVM.run(createOrderedSrc(getDeclarationWithContext(declNode.parentNode)));
contextSb.run(createOrderedSrc(getDeclarationWithContext(declNode.parentNode)));
if (Object.keys(cache) >= cacheLimit) cache.flush();
cache[cacheName] = contextVM;
cache[cacheName] = contextSb;
} catch {}
}
}
Expand Down
21 changes: 10 additions & 11 deletions src/modules/utils/evalInVm.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const getVM = require(__dirname + '/getVM');
const Sandbox = require(__dirname + '/sandbox');
const assert = require('node:assert');
const {badValue} = require(__dirname + '/../config');
const logger = require(__dirname + '/../utils/logger');
Expand Down Expand Up @@ -33,15 +33,13 @@ const trapStrings = [ // Rules for diffusing code traps.
let cache = {};
const maxCacheSize = 100;

/** @typedef {import('vm2')} VM */

/**
* Eval a string in a ~safe~ VM environment
* Eval a string in an ~isolated~ environment
* @param {string} stringToEval
* @param {VM} [vm] (optional) an existing vm loaded with context.
* @param {Sandbox} [sb] (optional) an existing sandbox loaded with context.
* @return {ASTNode|badValue} A node based on the eval result if successful; badValue string otherwise.
*/
function evalInVm(stringToEval, vm) {
function evalInVm(stringToEval, sb) {
const cacheName = `eval-${generateHash(stringToEval)}`;
if (cache[cacheName] === undefined) {
if (Object.keys(cache).length >= maxCacheSize) cache = {};
Expand All @@ -52,17 +50,18 @@ function evalInVm(stringToEval, vm) {
const ts = trapStrings[i];
stringToEval = stringToEval.replace(ts.trap, ts.replaceWith);
}
let v = vm || getVM();
const res = v.run(stringToEval);
let vm = sb || new Sandbox();
let res = vm.run(stringToEval);
// noinspection JSUnresolvedVariable
if (!res?.VMError && !badTypes.includes(getObjType(res))) {
if (vm.isReference(res) && !badTypes.includes(getObjType(res))) {
res = res.copySync();
// If the result is a builtin object / function, return a matching identifier
const objKeys = Object.keys(res).sort().join('');
if (matchingObjectKeys[objKeys]) cache[cacheName] = matchingObjectKeys[objKeys];
else {
// To exclude results based on randomness or timing, eval again and compare results
v = vm || getVM();
const res2 = v.run(stringToEval);
vm = sb || new Sandbox();
const res2 = vm.run(stringToEval).copySync();
assert.deepEqual(res.toString(), res2.toString());
cache[cacheName] = createNewNode(res);
}
Expand Down
8 changes: 2 additions & 6 deletions src/modules/utils/evalWithDom.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// noinspection HtmlRequiredLangAttribute,HtmlRequiredTitleElement

const fs = require('node:fs');
const {NodeVM} = require('vm2');
const Sandbox = require(__dirname + '/sandbox');
const {JSDOM} = require('jsdom');
const logger = require(__dirname + '/../utils/logger');
const generateHash = require(__dirname + '/../utils/generateHash');
Expand All @@ -22,11 +22,7 @@ function evalWithDom(stringToEval, injectjQuery = false) {
if (!cache[cacheName]) {
if (Object.keys(cache).length >= maxCacheSize) cache = {};
let out = '';
const vm = new NodeVM({
console: 'redirect',
timeout: 100 * 1000,
sandbox: {JSDOM},
});
const vm = new Sandbox();
try {
// Set up the DOM, and allow script to run wild: <img src='I_too_like_to_run_scripts_dangerously.jpg'/>
let runString = 'const dom = new JSDOM(`<html><head></head><body></body></html>`, {runScripts: \'dangerously\'}); ' +
Expand Down
21 changes: 0 additions & 21 deletions src/modules/utils/getVM.js

This file was deleted.

2 changes: 1 addition & 1 deletion src/modules/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ module.exports = {
getDescendants: require(__dirname + '/getDescendants'),
getMainDeclaredObjectOfMemberExpression: require(__dirname + '/getMainDeclaredObjectOfMemberExpression'),
getObjType: require(__dirname + '/getObjType'),
getVM: require(__dirname + '/getVM'),
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'),
};
Loading

0 comments on commit 26541a5

Please sign in to comment.