restringer
Version:
Deobfuscate Javascript with emphasis on reconstructing strings
206 lines (195 loc) • 7.88 kB
JavaScript
import {getCache} from './getCache.js';
import {generateHash} from './generateHash.js';
import {isNodeInRanges} from './isNodeInRanges.js';
import {propertiesThatModifyContent} from '../config.js';
import {doesDescendantMatchCondition} from './doesDescendantMatchCondition.js';
// Types that give no context by themselves
const irrelevantTypesToBeFilteredOut = [
'Literal',
'Identifier',
'MemberExpression',
];
// Relevant types for giving context
const typesToCollect = [
'CallExpression',
'ArrowFunctionExpression',
'AssignmentExpression',
'FunctionDeclaration',
'FunctionExpression',
'VariableDeclarator',
];
// Child nodes that can be skipped as they give no context
const irrelevantTypesToAvoidIteratingOver = [
'Literal',
'ThisExpression',
];
// Direct child nodes of an if statement
const ifKeys = ['consequent', 'alternate'];
// Node types which are acceptable when wrapping an anonymous function
const standaloneNodeTypes = ['ExpressionStatement', 'AssignmentExpression', 'VariableDeclarator'];
/**
* @param {ASTNode} targetNode
* @return {boolean} True if the target node is directly under an if statement; false otherwise
*/
function isConsequentOrAlternate(targetNode) {
return targetNode.parentNode.type === 'IfStatement' ||
ifKeys.includes(targetNode.parentKey) ||
ifKeys.includes(targetNode.parentNode.parentKey) ||
(targetNode.parentNode.parentNode.type === 'BlockStatement' && ifKeys.includes(targetNode.parentNode.parentNode.parentKey));
}
/**
* @param {ASTNode} n
* @return {boolean} True if the target node is the object of a member expression
* and its property is being assigned to; false otherwise.
*/
function isNodeAnAssignmentToProperty(n) {
return n.parentNode.type === 'MemberExpression' &&
!isConsequentOrAlternate(n.parentNode) &&
((n.parentNode.parentNode.type === 'AssignmentExpression' && // e.g. targetNode.prop = value
n.parentNode.parentKey === 'left') ||
(n.parentKey === 'object' &&
(n.parentNode.property.isMarked || // Marked references won't be collected
// propertiesThatModifyContent - e.g. targetNode.push(value) - changes the value of targetNode
propertiesThatModifyContent.includes(n.parentNode.property?.value || n.parentNode.property.name))));
}
/**
* @param {ASTNode[]} nodes
* @return {ASTNode[]} Nodes which aren't contained in other nodes from the array
*/
function removeRedundantNodes(nodes) {
/** @type {ASTNode[]} */
const keep = [];
for (let i = 0; i < nodes.length; i++) {
const targetNode = nodes[i],
targetStart = targetNode.start,
targetEnd = targetNode.end;
if (!nodes.some(n => n !== targetNode && n.start <= targetStart && n.end >= targetEnd)) {
keep.push(targetNode);
}
}
return keep;
}
/**
* @param {ASTNode} originNode
* @param {boolean} [excludeOriginNode] (optional) Do not return the originNode. Defaults to false.
* @return {ASTNode[]} A flat array of all available declarations and call expressions relevant to
* the context of the origin node.
*/
export function getDeclarationWithContext(originNode, excludeOriginNode = false) {
/** @type {ASTNode[]} */
const stack = [originNode]; // The working stack for nodes to be reviewed
/** @type {ASTNode[]} */
const collected = []; // These will be our context
/** @type {ASTNode[]} */
const seenNodes = []; // Collected to avoid re-iterating over the same nodes
/** @type {number[][]} */
const collectedRanges = []; // Collected to prevent collecting nodes from within collected nodes.
/**
* @param {ASTNode} node
*/
function addToStack(node) {
if (seenNodes.includes(node) ||
stack.includes(node) ||
irrelevantTypesToAvoidIteratingOver.includes(node.type)) {} else stack.push(node);
}
const cache = getCache(originNode.scriptHash);
const srcHash = generateHash(originNode.src);
const cacheNameId = `context-${originNode.nodeId}-${srcHash}`;
const cacheNameSrc = `context-${srcHash}`;
let cached = cache[cacheNameId] || cache[cacheNameSrc];
if (!cached) {
while (stack.length) {
const node = stack.shift();
if (seenNodes.includes(node)) continue;
seenNodes.push(node);
// Do not collect any context if one of the relevant nodes is marked to be replaced or deleted
if (node.isMarked || doesDescendantMatchCondition(node, n => n.isMarked)) {
collected.length = 0;
break;
}
if (typesToCollect.includes(node.type) && !isNodeInRanges(node, collectedRanges)) {
collected.push(node);
collectedRanges.push(node.range);
}
// For each node, whether collected or not, target relevant relative nodes for further review.
/** @type {ASTNode[]} */
const targetNodes = [node];
switch (node.type) {
case 'Identifier': {
const refs = node.references;
// Review the declaration of an identifier
if (node.declNode && node.declNode.parentNode) {
targetNodes.push(node.declNode.parentNode);
}
else if (refs?.length && node.parentNode) targetNodes.push(node.parentNode);
for (let i = 0; i < refs?.length; i++) {
const ref = refs[i];
// Review call expression that receive the identifier as an argument for possible augmenting functions
if ((ref.parentKey === 'arguments' && ref.parentNode.type === 'CallExpression') ||
// Review direct assignments to the identifier
(ref.parentKey === 'left' &&
ref.parentNode.type === 'AssignmentExpression' &&
node.parentNode.type !== 'FunctionDeclaration' && // Skip function reassignments
!isConsequentOrAlternate(ref))) {
targetNodes.push(ref.parentNode);
// Review assignments to property
} else if (isNodeAnAssignmentToProperty(ref)) {
targetNodes.push(ref.parentNode.parentNode);
}
}
break;
}
case 'MemberExpression':
if (node.property?.declNode) targetNodes.push(node.property.declNode);
break;
case 'FunctionExpression':
// Review the parent node of anonymous functions
if (!node.id) {
let targetParent = node;
while (targetParent.parentNode && !standaloneNodeTypes.includes(targetParent.type)) {
targetParent = targetParent.parentNode;
}
if (standaloneNodeTypes.includes(targetParent.type)) targetNodes.push(targetParent);
}
}
for (let i = 0; i < targetNodes.length; i++) {
const targetNode = targetNodes[i];
if (!seenNodes.includes(targetNode)) stack.push(targetNode);
// noinspection JSUnresolvedVariable
if (targetNode === targetNode.scope.block) {
// Collect out-of-scope variables used inside the scope
// noinspection JSUnresolvedReference
for (let j = 0; j < targetNode.scope.through.length; j++) {
// noinspection JSUnresolvedReference
addToStack(targetNode.scope.through[j].identifier);
}
}
for (let j = 0; j < targetNode?.childNodes.length; j++) {
addToStack(targetNode.childNodes[j]);
}
}
}
cached = new Set();
for (let i = 0; i < collected.length; i++) {
const n = collected[i];
if (!(
cached.has(n) ||
irrelevantTypesToBeFilteredOut.includes(n.type)) &&
!(excludeOriginNode && isNodeInRanges(n, [originNode.range]))) {
// A fix to ignore reassignments in cases where functions are overwritten as part of an anti-debugging mechanism
if (n.type === 'FunctionDeclaration' && n.id && n.id.references?.length) {
for (let j = 0; j < n.id.references.length; j++) {
const ref = n.id.references[j];
if (!(ref.parentKey === 'left' && ref.parentNode.type === 'AssignmentExpression')) {
cached.add(n);
}
}
} else cached.add(n);
}
}
cached = removeRedundantNodes([...cached]);
cache[cacheNameId] = cached; // Caching context for the same node
cache[cacheNameSrc] = cached; // Caching context for a different node with similar content
}
return cached;
}