#!/usr/bin/env python from __future__ import print_function import os import sys import socket import asyncore import threading import argparse import logging import logging.config import Queue import termcolor import struct from collections import defaultdict try: import cPickle as pickle except: import pickle try: import gdb in_gdb = True except: in_gdb = False try: import lldb in_lldb = True except: in_lldb = False try: import pygments import pygments.lexers import pygments.formatters have_pygments = True except: have_pygments = False SOCK = "/tmp/voltron.sock" READ_MAX = 0xFFFF LOG_CONFIG = { 'version': 1, 'formatters': { 'standard': {'format': '[%(levelname)s]: %(message)s'} }, 'handlers': { 'default': { 'class': 'logging.StreamHandler', 'formatter': 'standard' } }, 'loggers': { '': { 'handlers': ['default'], 'level': 'INFO', 'propogate': True, } } } ADDR_FORMAT_64 = '0x{0:0=16X}' ADDR_FORMAT_32 = '0x{0:0=8X}' ADDR_FORMAT_16 = '0x{0:0=4X}' SEGM_FORMAT_16 = '{0:0=4X}' COLOURS = { 'label': 'green', 'value': 'grey', 'modified': 'red', 'header': 'blue', 'flags': 'red', } DISASM_MAX = 32 STACK_MAX = 64 log = None clients = [] queue = None inst = None def main(debugger=None, dict=None): global log, queue, inst # Configure logging logging.config.dictConfig(LOG_CONFIG) log = logging.getLogger('') # Set up queue queue = Queue.Queue() if in_gdb: # Load GDB command log.debug('Loading GDB command') print("Voltron loaded.") inst = VoltronGDBCommand() elif in_lldb: # Load LLDB command log.debug('Loading LLDB command') inst = VoltronLLDBCommand(debugger, dict) else: # Set up command line arg parser parser = argparse.ArgumentParser() parser.add_argument('--debug', '-d', action='store_true', help='print debug logging') subparsers = parser.add_subparsers(title='subcommands', description='valid subcommands', help='additional help') # Set up a subcommand for each view class for cls in VoltronView.__subclasses__(): cls.configure_subparser(subparsers) # And subcommands for the loathsome red-headed stepchildren StandaloneServer.configure_subparser(subparsers) GDB6Proxy.configure_subparser(subparsers) # Parse args args = parser.parse_args() if args.debug: log.setLevel(logging.DEBUG) # Instantiate and run the appropriate module inst = args.func(args) try: inst.run() except Exception as e: log.error("Exception running module {}: {}".format(inst.__class__.__name__, str(e))) except KeyboardInterrupt: pass inst.cleanup() log.info('Exiting') # # Server side code # class VoltronCommand (object): running = False def handle_command(self, command): global log if "start" in command: if 'debug' in command: log.setLevel(logging.DEBUG) self.start() elif "stop" in command: self.stop() elif "status" in command: self.status() elif "update" in command: self.update() else: print("Usage: voltron ") def start(self): if not self.running: print("Starting voltron") self.running = True self.register_hooks() self.thread = ServerThread() self.thread.start() else: print("Already running") def stop(self): if self.running: print("Stopping voltron") self.unregister_hooks() self.thread.set_should_exit(True) self.thread.join(10) if self.thread.isAlive(): print("Failed to stop voltron :<") self.running = False else: print("Not running") def status(self): if self.running: print("There are {} clients attached".format(len(clients))) for client in clients: print("{} registered with config: {}".format(client, str(client.registration['config']))) else: print("Not running") def update(self): log.debug("Updating clients") for client in filter(lambda c: c.registration['config']['update_on'] == 'stop', clients): event = {'msg_type': 'update', } if client.registration['config']['type'] == 'cmd': event['data'] = self.get_cmd_output(client.registration['config']['cmd']) elif client.registration['config']['type'] == 'register': event['data'] = self.get_registers() elif client.registration['config']['type'] == 'disasm': event['data'] = self.get_disasm() elif client.registration['config']['type'] == 'stack': event['data'] = {'data': self.get_stack(), 'sp': self.get_register('rsp')} elif client.registration['config']['type'] == 'bt': event['data'] = self.get_backtrace() queue.put((client, event)) def register_hooks(self): pass def unregister_hooks(self): pass # This is the actual GDB command. Should be able to just add an LLDB version I guess. if in_gdb: class VoltronGDBCommand (VoltronCommand, gdb.Command): def __init__(self): super(VoltronCommand, self).__init__("voltron", gdb.COMMAND_NONE, gdb.COMPLETE_NONE) self.running = False def invoke(self, arg, from_tty): self.handle_command(arg) def register_hooks(self): gdb.events.stop.connect(self.stop_handler) def unregister_hooks(self): gdb.events.stop.disconnect(self.stop_handler) def stop_handler(self, event): self.update() def get_registers(self): log.debug('Getting registers') regs = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip','r8','r9','r10','r11','r12','r13','r14','r15','cs','ds','es','fs','gs','ss'] vals = {} for reg in regs: try: vals[reg] = int(gdb.parse_and_eval('(long long)$'+reg)) & 0xFFFFFFFFFFFFFFFF except: log.debug('Failed getting reg: ' + reg) vals[reg] = 'N/A' vals['rflags'] = str(gdb.parse_and_eval('$eflags')) log.debug('Got registers: ' + str(vals)) return vals def get_register(self, reg): log.debug('Getting register: ' + reg) return int(gdb.parse_and_eval('(long long)$'+reg)) & 0xFFFFFFFFFFFFFFFF def get_disasm(self): log.debug('Getting disasm') res = gdb.execute('x/{}i $rip'.format(DISASM_MAX), to_string=True) return res def get_stack(self): log.debug('Getting stack') rsp = int(gdb.parse_and_eval('(long long)$rsp')) & 0xFFFFFFFFFFFFFFFF res = str(gdb.selected_inferior().read_memory(rsp, STACK_MAX*16)) return res def get_backtrace(self): log.debug('Getting backtrace') res = gdb.execute('bt', to_string=True) return res def get_cmd_output(self, cmd=None): if cmd: log.debug('Getting command output: ' + cmd) res = gdb.execute(cmd, to_string=True) else: res = "" return res if in_lldb: # LLDB's initialisation routine def __lldb_init_module(debugger, dict): main(debugger, dict) def lldb_invoke(debugger, command, result, dict): inst.invoke(debugger, command, result, dict) class VoltronLLDBCommand (VoltronCommand): debugger = None def __init__(self, debugger, dict): self.debugger = debugger debugger.HandleCommand('command script add -f voltron.lldb_invoke voltron') self.running = False def invoke(self, debugger, command, result, dict): self.debugger = debugger self.handle_command(command) def register_hooks(self): self.debugger.HandleCommand('target stop-hook add -o \'voltron update\'') def unregister_hooks(self): # XXX: Fix this so it only removes our stop-hook self.debugger.HandleCommand('target stop-hook delete') def get_frame(self): return self.debugger.GetTargetAtIndex(0).process.selected_thread.GetFrameAtIndex(0) def get_registers(self): log.debug('Getting registers') frame = self.get_frame() regs = {x.name:int(x.value, 16) for x in list(list(frame.GetRegisters())[0])} return regs def get_register(self, reg): log.debug('Getting register: ' + reg) return self.get_registers()[reg] def get_disasm(self): log.debug('Getting disasm') res = self.get_cmd_output('disassemble -c {}'.format(DISASM_MAX)) return res def get_stack(self): log.debug('Getting stack') rsp = self.get_register('rsp') error = lldb.SBError() res = lldb.debugger.GetTargetAtIndex(0).process.ReadMemory(rsp, STACK_MAX*16, error) return res def get_backtrace(self): log.debug('Getting backtrace') res = self.get_cmd_output('bt') return res def get_cmd_output(self, cmd=None): if cmd: log.debug('Getting command output: ' + cmd) res = lldb.SBCommandReturnObject() self.debugger.GetCommandInterpreter().HandleCommand(cmd, res) res = res.GetOutput() else: res = "" return res class StandaloneServer (object): @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('server', help='standalone server for debuggers without python support') sp.set_defaults(func=StandaloneServer) def __init__(self, args={}): self.args = args def run(self): log.debug("Starting standalone server") self.thread = ServerThread() self.thread.start() while True: pass def cleanup(self): log.info("Exiting") self.thread.set_should_exit(True) self.thread.join(10) # Socket for talking to an individual client class ClientHandler (asyncore.dispatcher): def __init__(self, sock): asyncore.dispatcher.__init__(self, sock) self.registration = None def handle_read(self): data = self.recv(READ_MAX) if data.strip() != "": try: msg = pickle.loads(data) log.debug('Received msg: ' + str(msg)) except: log.error('Invalid message data: ' + data) if msg['msg_type'] == 'register': self.handle_register(msg) elif msg['msg_type'] == 'push_update': self.handle_push_update(msg) else: log.error('Invalid message type: ' + msg['msg_type']) def handle_register(self, msg): log.debug('Registering client {} with config: {}'.format(self, str(msg['config']))) self.registration = msg def handle_push_update(self, msg): log.debug('Got a push update from client {} of type {} with data: {}'.format(self, msg['update_type'], str(msg['data']))) event = {'msg_type': 'update', 'data': msg['data']} for client in clients: if client.registration != None and client.registration['config']['type'] == msg['update_type']: queue.put((client, event)) self.send(pickle.dumps({'msg_type': 'ack'})) def handle_close(self): self.close() clients.remove(self) def send_event(self, event): log.debug('Sending event to client {}: {}'.format(self, event)) self.send(pickle.dumps(event)) # Main server socket for accept()s class Server (asyncore.dispatcher): def __init__(self, sockfile): asyncore.dispatcher.__init__(self) try: os.remove(SOCK) except: pass self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) self.bind(sockfile) self.listen(1) def handle_accept(self): global clients pair = self.accept() if pair is not None: sock, addr = pair try: client = ClientHandler(sock) clients.append(client) except Exception as e: log.error("Exception handling accept: " + str(e)) # Thread spun off when the plugin is started to listen for incoming client connections, and send out any # events that have been queued by the hooks in the debugger command class class ServerThread (threading.Thread): def run(self): global clients, queue # Create a server instance serv = Server(SOCK) self.lock = threading.Lock() self.set_should_exit(False) # Main event loop while not self.should_exit(): # Check sockets for activity asyncore.loop(count=1, timeout=0.1) # Process any events in the queue while not queue.empty(): client, event = queue.get() client.send_event(event) # Clean up serv.close() def should_exit(self): self.lock.acquire() r = self._should_exit self.lock.release() return r def set_should_exit(self, should_exit): self.lock.acquire() self._should_exit = should_exit self.lock.release() # # Client-side code # # Socket to register with the server and receive messages, calls view's render() method when a message comes in class Client (asyncore.dispatcher): def __init__(self, view=None, config={}): asyncore.dispatcher.__init__(self) self.view = view self.config = config self.reg_info = None self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) self.connect(SOCK) def register(self): log.debug('Client {} registering with config: {}'.format(self, str(self.config))) msg = {'msg_type': 'register', 'config': self.config} log.debug('Sending: ' + str(msg)) self.send(pickle.dumps(msg)) def handle_read(self): data = self.recv(READ_MAX) try: msg = pickle.loads(data) log.debug('Received message: ' + str(msg)) if self.view: self.view.render(msg) except: log.debug('Invalid message: ' + data) # Parent class for all views class VoltronView (object): DEFAULT_CONFIG = { 'type': 'base', 'clear': True, 'show_header': True, 'show_footer': True, 'update_on': 'stop' } def __init__(self, args={}, config={}): log.debug('Loading view: ' + self.__class__.__name__) self.client = None self.config = config self.args = args os.system('tput civis') self.setup() self.connect() def setup(self): log.debug('Base view class setup') def cleanup(self): log.debug('Cleaning up view') os.system('tput cnorm') def connect(self): try: self.config = dict(self.DEFAULT_CONFIG.items() + self.config.items()) self.client = Client(view=self, config=self.config) self.client.register() except Exception as e: log.error('Exception connecting: ' + str(e)) raise e def run(self): os.system('clear') log.info('Waiting for an update from the debugger') asyncore.loop() def render(self, msg=None): log.warning('Might wanna implement render() in this view eh') def clear(self): # Clear the window - this sucks, should probably do it with ncurses at some stage os.system('clear') def window_size(self): # Get terminal size - this also sucks, but curses sucks more height, width = os.popen('stty size').read().split() height = int(height) width = int(width) return (height, width) def hexdump(self, src, length=16, sep='.', offset=0): FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or sep for x in range(256)]) lines = [] for c in xrange(0, len(src), length): chars = src[c:c+length] hex = ' '.join(["%02X" % ord(x) for x in chars]) if len(hex) > 24: hex = "%s %s" % (hex[:24], hex[24:]) printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or sep) for x in chars]) lines.append("%s: %-*s |%s|\n" % (ADDR_FORMAT_64.format(offset+c), length*3, hex, printable)) return ''.join(lines).strip() def format_header(self, title=None, left=None, right=None): if not self.config['show_header']: return '' height, width = self.window_size() # Left data header = '' if left != None: header = left # Dashes dashlen = width - len(header) - len(title) if dashlen < 0: dashlen = 1 header += '-' * dashlen # Title header = termcolor.colored(header, COLOURS['header']) + termcolor.colored(title, 'white', attrs=['bold']) return header def format_footer(self, title=None, left=None, right=None): if not self.config['show_footer']: return '' height, width = self.window_size() dashlen = width footer = '-' * dashlen footer = termcolor.colored(footer, COLOURS['header']) return footer # Class to actually render the view class RegisterView (VoltronView): FORMAT_INFO = { 'x64': [ { 'regs': ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip','r8','r9','r10','r11','r12','r13','r14','r15'], 'label_format': '{0:3s}:', 'label_mod': str.upper, 'label_colour': COLOURS['label'], 'value_format': ADDR_FORMAT_64, 'value_mod': None, 'value_colour': COLOURS['value'], 'value_colour_mod': COLOURS['modified'] }, { 'regs': ['cs','ds','es','fs','gs','ss'], 'label_format': '{0}:', 'label_mod': str.upper, 'label_colour': COLOURS['label'], 'value_format': SEGM_FORMAT_16, 'value_mod': None, 'value_colour': COLOURS['value'], 'value_colour_mod': COLOURS['modified'] }, { 'regs': ['rflags'], 'value_format': '{0}', 'value_mod': None }, ] } FORMAT_DEFAULTS = { 'label_format': '{}:', 'label_mod': str.upper, 'label_colour': COLOURS['label'], 'value_format': ADDR_FORMAT_64, 'value_mod': None, 'value_colour': COLOURS['value'], 'value_colour_mod': COLOURS['modified'] } TEMPLATE_H = ( "{raxl} {rax} {rbxl} {rbx} {rbpl} {rbp} {rspl} {rsp} {eflags}\n" "{rdil} {rdi} {rsil} {rsi} {rdxl} {rdx} {rcxl} {rcx} {ripl} {rip}\n" "{r8l} {r8} {r9l} {r9} {r10l} {r10} {r11l} {r11} {r12l} {r12}\n" "{r13l} {r13} {r14l} {r14} {r15l} {r15}\n" "{csl} {cs} {dsl} {ds} {esl} {es} {fsl} {fs} {gsl} {gs} {ssl} {ss}\n" ) TEMPLATE_V = ( "{ripl} {rip}\n\n" "{raxl} {rax}\n{rbxl} {rbx}\n{rbpl} {rbp}\n{rspl} {rsp}\n" "{rdil} {rdi}\n{rsil} {rsi}\n{rdxl} {rdx}\n{rcxl} {rcx}\n" "{r8l} {r8}\n{r9l} {r9}\n{r10l} {r10}\n{r11l} {r11}\n{r12l} {r12}\n" "{r13l} {r13}\n{r14l} {r14}\n{r15l} {r15}\n" "{csl} {cs} {dsl} {ds}\n{esl} {es} {fsl} {fs}\n{gsl} {gs} {ssl} {ss}\n" " {rflags}\n" ) last_regs = None @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('reg', help='register view') sp.set_defaults(func=RegisterView) g = sp.add_mutually_exclusive_group() g.add_argument('--horizontal', '-o', action='store_true', help='horizontal orientation (default)', default=False) g.add_argument('--vertical', '-v', action='store_true', help='vertical orientation', default=True) def setup(self): self.config['type'] = 'register' def render(self, msg=None): self.clear() # Grab the appropriate template template = self.TEMPLATE_V if self.args.vertical else self.TEMPLATE_H # Process formatting settings data = defaultdict(lambda: '') data.update(msg['data']) formats = self.FORMAT_INFO['x64'] formatted = {} for fmt in formats: # Apply defaults where they're missing fmt = dict(self.FORMAT_DEFAULTS.items() + fmt.items()) # Format the data for each register for reg in fmt['regs']: # Format the label label = fmt['label_format'].format(reg) if fmt['label_mod'] != None: label = fmt['label_mod'](label) formatted[reg+'l'] = termcolor.colored(label, fmt['label_colour']) # Format the value val = data[reg] if type(val) == str: formatted[reg] = termcolor.colored(val, fmt['value_colour']) else: colour = fmt['value_colour'] if self.last_regs == None or self.last_regs != None and val != self.last_regs[reg]: colour = fmt['value_colour_mod'] val = fmt['value_format'].format(val) if fmt['value_mod'] != None: val = fmt['value_mod'](val) formatted[reg] = termcolor.colored(val, colour) log.debug('Formatted: ' + str(formatted)) print(self.format_header('[regs]')) print(template.format(**formatted), end='') print(self.format_footer(), end='') sys.stdout.flush() # Store the regs self.last_regs = data class DisasmView (VoltronView): DISASM_SHOW_LINES = 16 DISASM_SEP_WIDTH = 90 @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('disasm', help='disassembly view') sp.set_defaults(func=DisasmView) def setup(self): self.config['type'] = 'disasm' def render(self, msg=None): self.clear() height, width = self.window_size() # Get the disasm disasm = msg['data'] disasm = '\n'.join(disasm.split('\n')[:height-2]) # Pygmentize output if have_pygments: try: lexer = pygments.lexers.get_lexer_by_name('gdb') disasm = pygments.highlight(disasm, lexer, pygments.formatters.Terminal256Formatter()) except Exception as e: log.warning('Failed to highlight disasm: ' + str(e)) # Print output print(self.format_header('[code]')) print(disasm.rstrip()) print(self.format_footer(), end='') sys.stdout.flush() class StackView (VoltronView): STACK_SHOW_LINES = 16 STACK_SEP_WIDTH = 90 @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('stack', help='stack view') sp.set_defaults(func=StackView) def setup(self): self.config['type'] = 'stack' def render(self, msg=None): self.clear() height, width = self.window_size() # Get the stack data data = msg['data'] stack_raw = data['data'] sp = data['sp'] stack_raw = stack_raw[:(height-2)*16] # Hexdump it lines = self.hexdump(stack_raw, offset=sp).split('\n') lines.reverse() stack = '\n'.join(lines) # Print output sp_addr = '[0x{0:0=4x}:'.format(len(stack_raw)) + ADDR_FORMAT_64.format(sp) + ']' print(self.format_header('[stack]', left=sp_addr)) print(stack.strip()) print(self.format_footer(), end='') sys.stdout.flush() class BacktraceView (VoltronView): @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('bt', help='backtrace view') sp.set_defaults(func=BacktraceView) def setup(self): self.config['type'] = 'bt' def render(self, msg=None): self.clear() height, width = self.window_size() # Get the back trace data data = msg['data'] lines = data.split('\n') pad = height - len(lines) - 2 if pad < 0: pad = 0 # Print output print(self.format_header('[backtrace]')) print(data.strip()) if pad > 0: print('\n' * pad) print(self.format_footer(), end='') sys.stdout.flush() class CommandView (VoltronView): @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('cmd', help='command view - specify a command to be run each time the debugger stops') sp.add_argument('command', action='store', help='command to run') sp.add_argument('--header', '-e', action='store_true', help='print header', default=False) sp.add_argument('--footer', '-f', action='store_true', help='print footer', default=False) sp.set_defaults(func=CommandView) def setup(self): self.config['type'] = 'cmd' self.config['cmd'] = self.args.command def render(self, msg=None): self.clear() print(self.format_header('[cmd:' + self.config['cmd'] + ']')) print(msg['data'].strip()) print(self.format_footer(), end='') sys.stdout.flush() # This class is called from the command line by GDBv6's stop-hook. The dumped registers and stack are collected, # parsed and sent to the voltron standalone server, which then sends the updates out to any registered clients. # I hate that this exists. Fuck GDBv6. class GDB6Proxy (asyncore.dispatcher): REGISTERS = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip','r8','r9','r10','r11','r12','r13','r14','r15','eflags'] @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('gdb6proxy', help='import a dump from GDBv6 and send it to the server') sp.add_argument('type', action='store', help='the type to proxy - reg or stack') sp.set_defaults(func=GDB6Proxy) def __init__(self, args={}): asyncore.dispatcher.__init__(self) self.args = args self.create_socket(socket.AF_UNIX, socket.SOCK_STREAM) self.connect(SOCK) def run(self): asyncore.loop() def handle_connect(self): if self.args.type == "reg": event = self.read_registers() elif self.args.type == "stack": event = self.read_stack() else: log.error("Invalid proxy type") log.debug("Pushing update to server") log.debug(str(event)) self.send(pickle.dumps(event)) def handle_read(self): data = self.recv(READ_MAX) msg = pickle.loads(data) if msg['msg_type'] != 'ack': log.error("Did not get ack: " + str(msg)) self.close() def read_registers(self): log.debug("Parsing register data") data = {} for reg in GDB6Proxy.REGISTERS: try: with open('/tmp/voltron.reg.'+reg, 'r+b') as f: if reg == 'eflags': (val,) = struct.unpack('