UNPKG

r2papi

Version:

r2api on top of r2pipe for typescript and js

1,001 lines (1,000 loc) 27.3 kB
"use strict"; // main r2papi file Object.defineProperty(exports, "__esModule", { value: true }); exports.NativePointer = exports.NativeCallback = exports.NativeFunction = exports.R2PapiSync = exports.Assembler = exports.ProcessClass = exports.ModuleClass = exports.ThreadClass = void 0; const shell_js_1 = require("./shell.js"); const r2pipe_js_1 = require("./r2pipe.js"); class ThreadClass { constructor(r2) { this.api = null; this.api = r2; } backtrace() { return r2pipe_js_1.r2.call("dbtj"); } sleep(seconds) { return r2pipe_js_1.r2.call("sleep " + seconds); } } exports.ThreadClass = ThreadClass; class ModuleClass { constructor(r2) { this.api = null; this.api = r2; } fileName() { return this.api.call("dpe").trim(); } name() { return "Module"; } findBaseAddress() { return "TODO"; } getBaseAddress(name) { return "TODO"; } getExportByName(name) { const res = r2pipe_js_1.r2.call("iE,name/eq/" + name + ",vaddr/cols,:quiet"); return ptr(res); } findExportByName(name) { return this.getExportByName(name); } enumerateExports() { // TODO: adjust to be the same output as Frida return r2pipe_js_1.r2.callj("iEj"); } enumerateImports() { // TODO: adjust to be the same output as Frida return r2pipe_js_1.r2.callj("iij"); } enumerateSymbols() { // TODO: adjust to be the same output as Frida return r2pipe_js_1.r2.callj("isj"); } enumerateEntrypoints() { // TODO: adjust to be the same output as Frida return r2pipe_js_1.r2.callj("iej"); } enumerateRanges() { // TODO: adjust to be the same output as Frida return r2pipe_js_1.r2.callj("omj"); } } exports.ModuleClass = ModuleClass; class ProcessClass { constructor(r2) { this.r2 = null; this.r2 = r2; } enumerateMallocRanges() { } enumerateSystemRanges() { } enumerateRanges() { } enumerateThreads() { return r2pipe_js_1.r2.callj("dptj"); } enumerateModules() { r2pipe_js_1.r2.call("cfg.json.num=string"); // to handle 64bit values properly if (r2pipe_js_1.r2.callj("e cfg.debug")) { const modules = r2pipe_js_1.r2.callj("dmmj"); const res = []; for (const mod of modules) { const entry = { base: new NativePointer(mod.addr), size: new NativePointer(mod.addr_end).sub(mod.addr), path: mod.file, name: mod.name }; res.push(entry); } return res; } else { const fname = (x) => { const y = x.split("/"); return y[y.length - 1]; }; const bobjs = r2pipe_js_1.r2.callj("obj"); const res = []; for (const obj of bobjs) { const entry = { base: new NativePointer(obj.addr), size: obj.size, path: obj.file, name: fname(obj.file) }; res.push(entry); } const libs = r2pipe_js_1.r2.callj("ilj"); for (const lib of libs) { const entry = { base: 0, size: 0, path: lib, name: fname(lib) }; res.push(entry); } return res; } } getModuleByAddress(addr) { } getModuleByName(moduleName) { } codeSigningPolicy() { return "optional"; } getTmpDir() { return this.r2.call("e dir.tmp").trim(); } getHomeDir() { return this.r2.call("e dir.home").trim(); } platform() { return this.r2.call("e asm.os").trim(); } getCurrentDir() { return this.r2.call("pwd").trim(); } getCurrentThreadId() { return +this.r2.call("dpq"); } pageSize() { if (this.r2.callj("e asm.bits") === 64 && this.r2.call("e asm.arch").startsWith("arm")) { return 16384; } return 4096; } isDebuggerAttached() { return this.r2.callj("e cfg.debug"); } setExceptionHandler() { // do nothing } id() { // return this.r2.callj("dpq").trim(); } pointerSize() { return r2pipe_js_1.r2.callj("e asm.bits") / 8; } } exports.ProcessClass = ProcessClass; /** * Assembler and disassembler facilities to decode and encode instructions * * @typedef Assembler */ class Assembler { constructor(myr2) { this.program = ""; this.labels = {}; this.endian = false; this.pc = ptr(0); if (myr2 === undefined) { this.r2 = (0, r2pipe_js_1.newAsyncR2PipeFromSync)(r2pipe_js_1.r2); } else { this.r2 = myr2; } this.program = ""; this.labels = {}; } /** * Change the address of the program counter, some instructions need to know where * are they located before being encoded or decoded. * * @param {NativePointerValue} */ setProgramCounter(pc) { this.pc = pc; } setEndian(big) { this.endian = big; } toString() { return this.program; } append(x) { // append text this.pc = this.pc.add(x.length / 2); this.program += x; } // api label(s) { const pos = this.pc; // this.#program.length / 4; this.labels[s] = this.pc; return pos; } /** * Encode (assemble) an instruction by taking the string representation. * * @param {string} the string representation of the instruction to assemble * @returns {string} the hexpairs that represent the assembled instruciton */ encode(s) { const output = this.r2.call(`pa ${s}`); return output.trim(); } /** * Decode (disassemble) an instruction by taking the hexpairs string as input. * TODO: should take an array of bytes too * * @param {string} the hexadecimal pairs of bytes to decode as an instruction * @returns {string} the mnemonic and operands of the resulting decoding */ decode(s) { const output = this.r2.call(`pad ${s}`); return output.trim(); } } exports.Assembler = Assembler; /** * High level abstraction on top of the r2 command interface provided by r2pipe. * * @typedef R2Papi */ class R2PapiSync { /** * Create a new instance of the R2Papi class, taking an r2pipe interface as reference. * * @param {R2PipeSync} the r2pipe instance to use as backend. * @returns {R2Papi} instance */ constructor(r2) { this.r2 = r2; } toString() { return "[object R2Papi]"; } toJSON() { return this.toString(); } /** * Get the base address used by the current loaded binary * * @returns {NativePointer} address of the base of the binary */ getBaseAddress() { return new NativePointer(this.cmd("e bin.baddr")); } jsonToTypescript(name, a) { let str = `interface ${name} {\n`; if (a.length && a.length > 0) { a = a[0]; } for (const k of Object.keys(a)) { const typ = typeof a[k]; const nam = k; str += ` ${nam}: ${typ};\n`; } return `${str}}\n`; } /** * Get the general purpose register size of the targize architecture in bits * * @returns {number} the regsize */ getBits() { return +this.cmd("-b"); } /** * Get the name of the arch plugin selected, which tends to be the same target architecture. * Note that on some situations, this info will be stored protected bby the AirForce. * When using the r2ghidra arch plugin the underlying arch is in `asm.cpu`: * * @returns {string} the name of the target architecture. */ getArch() { return this.cmdTrim("-a"); } callTrim(x) { const res = this.call(x); return res.trim(); } cmdTrim(x) { const res = this.cmd(x); return res.trim(); } /** * Get the name of the selected CPU for the current selected architecture. * * @returns {string} the value of asm.cpu */ getCpu() { // return this.cmd('-c'); return this.cmdTrim("-e asm.cpu"); // use arch.cpu } // TODO: setEndian, setCpu, ... setArch(arch, bits) { this.cmd("-a " + arch); if (bits !== undefined) { this.cmd("-b " + bits); } } setFlagSpace(name) { this.cmd("fs " + name); } demangleSymbol(lang, mangledName) { return this.cmdTrim("iD " + lang + " " + mangledName); } setLogLevel(level) { this.cmd("e log.level=" + level); } /** * should return the id for the new map using the given file descriptor */ // rename to createMap or mapFile? newMap(fd, vaddr, size, paddr, perm, name = "") { this.cmd(`om ${fd} ${vaddr} ${size} ${paddr} ${perm} ${name}`); } at(a) { return new NativePointer(a); } getShell() { return new shell_js_1.R2Shell(this); } // Radare/Frida version() { const v = this.r2.cmd("?Vq"); return v.trim(); } // Process platform() { const output = this.r2.cmd("uname"); return output.trim(); } arch() { const output = this.r2.cmd("uname -a"); return output.trim(); } bits() { const output = this.r2.cmd("uname -b"); return output.trim(); } id() { // getpid(); return +this.r2.cmd("?vi:$p"); } // Other stuff printAt(msg, x, y) { // see pg, but pg is obrken :D } clearScreen() { this.r2.cmd("!clear"); return this; } getConfig(key) { if (key === "") { return new Error("Empty key"); } const exist = this.r2.cmd(`e~^${key} =`); if (exist.trim() === "") { return new Error("Config key does not exist"); } const value = this.r2.call("e " + key); return value.trim(); } setConfig(key, val) { this.r2.call("e " + key + "=" + val); return this; } getRegisterStateForEsil() { const dre = this.cmdj("dre"); return this.cmdj("dre"); } getRegisters() { // this.r2.log("winrar" + JSON.stringify(JSON.parse(this.r2.cmd("drj")),null, 2) ); return this.cmdj("drj"); } resizeFile(newSize) { this.cmd(`r ${newSize}`); return this; } insertNullBytes(newSize, at) { if (at === undefined) { at = "$$"; } this.cmd(`r+${newSize}@${at}`); return this; } removeBytes(newSize, at) { if (at === undefined) { at = "$$"; } this.cmd(`r-${newSize}@${at}`); return this; } seek(addr) { this.cmd(`s ${addr}`); return this; } currentSeek() { return new NativePointer("$$"); } seekToRelativeOpcode(nth) { this.cmd(`so ${nth}`); return this.currentSeek(); } getBlockSize() { return +this.cmd("b"); } setBlockSize(a) { this.cmd(`b ${a}`); return this; } countFlags() { return Number(this.cmd("f~?")); } countFunctions() { return Number(this.cmd("aflc")); } analyzeFunctionsWithEsil(depth) { this.cmd("aaef"); } analyzeProgramWithEsil(depth) { this.cmd("aae"); } analyzeProgram(depth) { if (depth === undefined) { depth = 0; } switch (depth) { case 0: this.cmd("aa"); break; case 1: this.cmd("aaa"); break; case 2: this.cmd("aaaa"); break; case 3: this.cmd("aaaaa"); break; } return this; } enumerateThreads() { // TODO: use apt/dpt to list threads at iterate over them to get the registers const regs0 = this.cmdj("drj"); const thread0 = { context: regs0, id: 0, state: "waiting", selected: true }; return [thread0]; } currentThreadId() { if (+this.cmd("e cfg.debug")) { return +this.cmd("dpt."); } return this.id(); } setRegisters(obj) { for (const r of Object.keys(obj)) { const v = obj[r]; this.r2.cmd("dr " + r + "=" + v); } } hex(s) { const output = this.r2.cmd("?v " + s); return output.trim(); } step() { this.r2.cmd("ds"); return this; } stepOver() { this.r2.cmd("dso"); return this; } math(expr) { return +this.r2.cmd("?v " + expr); } stepUntil(dst) { this.cmd(`dsu ${dst}`); } enumerateXrefsTo(s) { const output = this.call("axtq " + s); return output.trim().split(/\n/); } // TODO: rename to searchXrefsTo ? findXrefsTo(s, use_esil) { if (use_esil) { this.call("/r " + s); } else { this.call("/re " + s); } } analyzeFunctionsFromCalls() { this.call("aac"); return this; } autonameAllFunctions() { this.call("aan"); return this; } analyzeFunctionsWithPreludes() { this.call("aap"); return this; } analyzeObjCReferences() { this.cmd("aao"); return this; } analyzeImports() { this.cmd("af @ sym.imp.*"); return this; } searchDisasm(s) { const res = this.callj("/ad " + s); return res; } searchString(s) { const res = this.cmdj("/j " + s); return res; } searchBytes(data) { function num2hex(data) { return (data & 0xff).toString(16); } const s = data.map(num2hex).join(""); const res = this.cmdj("/xj " + s); return res; } binInfo() { try { return this.cmdj("ij~{bin}"); } catch (e) { return {}; } } // TODO: take a BinFile as argument instead of number selectBinary(id) { this.call(`ob ${id}`); } openFile(name) { const ofd = this.call("oqq"); this.call(`o ${name}`); const nfd = this.call("oqq"); if (ofd.trim() === nfd.trim()) { return new Error("Cannot open file"); } return parseInt(nfd); } openFileNomap(name) { const ofd = this.call("oqq"); this.call(`of ${name}`); const nfd = this.call("oqq"); if (ofd.trim() === nfd.trim()) { return new Error("Cannot open file"); } return parseInt(nfd); } currentFile(name) { return (this.call("o.")).trim(); } enumeratePlugins(type) { switch (type) { case "bin": return this.callj("Lij"); case "io": return this.callj("Loj"); case "core": return this.callj("Lcj"); case "arch": return this.callj("LAj"); case "anal": return this.callj("Laj"); case "lang": return this.callj("Llj"); } return []; } enumerateModules() { return this.callj("dmmj"); } enumerateFiles() { return this.callj("oj"); } enumerateBinaries() { return this.callj("obj"); } enumerateMaps() { return this.callj("omj"); } enumerateClasses() { return this.callj("icj"); } enumerateSymbols() { return this.callj("isj"); } enumerateExports() { return this.callj("iEj"); } enumerateImports() { return this.callj("iij"); } enumerateLibraries() { return this.callj("ilj"); } enumerateSections() { return this.callj("iSj"); } enumerateSegments() { return this.callj("iSSj"); } enumerateEntrypoints() { return this.callj("iej"); } enumerateRelocations() { return this.callj("irj"); } enumerateFunctions() { return this.cmdj("aflj"); } enumerateFlags() { return this.cmdj("fj"); } skip() { this.r2.cmd("dss"); } ptr(s) { return new NativePointer(s, this); } call(s) { return this.r2.call(s); } callj(s) { return JSON.parse(this.call(s)); } cmd(s) { return this.r2.cmd(s); } cmdj(s) { return JSON.parse(this.cmd(s)); } log(s) { return this.r2.log(s); } clippy(msg) { this.r2.log(this.r2.cmd("?E " + msg)); } ascii(msg) { this.r2.log(this.r2.cmd("?ea " + msg)); } } exports.R2PapiSync = R2PapiSync; // useful to call functions via dxc and to define and describe function signatures class NativeFunction { constructor() { } } exports.NativeFunction = NativeFunction; // uhm not sure how to map this into r2 yet class NativeCallback { constructor() { } } exports.NativeCallback = NativeCallback; /** * Class providing a way to work with 64bit pointers from Javascript, this API mimics the same * well-known promitive available in Frida, but it's baked by the current session of r2. * * It is also possible to use this class via the global `ptr` function. * * @typedef NativePointer */ class NativePointer { constructor(s, api) { if (api === undefined) { this.api = exports.R; } else { this.api = api; } // this.api.r2.log("NP " + s); this.addr = ("" + s).trim(); } /** * Filter a string to be used as a valid flag name * * @param {string} name of the symbol name * @returns {string} filtered name to be used as a flag */ filterFlag(name) { return this.api.call(`fD ${name}`); } /** * Set a flag (name) at the offset pointed * * @param {string} name of the flag to set * @returns {string} base64 decoded string */ setFlag(name) { this.api.call(`f ${name}=${this.addr}`); } /** * Remove the flag in the current offset * */ unsetFlag() { this.api.call(`f-${this.addr}`); } /** * Render an hexadecimal dump of the bytes contained in the range starting * in the current pointer and given length. * * @param {number} length optional amount of bytes to dump, using blocksize * @returns {string} string containing the hexadecimal dump of memory */ hexdump(length) { const len = length === undefined ? "" : "" + length; return this.api.cmd(`x${len}@${this.addr}`); } functionGraph(format) { if (format === "dot") { return this.api.cmd(`agfd@ ${this.addr}`); } if (format === "json") { return this.api.cmd(`agfj@${this.addr}`); } if (format === "mermaid") { return this.api.cmd(`agfm@${this.addr}`); } return this.api.cmd(`agf@${this.addr}`); } readByteArray(len) { return JSON.parse(this.api.cmd(`p8j ${len}@${this.addr}`)); } readHexString(len) { return (this.api.cmd(`p8 ${len}@${this.addr}`)).trim(); } and(a) { const addr = this.api.call(`?v ${this.addr} & ${a}`); return new NativePointer(addr.trim()); } or(a) { const addr = this.api.call(`?v ${this.addr} | ${a}`); return new NativePointer(addr.trim()); } add(a) { const addr = this.api.call(`?v ${this.addr}+${a}`); return new NativePointer(addr); } sub(a) { const addr = this.api.call(`?v ${this.addr}-${a}`); return new NativePointer(addr); } writeByteArray(data) { this.api.cmd("wx " + data.join("")); return this; } writeAssembly(instruction) { this.api.cmd(`wa ${instruction} @ ${this.addr}`); return this; } writeCString(s) { this.api.call("w " + s); return this; } writeWideString(s) { this.api.call("ww " + s); return this; } /** * Check if it's a pointer to the address zero. Also known as null pointer. * * @returns {boolean} true if null */ isNull() { return (this.toNumber()) == 0; } /** * Compare current pointer with the passed one, and return -1, 0 or 1. * * * if (this < arg) return -1; * * if (this > arg) return 1; * * if (this == arg) return 0; * * @returns {number} returns -1, 0 or 1 depending on the comparison of the pointers */ compare(a) { const bv = typeof a === "string" || typeof a === "number" ? new NativePointer(a) : a; const dist = r2pipe_js_1.r2.call(`?vi ${this.addr} - ${bv.addr}`); if (dist[0] === "-") { return -1; } if (dist[0] === "0") { return 0; } return 1; } /** * Check if it's a pointer to the address zero. Also known as null pointer. * * @returns {boolean} true if null */ pointsToNull() { const value = this.readPointer(); return (value.compare(0)) == 0; } toJSON() { const output = this.api.cmd("?vi " + this.addr.trim()); return output.trim(); } toString() { return (this.api.cmd("?v " + this.addr.trim())).trim(); } toNumber() { return parseInt(this.toString()); } writePointer(p) { this.api.cmd(`wvp ${p}@${this}`); // requires 5.8.2 } readRelativePointer() { return this.add(this.readS32()); } readPointer() { const address = this.api.call("pvp@" + this.addr); return new NativePointer(address); } readS8() { // requires 5.8.9 return parseInt(this.api.cmd(`pv1d@${this.addr}`)); } readU8() { return parseInt(this.api.cmd(`pv1u@${this.addr}`)); } readU16() { return parseInt(this.api.cmd(`pv2d@${this.addr}`)); } readU16le() { return parseInt(this.api.cmd(`pv2d@${this.addr}@e:cfg.bigendian=false`)); // requires 5.8.9 } readU16be() { return parseInt(this.api.cmd(`pv2d@${this.addr}@e:cfg.bigendian=true`)); // requires 5.8.9 } readS16() { return parseInt(this.api.cmd(`pv2d@${this.addr}`)); // requires 5.8.9 } readS16le() { return parseInt(this.api.cmd(`pv2d@${this.addr}@e:cfg.bigendian=false`)); // requires 5.8.9 } readS16be() { return parseInt(this.api.cmd(`pv2d@${this.addr}@e:cfg.bigendian=true`)); // requires 5.8.9 } readS32() { // same as readInt32() return parseInt(this.api.cmd(`pv4d@${this.addr}`)); // requires 5.8.9 } readU32() { return parseInt(this.api.cmd(`pv4u@${this.addr}`)); // requires 5.8.9 } readU32le() { return parseInt(this.api.cmd(`pv4u@${this.addr}@e:cfg.bigendian=false`)); // requires 5.8.9 } readU32be() { return parseInt(this.api.cmd(`pv4u@${this.addr}@e:cfg.bigendian=true`)); // requires 5.8.9 } readU64() { // XXX: use bignum or string here return parseInt(this.api.cmd(`pv8u@${this.addr}`)); } readU64le() { return parseInt(this.api.cmd(`pv8u@${this.addr}@e:cfg.bigendian=false`)); // requires 5.8.9 } readU64be() { return parseInt(this.api.cmd(`pv8u@${this.addr}@e:cfg.bigendian=true`)); // requires 5.8.9 } writeInt(n) { return this.writeU32(n); } /** * Write a byte in the current offset, the value must be between 0 and 255 * * @param {string} n number to write in the pointed byte in the current address * @returns {boolean} false if the operation failed */ writeU8(n) { this.api.cmd(`wv1 ${n}@${this.addr}`); return true; } writeU16(n) { this.api.cmd(`wv2 ${n}@${this.addr}`); return true; } writeU16be(n) { this.api.cmd(`wv2 ${n}@${this.addr}@e:cfg.bigendian=true`); return true; } writeU16le(n) { this.api.cmd(`wv2 ${n}@${this.addr}@e:cfg.bigendian=false`); return true; } writeU32(n) { this.api.cmd(`wv4 ${n}@${this.addr}`); return true; } writeU32be(n) { this.api.cmd(`wv4 ${n}@${this.addr}@e:cfg.bigendian=true`); return true; } writeU32le(n) { this.api.cmd(`wv4 ${n}@${this.addr}@e:cfg.bigendian=false`); return true; } writeU64(n) { this.api.cmd(`wv8 ${n}@${this.addr}`); return true; } writeU64be(n) { this.api.cmd(`wv8 ${n}@${this.addr}@e:cfg.bigendian=true`); return true; } writeU64le(n) { this.api.cmd(`wv8 ${n}@${this.addr}@e:cfg.bigendian=false`); return true; } readInt32() { return this.readU32(); } readCString() { const output = this.api.cmd(`pszj@${this.addr}`); return JSON.parse(output).string; } readWideString() { const output = this.api.cmd(`pswj@${this.addr}`); return JSON.parse(output).string; } readPascalString() { const output = this.api.cmd(`pspj@${this.addr}`); return JSON.parse(output).string; } instruction() { const output = this.api.cmdj(`aoj@${this.addr}`); return output[0]; } disassemble(length) { const len = length === undefined ? "" : "" + length; return this.api.cmd(`pd ${len}@${this.addr}`); } analyzeFunction() { this.api.cmd("af@" + this.addr); return this; } analyzeFunctionRecursively() { this.api.cmd("afr@" + this.addr); return this; } name() { return (this.api.cmd("fd " + this.addr)).trim(); } methodName() { // TODO: @ should be optional here, as addr should be passable as argument imho return (this.api.cmd("ic.@" + this.addr)).trim(); } symbolName() { // TODO: @ should be optional here, as addr should be passable as argument imho const name = this.api.cmd("isj.@" + this.addr); return name.trim(); } getFunction() { return this.api.cmdj("afij@" + this.addr); } basicBlock() { return this.api.cmdj("abj@" + this.addr); } functionBasicBlocks() { return this.api.cmdj("afbj@" + this.addr); } xrefs() { return this.api.cmdj("axtj@" + this.addr); } } exports.NativePointer = NativePointer;