-
Notifications
You must be signed in to change notification settings - Fork 41
/
Copy pathresolveLocalCalls.js
109 lines (104 loc) · 4.41 KB
/
resolveLocalCalls.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import {Sandbox} from '../utils/sandbox.js';
import {evalInVm} from '../utils/evalInVm.js';
import {getCache} from '../utils/getCache.js';
import {getCalleeName} from '../utils/getCalleeName.js';
import {isNodeInRanges} from '../utils/isNodeInRanges.js';
import {createOrderedSrc} from '../utils/createOrderedSrc.js';
import {getDeclarationWithContext} from '../utils/getDeclarationWithContext.js';
import {badValue, badArgumentTypes, skipIdentifiers, skipProperties} from '../config.js';
let appearances = new Map();
const cacheLimit = 100;
/**
* @param {ASTNode} a
* @param {ASTNode} b
*/
function sortByApperanceFrequency(a, b) {
return appearances.get(getCalleeName(b)) - appearances.get(getCalleeName(a));
}
/**
* @param {ASTNode} node
* @return {number}
*/
function countAppearances(node) {
const callee = getCalleeName(node);
const count = (appearances.get(callee) || 0) + 1;
appearances.set(callee, count);
return count;
}
/**
* Collect all available context on call expressions where the callee is defined in the script and attempt
* to resolve their value.
* @param {Arborist} arb
* @param {Function} candidateFilter (optional) a filter to apply on the candidates list
* @return {Arborist}
*/
export default function resolveLocalCalls(arb, candidateFilter = () => true) {
appearances = new Map();
const cache = getCache(arb.ast[0].scriptHash);
const candidates = [];
const relevantNodes = [
...(arb.ast[0].typeMap.CallExpression || []),
];
for (let i = 0; i < relevantNodes.length; i++) {
const n = relevantNodes[i];
if ((n.callee?.declNode ||
(n.callee?.object?.declNode &&
!skipProperties.includes(n.callee.property?.value || n.callee.property?.name)) ||
n.callee?.object?.type === 'Literal') &&
countAppearances(n) &&
candidateFilter(n)) {
candidates.push(n);
}
}
candidates.sort(sortByApperanceFrequency);
const modifiedRanges = [];
candidateLoop: for (let i = 0; i < candidates.length; i++) {
const c = candidates[i];
if (isNodeInRanges(c, modifiedRanges)) continue;
for (let j = 0; j < c.arguments.length; j++) {
const arg = c.arguments[j];
if (badArgumentTypes.includes(arg.type)) continue candidateLoop;
}
const callee = c.callee?.object || c.callee;
const declNode = c.callee?.declNode || c.callee?.object?.declNode;
if (declNode?.parentNode?.body?.body?.[0]?.type === 'ReturnStatement') {
// Leave this replacement to a safe function
const returnArg = declNode.parentNode.body.body[0].argument;
if (['Literal', 'Identifier'].includes(returnArg.type) || returnArg.type.includes('unction')) continue; // Unwrap identifier
else if (returnArg.type === 'CallExpression' &&
returnArg.callee?.object?.type === 'FunctionExpression' &&
(returnArg.callee.property?.name || returnArg.callee.property?.value) === 'apply') continue; // Unwrap function shells
}
const cacheName = `rlc-${callee.name || callee.value}-${declNode?.nodeId}`;
if (!cache[cacheName]) {
cache[cacheName] = badValue;
// Skip call expressions with problematic values
if (skipIdentifiers.includes(callee.name) ||
(callee.type === 'ArrayExpression' && !callee.elements.length) ||
(callee.arguments || []).some(a => skipIdentifiers.includes(a) || a?.type === 'ThisExpression')) continue;
if (declNode) {
// Verify the declNode isn't a simple wrapper for an identifier
if (declNode.parentNode.type === 'FunctionDeclaration' &&
['Identifier', 'Literal'].includes(declNode.parentNode?.body?.body?.[0]?.argument?.type)) continue;
const contextSb = new Sandbox();
try {
contextSb.run(createOrderedSrc(getDeclarationWithContext(declNode.parentNode)));
if (Object.keys(cache) >= cacheLimit) cache.flush();
cache[cacheName] = contextSb;
} catch {}
}
}
const contextVM = cache[cacheName];
const nodeSrc = createOrderedSrc([c]);
const replacementNode = contextVM === badValue ? evalInVm(nodeSrc) : evalInVm(nodeSrc, contextVM);
if (replacementNode !== badValue && replacementNode.type !== 'FunctionDeclaration' && replacementNode.name !== 'undefined') {
// Prevent resolving a function's toString as it might be an anti-debugging mechanism
// which will spring if the code is beautified
if (c.callee.type === 'MemberExpression' && (c.callee.property?.name || c.callee.property?.value) === 'toString' &&
replacementNode?.value.substring(0, 8) === 'function') continue;
arb.markNode(c, replacementNode);
modifiedRanges.push(c.range);
}
}
return arb;
}