r2papi
Version:
r2api on top of r2pipe for typescript and js
1,001 lines (1,000 loc) • 27.3 kB
JavaScript
"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}:cfg.bigendian=false`)); // requires 5.8.9
}
readU16be() {
return parseInt(this.api.cmd(`pv2d@${this.addr}: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}:cfg.bigendian=false`)); // requires 5.8.9
}
readS16be() {
return parseInt(this.api.cmd(`pv2d@${this.addr}: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}:cfg.bigendian=false`)); // requires 5.8.9
}
readU32be() {
return parseInt(this.api.cmd(`pv4u@${this.addr}: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}:cfg.bigendian=false`)); // requires 5.8.9
}
readU64be() {
return parseInt(this.api.cmd(`pv8u@${this.addr}: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}:cfg.bigendian=true`);
return true;
}
writeU16le(n) {
this.api.cmd(`wv2 ${n}@${this.addr}: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}:cfg.bigendian=true`);
return true;
}
writeU32le(n) {
this.api.cmd(`wv4 ${n}@${this.addr}: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}:cfg.bigendian=true`);
return true;
}
writeU64le(n) {
this.api.cmd(`wv8 ${n}@${this.addr}: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;