UNPKG

blessr2

Version:

bless-based ui for radare2

1,074 lines (1,018 loc) 27.9 kB
#!/usr/bin/env node /* * ---------------- * Blessr2 frontend * ---------------- * author: pancake@nopcode.org * date: 2015-12-25 */ 'use strict'; const r2pipe = require('r2pipe'); const process = require('process'); const blessed = require('blessed'); const contrib = require('blessed-contrib'); const program = blessed.program(); /* global vars (must die at some point) */ var regs = undefined; var offsetStack = []; //const target = 'http://cloud.radare.org/cmd/'; function parseOptions() { var config = {}; const argv = require('optimist').argv; if (argv.h) { console.log(`Usage: blessr2 [-HtnwdD] [file|url] Examples: $ blessr2 http://cloud.radare.org/cmd/ $ blessr2 -H /bin/ls ; blessr2 http://localhost:9090/cmd/ $ blessr2 -d /bin/ls $ blessr2 -nw /etc/crontab `); process.exit(0); } config.brand = 'blessr2'; config.debug = argv.d; config.https = argv.H; config.write = argv.w; config.nobin = argv.n; config.trans = argv.t; config.demos = argv.D; config.theme = argv.r; config.border = 'line'; config.target = (argv._.length > 0) ? argv._[0] : '/bin/ls'; return config; } function initScreen() { var options = { smartCSR: true, }; const isWin = /^win/.test(process.platform); if (isWin) { options.terminal = 'vt110'; options.fullUnicode = false; } else { options.terminal = 'xterm-256color'; options.fullUnicode = true; } var screen = blessed.screen(options); return screen; } var config = parseOptions(); var notes = undefined; var screen = initScreen(); screen.title = '[blessr2 ' + config.target + ']'; const title = blessed.box({ style: { fg: 'white', bg: '#000070', }, top: 0, left: 0, width: '100%', height: 1, }); var bgbox = blessed.box({ style: { fg: 'white', bg: 'lightblue', }, top: 1, width: '100%', height: '100%', }); bgbox.cmd = 'izq'; function uiQuestion(title, text, cb) { var question = blessed.prompt({ parent: screen, border: config.border, height: 'shrink', width: 'half', top: 'center', left: 'center', label: title, tags: true, keys: true, vi: true }); question.input('\n ' + text, (err, val) => { question.destroy(); screen.render(); if (!err && val) { cb(val); screen.render(); } }); return question; } function uiMessage(text, cb) { if (!text) text = ''; var msg = blessed.message({ parent: screen, border: config.border, height: 'shrink', width: 'half', top: 'center', left: 'center', label: ' {blue-fg}Message{/blue-fg} ', tags: true, keys: true, hidden: true, vi: true }); msg.display(text, -1, cb); } function newBox(name, obj) { if (!obj) obj = {}; if (obj.shadow === undefined) { obj.shadow = true; } if (obj.style === undefined) { obj.style = { fg: 'white', bg: 'black', selectedFg: 'green', selectedBg: 'red' }; if (config.trans) { obj.style.transparent = true; } } if (obj.style.fg === undefined) obj.style.fg = 'white'; if (obj.style.bg === undefined) obj.style.bg = 'green'; if (obj.style.selectedFg === undefined) obj.style.selectedFg = 'green'; if (obj.style.selectedBg === undefined) obj.style.selectedBg = 'white'; if (obj.draggable === undefined) obj.draggable = true; if (obj.left === undefined) obj.left = '5%'; if (obj.top === undefined) obj.top = 1; if (obj.width === undefined) obj.width = 'shrink'; if (obj.height === undefined) obj.height = '80%'; if (obj.border === undefined) obj.border = config.border; // if (obj.scrollable === undefined) obj.scrollable = true; // if (obj.scrollbar === undefined) obj.scrollbar = { bg: 'red', fg: 'white' }; function isaBox(x) { if (x.indexOf('ag') != -1) return true; return false; } function isaText(x) { return (x[0] == ':'); } let res = undefined; if (isaText(name)) { res = blessed.textarea(obj); res.select = function() { /* do nothing */ }; } else if (isaBox(name)) { res = blessed.box(obj); res.select = function() { /* do nothing */ }; } else { obj.selectedFg = 'white'; obj.selectedBg = '#000070'; res = blessed.list(obj); res.setContent = function(x) { res.setItems(x.split('\n')); } } res.cmd = name; return res; } const box = newBox('pd 200', { width: 90, height: 20 }); const gbox = newBox('af;agf', { width: 80, height: 20 }); const xbox = newBox('px 2048', { width: 80, height: 20 }); title.setText(config.brand + ' ' + config.target + ' @ entry0'); function walkInto(r2, foo) { gotoOffset(r2, foo, screen.focused); r2.cmd("e asm.cmtcol=55"); r2.cmd('e asm.bytes=false'); r2.cmd('e scr.html=false'); r2.cmd('e scr.color=true'); r2.cmd('e anal.hasnext=true'); if (!config.debug && !config.nobin) { r2.cmd('om-*'); r2.cmd('e io.sectonly=true'); } //r2.cmd("e asm.emu=true"); box.setText('Loading...\n'); screen.render(); [bgbox, box, xbox, gbox].forEach((b) => { r2.cmd(b.cmd, (txt) => { b.setContent(txt); screen.render(); }); }); } function demoStuff() { var donut = contrib.donut({ label: 'Test', radius: 8, arcWidth: 3, spacing: 2, yPadding: 2, width: '70%', height: '50%', left: 0, top: 0, draggable: true, border: config.border, data: [{ percent: 60, label: '.text', color: 'green', }, { percent: 30, label: '.data', color: 'red', }, { percent: 10, label: '.header', color: 'yellow', }] }); screen.append(donut); var map = contrib.map({ label: 'World Map', border: config.border, width: '80%', height: '60%', draggable: true }); //map.addMarker({"lon" : "-79.0000", "lat" : "37.5000", color: "red", char: "X" }) screen.append(map); var tree = contrib.tree({ fg: 'orange', label: 'Fruit Tree', border: config.border, width: '50%', height: '30%', draggable: true }); //allow control the table with the keyboard tree.focus(); tree.on('select', function(node) { if (node.myCustomProperty) { console.log(node.myCustomProperty); } console.log(node.name); }); screen.append(tree); // you can specify a name property at root level to display root tree.setData({ extended: true, children: { 'Fruit': { children: { 'Banana': {}, 'Apple': {}, 'Cherry': {}, 'Exotics': { children: { 'Mango': {}, 'Papaya': {}, 'Kiwi': { name: 'Kiwi (not the bird!)', myCustomProperty: 'hairy fruit' } } }, 'Pear': {} } }, 'Vegetables': { children: { 'Peas': {}, 'Lettuce': {}, 'Pepper': {} } } } }); } box.focus(); function layout(style) { const box = screen.focused; if (!box) return; for (var x in style) { box[x] = style[x]; } screen.render(); } function uiNewFrame(r2, cmd, opts) { const box = newBox(cmd, opts || { width: 80, height: '40%' }); if (cmd && box.cmd) { r2.cmd(box.cmd, (txt) => { box.setContent(txt); screen.render(); }); } box.setContent('Loading...'); box.focus(); screen.append(box); screen.render(); return box; } function refreshCurrentBox(r2) { const box = screen.focused; if (!box) return; r2.cmd(box.cmd, (txt) => { box.setContent(txt); screen.render(); }); } function keysHelp() { return `blessr2 keybindings =================== / - search = - start webserver : - run command in background console ; - enter comment ! - run command in new frame ? - show this help w - close current window a - show basic block graph A - analyze function d - open disasm view D - open pds string disasm view e - eval configuration var g - goto offset/flag G - goto offset/flag in new frame i - show file info I - show functions and symbols list jk - scroll few lines down/up JK - scroll page down/up hl - move left/right HL - move up/down Q - quit blessr2 r - show registers R - refresh frame s - step into t - text notes window. press 'e' to edit and then 'esc' x - open hexdump view X - open pxa hexdump w - close window (same as 'q') z - show strings in frame [] - horizontal resize of frame vV - vertical resize of frame 0-9 tile for different layouts enter - follow jump/call/ref space - goto address in selected line o - open file dialog (WIP)`; } function findLastOffset(b) { if (!b || b.selected < 0) { return -1; } let idx = b.selected; while (idx >= 0) { const item = b.items[idx]; if (!item) break; const line = '' + item.content; const addr = line.indexOf('0x'); if (addr != -1) { return parseInt(line.substring(addr), 16); } idx--; } return -1; } function scrollBox(r2, b, delta, hardscroll) { if (!b || !b.scroll) return; b.scroll(delta); b.select(b.selected + delta); if (b.cmd && hardscroll) { if (delta < 0) { if (b.getScroll() < 1) { gotoOffset(r2, '$$-64', b); } } else { if (b.items && 2 + b.getScroll() >= b.items.length) { const at = findLastOffset(b); if (at != -1) gotoOffset(r2, at, b); } } } screen.render(); } function popOffset(r2, box) { if (offsetStack.length > 0) { let off = offsetStack[offsetStack.length - 1]; gotoOffset(r2, off, box); offsetStack = offsetStack.slice(0, offsetStack.length - 2); if (!offsetStack) offsetStack = []; } } function seekLine(r2, box) { if (!r2 || !box) return; const item = box.items[box.selected] if (!item) return; const line = '' + item.content; const addr = line.indexOf('0x'); if (addr != -1) { const at = parseInt(line.substring(addr), 16); gotoOffset(r2, at, box); } } function nextWindow() { const curbox = screen.focused; for (let box of screen.children) { if (box && box !== bgbox && box !== title && box !== curbox) { box.setFront(); box.focus(); screen.render(); break; } } } function prevWindow() { const curbox = screen.focused; let rch = screen.children; let nextIsGood = false; for (let box of rch) { if (nextIsGood) { box.setFront(); box.focus(); nextIsGood = false; break; } if (box && box !== bgbox && box !== title && box !== curbox) { nextIsGood = true; } } if (nextIsGood) { for (let box of rch) { if (box && box !== bgbox && box !== title && box !== curbox) { continue; } box.setFront(); box.focus(); break; } } screen.render(); } function activateLine(r2, box) { if (box) { const item = box.items ? box.items[box.selected] : undefined; if (!item) return; const line = '' + item.content; const addr = line.indexOf('0x'); if (addr != -1) { const at = parseInt(line.substring(addr), 16); const isCode = true; //item.cmd && item.cmd.indexOf && item.cmd.indexOf('pd') != -1; if (isCode) { r2.cmd('e scr.color=0;ao@' + at + ';e scr.color=1', (txt) => { let obj = {} txt.split('\n').forEach((x) => { const ab = x.split(': '); if (ab.length > 1) { obj[ab[0]] = ab[1]; } }); if (obj.jump && obj.jump != -1) { gotoOffset(r2, obj.jump, box); } else if (obj.ptr && obj.ptr != -1) { gotoOffset(r2, obj.ptr, box); } }); } else { gotoOffset(r2, at, box); } } } } function stepInto(r2, box) { r2.cmd(config.debug ? 'ds;dr*' : 'aes;.aer*', (cmds) => { r2.cmd(cmds.split('\n').join(';')); if (!box) box = bgbox; r2.cmd(box.cmd, (txt) => { box.setContent(txt); screen.render(); r2.cmd(bgbox.cmd, (txt) => { bgbox.setContent(txt); screen.render(); }); if (regs !== undefined) { r2.cmd(regs.cmd, (txt) => { regs.setContent(txt); screen.render(); }); } }); }); screen.render(); } function addComment(r2, box) { if (box) { uiQuestion('Comment', 'Enter your notes', (txt) => { const item = box.items[box.selected] if (!item) return; const line = '' + item.content; const addr = line.indexOf('0x'); if (addr != -1) { const at = parseInt(line.substring(addr), 16); const cc = (txt === '-') ? 'CC' : 'CC '; r2.cmd(cc + txt + '@' + at, () => { refreshCurrentBox(r2); }); } else { uiMessage('Cannot determine offset'); } }); } } function gotoOffset(r2, val, box, nostack) { val = '' + val; if (val === '') { return; } if (offsetStack) { offsetStack.push(val); } r2.cmd('s ' + val + ';' + box.cmd, (txt) => { box.setContent(txt); box.setScroll(0); box.selected = 0; screen.render(); }); } function handleKeys(r2) { [ ['S-q', (ch, key) => { program.clear(); program.disableMouse(); program.showCursor(); program.normalBuffer(); screen.destroy(); return process.exit(0); }], ['z', () => { uiNewFrame(r2, 'izq'); }], ['i', () => { uiNewFrame(r2, 'afi;?e;i'); }], ['S-i', () => { uiNewFrame(r2, 'afl;isq'); }], ['r', () => { regs = uiNewFrame(r2, 'aer='); }], ['x', () => { uiNewFrame(r2, 'px 2048'); }], ['S-x', () => { uiNewFrame(r2, 'pxa 2048'); }], ['d', () => { uiNewFrame(r2, 'pdf'); }], ['S-d', () => { uiNewFrame(r2, 'af;pdsf'); }], ['a', () => { uiNewFrame(r2, 'af;agf'); }], ['n', nextWindow], ['S-n', prevWindow], ['s', stepInto], [';', () => { addComment(r2, screen.focused); }], ['/', () => { uiQuestion('String', 'String to search', (val) => { let box = uiNewFrame(r2, val); box.setContent('Loading...'); r2.cmd('/ ' + val, (txt) => { box.setContent(txt); screen.render(); r2.cmd('fs searches;f', (txt) => { box.setContent('Search for: "' + val + '":\n\n' + txt); screen.render(); }); }); }); }], [ ['q', 'w'], () => { const box = screen.focused; if (!box || !box.hide) return; if (box === bgbox || box === title) return; box.hide(); box.destroy(); screen.render(); } ], ['S-a', () => { r2.cmd('af', () => { let box = screen.focused; r2.cmd(box.cmd, (txt) => { box.setContent(txt); screen.render(); }); }); }], [':', () => { uiQuestion('Input', 'Command to run', (val) => { r2.cmd(bgbox.cmd = val, (txt) => { bgbox.setContent(txt); screen.render(); }); }); }], ['!', () => { uiQuestion('Input', 'Command to run', (val) => { r2.cmd(val, (txt) => { uiNewFrame(r2, val); const box = screen.focused; if (box) { r2.cmd(box.cmd, (x) => { box.setContent(x); }); } }); }); }], ['=', () => { function httpsPopup() { const already = config.https ? ' is Already ' : ' '; uiMessage('\n Background r2 Webserver' + already + 'Running\n\n' + ' $ blessr2 http://localhost:9090/cmd/\n'); } if (config.https) { httpsPopup(); } else { r2.cmd('=h&', () => { httpsPopup(); config.https = true; }); } }], ['?', () => { const box = uiNewFrame(r2, ''); box.setContent(keysHelp()); screen.render(); }], ['S-e', () => { let ef = uiNewFrame(r2, 'e??', { width: '100%', height: '100%', left: 0, top: 0 }); }], ['e', () => { const box = screen.focused; if (notes !== undefined && box == notes) { notes.readEditor(function() {}); screen.render(); } else { uiQuestion('Eval Config', 'key=value', (val) => { r2.cmd('e ' + val, (txt) => { screen.render(); }); }); } }], ['u', () => { const b = screen.focused; if (b) popOffset(r2, b); }], ['g', () => { uiQuestion('Offset', 'Where to go?', (val) => { gotoOffset(r2, val, screen.focused); }); }], ['S-g', () => { const obox = screen.focused; if (!obox) return; const box = newBox(obox.cmd, { width: 'shrink', height: 'shrink' }); screen.append(box); uiQuestion('Offset', 'Where to go?', (val) => { r2.cmd('s ' + val + ';' + box.cmd, (txt) => { box.setContent(txt); screen.render(); }); }); }], ['o', () => { const fm = blessed.FileManager({ parent: screen, border: config.border, style: { bg: 'blue', fg: 'white' }, left: 'center', top: 'center', height: '40%', width: '40%' }); fm.refresh(); fm.focus(); screen.append(fm); screen.render(); }], ['S-r', () => { refreshCurrentBox(r2); }], [ ['j', 'down'], () => { scrollBox(r2, screen.focused, 1, false); } ], [ ['k', 'up'], () => { scrollBox(r2, screen.focused, -1, false); } ], [ ['S-j', 'pagedown'], () => { const box = screen.focused; if (box) scrollBox(r2, box, box.height / 2, true); } ], [ ['S-k', 'pageup'], () => { const box = screen.focused; if (box) scrollBox(r2, box, -box.height / 2, true); } ], ['t', () => { if (notes === undefined) { notes = newBox(':', { width: '70%', height: '40%' }); screen.append(notes); notes.focus(); screen.render(); } }], [ 'space', () => { seekToLine(r2, screen.focused); } ], [ 'enter', () => { activateLine(r2, screen.focused); } ] ].forEach((k) => { screen.key(k[0], k[1]); }); /* layouts */ screen.key('0', () => { layout({ left: 0, top: 1, width: '100%', height: '99%' }); }); screen.key('1', () => { layout({ left: 0, top: 1, width: '70%', height: '99%' }); }); screen.key('2', () => { layout({ left: '70%', top: 1, width: '30%', height: '99%' }); }); screen.key('3', () => { layout({ left: '0', top: 1, width: '100%', height: '70%' }); }); screen.key('4', () => { layout({ left: '0', top: '72%', width: '100%', height: '30%' }); }); screen.key('5', () => { layout({ left: 'center', top: 'center', width: '80%', height: '70%' }); }); screen.key('6', () => { layout({ left: 'center', top: 'center', width: '60%', height: '40%' }); }); screen.key('7', () => { layout({ left: 'center', top: 'center', width: '40%', height: '50%' }); }); screen.key('8', () => { layout({ left: 'center', top: 'center', width: 80, height: '20%' }); }); screen.key('9', () => { layout({ left: 'center', top: 'center', width: '70%', height: '50%' }); }); /* window position and size */ screen.key('[', function() { const box = screen.focused; if (box) { const pc = ('' + box.width).indexOf('%'); if (pc != -1) { const n = box.width.substring(0, pc); box.width = (n - 10) + '%'; } else { box.width -= 2; if (box.width < 2) box.width = 2; } screen.render(); } }); screen.key(']', function() { const box = screen.focused; if (box) { const pc = ('' + box.width).indexOf('%'); if (pc != -1) { const n = box.width.substring(0, pc); box.width = (n + 10) + '%'; } else { box.width += 2; } screen.render(); } }); screen.key('h', function() { const box = screen.focused; if (box) { const pc = ('' + box.left).indexOf('%'); if (pc != -1) { const n = box.left.substring(0, pc); box.left = (n - 10) + '%'; } else { box.left -= 1; } screen.render(); } }); screen.key('l', function() { const box = screen.focused; if (box) { const pc = ('' + box.left).indexOf('%'); if (pc != -1) { const n = box.left.substring(0, pc); box.left = (n + 10) + '%'; } else { box.left += 1; } screen.render(); } }); screen.key('S-h', function() { const box = screen.focused; if (box) { const pc = ('' + box.top).indexOf('%'); if (pc != -1) { const n = box.top.substring(0, pc); box.top = (n - 10) + '%'; } else { box.top -= 1; } screen.render(); } }); screen.key('S-l', function() { const box = screen.focused; if (box) { const pc = ('' + box.top).indexOf('%'); if (pc != -1) { const n = box.top.substring(0, pc); box.top = (n + 10) + '%'; } else { box.top += 1; } screen.render(); } }); screen.key('v', function() { const box = screen.focused; if (box) { const pc = ('' + box.height).indexOf('%'); if (pc != -1) { const n = box.height.substring(0, pc); box.height = (n + 10) + '%'; } else { box.height += 1; } screen.render(); } }); screen.key('S-v', function() { const box = screen.focused; if (box) { const pc = ('' + box.height).indexOf('%'); if (pc != -1) { const n = box.height.substring(0, pc); box.height = (n + 10) + '%'; } else { box.height -= 1; if (box.height < 2) box.height = 1; } screen.render(); } }); } function handleMouse(r2) { /* handle mouse */ program.on('mouse', function(data) { const b = screen.focused; if (!b || !b.scroll) return; switch (data.action) { case 'wheelup': scrollBox(r2, b, -4, true); break; case 'wheeldown': scrollBox(r2, b, 4, true); break; } }); } function main(r2) { config.theme && r2.cmd("ecr"); handleKeys(r2); handleMouse(r2); walkInto(r2, 'entry0'); } /* main */ program.enableMouse(); program.hideCursor(); screen.append(bgbox); screen.append(title); screen.append(xbox); screen.append(box); screen.render(); if (config.demos) { demoStuff(); } if (+process.env.R2PIPE_IN) { r2pipe.lpipe(main); } else { if (config.target.indexOf('http') == 0) { r2pipe.connect(config.target, main); } else { if (config.nobin) r2pipe.options.push('-n'); if (config.debug) r2pipe.options.push('-d'); if (config.write) r2pipe.options.push('-w'); r2pipe.open(config.target, main); } }