UNPKG

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
"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