shift-interpreter
Version:
Shift-interpreter is an experimental JavaScript meta-interpreter useful for reverse engineering and analysis. One notable difference from other projects is that shift-interpreter retains state over an entire script but can be fed expressions and statement
431 lines • 17.3 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Interpreter = exports.InterpreterCompleteEvent = exports.InterpreterEvent = exports.InterpreterEventName = void 0;
const chalk_1 = __importDefault(require("chalk"));
const debug_1 = __importDefault(require("debug"));
const shift_ast_1 = require("shift-ast");
const codegen = __importStar(require("shift-printer"));
const shift_scope_1 = __importStar(require("shift-scope"));
const errors_1 = require("./errors");
const instruction_buffer_1 = require("./instruction-buffer");
const node_handler_1 = require("./node-handler");
const util_1 = require("./util");
const debug = debug_1.default('shift-interpreter');
var InterpreterEventName;
(function (InterpreterEventName) {
InterpreterEventName["COMPLETE"] = "complete";
})(InterpreterEventName = exports.InterpreterEventName || (exports.InterpreterEventName = {}));
class InterpreterEvent {
}
exports.InterpreterEvent = InterpreterEvent;
InterpreterEvent.type = InterpreterEventName;
class InterpreterCompleteEvent extends InterpreterEvent {
constructor(result) {
super();
this.result = result;
}
}
exports.InterpreterCompleteEvent = InterpreterCompleteEvent;
class Interpreter {
constructor(options = {}) {
this.contexts = [];
this.globalScope = shift_scope_1.default(new shift_ast_1.Script({ directives: [], statements: [] }));
this.lookupTable = new shift_scope_1.ScopeLookup(this.globalScope);
this.scopeMap = new WeakMap();
this.scopeOwnerMap = new WeakMap();
this.variableMap = new Map();
this.loadedScript = new shift_ast_1.Script({ directives: [], statements: [] });
this.contextProxies = new WeakMap();
this.pointer = new instruction_buffer_1.InstructionBuffer();
this.lastStatement = new shift_ast_1.EmptyStatement();
this.lastInstruction = new instruction_buffer_1.Instruction(new shift_ast_1.EmptyStatement(), -1);
this._isReturning = false;
this._isBreaking = false;
this._isContinuing = false;
this.options = options;
if (this.options.handler) {
this.handler = new this.options.handler(this);
}
else {
this.handler = new node_handler_1.NodeHandler(this);
}
}
print(node) {
return codegen.prettyPrint(node || this.lastInstruction.node);
}
logNode(node) {
codegen.log(node);
}
codegen(node) {
codegen.printTruncated(node);
}
skipOrThrow(type) {
if (this.options.skipUnsupported)
return;
throw new errors_1.InterpreterRuntimeError(`Unsupported node ${type}`);
}
load(script, context = {}) {
debug('loading script');
this.globalScope = shift_scope_1.default(script);
this.lookupTable = new shift_scope_1.ScopeLookup(this.globalScope);
this.buildScopeMap();
this.loadedScript = script;
this.pushContext(context);
}
buildScopeMap() {
const lookupTable = this.lookupTable;
this.scopeMap = new WeakMap();
const recurse = (scope) => {
this.scopeOwnerMap.set(scope.astNode, scope);
scope.variableList.forEach((variable) => {
this.scopeMap.set(variable, scope);
});
scope.children.forEach(recurse);
};
recurse(lookupTable.scope);
}
pushContext(context) {
this.contexts.push(context);
}
popContext() {
return this.contexts.pop();
}
getCurrentContext() {
if (this.contexts.length === 0) {
debug('interpreter created with no context, creating empty context.');
this.pushContext({});
}
const context = this.contexts[this.contexts.length - 1];
if (context === undefined)
return this.contexts[0];
return context;
}
getContexts() {
return this.contexts;
}
getContext(index) {
const context = this.contexts[index];
if (context === undefined)
return this.contexts[0];
return context;
}
isReturning(state) {
if (state !== undefined)
this._isReturning = state;
return this._isReturning;
}
isBreaking(state) {
if (state !== undefined)
this._isBreaking = state;
return this._isBreaking;
}
isContinuing(state) {
if (state !== undefined)
this._isContinuing = state;
return this._isContinuing;
}
run(passedNode) {
let nodeToEvaluate = undefined;
if (passedNode) {
if (passedNode.type === 'Script') {
this.load(passedNode);
}
nodeToEvaluate = passedNode;
}
else if (this.loadedScript) {
nodeToEvaluate = this.loadedScript;
}
if (!this.loadedScript) {
// If we don't have a currentScript (haven't run load()) but were passed a node
// the node must be a Statement or Expression (or bad object) and we shouldn't run
// it without the user knowing what they are doing.
if (passedNode)
throw new errors_1.InterpreterRuntimeError(`Can not evaluate ${passedNode.type} node without loading a program (Script node) first.`);
else
throw new errors_1.InterpreterRuntimeError('No program to evaluate');
}
if (!nodeToEvaluate) {
throw new errors_1.InterpreterRuntimeError('No program to evaluate');
}
debug('starting execution');
let programResult = null;
try {
programResult = this.evaluate(nodeToEvaluate);
debug(`completed execution with result: %o`, programResult);
return programResult;
}
catch (e) {
this.errorLocation = {
lastStatement: this.lastStatement,
lastInstruction: this.lastInstruction,
};
this.handleError(e);
throw e;
}
}
handleError(e) {
debug.extend('error')(`Error during execution`);
if (this.errorLocation) {
const statementSrc = codegen.printSummary(this.errorLocation.lastStatement);
const nodeSrc = codegen.printSummary(this.errorLocation.lastInstruction.node);
console.log(statementSrc.replace(nodeSrc, `👉👉👉${chalk_1.default.red(nodeSrc)}`));
}
else {
console.log('No error location recorded.');
}
}
runToFirstError(passedNode) {
try {
return this.run(passedNode);
}
catch (e) { }
}
evaluateInstruction(instruction) {
this.lastInstruction = instruction;
const node = instruction.node;
if (util_1.isStatement(node))
this.lastStatement = node;
let result = this.handler[node.type](node);
return (instruction.result = result);
}
evaluate(node) {
if (node === null) {
return undefined;
}
else {
// yeah this looks weird, it's a remnant of when this was all async. You used
// to be able to stop, start, and break the program but the implementation with native JS promises
// caused problems. I'm keeping the instruction buffer because hey, maybe it'll come back.
this.pointer.add(node);
const instruction = this.pointer.nextInstruction();
if (!instruction) {
debug(`no instruction to evaluate, returning`);
return;
}
debug(`evaluating instruction from %o -> %o`, this.lastInstruction.node.type, node.type);
return this.evaluateInstruction(instruction);
}
}
hoistFunctions(block) {
const functions = block.statements.filter(s => s.type === 'FunctionDeclaration');
if (functions.length)
debug(`hoisting %o functions in %o`, functions.length, block.type);
for (let fnDecl of functions) {
this.evaluate(fnDecl);
}
}
hoistVars(block) {
const vars = block.statements
.filter((stmt => stmt.type === 'VariableDeclarationStatement'))
.filter((decl) => decl.declaration.kind === 'var');
if (vars.length)
debug(`hoisting %o var statements in %o`, vars.length, block.type);
for (let varDecl of vars) {
for (let declarator of varDecl.declaration.declarators)
this.bindVariable(declarator.binding, undefined);
}
}
declareVariables(decl) {
for (let declarator of decl.declarators) {
this.evaluate(declarator);
}
}
createFunction(node) {
const _debug = debug.extend('createFunction');
let name = undefined;
if (node.name) {
switch (node.name.type) {
case 'BindingIdentifier':
name = node.name.name;
break;
case 'ComputedPropertyName':
name = this.evaluate(node.name.expression);
break;
case 'StaticPropertyName':
name = node.name.value;
}
}
_debug(`creating intermediary %o: %o`, node.type, name);
const interpreter = this;
const fnDebug = debug.extend('function');
let fn;
// anonymous functions have an empty string as the name
if (!name)
name = '';
// creating a function like this, i.e. { someName: function(){} )
// allows us to create a named function by inferring the name from the property value.
fn = {
[name]: function (...args) {
fnDebug(`calling intermediary %o: %o`, node.type, name);
interpreter.pushContext(this);
const scope = interpreter.scopeOwnerMap.get(node);
if (scope) {
const argsRef = scope.variables.get('arguments');
if (argsRef)
interpreter.setRuntimeValue(argsRef, arguments);
}
if (node.type === 'Getter') {
// nothing
}
else if (node.type === 'Setter') {
fnDebug(`setter: binding passed parameter`);
interpreter.bindVariable(node.param, args[0]);
}
else {
node.params.items.forEach((el, i) => {
fnDebug(`binding function argument %o`, i + 1);
return interpreter.bindVariable(el, args[i]);
});
}
fnDebug('evaluating function body');
const result = interpreter.evaluate(node.body);
fnDebug('completed evaluating function body');
interpreter.popContext();
if (new.target) {
if (interpreter.isReturning()) {
interpreter.isReturning(false);
if (typeof result === 'object')
return result;
}
return this;
}
else {
if (interpreter.isReturning()) {
interpreter.isReturning(false);
}
return result;
}
},
}[name];
return Object.assign(fn, { _interp: true });
}
bindVariable(binding, init) {
const _debug = debug.extend('bindVariable');
switch (binding.type) {
case 'BindingIdentifier':
{
const variables = this.lookupTable.variableMap.get(binding);
if (variables.length > 1)
throw new Error('reproduce this and handle it better');
const variable = variables[0];
_debug(`binding %o to %o`, binding.name, init);
this.setRuntimeValue(variable, init);
}
break;
case 'ArrayBinding':
{
for (let i = 0; i < binding.elements.length; i++) {
const el = binding.elements[i];
const indexElement = init[i];
if (el)
this.bindVariable(el, indexElement);
}
if (binding.rest)
this.skipOrThrow('ArrayBinding->Rest/Spread');
}
break;
case 'ObjectBinding':
{
for (let i = 0; i < binding.properties.length; i++) {
const prop = binding.properties[i];
if (prop.type === 'BindingPropertyIdentifier') {
const name = prop.binding.name;
if (init[name] === undefined && prop.init) {
this.bindVariable(prop.binding, this.evaluate(prop.init));
}
else {
this.bindVariable(prop.binding, init[name]);
}
}
else {
const name = prop.name.type === 'ComputedPropertyName' ? this.evaluate(prop.name.expression) : prop.name.value;
this.bindVariable(prop.binding, init[name]);
}
}
if (binding.rest)
this.skipOrThrow('ObjectBinding->Rest/Spread');
}
break;
case 'BindingWithDefault':
if (init === undefined) {
_debug(`evaluating default for undefined argument`);
const defaults = this.evaluate(binding.init);
_debug(`binding default`);
this.bindVariable(binding.binding, defaults);
}
else {
this.bindVariable(binding.binding, init);
}
break;
}
}
updateVariableValue(node, value) {
const variables = this.lookupTable.variableMap.get(node);
if (variables.length > 1)
throw new Error('reproduce this and handle it better');
const variable = variables[0];
const decl = variable.declarations[0];
if (decl && decl.type.name === 'Const')
throw new TypeError('Assignment to constant variable.');
this.setRuntimeValue(variable, value);
return value;
}
setRuntimeValue(variable, value) {
this.variableMap.set(variable, value);
}
getRuntimeValue(node) {
const _debug = debug.extend('getVariableValue');
_debug(`retrieving value for ${node.name}`);
const variables = this.lookupTable.variableMap.get(node);
if (!variables) {
throw new Error(`${node.type} variable not found. Make sure you are passing a valid Identifier node.`);
}
if (variables.length > 1) {
_debug(`>1 variable returned, ${variables}`);
throw new Error('reproduce this and handle it better');
}
const variable = variables[0];
if (this.variableMap.has(variable)) {
const value = this.variableMap.get(variable);
return value;
}
else {
const contexts = this.getContexts();
for (let i = contexts.length - 1; i > -1; i--) {
const context = this.getContext(i);
if (!context) {
throw new Error('No context to evaluate in.');
}
if (variable.name in context) {
const value = context[variable.name];
return value;
}
}
throw new ReferenceError(`${node.name} is not defined`);
}
}
}
exports.Interpreter = Interpreter;
//# sourceMappingURL=interpreter.js.map