restringer
Advanced tools
Comparing version 1.2.1 to 1.2.2
{ | ||
"name": "restringer", | ||
"version": "1.2.1", | ||
"version": "1.2.2", | ||
"description": "Deobfuscate Javascript with emphasis on reconstructing strings", | ||
@@ -5,0 +5,0 @@ "main": "index.js", |
@@ -18,4 +18,2 @@ # Restringer | ||
* [Read More](#read-more) | ||
* [TODO](#todo) | ||
*** | ||
@@ -41,14 +39,17 @@ | ||
Output deobfuscated result to STDOUT (nothing will print if deobfuscation failed) | ||
> node restringer.js script.js | ||
``` | ||
Usage: restringer input_filename [-h] [-c] [-q|-v] [-o [output_filename]] | ||
Show debug information and save deobfuscated script to `script.js-<obfuscation-type>-deob.js` | ||
> export DEOBDEBUG=true && node restringer.js script.js | ||
positional arguments: | ||
input_filename The obfuscated JS file | ||
Log level can be adusted via the `DEOBDEBUGLEVEL` environment variable for more or less granular | ||
log output. The default level is an arbitrary 50, simply to leave space for other levels to be added when needed. | ||
> export DEOBDEBUG=true && DEOBDEBUGLEVEL=50 && node restringer.js script.js | ||
Level 1 is most verbose, level 2 is a good value to use for debugging. | ||
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. | ||
-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. | ||
Use <input_filename>-deob.js if no filename is provided. | ||
``` | ||
### Use as a Module | ||
@@ -55,0 +56,0 @@ |
@@ -12,15 +12,21 @@ /** | ||
n.parentNode.type === 'BlockStatement'); | ||
for (const c of candidates) { | ||
if (c.parentNode.body?.length > 1) { | ||
const parent = c.parentNode; | ||
if (parent.body?.length > 1) { | ||
if (c.body.length === 1) arb.markNode(c, c.body[0]); | ||
else { | ||
const currentIdx = c.parentNode.body.indexOf(c); | ||
const currentIdx = parent.body.indexOf(c); | ||
const replacementNode = { | ||
type: 'BlockStatement', | ||
body: [...c.parentNode.body.slice(0, currentIdx), ...c.body, ...c.parentNode.body.slice(currentIdx + 1)], | ||
body: [ | ||
...parent.body.slice(0, currentIdx), | ||
...c.body, | ||
...parent.body.slice(currentIdx + 1) | ||
], | ||
}; | ||
arb.markNode(c.parentNode, replacementNode); | ||
arb.markNode(parent, replacementNode); | ||
} | ||
} | ||
else arb.markNode(c.parentNode, c); | ||
else arb.markNode(parent, c); | ||
} | ||
@@ -27,0 +33,0 @@ return arb; |
@@ -18,3 +18,3 @@ const {badIdentifierCharsRegex, validIdentifierBeginning} = require(__dirname + '/../config'); | ||
validIdentifierBeginning.test(n.property.value) && | ||
!badIdentifierCharsRegex.exec(n.property.value)) || | ||
!badIdentifierCharsRegex.test(n.property.value)) || | ||
/** | ||
@@ -34,12 +34,14 @@ * Ignore the same cases for method names and object properties, for example | ||
validIdentifierBeginning.test(n.key.value) && | ||
!badIdentifierCharsRegex.exec(n.key.value))); | ||
!badIdentifierCharsRegex.test(n.key.value))); | ||
for (const c of candidates) { | ||
const relevantProperty = c.type === 'MemberExpression' ? 'property' : 'key'; | ||
const nonComputed = {...c}; | ||
nonComputed.computed = false; | ||
nonComputed[relevantProperty] = { | ||
type: 'Identifier', | ||
name: c[relevantProperty].value, | ||
}; | ||
arb.markNode(c, nonComputed); | ||
arb.markNode(c, { | ||
...c, | ||
computed: false, | ||
[relevantProperty]: { | ||
type: 'Identifier', | ||
name: c[relevantProperty].value, | ||
}, | ||
}); | ||
} | ||
@@ -46,0 +48,0 @@ return arb; |
@@ -8,2 +8,3 @@ /** | ||
const candidates = arb.ast.filter(n => n.type === 'EmptyStatement'); | ||
for (const c of candidates) { | ||
@@ -10,0 +11,0 @@ // A for loop is sometimes used to assign variables without providing a loop body, just an empty statement. |
@@ -1,2 +0,1 @@ | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const createNewNode = require(__dirname + '/../utils/createNewNode'); | ||
@@ -13,3 +12,4 @@ | ||
n.type === 'TemplateLiteral' && | ||
!n.expressions.filter(exp => exp.type !== 'Literal').length); | ||
!n.expressions.find(exp => exp.type !== 'Literal')); | ||
for (const c of candidates) { | ||
@@ -21,3 +21,3 @@ let newStringLiteral = ''; | ||
newStringLiteral += c.quasis.slice(-1)[0].value.raw; | ||
arb.markNode(c, createNewNode(newStringLiteral, logger)); | ||
arb.markNode(c, createNewNode(newStringLiteral)); | ||
} | ||
@@ -24,0 +24,0 @@ return arb; |
@@ -0,1 +1,3 @@ | ||
const relevantParents = ['VariableDeclarator', 'AssignmentExpression', 'FunctionDeclaration', 'ClassDeclaration']; | ||
/** | ||
@@ -9,3 +11,2 @@ * Remove nodes code which is only declared but never used. | ||
function removeDeadNodes(arb) { | ||
const relevantParents = ['VariableDeclarator', 'AssignmentExpression', 'FunctionDeclaration', 'ClassDeclaration']; | ||
const candidates = arb.ast.filter(n => | ||
@@ -16,4 +17,7 @@ n.type === 'Identifier' && | ||
.map(n => n.parentNode); | ||
for (const c of candidates) { | ||
arb.markNode(c); | ||
// Do not remove root nodes as they might be referenced in another script | ||
if (c.parentNode.type === 'Program') continue; | ||
arb.markNode(c?.parentNode?.type === 'ExpressionStatement' ? c.parentNode : c); | ||
} | ||
@@ -20,0 +24,0 @@ return arb; |
@@ -17,2 +17,3 @@ /** | ||
n.callee.declNode.parentKey === 'id'))); | ||
for (const c of candidates) { | ||
@@ -19,0 +20,0 @@ const declBody = c.callee.declNode.parentNode?.init?.body || c.callee.declNode.parentNode?.body; |
@@ -10,2 +10,3 @@ const {generateFlatAST} = require('flast'); | ||
* eval('console.log("hello world")'); // <-- will be replaced with console.log("hello world"); | ||
* eval('a(); b();'); // <-- will be replaced with '{a(); b();}' | ||
* @param {Arborist} arb | ||
@@ -20,2 +21,3 @@ * @return {Arborist} | ||
n.arguments[0]?.type === 'Literal'); | ||
for (const c of candidates) { | ||
@@ -56,3 +58,3 @@ const cacheName = `replaceEval-${generateHash(c.src)}`; | ||
} catch (e) { | ||
logger.error(`[-] Unable to replace eval's body with call expression: ${e}`, 1); | ||
logger.debug(`[-] Unable to replace eval's body with call expression: ${e}`); | ||
} | ||
@@ -59,0 +61,0 @@ } |
@@ -9,5 +9,6 @@ /** | ||
n.type === 'FunctionDeclaration' && | ||
n.body?.body?.length === 1 && | ||
n.body?.body?.length && | ||
n.body.body[0].type === 'ReturnStatement' && | ||
['Literal', 'Identifier'].includes(n.body.body[0].argument?.type)); | ||
for (const c of candidates) { | ||
@@ -14,0 +15,0 @@ const replacementNode = c.body.body[0].argument; |
@@ -11,9 +11,9 @@ /** | ||
!n.parentNode.arguments.length && | ||
n.body?.body?.length === 1 && | ||
n.body?.body?.length && | ||
n.body.body[0].type === 'ReturnStatement' && | ||
['Literal', 'Identifier'].includes(n.body.body[0].argument?.type)) | ||
.map(n => n.parentNode); | ||
for (const c of candidates) { | ||
const replacementNode = c.callee.body.body[0].argument; | ||
arb.markNode(c, replacementNode); | ||
arb.markNode(c, c.callee.body.body[0].argument); | ||
} | ||
@@ -20,0 +20,0 @@ return arb; |
@@ -12,2 +12,3 @@ const areReferencesModified = require(__dirname + '/../utils/areReferencesModified'); | ||
!(n.parentKey === 'property' && n.parentNode.type === 'ObjectExpression')); | ||
for (const c of candidates) { | ||
@@ -14,0 +15,0 @@ const valueNode = c.declNode.parentNode.init; |
@@ -17,5 +17,5 @@ const areReferencesModified = require(__dirname + '/../utils/areReferencesModified'); | ||
r.parentNode.type === 'AssignmentExpression' && | ||
getMainDeclaredObjectOfMemberExpression(r.parentNode.left).nodeId === r.nodeId).length === 1 && | ||
!n.references.filter(r => | ||
(/For.*Statement/.exec(r.parentNode.type) && | ||
getMainDeclaredObjectOfMemberExpression(r.parentNode.left) === r).length === 1 && | ||
!n.references.find(r => | ||
(/For.*Statement/.test(r.parentNode.type) && | ||
r.parentKey === 'left') || | ||
@@ -28,10 +28,11 @@ // This covers cases like: | ||
r.parentNode.parentNode?.parentNode?.parentNode?.type, | ||
].includes('ConditionalExpression')).length); | ||
].includes('ConditionalExpression'))); | ||
for (const c of candidates) { | ||
const assignmentNode = c.references.filter(r => | ||
const assignmentNode = c.references.find(r => | ||
r.parentNode.type === 'AssignmentExpression' && | ||
getMainDeclaredObjectOfMemberExpression(r.parentNode.left).nodeId === r.nodeId)[0]; | ||
getMainDeclaredObjectOfMemberExpression(r.parentNode.left) === r); | ||
const valueNode = assignmentNode.parentNode.right; | ||
if (valueNode.type !== 'Literal') continue; | ||
const refs = c.references.filter(r => r.nodeId !== assignmentNode.nodeId); | ||
const refs = c.references.filter(r => r !== assignmentNode); | ||
if (!areReferencesModified(arb.ast, refs)) { | ||
@@ -38,0 +39,0 @@ for (const ref of refs) { |
@@ -14,2 +14,3 @@ /** | ||
n.test.type === 'Literal'); | ||
for (const c of candidates) { | ||
@@ -16,0 +17,0 @@ if (c.test.value) { |
@@ -16,8 +16,6 @@ const {generateFlatAST} = require('flast'); | ||
for (const c of candidates) { | ||
// TODO: Without the next line we get an anonymous function. Is that bad? | ||
if (!['VariableDeclarator', 'AssignmentExpression'].includes(c.parentNode.type)) continue; | ||
let args = ''; | ||
if (c.arguments.length > 1) { | ||
const originalArgs = c.arguments.slice(0, -1); | ||
if (originalArgs.filter(n => n.type !== 'Literal').length) continue; | ||
if (originalArgs.find(n => n.type !== 'Literal')) continue; | ||
args = originalArgs.map(n => n.value).join(', '); | ||
@@ -24,0 +22,0 @@ } |
@@ -0,1 +1,5 @@ | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const minArrayLength = 20; | ||
/** | ||
@@ -8,7 +12,5 @@ * Resolve member expressions to their targeted index in an array. | ||
* @param {Arborist} arb | ||
* @param {object} logger | ||
* @return {Arborist} | ||
*/ | ||
function resolveMemberExpressionReferencesToArrayIndex(arb, logger) { | ||
const minArrayLength = 20; | ||
function resolveMemberExpressionReferencesToArrayIndex(arb) { | ||
const candidates = arb.ast.filter(n => | ||
@@ -19,2 +21,3 @@ n.type === 'VariableDeclarator' && | ||
n.init.elements.length > minArrayLength); | ||
for (const c of candidates) { | ||
@@ -24,7 +27,7 @@ const refs = c.id.references.map(n => n.parentNode); | ||
if ((ref.parentNode.type === 'AssignmentExpression' && ref.parentKey === 'left') || ref.type !== 'MemberExpression') continue; | ||
else if ((ref.property && ref.property.type !== 'Literal') || Number.isNaN(parseInt(ref.property?.value))) continue; | ||
if ((ref.property && ref.property.type !== 'Literal') || Number.isNaN(parseInt(ref.property?.value))) continue; | ||
try { | ||
arb.markNode(ref, c.init.elements[parseInt(ref.property.value)]); | ||
} catch (e) { | ||
logger.error(`[-] Unable to mark node for replacement: ${e}`, 1); | ||
logger.debug(`[-] Unable to mark node for replacement: ${e}`); | ||
} | ||
@@ -31,0 +34,0 @@ } |
@@ -17,2 +17,3 @@ /** | ||
n.parentNode.right.type === 'Literal'); | ||
for (const c of candidates) { | ||
@@ -25,3 +26,3 @@ const prop = c.property?.value || c.property?.name; | ||
n.parentNode.property?.name === prop) && | ||
n.parentNode.nodeId !== c.nodeId) | ||
n.parentNode !== c) | ||
.map(n => n.parentNode); | ||
@@ -28,0 +29,0 @@ if (valueUses.length) { |
@@ -25,6 +25,7 @@ /** | ||
n.body.body[0].argument.callee.type === 'Identifier'); | ||
for (const c of candidates) { | ||
const funcId = c.id; | ||
const funcName = c.id; | ||
const ret = c.body.body[0].argument; | ||
let transitiveArguments = true; | ||
let transitiveArguments = true; | ||
try { | ||
@@ -40,5 +41,6 @@ for (let i = 0; i < c.params.length; i++) { | ||
} | ||
if (!transitiveArguments) continue; | ||
for (const ref of funcId.references || []) { | ||
arb.markNode(ref, ret.callee); | ||
if (transitiveArguments) { | ||
for (const ref of funcName.references || []) { | ||
arb.markNode(ref, ret.callee); | ||
} | ||
} | ||
@@ -45,0 +47,0 @@ } |
@@ -20,2 +20,3 @@ const getDescendants = require(__dirname + '/../utils/getDescendants'); | ||
!/For.*Statement/.test(n.parentNode?.parentNode?.type)); | ||
for (const c of candidates) { | ||
@@ -26,5 +27,5 @@ const relevantIdentifier = getMainDeclaredObjectOfMemberExpression(c.id)?.declNode || c.id; | ||
const replacementMainIdentifier = getMainDeclaredObjectOfMemberExpression(c.init)?.declNode; | ||
if (replacementMainIdentifier && replacementMainIdentifier.nodeId === relevantIdentifier.nodeId) continue; | ||
if (replacementMainIdentifier && replacementMainIdentifier === relevantIdentifier) continue; | ||
// Exclude changes in the identifier's own init | ||
if (getDescendants(c.init).find(n => n.declNode?.nodeId === relevantIdentifier.nodeId)) continue; | ||
if (getDescendants(c.init).find(n => n.declNode === relevantIdentifier)) continue; | ||
if (refs.length && !areReferencesModified(arb.ast, refs) && !areReferencesModified(arb.ast, [replacementNode])) { | ||
@@ -31,0 +32,0 @@ for (const ref of refs) { |
@@ -15,2 +15,3 @@ const areReferencesModified = require(__dirname + '/../utils/areReferencesModified'); | ||
n?.init?.type === 'Identifier'))]; | ||
for (const c of candidates) { | ||
@@ -17,0 +18,0 @@ const refs = c.id.references || []; |
@@ -13,2 +13,3 @@ /** | ||
n.test.type === 'LogicalExpression'); | ||
for (const c of candidates) { | ||
@@ -15,0 +16,0 @@ if (c.test.operator === '&&') { |
@@ -21,2 +21,3 @@ /** | ||
n.body.body[0].argument.callee.object.type === 'FunctionExpression'); | ||
for (const c of candidates) { | ||
@@ -23,0 +24,0 @@ const replacementNode = c.body.body[0].argument.callee.object; |
const {VM} = require('vm2'); | ||
const assert = require('node:assert'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const defaultLogger = require(__dirname + '/../utils/logger'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const getObjType = require(__dirname + '/../utils/getObjType'); | ||
@@ -43,6 +44,5 @@ const generateHash = require(__dirname + '/../utils/generateHash'); | ||
* @param {string} stringToEval | ||
* @param {object} logger (optional) logging functions. | ||
* @return {string|ASTNode} A node based on the eval result if successful; badValue string otherwise. | ||
*/ | ||
function evalInVm(stringToEval, logger = defaultLogger) { | ||
function evalInVm(stringToEval) { | ||
const cacheName = `eval-${generateHash(stringToEval)}`; | ||
@@ -56,12 +56,10 @@ if (cache[cacheName] === undefined) { | ||
const res = (new VM(vmOptions)).run(stringToEval); | ||
// noinspection JSUnresolvedVariable | ||
if (!res.VMError && !badTypes.includes(getObjType(res))) { | ||
// To exclude results based on randomness or timing, eval again and compare results | ||
const res2 = (new VM(vmOptions)).run(stringToEval); | ||
if (JSON.stringify(res) === JSON.stringify(res2)) { | ||
cache[cacheName] = createNewNode(res); | ||
} | ||
assert.deepEqual(res, res2); | ||
cache[cacheName] = createNewNode(res); | ||
} | ||
} catch (e) { | ||
logger.error(`[-] Error in _evalInVm: ${e}`, 1); | ||
logger.debug(`[-] Error in _evalInVm: ${e}`); | ||
} | ||
@@ -68,0 +66,0 @@ } |
// noinspection HtmlRequiredLangAttribute,HtmlRequiredTitleElement | ||
const fs = require('fs'); | ||
const fs = require('node:fs'); | ||
const {NodeVM} = require('vm2'); | ||
const {JSDOM} = require('jsdom'); | ||
const defaultLogger = require(__dirname + '/../utils/logger'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const generateHash = require(__dirname + '/../utils/generateHash'); | ||
@@ -18,6 +18,5 @@ | ||
* @param {boolean} injectjQuery Inject jQuery into the VM if true. | ||
* @param {object} logger (optional) logging functions. | ||
* @return {string} The output string if successful; empty string otherwise. | ||
*/ | ||
function evalWithDom(stringToEval, injectjQuery = false, logger = defaultLogger) { | ||
function evalWithDom(stringToEval, injectjQuery = false) { | ||
const cacheName = `evalWithDom-${generateHash(stringToEval)}`; | ||
@@ -54,3 +53,3 @@ if (!cache[cacheName]) { | ||
} catch (e) { | ||
logger.error(`[-] Error in evalWithDom: ${e}`, 1); | ||
logger.debug(`[-] Error in evalWithDom: ${e}`); | ||
} | ||
@@ -57,0 +56,0 @@ cache[cacheName] = out; |
@@ -5,17 +5,18 @@ const evalInVm = require(__dirname + '/evalInVm'); | ||
const relevantNodeTypes = ['Literal', 'ArrayExpression', 'ObjectExpression', 'UnaryExpression']; | ||
/** | ||
* Replace redundant not operators with actual value (e.g. !true -> false) | ||
* @param {Arborist} arb | ||
* @param {object?} logger (optional) logging functions. | ||
* @return {Arborist} | ||
*/ | ||
function normalizeRedundantNotOperator(arb, logger = {error: () => {}}) { | ||
const relevantNodeTypes = ['Literal', 'ArrayExpression', 'ObjectExpression', 'UnaryExpression']; | ||
function normalizeRedundantNotOperator(arb) { | ||
const candidates = arb.ast.filter(n => | ||
n.operator === '!' && | ||
n.type === 'UnaryExpression' && | ||
relevantNodeTypes.includes(n.argument.type) && | ||
n.operator === '!'); | ||
relevantNodeTypes.includes(n.argument.type)); | ||
for (const c of candidates) { | ||
if (canUnaryExpressionBeResolved(c.argument)) { | ||
const newNode = evalInVm(c.src, logger); | ||
const newNode = evalInVm(c.src); | ||
if (newNode !== badValue) arb.markNode(c, newNode); | ||
@@ -22,0 +23,0 @@ } |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const getDescendants = require(__dirname + '/../utils/getDescendants'); | ||
@@ -16,7 +15,8 @@ | ||
n.type === 'FunctionDeclaration' && n.id); | ||
for (const c of candidates) { | ||
const descendants = getDescendants(c); | ||
if (descendants.filter(d => | ||
if (descendants.find(d => | ||
d.type === 'AssignmentExpression' && | ||
d.left?.name === c.id?.name).length) { | ||
d.left?.name === c.id?.name)) { | ||
const arrDecryptor = c; | ||
@@ -26,2 +26,3 @@ const arrCandidates = descendants.filter(n => | ||
.map(n => n.object); | ||
for (const ac of arrCandidates) { | ||
@@ -39,4 +40,3 @@ // If a direct reference to a global variable pointing at an array | ||
if (arrRef) { | ||
const arrRefId = ac.declNode.nodeId; | ||
const iifes = arb.ast.filter(n => | ||
const iife = arb.ast.find(n => | ||
n.type === 'ExpressionStatement' && | ||
@@ -47,13 +47,12 @@ n.expression.type === 'CallExpression' && | ||
n.expression.arguments[0].type === 'Identifier' && | ||
n.expression.arguments[0].declNode.nodeId === arrRefId); | ||
if (iifes.length) { | ||
const iife = iifes[0]; | ||
n.expression.arguments[0].declNode === ac.declNode); | ||
if (iife) { | ||
const context = [arrRef.src, arrDecryptor.src, iife.src].join('\n'); | ||
const skipScopes = [arrRef.scope.scopeId, arrDecryptor.scope.scopeId, iife.expression.callee.scope.scopeId]; | ||
const skipScopes = [arrRef.scope, arrDecryptor.scope, iife.expression.callee.scope]; | ||
const replacementCandidates = arb.ast.filter(n => | ||
n?.callee?.name === arrDecryptor.id.name && | ||
!skipScopes.includes(n.scope.scopeId)); | ||
!skipScopes.includes(n.scope)); | ||
for (const rc of replacementCandidates) { | ||
const src = `${context}\n${rc.src}`; | ||
const newNode = evalInVm(src, logger); | ||
const newNode = evalInVm(src); | ||
if (newNode !== badValue) arb.markNode(rc, newNode); | ||
@@ -60,0 +59,0 @@ } |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const createNewNode = require(__dirname + '/../utils/createNewNode'); | ||
@@ -18,3 +17,4 @@ const safeImplementations = require(__dirname + '/../utils/safeImplementations'); | ||
n.type === 'CallExpression' && | ||
!n.arguments.filter(a => a.type !== 'Literal').length); | ||
!n.arguments.find(a => a.type !== 'Literal')); | ||
const candidates = callsWithOnlyLiteralArugments.filter(n => | ||
@@ -24,2 +24,3 @@ n.callee.type === 'Identifier' && | ||
!skipBuiltinFunctions.includes(n.callee.name)); | ||
candidates.push(...callsWithOnlyLiteralArugments.filter(n => | ||
@@ -30,5 +31,7 @@ n.callee.type === 'MemberExpression' && | ||
!skipProperties.includes(n.callee.property?.name || n.callee.property?.value))); | ||
candidates.push(...arb.ast.filter(n => | ||
n.type === 'CallExpression' && | ||
availableSafeImplementations.includes((n.callee.name)))); | ||
for (const c of candidates) { | ||
@@ -43,6 +46,6 @@ try { | ||
if (tempValue) { | ||
arb.markNode(c, createNewNode(tempValue, logger)); | ||
arb.markNode(c, createNewNode(tempValue)); | ||
} | ||
} else { | ||
const newNode = evalInVm(c.src, logger); | ||
const newNode = evalInVm(c.src); | ||
if (newNode !== badValue) arb.markNode(c, newNode); | ||
@@ -49,0 +52,0 @@ } |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const doesBinaryExpressionContainOnlyLiterals = require(__dirname + '/../utils/doesBinaryExpressionContainOnlyLiterals'); | ||
/** | ||
@@ -17,4 +17,5 @@ * Resolve definite binary expressions. | ||
doesBinaryExpressionContainOnlyLiterals(n)); | ||
for (const c of candidates) { | ||
const newNode = evalInVm(c.src, logger); | ||
const newNode = evalInVm(c.src); | ||
if (newNode !== badValue) arb.markNode(c, newNode); | ||
@@ -21,0 +22,0 @@ } |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
@@ -16,2 +15,3 @@ /** | ||
n.type === 'MemberExpression' && | ||
n.parentNode.type !== 'UpdateExpression' && // Prevent replacing (++[[]][0]) with (++1) | ||
(n.property.type === 'Literal' || | ||
@@ -21,5 +21,5 @@ (n.property.name && !n.computed)) && | ||
(n.object?.value?.length || n.object?.elements?.length)); | ||
for (const c of candidates) { | ||
if (c.parentNode.type === 'UpdateExpression') continue; // Prevent replacing (++[[]][0]) with (++1) | ||
const newNode = evalInVm(c.src, logger); | ||
const newNode = evalInVm(c.src); | ||
if (newNode !== badValue) arb.markNode(c, newNode); | ||
@@ -26,0 +26,0 @@ } |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
@@ -15,4 +14,5 @@ /** | ||
n.test.type === 'Literal'); | ||
for (const c of candidates) { | ||
const newNode = evalInVm(`!!(${c.test.src});`, logger); | ||
const newNode = evalInVm(`Boolean(${c.test.src});`); | ||
if (newNode.type === 'Literal') { | ||
@@ -19,0 +19,0 @@ arb.markNode(c, newNode.value ? c.consequent : c.alternate); |
const {parseCode} = require('flast'); | ||
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
@@ -19,6 +18,7 @@ /** | ||
n.arguments[0].type !== 'Literal'); | ||
for (const c of candidates) { | ||
const argument = c.arguments[0]; | ||
const src = `var __a_ = ${argument.src}\n;__a_`; | ||
const newNode = evalInVm(src, logger); | ||
const newNode = evalInVm(src); | ||
const targetNode = c.parentNode.type === 'ExpressionStatement' ? c.parentNode : c; | ||
@@ -40,3 +40,3 @@ let replacementNode = newNode; | ||
} catch {} | ||
if (replacementNode !== badValue) {arb.markNode(targetNode, replacementNode);} | ||
if (replacementNode !== badValue) arb.markNode(targetNode, replacementNode); | ||
} | ||
@@ -43,0 +43,0 @@ return arb; |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const createOrderedSrc = require(__dirname + '/../utils/createOrderedSrc'); | ||
@@ -21,3 +20,4 @@ const getDeclarationWithContext = require(__dirname + '/../utils/getDeclarationWithContext'); | ||
n.operator === '=' && | ||
(/FunctionExpression/.test(n.right?.type) || n.right?.type === 'Identifier')); | ||
(/FunctionExpression|Identifier/.test(n.right?.type))); | ||
for (const c of candidates) { | ||
@@ -30,2 +30,3 @@ const methodName = c.left.property?.name || c.left.property?.value; | ||
(n.callee.property?.name || n.callee.property?.value) === methodName); | ||
for (const ref of references) { | ||
@@ -40,3 +41,3 @@ const refContext = [ | ||
const src = `${createOrderedSrc([...context, ...refContext])}\n${ref.src}`; | ||
const newNode = evalInVm(src, logger); | ||
const newNode = evalInVm(src); | ||
if (newNode !== badValue) arb.markNode(ref, newNode); | ||
@@ -43,0 +44,0 @@ } |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const getCache = require(__dirname + '/../utils/getCache'); | ||
const getCalleeName = require(__dirname + '/../utils/getCalleeName'); | ||
const isNodeInRanges = require(__dirname + '/../utils/isNodeInRanges'); | ||
const createOrderedSrc = require(__dirname + '/../utils/createOrderedSrc'); | ||
const doesNodeContainRanges = require(__dirname + '/../utils/doesNodeContainRanges'); | ||
const getDeclarationWithContext = require(__dirname + '/../utils/getDeclarationWithContext'); | ||
@@ -38,4 +37,4 @@ const {badValue, badArgumentTypes, skipIdentifiers, skipProperties} = require(__dirname + '/../config'); | ||
for (const c of candidates.sort(sortByFrequency)) { | ||
if (c.arguments.filter(a => badArgumentTypes.includes(a.type)).length) continue; | ||
if (doesNodeContainRanges(c, modifiedRanges)) continue; | ||
if (c.arguments.find(a => badArgumentTypes.includes(a.type))) continue; | ||
if (isNodeInRanges(c, modifiedRanges)) continue; | ||
const callee = c.callee?.object || c.callee; | ||
@@ -46,3 +45,3 @@ const declNode = c.callee?.declNode || c.callee?.object?.declNode; | ||
const returnArg = declNode.parentNode.body.body[0].argument; | ||
if (['Literal', 'Identifier'].includes(returnArg.type) || /Function/.exec(returnArg.type)) continue; // Unwrap identifier | ||
if (['Literal', 'Identifier'].includes(returnArg.type) || /Function/.test(returnArg.type)) continue; // Unwrap identifier | ||
else if (returnArg.type === 'CallExpression' && | ||
@@ -57,3 +56,3 @@ returnArg.callee?.object?.type === 'FunctionExpression' && | ||
(callee.type === 'ArrayExpression' && !callee.elements.length) || | ||
!!(callee.arguments || []).filter(a => skipIdentifiers.includes(a) || a?.type === 'ThisExpression').length) continue; | ||
!!(callee.arguments || []).find(a => skipIdentifiers.includes(a) || a?.type === 'ThisExpression')) continue; | ||
if (declNode) { | ||
@@ -69,3 +68,3 @@ // Verify the declNode isn't a simple wrapper for an identifier | ||
const src = context ? `${context}\n${c.src}` : c.src; | ||
const newNode = evalInVm(src, logger); | ||
const newNode = evalInVm(src); | ||
if (newNode !== badValue && newNode.type !== 'FunctionDeclaration') { | ||
@@ -72,0 +71,0 @@ arb.markNode(c, newNode); |
@@ -1,2 +0,1 @@ | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const evalInVm = require(__dirname + '/evalInVm'); | ||
@@ -26,2 +25,3 @@ const {badValue, skipProperties} = require(__dirname + '/../config'); | ||
!skipProperties.includes(n.property?.name || n.property?.value)); | ||
for (const c of candidates) { | ||
@@ -42,7 +42,7 @@ // If this member expression is the callee of a call expression - skip it | ||
if (/Function/.test(declNode.parentNode.type) && | ||
(declNode.parentNode.params || []).filter(p => p.nodeId === declNode.nodeId).length) continue; | ||
(declNode.parentNode.params || []).find(p => p === declNode)) continue; | ||
const context = createOrderedSrc(getDeclarationWithContext(relevantIdentifier.declNode.parentNode)); | ||
if (context) { | ||
const src = `${context}\n${c.src}`; | ||
const newNode = evalInVm(src, logger); | ||
const newNode = evalInVm(src); | ||
if (newNode !== badValue) { | ||
@@ -49,0 +49,0 @@ let isEmptyReplacement = false; |
const evalInVm = require(__dirname + '/evalInVm'); | ||
const {badValue} = require(__dirname + '/../config'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const getDescendants = require(__dirname + '/../utils/getDescendants'); | ||
@@ -16,3 +15,3 @@ | ||
(n.type === 'UnaryExpression' && | ||
((n.argument.type === 'Literal' && /^\D/.exec(n.argument.raw[0])) || | ||
((n.argument.type === 'Literal' && /^\D/.test(n.argument.raw[0])) || | ||
n.argument.type === 'ArrayExpression')) || | ||
@@ -23,5 +22,6 @@ (n.type === 'BinaryExpression' && | ||
![n.left?.type, n.right?.type].includes('ThisExpression'))); | ||
for (const c of candidates) { | ||
if (getDescendants(c).find(n => n.type === 'ThisExpression')) continue; | ||
const newNode = evalInVm(c.src, logger); | ||
const newNode = evalInVm(c.src); | ||
if (newNode !== badValue) { | ||
@@ -28,0 +28,0 @@ arb.markNode(c, newNode); |
@@ -10,10 +10,15 @@ const {propertiesThatModifyContent} = require(__dirname + '/../config'); | ||
// Verify no reference is on the left side of an assignment | ||
return Boolean(refs.find(r => r.parentKey === 'left' && | ||
['AssignmentExpression', 'ForInStatement', 'ForOfStatement'].includes(r.parentNode.type)) || | ||
return Boolean(refs.find(r => | ||
(r.parentKey === 'left' && ['AssignmentExpression', 'ForInStatement', 'ForOfStatement'].includes(r.parentNode.type)) || | ||
// Verify no reference is part of an update expression | ||
refs.find(r => r.parentNode.type === 'UpdateExpression') || | ||
r.parentNode.type === 'UpdateExpression' || | ||
// Verify no variable with the same name is declared in a subscope | ||
refs.find(r => r.parentNode.type === 'VariableDeclarator' && r.parentKey === 'id') || | ||
(r.parentNode.type === 'VariableDeclarator' && r.parentKey === 'id') || | ||
// Verify no modifying calls are executed on any of the references | ||
(r.parentNode.type === 'MemberExpression' && | ||
r.parentNode.parentNode.type === 'CallExpression' && | ||
r.parentNode.parentNode.callee?.object === r && | ||
propertiesThatModifyContent.includes(r.parentNode.property?.value || r.parentNode.property?.name)) || | ||
// Verify there are no member expressions among the references which are being assigned to | ||
refs.find(r => r.type === 'MemberExpression' && | ||
(r.type === 'MemberExpression' && | ||
ast.find(n => n.type === 'AssignmentExpression' && | ||
@@ -23,10 +28,5 @@ n.left.type === 'MemberExpression' && | ||
(n.left.property?.name || n.left.property?.value === r.property?.name || r.property?.value) && | ||
(n.left.object.declNode?.nodeId && (r.object.declNode?.nodeId || r.object?.nodeId) === n.left.object.declNode.nodeId))) || | ||
// Verify no modifying calls are executed on any of the references | ||
refs.find(r => r.parentNode.type === 'MemberExpression' && | ||
r.parentNode.parentNode.type === 'CallExpression' && | ||
r.parentNode.parentNode.callee?.object?.nodeId === r.nodeId && | ||
propertiesThatModifyContent.includes(r.parentNode.property?.value || r.parentNode.property?.name))); | ||
(n.left.object.declNode && (r.object.declNode || r.object) === n.left.object.declNode))))); | ||
} | ||
module.exports = areReferencesModified; |
@@ -0,1 +1,2 @@ | ||
const logger = require(__dirname + '/logger'); | ||
const {generateCode, parseCode} = require('flast'); | ||
@@ -8,6 +9,5 @@ const {badValue} = require(__dirname + '/../config'); | ||
* @param {*} value The value to be parsed into an ASTNode. | ||
* @param {object?} logger (optional) logging functions. | ||
* @returns {ASTNode|badValue} The newly created node if successful; badValue string otherwise. | ||
*/ | ||
function createNewNode(value, logger = {debugErr: () => {}}) { | ||
function createNewNode(value) { | ||
let newNode = badValue; | ||
@@ -99,3 +99,3 @@ try { | ||
} catch (e) { | ||
logger.debugErr(`[-] Unable to create a new node: ${e}`, 1); | ||
logger.debug(`[-] Unable to create a new node: ${e}`); | ||
} | ||
@@ -102,0 +102,0 @@ return newNode; |
@@ -1,2 +0,2 @@ | ||
const crypto = require('crypto'); | ||
const crypto = require('node:crypto'); | ||
@@ -3,0 +3,0 @@ function generateHash(script) { |
@@ -27,12 +27,10 @@ const getCache = require(__dirname + '/getCache'); | ||
const collectedContext = [originNode]; | ||
const examinedNodes = []; | ||
const examineStack = [originNode]; | ||
const collectedContextIds = []; | ||
const collectedRanges = []; | ||
const examinedIds = []; | ||
while (examineStack.length) { | ||
const relevantNode = examineStack.pop(); | ||
if (examinedIds.includes(relevantNode.nodeId)) continue; | ||
else examinedIds.push(relevantNode.nodeId); | ||
if (examinedNodes.includes(relevantNode)) continue; | ||
else examinedNodes.push(relevantNode); | ||
if (isNodeMarked(relevantNode)) continue; | ||
collectedContextIds.push(relevantNode.nodeId); | ||
collectedRanges.push(relevantNode.range); | ||
@@ -91,5 +89,4 @@ let relevantScope = relevantNode.scope; | ||
for (const rn of contextToCollect) { | ||
if (rn && !collectedContextIds.includes(rn.nodeId) && !isNodeInRanges(rn, collectedRanges)) { | ||
if (rn && !collectedContext.includes(rn) && !isNodeInRanges(rn, collectedRanges)) { | ||
collectedRanges.push(rn.range); | ||
collectedContextIds.push(rn.nodeId); | ||
collectedContext.push(rn); | ||
@@ -96,0 +93,0 @@ examineStack.push(rn); |
@@ -7,3 +7,2 @@ module.exports = { | ||
doesBinaryExpressionContainOnlyLiterals: require(__dirname + '/doesBinaryExpressionContainOnlyLiterals'), | ||
doesNodeContainRanges: require(__dirname + '/doesNodeContainRanges'), | ||
generateHash: require(__dirname + '/generateHash'), | ||
@@ -10,0 +9,0 @@ getCache: require(__dirname + '/getCache'), |
@@ -7,14 +7,39 @@ /** | ||
*/ | ||
const logLevels = { | ||
DEBUG: 1, | ||
LOG: 2, | ||
ERROR: 3, | ||
NONE: 9e10, | ||
}; | ||
let currentLogLevel = logLevels.NONE; | ||
const isDebugModeOn = process.env.DEOBDEBUG === 'true' || false; | ||
const defaultDebugLevel = 50; | ||
let debugLevel = process.env.DEOBDEBUGLEVEL || defaultDebugLevel; // The lower the number the more verbose it is | ||
const log = (msg, level = defaultDebugLevel) => isDebugModeOn && level >= debugLevel ? console.log(msg) : undefined; | ||
const error = (msg, level = defaultDebugLevel) => isDebugModeOn && level >= debugLevel ? console.error(msg) : undefined; | ||
function createLoggerForLevel(logLevel) { | ||
if (!Object.values(logLevels).includes(logLevel)) throw new Error(`Unknown log level ${logLevel}.`); | ||
return msg => logLevel >= currentLogLevel ? console.log(msg) : undefined; | ||
} | ||
const debug = createLoggerForLevel(logLevels.DEBUG); | ||
const log = createLoggerForLevel(logLevels.LOG); | ||
const error = createLoggerForLevel(logLevels.ERROR); | ||
/** | ||
* Set the current log level | ||
* @param {number} newLogLevel | ||
*/ | ||
function setLogLevel(newLogLevel) { | ||
if (!Object.values(logLevels).includes(newLogLevel)) throw new Error(`Unknown log level ${newLogLevel}.`); | ||
currentLogLevel = newLogLevel; | ||
} | ||
function isLogging() { | ||
return currentLogLevel > 0; | ||
} | ||
module.exports = { | ||
isDebugModeOn, | ||
debugLevel, | ||
currentLogLevel, | ||
debug, | ||
error, | ||
isLogging, | ||
log, | ||
error, | ||
logLevels, | ||
setLogLevel, | ||
}; |
const {Arborist} = require('flast'); | ||
const generateHash = require(__dirname + '/generateHash'); | ||
const defaultLogger = require(__dirname + '/../utils/logger'); | ||
const logger = require(__dirname + '/../utils/logger'); | ||
const {defaultMaxIterations} = require(__dirname + '/../config'); | ||
@@ -12,7 +12,6 @@ | ||
* @param {function[]} funcs | ||
* @param {object?} logger (optional) logging functions. | ||
* @param {number?} maxIterations (optional) Stop the loop after this many iterations at most. | ||
* @return {string} The possibly modified script. | ||
*/ | ||
function runLoop(script, funcs, maxIterations = defaultMaxIterations, logger = defaultLogger) { | ||
function runLoop(script, funcs, maxIterations = defaultMaxIterations) { | ||
let scriptSnapshot = ''; | ||
@@ -31,3 +30,3 @@ let currentIteration = 0; | ||
try { | ||
logger.log(`\t[!] Running ${func.name}...`, 1); | ||
logger.debug(`\t[!] Running ${func.name}...`); | ||
arborist = func(arborist); | ||
@@ -48,4 +47,4 @@ if (!arborist.ast.length) break; | ||
} finally { | ||
logger.log(`\t\t[!] Running ${func.name} completed in ` + | ||
`${((+new Date() - funcStartTime) / 1000).toFixed(3)} seconds`, 1); | ||
logger.debug(`\t\t[!] Running ${func.name} completed in ` + | ||
`${((+new Date() - funcStartTime) / 1000).toFixed(3)} seconds`); | ||
} | ||
@@ -57,3 +56,2 @@ } | ||
` with ${changesCounter ? changesCounter : 'no'} changes (${arborist.ast.length} nodes)`); | ||
if (maxIterations) break; | ||
} | ||
@@ -60,0 +58,0 @@ if (changesCounter) script = arborist.script; |
@@ -1,14 +0,1 @@ | ||
const { | ||
unsafe: { | ||
evalInVm | ||
}, | ||
config: { | ||
badValue | ||
}, | ||
utils: { | ||
createOrderedSrc, | ||
getDeclarationWithContext, | ||
}, | ||
} = require(__dirname + '/../modules'); | ||
/** | ||
@@ -30,2 +17,14 @@ * Augmented Array Replacements | ||
*/ | ||
const { | ||
unsafe: { | ||
evalInVm | ||
}, | ||
config: { | ||
badValue | ||
}, | ||
utils: { | ||
createOrderedSrc, | ||
getDeclarationWithContext, | ||
}, | ||
} = require(__dirname + '/../modules'); | ||
@@ -45,4 +44,5 @@ /** | ||
n.arguments[1].type === 'Literal' && !Number.isNaN(parseInt(n.arguments[1].value))); | ||
for (const candidate of candidates) { | ||
const relevantArrayIdentifier = candidate.arguments.filter(n => n.type === 'Identifier')[0]; | ||
const relevantArrayIdentifier = candidate.arguments.find(n => n.type === 'Identifier'); | ||
// The context for this eval is the relevant array and the IIFE augmenting it (the candidate). | ||
@@ -49,0 +49,0 @@ const context = `var ${relevantArrayIdentifier.declNode.parentNode.src}\n!${createOrderedSrc(getDeclarationWithContext(candidate))}`; |
const {unsafe: {evalWithDom}, safe: {removeDeadNodes}} = require(__dirname + '/../modules'); | ||
const {generateCode, Arborist} = require('flast'); | ||
const {Arborist} = require('flast'); | ||
@@ -37,3 +37,3 @@ const lineWithFinalAssignmentRegex = /(\w{3})\[.*]\s*=.*\((\w{3})\).*=\s*\1\s*\+\s*['"]/ms; | ||
// We can catch the variable holding the code before it's injected and output it instead. | ||
let script = generateCode(arb.ast[0]); | ||
let script = arb.script; | ||
@@ -40,0 +40,0 @@ const matches = lineWithFinalAssignmentRegex.exec(script); |
@@ -0,1 +1,5 @@ | ||
/** | ||
* Function To Array Replacements | ||
* The obfuscated script dynamically generates an array which is referenced throughout the script. | ||
*/ | ||
const { | ||
@@ -15,7 +19,2 @@ unsafe: { | ||
/** | ||
* Function To Array Replacements | ||
* The obfuscated script dynamically generates an array which is referenced throughout the script. | ||
*/ | ||
/** | ||
* Run the generating function and replace it with the actual array. | ||
@@ -35,3 +34,4 @@ * Candidates are variables which are assigned a call expression, and every reference to them is a member expression. | ||
n.id?.references && | ||
n.id?.references.filter(r => r.parentNode.type === 'MemberExpression').length === n.id?.references.length); | ||
!n.id.references.find(r => r.parentNode.type !== 'MemberExpression')); | ||
for (const c of candidates) { | ||
@@ -38,0 +38,0 @@ const targetNode = c.init.callee?.declNode?.parentNode || c.init; |
@@ -18,3 +18,5 @@ /** | ||
const candidates = arb.ast.filter(n => | ||
n.type === 'Literal' && ['newState', 'removeCookie'].includes(n.value)); | ||
n.type === 'Literal' && | ||
['newState', 'removeCookie'].includes(n.value)); | ||
for (const c of candidates) { | ||
@@ -21,0 +23,0 @@ let targetNode; |
#!/usr/bin/env node | ||
const fs = require('fs'); | ||
const processors = require(__dirname + '/processors'); | ||
const detectObfuscation = require('obfuscation-detector'); | ||
const version = require(__dirname + '/../package').version; | ||
const detectObfuscation = require('obfuscation-detector'); | ||
const processors = require(__dirname + '/processors'); | ||
const { | ||
utils: { | ||
runLoop: staticRunLoop, | ||
runLoop, | ||
normalizeScript, | ||
@@ -66,2 +64,4 @@ logger, | ||
this._postprocessors = []; | ||
this.logger = logger; | ||
this.logger.setLogLevel(logger.logLevels.LOG); // Default log level | ||
} | ||
@@ -76,4 +76,4 @@ | ||
const relevantProcessors = processors[detectedObfuscationType](); | ||
if (relevantProcessors?.preprocessors?.length) this._preprocessors = relevantProcessors.preprocessors; | ||
if (relevantProcessors?.postprocessors?.length) this._postprocessors = relevantProcessors.postprocessors; | ||
this._preprocessors = relevantProcessors?.preprocessors || []; | ||
this._postprocessors = relevantProcessors?.postprocessors || []; | ||
this.obfuscationName = detectedObfuscationType; | ||
@@ -139,3 +139,4 @@ } | ||
this.modified = false; | ||
script = staticRunLoop(this.script, this._safeDeobfuscationMethods()); | ||
script = runLoop(this.script, this._safeDeobfuscationMethods()); | ||
script = runLoop(script, this._unsafeDeobfuscationMethods(), 1); | ||
if (this.script !== script) { | ||
@@ -145,7 +146,2 @@ this.modified = true; | ||
} | ||
script = staticRunLoop(this.script, this._unsafeDeobfuscationMethods(), 1); | ||
if (this.script !== script) { | ||
this.modified = true; | ||
this.script = script; | ||
} | ||
if (this.modified) modified = true; | ||
@@ -170,3 +166,3 @@ } while (this.modified); // Run this loop until the deobfuscation methods stop being effective. | ||
if (this.normalize) this.script = normalizeScript(this.script); | ||
if (clean) this.script = staticRunLoop(this.script, [removeDeadNodes]); | ||
if (clean) this.script = runLoop(this.script, [removeDeadNodes]); | ||
return this.modified; | ||
@@ -181,3 +177,3 @@ } | ||
_runProcessors(processors) { | ||
processors.forEach(proc => this.script = staticRunLoop(this.script, [proc], 1)); | ||
processors.forEach(proc => this.script = runLoop(this.script, [proc], 1)); | ||
} | ||
@@ -188,23 +184,26 @@ } | ||
if (require.main === module) { | ||
const {parseArgs, printHelp} = require(__dirname + '/utils/parseArgs'); | ||
try { | ||
const argv = process.argv; | ||
if (argv.length > 2) { | ||
const inputFilename = argv[2]; | ||
let content = fs.readFileSync(inputFilename, 'utf-8'); | ||
const args = parseArgs(process.argv.slice(2)); | ||
if (Object.keys(args).length && !(args.verbose && args.quiet) && args.inputFilename) { | ||
const fs = require('node:fs'); | ||
let content = fs.readFileSync(args.inputFilename, 'utf-8'); | ||
const startTime = Date.now(); | ||
const originalInputLength = content.length; | ||
logger.log(`[!] Attempting to deobfuscate ${inputFilename} (length: ${originalInputLength})\n`); | ||
logger.log(`[!] Deobfuscating ${args.inputFilename}...\n`); | ||
const restringer = new REstringer(content); | ||
restringer.deobfuscate(argv[3] === '--clean'); | ||
const outputFilename = `${inputFilename}-${restringer.obfuscationName}-deob.js`; | ||
if (args.quiet) restringer.logger.setLogLevel(logger.logLevels.NONE); | ||
else if (args.verbose) restringer.logger.setLogLevel(logger.logLevels.DEBUG); | ||
restringer.deobfuscate(); | ||
if (restringer.modified) { | ||
logger.log(`[+] Output saved to ${outputFilename}\n\tLength: ${restringer.script.length} ` + | ||
`(difference is ${restringer.script.length - content.length})\n\tChanges: ${restringer.totalChangesCounter}`); | ||
logger.log(`[!] Deobfuscation took ${(Date.now() - startTime) / 1000} seconds`); | ||
if (logger.isDebugModeOn) fs.writeFileSync(outputFilename, restringer.script, {encoding: 'utf-8'}); | ||
logger.log(`[+] Saved ${args.outputFilename}`); | ||
logger.log(`[!] Deobfuscation took ${(Date.now() - startTime) / 1000} seconds, with ${restringer.totalChangesCounter} changes.`); | ||
if (args.outputToFile) fs.writeFileSync(args.outputFilename, restringer.script, {encoding: 'utf-8'}); | ||
else console.log(restringer.script); | ||
} else logger.log(`[-] Nothing was deobfuscated ¯\\_(ツ)_/¯`); | ||
} else console.log('Usage:\n\trestringer.js obfuscated.js \t\t# Print deobfuscated file to stdout\n\t' + | ||
'restringer.js obfuscated.js --clean \t# Print deobfuscated file to stdout and remove dead nodes'); | ||
} else { | ||
if (!args.inputFilename) console.log(`Input filename must be provided`); | ||
else if (args.verbose && args.quiet) console.log(`Don't set both -q and -v at the same time *smh*`); | ||
console.log(printHelp()); | ||
} | ||
} catch (e) { | ||
@@ -211,0 +210,0 @@ logger.error(`[-] Critical Error: ${e}`); |
const {generateFlatAST} = require('flast'); | ||
const {badValue} = require(__dirname + '/../src/modules/config'); | ||
@@ -28,2 +29,17 @@ module.exports = [ | ||
enabled: true, | ||
name: 'consolidateNestedBlockStatements - TP-3', | ||
func: __dirname + '/../src/modules/safe/consolidateNestedBlockStatements', | ||
source: `if (a) {{do_a();} do_b();}`, | ||
expected: `if (a) {\n do_a();\n do_b();\n}`, | ||
}, | ||
{ | ||
enabled: true, | ||
looped: true, | ||
name: 'consolidateNestedBlockStatements - TP-4', | ||
func: __dirname + '/../src/modules/safe/consolidateNestedBlockStatements', | ||
source: `if (a) {{{{{do_a();}}}} do_b();}`, | ||
expected: `if (a) {\n do_a();\n do_b();\n}`, | ||
}, | ||
{ | ||
enabled: true, | ||
name: 'normalizeComputed - TP-1', | ||
@@ -155,6 +171,6 @@ func: __dirname + '/../src/modules/safe/normalizeComputed', | ||
enabled: true, | ||
name: 'resolveFunctionConstructorCalls - TN-1', | ||
name: 'resolveFunctionConstructorCalls - TP-2', | ||
func: __dirname + '/../src/modules/safe/resolveFunctionConstructorCalls', | ||
source: `a = Function.constructor('return /" + this + "/')().constructor('^([^ ]+( +[^ ]+)+)+[^ ]}');`, | ||
expected: `a = Function.constructor('return /" + this + "/')().constructor('^([^ ]+( +[^ ]+)+)+[^ ]}');`, | ||
expected: `a = function () {\n return /" + this + "/;\n}().constructor('^([^ ]+( +[^ ]+)+)+[^ ]}');`, | ||
}, | ||
@@ -249,10 +265,10 @@ { | ||
{ | ||
enabled: false, | ||
reason: 'TODO: Consider proper tests for function', | ||
name: 'evalInVm - TP-1', | ||
enabled: true, | ||
isUtil: true, | ||
name: 'evalInVm - TN-1', | ||
func: __dirname + '/../src/modules/unsafe/evalInVm', | ||
prepareTest: () => {}, | ||
prepareResult: () => {}, | ||
source: ``, | ||
expected: `function a(x) {\n return x + 3;\n}`, | ||
prepareTest: a => [a], | ||
prepareResult: b => b, | ||
source: `Math.random();`, | ||
expected: badValue, | ||
}, | ||
@@ -442,2 +458,3 @@ { | ||
enabled: true, | ||
isUtil: true, | ||
name: 'areReferencesModified - TP-1', | ||
@@ -455,2 +472,3 @@ func: __dirname + '/../src/modules/utils/areReferencesModified', | ||
enabled: true, | ||
isUtil: true, | ||
name: 'areReferencesModified - TP-2', | ||
@@ -468,2 +486,3 @@ func: __dirname + '/../src/modules/utils/areReferencesModified', | ||
enabled: true, | ||
isUtil: true, | ||
name: 'areReferencesModified - TN-1', | ||
@@ -470,0 +489,0 @@ func: __dirname + '/../src/modules/utils/areReferencesModified', |
@@ -7,7 +7,7 @@ module.exports = { | ||
'Prototype Calls': 'prototypeCalls.js', | ||
'Obfuscator.io': 'obfuscatorIo.js', | ||
'Caesar+': 'caesar.js', | ||
'eval(Ox$': 'evalOxd.js', | ||
'$s': 'ds.js', | ||
'Obfuscator.io': 'obfuscatorIo.js', | ||
'Local Proxies': 'localProxies.js', | ||
}; |
@@ -814,4 +814,4 @@ var _0x2d93 = [ | ||
var _0x5efb50 = function () { | ||
var _0x5d91f6 = _0x5efb50.constructor('return /" + this + "/')().constructor('^([^ ]+( +[^ ]+)+)+[^ ]}'); | ||
return !_0x5d91f6.test(_0x5d8b17); | ||
var _0x5d91f6 = /^([^ ]+( +[^ ]+)+)+[^ ]}/; | ||
return !/^([^ ]+( +[^ ]+)+)+[^ ]}/.test(_0x5d8b17); | ||
}; | ||
@@ -818,0 +818,0 @@ return _0x5efb50(); |
@@ -145,4 +145,4 @@ var _ya = [ | ||
var a = function () { | ||
var b = a.constructor('return /" + this + "/')().compile('^([^ ]+( +[^ ]+)+)+[^ ]}'); | ||
return !b.test(_yh); | ||
var b = /^([^ ]+( +[^ ]+)+)+[^ ]}/; | ||
return !/^([^ ]+( +[^ ]+)+)+[^ ]}/.test(_yh); | ||
}; | ||
@@ -203,13 +203,15 @@ return a(); | ||
if (typeof c === 'string') { | ||
return function (e) { | ||
}.constructor('while (true) {}').apply('counter'); | ||
return function () { | ||
while (true) { | ||
} | ||
}.apply('counter'); | ||
} else { | ||
if (('' + c / c).length !== 1 || c % 20 === 0) { | ||
(function () { | ||
return true; | ||
}.constructor('debugge_').call('action')); | ||
debugge_; | ||
}.call('action')); | ||
} else { | ||
(function () { | ||
return false; | ||
}.constructor('debugge_').apply('stateObject')); | ||
debugge_; | ||
}.apply('stateObject')); | ||
} | ||
@@ -216,0 +218,0 @@ } |
@@ -1,2 +0,2 @@ | ||
const assert = require('assert'); | ||
const assert = require('node:assert'); | ||
const {REstringer} = require(__dirname + '/..'); | ||
@@ -18,2 +18,3 @@ | ||
const restringer = new REstringer(source); | ||
restringer.logger.setLogLevel(restringer.logger.logLevels.NONE); | ||
restringer.deobfuscate(); | ||
@@ -20,0 +21,0 @@ assert((restringer.script === expected || |
@@ -1,3 +0,4 @@ | ||
const assert = require('assert'); | ||
const assert = require('node:assert'); | ||
const {Arborist} = require('flast'); | ||
const {runLoop, logger} = require(__dirname + '/../src/modules').utils; | ||
@@ -20,3 +21,3 @@ const tests = { | ||
*/ | ||
function testModule(testName, testFunc, source, expected, prepTest = defaultPrepTest, prepRes = defaultPrepRes) { | ||
function testModuleOnce(testName, testFunc, source, expected, prepTest = defaultPrepTest, prepRes = defaultPrepRes) { | ||
process.stdout.write(`Testing ${testName}... `); | ||
@@ -27,9 +28,28 @@ console.time('PASS'); | ||
const result = prepRes(rawRes); | ||
assert(result === expected, | ||
`\n\tFAIL: deobfuscation result !== expected:\n-------------\n${result}\n\t!==\n${expected}\n-------------`); | ||
assert.equal(result, expected); | ||
console.timeEnd('PASS'); | ||
} | ||
/** | ||
* Generic function for verifying source code is deobfuscated as expected. | ||
* @param testName {string} - The name of the test to be displayed. | ||
* @param testFunc {function} - The tested function. | ||
* @param source {string} - The source code to be deobfuscated. | ||
* @param expected {string} - The expected output. | ||
* @param prepTest {function} - (optional) Function for preparing the test input. | ||
* @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}... `); | ||
console.time('PASS'); | ||
const testInput = prepTest ? prepTest(source) : source; | ||
const rawResult = runLoop(testInput, [testFunc]); | ||
const result = prepRes ? prepRes(rawResult) : rawResult; | ||
assert.equal(result, expected); | ||
console.timeEnd('PASS'); | ||
} | ||
let allTests = 0; | ||
let skippedTests = 0; | ||
logger.setLogLevel(logger.logLevels.NONE); | ||
console.time('tests in'); | ||
@@ -41,3 +61,6 @@ for (const [moduleName, moduleTests] of Object.entries(tests)) { | ||
if (test.enabled) { | ||
testModule(`[${moduleName}] ${test.name}`.padEnd(90, '.'), require(test.func), test.source, test.expected, test.prepareTest, test.prepareResult); | ||
// 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) | ||
if (!test.isUtil) testModuleInLoop(`[${moduleName}] ${test.name} (looped)`.padEnd(90, '.'), require(test.func), test.source, test.expected, test.prepareTest, test.prepareResult); | ||
} else { | ||
@@ -44,0 +67,0 @@ skippedTests++; |
@@ -1,7 +0,6 @@ | ||
const fs = require('fs'); | ||
const assert = require('assert'); | ||
const fs = require('node:fs'); | ||
const assert = require('node:assert'); | ||
const {REstringer} = require(__dirname + '/..'); | ||
const obfuscatedSamples = require(__dirname + '/obfuscated-samples'); | ||
const {REstringer} = require(__dirname + '/..'); | ||
const resourcePath = __dirname + '/resources'; | ||
@@ -16,6 +15,6 @@ | ||
const restringer = new REstringer(obfuscatedSource); | ||
restringer.logger.setLogLevel(restringer.logger.logLevels.NONE); | ||
restringer.deobfuscate(); | ||
const deobfuscationResult = restringer.script.replace(/[\n\r]/g, ' ').replace(/\s{2,}/g, ' '); | ||
assert(deobfuscationResult === deobfuscatedTarget, | ||
`Deobfuscation result of '${testSampleName}' does not match the expected result!\nEXPECTED:\n${deobfuscatedTarget}\n\nOUTPUT:\n${deobfuscationResult}`); | ||
assert.equal(deobfuscationResult, deobfuscatedTarget); | ||
console.timeEnd(' PASS'); | ||
@@ -22,0 +21,0 @@ } |
@@ -1,3 +0,3 @@ | ||
const assert = require('assert'); | ||
const {generateFlatAST, generateCode, Arborist} = require('flast'); | ||
const {Arborist} = require('flast'); | ||
const assert = require('node:assert'); | ||
@@ -23,9 +23,7 @@ const tests = { | ||
console.time('PASS'); | ||
const testInput = prepTest(source); | ||
let rawRes = testInput; | ||
let rawRes = prepTest(source); | ||
testProcs.preprocessors.forEach(proc => rawRes = proc(...(Array.isArray(rawRes) ? rawRes : [rawRes]))); | ||
testProcs.postprocessors.forEach(proc => rawRes = proc(...(Array.isArray(rawRes) ? rawRes : [rawRes]))); | ||
const result = prepRes(rawRes); | ||
assert(result === expected, | ||
`\n\tFAIL: deobfuscation result !== expected:\n-------------\n${result}\n\t!==\n${expected}\n-------------`); | ||
assert.equal(result, expected); | ||
console.timeEnd('PASS'); | ||
@@ -32,0 +30,0 @@ } |
@@ -7,2 +7,3 @@ const availableTests = { | ||
}; | ||
console.time('\nAll tests completed in'); | ||
@@ -9,0 +10,0 @@ let exception = ''; |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
584924
9703
75
163
105