pax_global_header00006660000000000000000000000064127071721250014516gustar00rootroot0000000000000052 comment=b7f1cc0261ddfc5fbe1d8de6735f91be85961008 voltron-0.1.4/000077500000000000000000000000001270717212500132235ustar00rootroot00000000000000voltron-0.1.4/.gitignore000066400000000000000000000005521270717212500152150ustar00rootroot00000000000000### Python ### *.py[cod] # C extensions *.so # Packages *.egg *.egg-info dist build eggs parts bin var sdist develop-eggs .installed.cfg lib lib64 __pycache__ # Installer logs pip-log.txt # Unit test / coverage reports .coverage .tox nosetests.xml # Translations *.mo # Mr Developer .mr.developer.cfg .project .pydevproject # Rope .ropeproject .DS_Store voltron-0.1.4/.travis.yml000066400000000000000000000022371270717212500153400ustar00rootroot00000000000000language: python matrix: include: - os: linux sudo: required dist: trusty python: "3.3" - os: linux sudo: required dist: trusty python: "3.4" - os: linux sudo: required dist: trusty python: "3.5" - os: osx osx_image: xcode7.2 language: generic addons: apt: packages: - build-essential - gdb before_install: - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; brew install python ; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -qq ; sudo apt-get install lldb-3.4 ; fi install: - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install mock pexpect nose ; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install mock pexpect nose --user; fi - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install . ; fi - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install . --user; fi script: - python --version - mkdir ~/.voltron - echo '{"general":{"debug_logging":true}}' >~/.voltron/config - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then python -m nose -sv tests/gdb_cli_tests.py ; fi - python -m nose -sv tests/lldb_cli_tests.py voltron-0.1.4/LICENSE000066400000000000000000000020601270717212500142260ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 snare Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. voltron-0.1.4/README.md000066400000000000000000000162001270717212500145010ustar00rootroot00000000000000Voltron ======= ![build](https://travis-ci.org/snare/voltron.svg?branch=master) Voltron is an extensible debugger UI toolkit written in Python. It aims to improve the user experience of various debuggers (LLDB, GDB, VDB and WinDbg) by enabling the attachment of utility views that can retrieve and display data from the debugger host. By running these views in other TTYs, you can build a customised debugger user interface to suit your needs. Voltron does not aim to be everything to everyone. It's not a wholesale replacement for your debugger's CLI. Rather, it aims to complement your existing setup and allow you to extend your CLI debugger as much or as little as you like. If you just want a view of the register contents in a window alongside your debugger, you can do that. If you want to go all out and have something that looks more like OllyDbg, you can do that too. Built-in views are provided for: - Registers - Disassembly - Stack - Memory - Breakpoints - Backtrace The author's setup looks something like this: ![voltron example LLDB](http://i.imgur.com/p3XcagJ.png) Any debugger command can be split off into a view and highlighted with a specified Pygments lexer: ![command views](http://i.imgur.com/mqptE3Z.png) More screenshots are [here](https://github.com/snare/voltron/wiki/Screenshots). Support ------- `voltron` supports LLDB, GDB, VDB and WinDbg/CDB (via [PyKD](https://pykd.codeplex.com/)) and runs on OS X, Linux and Windows. WinDbg support is new, please [open an issue](https://github.com/snare/voltron/issues) if you have problems. The following architectures are supported: | | lldb | gdb | vdb | windbg | |---------|------|-----|-----|--------| | x86 | ✓ | ✓ | ✓ | ✓ | | x86_64 | ✓ | ✓ | ✓ | ✓ | | arm | ✓ | ✓ | ✓ | ✗ | | arm64 | ✓ | ✗ | ✗ | ✗ | | powerpc | ✗ | ✓ | ✗ | ✗ | Installation ------------ Releases are on PyPI. Install with `pip`: $ pip install voltron If you want to be bleeding edge, clone this repo and install with `setup.py`: $ python setup.py install Quick Start ----------- 1. If your debugger has an init script (`.lldbinit` for LLDB or `.gdbinit` for GDB) configure it to load Voltron when it starts by sourcing the `entry.py` entry point script. The full path will be inside the `voltron` package. For example, on OS X it might be */Library/Python/2.7/site-packages/voltron/entry.py*. If you don't add this to your init script, you'll need to execute the commands after starting your debugger. LLDB: command script import /path/to/voltron/entry.py GDB: source /path/to/voltron/entry.py voltron init set disassembly-flavor intel 2. Start your debugger and initialise Voltron manually if necessary. On recent versions of LLDB you do not need to initialise Voltron manually: $ lldb target_binary On older versions of LLDB you need to call `voltron init` after you load the inferior: $ lldb target_binary (lldb) voltron init GDB: $ gdb target_binary VDB: $ ./vdbbin target_binary > script /path/to/voltron/entry.py WinDbg/CDB (requires [PyKD](https://pykd.codeplex.com/)): > cdb -c '.load C:\path\to\pykd.pyd ; !py --global C:\path\to\voltron\entry.py' target_binary 3. In another terminal (I use iTerm panes) start one of the UI views. On LLDB and WinDbg the views will update immediately. On GDB and VDB they will not update until the inferior stops (at a breakpoint, after a step, etc): $ voltron view register $ voltron view stack $ voltron view disasm $ voltron view backtrace 4. Set a breakpoint and run your inferior. (*db) b main (*db) run 5. When the debugger hits the breakpoint, the views will be updated to reflect the current state of registers, stack, memory, etc. Views are updated after each command is executed in the debugger CLI, using the debugger's "stop hook" mechanism. So each time you step, or continue and hit a breakpoint, the views will update. Documentation ------------- See the [wiki](https://github.com/snare/voltron/wiki) on github. Bugs and Errata --------------- See the [issue tracker](https://github.com/snare/voltron/issues) on github for more information or to submit issues. ### GDB 1. GDB on some distros is built with Python 3, but the system's Python is version 2. If Voltron is installed into Python 2's `site-packages` it will not work with GDB. See [this page on the wiki](https://github.com/snare/voltron/wiki/Voltron-on-Ubuntu-14.04-with-GDB) for installation instructions. 2. There is no clean way to hook GDB's exit, only the inferior's exit, so the Voltron server is started and stopped along with the inferior. This results in views showing "Connection refused" before the inferior has been started. 3. Due to a limitation in the GDB API, the views are only updated each time the debugger is stopped (e.g. by hitting a breakpoint), so view contents are not populated immediately when the view is connected, only when the first breakpoint is hit. 4. If the stack view is causing GDB to hang then it must be launched **after** the debugger has been launched, the inferior started, and the debugger stopped (e.g. a breakpoint hit). This has been fixed, but this note will remain until another release is issued. ### LLDB On older versions of LLDB, the `voltron init` command must be run manually after loading the debug target, as a target must be loaded before Voltron's hooks can be installed. Voltron will attempt to automatically register its event handler, and it will inform the user if `voltron init` is required. ### WinDbg More information about WinDbg/CDB support [here](https://github.com/snare/voltron/wiki/WinDbg). ### Misc 1. The authors primarily use Voltron with the most recent version of LLDB on OS X. We will try to test everything on as many platforms and architectures as possible before releases, but LLDB/OS X/x64 is going to be by far the most frequently-used combination. Hopefully Voltron doesn't set your pets on fire, but YMMV. 2. Intel is the only disassembly flavour currently supported for syntax highlighting. License ------- See the [LICENSE](https://github.com/snare/voltron/blob/master/LICENSE) file. If you use this and don't hate it, buy me a beer at a conference some time. This license also extends to other contributors - [richo](http://github.com/richo) definitely deserves a few beers for his contributions. Credits ------- Thanks to Azimuth Security for letting me spend time working on this. Props to [richo](http://github.com/richo) for all his contributions to Voltron. [fG!](http://github.com/gdbinit)'s gdbinit was the original inspiration for this project. Thanks to [Willi](http://github.com/williballenthin) for implementing the VDB support. Voltron now uses [Capstone](http://www.capstone-engine.org) for disassembly as well as the debugger hosts' internal disassembly mechanism. [Capstone](http://www.capstone-engine.org) is a powerful, open source, multi-architecture disassembler upon which the next generation of reverse engineering and debugging tools are being built. Check it out. voltron-0.1.4/examples/000077500000000000000000000000001270717212500150415ustar00rootroot00000000000000voltron-0.1.4/examples/angularview/000077500000000000000000000000001270717212500173655ustar00rootroot00000000000000voltron-0.1.4/examples/angularview/angularview.py000066400000000000000000000017211270717212500222640ustar00rootroot00000000000000import logging import pygments from voltron.plugin import * from voltron.lexers import * from voltron.api import * from flask import * log = logging.getLogger('api') class AngularViewPlugin(WebPlugin): name = 'angularview' class FormatDisassemblyRequest(APIRequest): _fields = {'disassembly': True} def dispatch(self): try: res = FormatDisassemblyResponse( disassembly=pygments.highlight(self.disassembly.strip(), LLDBIntelLexer(), pygments.formatters.HtmlFormatter())) except Exception as e: msg = "Exception formatting disassembly: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class FormatDisassemblyResponse(APIResponse): _fields = {'disassembly': True} class FormatDisassemblyPlugin(APIPlugin): request = "format_disasm" request_class = FormatDisassemblyRequest response_class = FormatDisassemblyResponse voltron-0.1.4/examples/angularview/static/000077500000000000000000000000001270717212500206545ustar00rootroot00000000000000voltron-0.1.4/examples/angularview/static/css/000077500000000000000000000000001270717212500214445ustar00rootroot00000000000000voltron-0.1.4/examples/angularview/static/css/solarized.css000066400000000000000000000105551270717212500241600ustar00rootroot00000000000000/* Solarized Dark For use with Jekyll and Pygments http://ethanschoonover.com/solarized SOLARIZED HEX ROLE --------- -------- ------------------------------------------ base03 #002b36 background base01 #586e75 comments / secondary content base1 #93a1a1 body text / default code / primary content orange #cb4b16 constants red #dc322f regex, special keywords blue #268bd2 reserved keywords cyan #2aa198 strings, numbers green #859900 operators, other keywords */ /*.highlight { background-color: #002b36; color: #93a1a1 }*/ .highlight { background-color: #073642; color: #93a1a1 } .highlight .c { color: #586e75 } /* Comment */ .highlight .err { color: #93a1a1 } /* Error */ .highlight .g { color: #93a1a1 } /* Generic */ .highlight .k { color: #859900 } /* Keyword */ .highlight .l { color: #93a1a1 } /* Literal */ .highlight .n { color: #93a1a1 } /* Name */ .highlight .o { color: #859900 } /* Operator */ .highlight .x { color: #cb4b16 } /* Other */ .highlight .p { color: #93a1a1 } /* Punctuation */ .highlight .cm { color: #586e75 } /* Comment.Multiline */ .highlight .cp { color: #859900 } /* Comment.Preproc */ .highlight .c1 { color: #586e75 } /* Comment.Single */ .highlight .cs { color: #859900 } /* Comment.Special */ .highlight .gd { color: #2aa198 } /* Generic.Deleted */ .highlight .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ .highlight .gr { color: #dc322f } /* Generic.Error */ .highlight .gh { color: #cb4b16 } /* Generic.Heading */ .highlight .gi { color: #859900 } /* Generic.Inserted */ .highlight .go { color: #93a1a1 } /* Generic.Output */ .highlight .gp { color: #93a1a1 } /* Generic.Prompt */ .highlight .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ .highlight .gu { color: #cb4b16 } /* Generic.Subheading */ .highlight .gt { color: #93a1a1 } /* Generic.Traceback */ .highlight .kc { color: #cb4b16 } /* Keyword.Constant */ .highlight .kd { color: #268bd2 } /* Keyword.Declaration */ .highlight .kn { color: #859900 } /* Keyword.Namespace */ .highlight .kp { color: #859900 } /* Keyword.Pseudo */ .highlight .kr { color: #268bd2 } /* Keyword.Reserved */ .highlight .kt { color: #dc322f } /* Keyword.Type */ .highlight .ld { color: #93a1a1 } /* Literal.Date */ .highlight .m { color: #2aa198 } /* Literal.Number */ .highlight .s { color: #2aa198 } /* Literal.String */ .highlight .na { color: #93a1a1 } /* Name.Attribute */ .highlight .nb { color: #B58900 } /* Name.Builtin */ .highlight .nc { color: #268bd2 } /* Name.Class */ .highlight .no { color: #cb4b16 } /* Name.Constant */ .highlight .nd { color: #268bd2 } /* Name.Decorator */ .highlight .ni { color: #cb4b16 } /* Name.Entity */ .highlight .ne { color: #cb4b16 } /* Name.Exception */ .highlight .nf { color: #268bd2 } /* Name.Function */ .highlight .nl { color: #93a1a1 } /* Name.Label */ .highlight .nn { color: #93a1a1 } /* Name.Namespace */ .highlight .nx { color: #93a1a1 } /* Name.Other */ .highlight .py { color: #93a1a1 } /* Name.Property */ .highlight .nt { color: #268bd2 } /* Name.Tag */ .highlight .nv { color: #268bd2 } /* Name.Variable */ .highlight .ow { color: #859900 } /* Operator.Word */ .highlight .w { color: #93a1a1 } /* Text.Whitespace */ .highlight .mf { color: #2aa198 } /* Literal.Number.Float */ .highlight .mh { color: #2aa198 } /* Literal.Number.Hex */ .highlight .mi { color: #2aa198 } /* Literal.Number.Integer */ .highlight .mo { color: #2aa198 } /* Literal.Number.Oct */ .highlight .sb { color: #586e75 } /* Literal.String.Backtick */ .highlight .sc { color: #2aa198 } /* Literal.String.Char */ .highlight .sd { color: #93a1a1 } /* Literal.String.Doc */ .highlight .s2 { color: #2aa198 } /* Literal.String.Double */ .highlight .se { color: #cb4b16 } /* Literal.String.Escape */ .highlight .sh { color: #93a1a1 } /* Literal.String.Heredoc */ .highlight .si { color: #2aa198 } /* Literal.String.Interpol */ .highlight .sx { color: #2aa198 } /* Literal.String.Other */ .highlight .sr { color: #dc322f } /* Literal.String.Regex */ .highlight .s1 { color: #2aa198 } /* Literal.String.Single */ .highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ .highlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ .highlight .vc { color: #268bd2 } /* Name.Variable.Class */ .highlight .vg { color: #268bd2 } /* Name.Variable.Global */ .highlight .vi { color: #268bd2 } /* Name.Variable.Instance */ .highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ voltron-0.1.4/examples/angularview/static/css/style.css000066400000000000000000000005711270717212500233210ustar00rootroot00000000000000.reg_label { color: #b58900; font-weight: bold; } .reg_highlight { color: #dc322f; } .reg_normal { } #tile { display:inline-block; vertical-align: top; background-color: #073642; padding-left: 0.5em; padding-right: 0.5em; padding-top: 0.3em; padding-bottom: 0.3em; } pre { margin: 0; } #disasm { background-color: #073642; }voltron-0.1.4/examples/angularview/static/index.html000066400000000000000000000017571270717212500226630ustar00rootroot00000000000000 voltron
registers
{{reg.name}} {{reg.value}}
disassembly
voltron-0.1.4/examples/angularview/static/js/000077500000000000000000000000001270717212500212705ustar00rootroot00000000000000voltron-0.1.4/examples/angularview/static/js/app.js000066400000000000000000000002361270717212500224070ustar00rootroot00000000000000angular.module('VoltronApp', [ 'VoltronApp.controllers', 'VoltronApp.services' ]) .config(function($sceProvider) { $sceProvider.enabled(false); }); voltron-0.1.4/examples/angularview/static/js/controllers.js000066400000000000000000000053211270717212500241750ustar00rootroot00000000000000function f_hex(number, pad) { return ("000000000000000000000000000000" + number.toString(16)).substr(-pad).toUpperCase(); } angular.module('VoltronApp.controllers', []). controller('voltronController', function($scope, voltronAPIservice) { $scope.version = null; $scope.registers = []; $scope.disassembly = null; var state = null; var new_regs = null; var old_regs = []; voltronAPIservice.version().success(function (response) { $scope.version = response.data; }); var format_registers = function(new_regs, old_regs, arch) { fmt = [] if (arch == 'x86_64') { regs = ['rip','rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','r8','r9','r10','r11','r12','r13','r14','r15'] for (i = 0; i < regs.length; i++) { fmt.push({ name: (regs[i].length == 2 ? String.fromCharCode(160) + regs[i] : regs[i]), value: f_hex(new_regs[regs[i]], 16), class: (new_regs[regs[i]] != old_regs[regs[i]] ? "reg_highlight" : "reg_normal") }) } } return fmt } var format_disasm = function(disasm) { lines = disasm.split(); // trim lldb's "inferior`main:" so hljs works if (lines[0].indexOf("`") > -1) { lines.splice(0, 1); } return lines.join('\n'); } var update = function() { // get target info voltronAPIservice.targets().success(function (response) { targets = response.data.targets; // make sure we have a target if (targets[0]['arch'] != null) { // update registers voltronAPIservice.registers().success(function (response) { // get new register values new_regs = response.data.registers // format registers $scope.registers = format_registers(new_regs, old_regs, targets[0]['arch']); // keep old registers old_regs = new_regs; }); // update disassembly voltronAPIservice.disassemble(null, 32).success(function (response) { $scope.disassembly = response.data.formatted; }); } }); } var poll = function() { // wait for the next state change response = voltronAPIservice.wait(30).success(function (response) { if (response.status == "success") { update(); } // wait for the next state change poll(); }); }; // initial update update(); // start waiting for debugger stops poll(); });voltron-0.1.4/examples/angularview/static/js/services.js000066400000000000000000000033721270717212500234560ustar00rootroot00000000000000angular.module('VoltronApp.services', []). factory('voltronAPIservice', function($http) { var voltronAPI = {}; function createRequest(requestType, data) { return {type: "request", request: requestType, data: data} } voltronAPI.request = function(request) { return $http({ method: 'POST', url: '/api/request', data: request }); } voltronAPI.disassemble_format = function(data) { res = voltronAPI.request(createRequest('disassemble', {address: address, count: count})) return $http({ method: 'POST', url: '/api/request', data: createRequest('format_disasm', {disassembly: res.data.disassembly}) }); } voltronAPI.disassemble = function(address, count) { return voltronAPI.request(createRequest('disassemble', {address: address, count: count})) } voltronAPI.command = function(command) { return voltronAPI.request(createRequest('command', {command: command})) } voltronAPI.targets = function() { return voltronAPI.request(createRequest('targets', {})) } voltronAPI.memory = function(address, length) { return voltronAPI.request(createRequest('memory', {address: address, length: length})) } voltronAPI.registers = function() { return voltronAPI.request(createRequest('registers', {})) } voltronAPI.stack = function(length) { return voltronAPI.request(createRequest('stack', {length: length})) } voltronAPI.state = function() { return voltronAPI.request(createRequest('state', {})) } voltronAPI.version = function() { return voltronAPI.request(createRequest('version', {})) } return voltronAPI; }); voltron-0.1.4/examples/client.py000066400000000000000000000037111270717212500166730ustar00rootroot00000000000000#!/usr/bin/env python """ Example Voltron client. Start your debugger as follows: $ lldb /tmp/inferior Voltron loaded. Run `voltron init` after you load a target. (lldb) target create "/tmp/inferior" Current executable set to '/tmp/inferior' (x86_64). (lldb) voltron init Registered stop-hook (lldb) b main Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0 (lldb) run Process 13185 launched: '/Volumes/Data/Users/snare/code/voltron/repo/tests/inferior' (x86_64) Process 13185 stopped * thread #1: tid = 0x1ee63, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100000cf0 inferior`main inferior`main: -> 0x100000cf0: push rbp 0x100000cf1: mov rbp, rsp 0x100000cf4: sub rsp, 0x50 0x100000cf8: mov dword ptr [rbp - 0x4], 0x0 Run this client in another terminal. Each time you `stepi` in the debugger, the client will output the current RIP: $ python client.py Instruction pointer is: 0x100000CFF Instruction pointer is: 0x100000D02 Instruction pointer is: 0x100000D06 Instruction pointer is: 0x100000D0D Instruction pointer is: 0x100000D15 Instruction pointer is: 0x100000D1C """ import voltron from voltron.core import Client def main(): # Create a client and connect to the server client = Client() # Main event loop while True: # Wait for the debugger to stop again res = client.perform_request('version', block=True) if res.is_success: # If nothing went wrong, get the instruction pointer and print it res = client.perform_request('registers', registers=['rip']) if res.is_success: print("Instruction pointer is: 0x{:X}".format(res.registers['rip'])) else: print("Failed to get registers: {}".format(res)) else: print("Error waiting for the debugger to stop: {}".format(res)) break if __name__ == "__main__": main() voltron-0.1.4/examples/command.py000066400000000000000000000037631270717212500170420ustar00rootroot00000000000000""" Example command plugin Copy this to your ~/.voltron/plugins directory. It will be loaded when Voltron is initialised, and register a debugger command that works as follows: $ lldb /tmp/inferior Voltron loaded. Run `voltron init` after you load a target. (lldb) target create "/tmp/inferior" Current executable set to '/tmp/inferior' (x86_64). (lldb) b main Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0 (lldb) run Process 12561 launched: '/tmp/inferior' (x86_64) Process 12561 stopped * thread #1: tid = 0x1d33f, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100000cf0 inferior`main inferior`main: -> 0x100000cf0: push rbp 0x100000cf1: mov rbp, rsp 0x100000cf4: sub rsp, 0x50 0x100000cf8: mov dword ptr [rbp - 0x4], 0x0 (lldb) example rax 0000000100000CF0 rbx 0000000000000000 rcx 00007FFF5FBFFA70 rdx 00007FFF5FBFF978 rbp 00007FFF5FBFF958 rsp 00007FFF5FBFF948 rdi 0000000000000001 rsi 00007FFF5FBFF968 rip 0000000100000CF0 r8 0000000000000000 r9 00007FFF5FBFEA08 r10 0000000000000032 r11 0000000000000246 r12 0000000000000000 r13 0000000000000000 r14 0000000000000000 r15 0000000000000000 Provided you stick to the adaptor API that is implemented in every *DBAdaptor class, custom command plugins should work across all debugger hosts. This is a quick example that will only work on an x86_64 target. """ import blessed import voltron from voltron.plugin import CommandPlugin from voltron.command import VoltronCommand class ExampleCommand(VoltronCommand): def invoke(self, *args): regs = voltron.debugger.registers() reg_list = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip', 'r8','r9','r10','r11','r12','r13','r14','r15'] for name in reg_list: print("{t.bold}{:3} {t.normal}{:0=16X}".format(name, regs[name], t=blessed.Terminal())) class ExampleCommandPlugin(CommandPlugin): name = 'example' command_class = ExampleCommandvoltron-0.1.4/examples/view.py000066400000000000000000000041051270717212500163650ustar00rootroot00000000000000""" Example Voltron view. Copy this to your ~/.voltron/plugins directory. When the `voltron view` command is executed, 'example' should be visible in the list of valid view names. Start your debugger as follows: $ lldb /tmp/inferior Voltron loaded. Run `voltron init` after you load a target. (lldb) target create "/tmp/inferior" Current executable set to '/tmp/inferior' (x86_64). (lldb) voltron init Registered stop-hook (lldb) b main Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0 (lldb) run Process 13185 launched: '/Volumes/Data/Users/snare/code/voltron/repo/tests/inferior' (x86_64) Process 13185 stopped * thread #1: tid = 0x1ee63, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 frame #0: 0x0000000100000cf0 inferior`main inferior`main: -> 0x100000cf0: push rbp 0x100000cf1: mov rbp, rsp 0x100000cf4: sub rsp, 0x50 0x100000cf8: mov dword ptr [rbp - 0x4], 0x0 Run this view in another terminal (as follows). Each time you `stepi` in the debugger, the view will update and display the current register values. $ voltron view example """ from voltron.view import TerminalView from voltron.plugin import ViewPlugin class ExampleView(TerminalView): def render(self, *args, **kwargs): # Perform the request res = self.client.perform_request('registers') if res.is_success: # Process the registers and set the body to the formatted list reg_list = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip', 'r8','r9','r10','r11','r12','r13','r14','r15'] lines = map(lambda x: '{:3}: {:016X}'.format(x, res.registers[x]), reg_list) self.body = '\n'.join(lines) else: self.body = "Failed to get registers: {}".format(res) # Set the title and info self.title = '[example]' self.info = 'some infoz' # Let the parent do the rendering super(ExampleView, self).render() class ExampleViewPlugin(ViewPlugin): name = 'example' view_class = ExampleView voltron-0.1.4/setup.cfg000066400000000000000000000000321270717212500150370ustar00rootroot00000000000000[bdist_wheel] universal=1 voltron-0.1.4/setup.py000077500000000000000000000014401270717212500147370ustar00rootroot00000000000000import sys from setuptools import setup, find_packages requirements = [ 'scruffington>=0.3.2', 'flask', 'flask_restful', 'blessed', 'pygments', 'requests', 'requests_unixsocket', 'six' ] if sys.platform == 'win32': requirements.append('cursor') setup( name="voltron", version="0.1.4", author="snare", author_email="snare@ho.ax", description=("A debugger UI"), license="MIT", keywords="voltron debugger ui gdb lldb vdb vivisect vtrace windbg cdb pykd", url="https://github.com/snare/voltron", packages=find_packages(exclude=['tests', 'examples']), install_requires=requirements, package_data={'voltron': ['config/*']}, entry_points={ 'console_scripts': ['voltron=voltron:main'] }, zip_safe=False ) voltron-0.1.4/tests/000077500000000000000000000000001270717212500143655ustar00rootroot00000000000000voltron-0.1.4/tests/Vagrantfile000066400000000000000000000020601270717212500165500ustar00rootroot00000000000000Vagrant.configure(2) do |config| config.vm.hostname = "voltron" config.vm.box = "ubuntu/trusty64" config.vm.provider :virtualbox do |v| v.memory = 1024 v.linked_clone = true end config.vm.network "forwarded_port", guest: 5555, host: 5556 config.vm.synced_folder "../", "/voltron" config.vm.provision "shell", inline: <<-SHELL sudo apt-get update sudo apt-get upgrade -y sudo apt-get install -y \ build-essential \ gdb \ zsh \ python3-pip \ lldb-3.4 sudo pip3 install six --upgrade sudo pip3 install -e /voltron echo "source /voltron/voltron/entry.py" >/home/vagrant/.gdbinit echo "command script import /voltron/voltron/entry.py" >/home/vagrant/.lldbinit echo "voltron init" >>/home/vagrant/.gdbinit echo "set disassembly-flavor intel" >>/home/vagrant/.gdbinit mkdir -p /home/vagrant/.voltron chown vagrant:vagrant /home/vagrant/.voltron cat >/home/vagrant/.voltron/config < Server -> LLDBAdaptor Using an instantiated SBDebugger instance """ import tempfile import sys import json import time import logging import subprocess import time from mock import Mock from nose.tools import * import voltron from voltron.core import * from voltron.api import * from voltron.plugin import * import platform if platform.system() == 'Darwin': sys.path.append("/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python") try: import lldb from .common import * log = logging.getLogger("tests") def setup(): global server, client, target, pm, adaptor, methods log.info("setting up API tests") # set up voltron voltron.setup_env() pm = PluginManager() plugin = pm.debugger_plugin_for_host('lldb') adaptor = plugin.adaptor_class() voltron.debugger = adaptor # start up a voltron server server = Server() # import pdb;pdb.set_trace() server.start() time.sleep(0.1) # set up client client = Client() # compile and load the test inferior subprocess.call("cc -o tests/inferior tests/inferior.c", shell=True) target = adaptor.host.CreateTargetWithFileAndArch("tests/inferior", lldb.LLDB_ARCH_DEFAULT) main_bp = target.BreakpointCreateByName("main", target.GetExecutable().GetFilename()) def teardown(): server.stop() time.sleep(5) def test_state_no_target(): req = api_request('state') res = client.send_request(req) assert res.is_error assert res.code == 4101 def test_state_stopped(): process = target.LaunchSimple(None, None, os.getcwd()) req = api_request('state') res = client.send_request(req) assert res.status == 'success' assert res.state == "stopped" target.process.Destroy() def test_targets(): req = api_request('targets') res = client.send_request(req) assert res.status == 'success' t = res.targets[0] assert t["id"] == 0 assert t["arch"] == "x86_64" assert t["file"].endswith("inferior") def test_registers(): process = target.LaunchSimple(None, None, os.getcwd()) req = api_request('registers') res = client.send_request(req) assert res.status == 'success' assert len(res.registers) > 0 assert res.registers['rip'] != 0 target.process.Destroy() def test_memory(): process = target.LaunchSimple(None, None, os.getcwd()) res = client.perform_request('registers') rsp = res.registers['rsp'] res = client.perform_request('memory', address=rsp, length=0x40) assert res.status == 'success' assert len(res.memory) > 0 res = client.perform_request('memory', address=rsp, length=0x40, deref=True) assert res.status == 'success' assert len(res.deref) > 0 target.process.Destroy() def test_stack(): process = target.LaunchSimple(None, None, os.getcwd()) req = api_request('stack', length=0x40) res = client.send_request(req) assert res.status == 'success' assert len(res.memory) > 0 target.process.Destroy() def test_command(): process = target.LaunchSimple(None, None, os.getcwd()) req = api_request('command', command="reg read") res = client.send_request(req) assert res.status == 'success' assert len(res.output) > 0 assert 'rax' in res.output target.process.Destroy() def test_disassemble(): process = target.LaunchSimple(None, None, os.getcwd()) req = api_request('disassemble', count=16) res = client.send_request(req) assert res.status == 'success' assert len(res.disassembly) > 0 assert 'push' in res.disassembly req = api_request('disassemble', count=16, use_capstone=True) res = client.send_request(req) assert res.status == 'success' assert len(res.disassembly) > 0 assert 'push' in res.disassembly target.process.Destroy() def test_dereference(): process = target.LaunchSimple(None, None, os.getcwd()) res = client.perform_request('registers') res = client.perform_request('dereference', pointer=res.registers['rsp']) assert res.status == 'success' assert res.output[0][0] == 'pointer' assert res.output[-1][1] == 'start + 0x1' target.process.Destroy() def test_breakpoints(): process = target.LaunchSimple(None, None, os.getcwd()) res = client.perform_request('breakpoints') assert res.status == 'success' assert len(res.breakpoints) == 1 assert res.breakpoints[0]['one_shot'] == False assert res.breakpoints[0]['enabled'] assert res.breakpoints[0]['id'] == 1 assert res.breakpoints[0]['hit_count'] > 0 assert res.breakpoints[0]['locations'][0]['name'] == "inferior`main" target.process.Destroy() def test_multi_request(): process = target.LaunchSimple(None, None, os.getcwd()) reg_res, dis_res = client.send_requests(api_request('registers'), api_request('disassemble', count=16)) assert reg_res.status == 'success' assert len(reg_res.registers) > 0 assert reg_res.registers['rip'] != 0 assert dis_res.status == 'success' assert len(dis_res.disassembly) > 0 assert 'push' in dis_res.disassembly target.process.Destroy() except: print("No LLDB") voltron-0.1.4/tests/gdb_cli_tests.py000066400000000000000000000074621270717212500175550ustar00rootroot00000000000000""" Tests that test voltron in the gdb cli driver Tests: Client -> Server -> GDBAdaptor Inside a GDB instance """ from __future__ import print_function import tempfile import sys import json import time import logging import pexpect import os from mock import Mock from nose.tools import * import voltron from voltron.core import * from voltron.api import * from voltron.plugin import PluginManager, DebuggerAdaptorPlugin from .common import * log = logging.getLogger('tests') p = None client = None def setup(): global p, client, pm log.info("setting up GDB CLI tests") voltron.setup_env() # compile test inferior pexpect.run("cc -o tests/inferior tests/inferior.c") # start debugger start_debugger() def teardown(): read_data() p.terminate(True) def start_debugger(do_break=True): global p, client p = pexpect.spawn('gdb') p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python3.5.0/lib/python3.5/site-packages')") p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python3.4.3/lib/python3.4/site-packages')") p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python3.3.6/lib/python3.3/site-packages')") p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python2.7.10/lib/python2.7/site-packages')") p.sendline("source voltron/entry.py") p.sendline("file tests/inferior") p.sendline("set disassembly-flavor intel") p.sendline("voltron init") if do_break: p.sendline("b main") p.sendline("run loop") read_data() time.sleep(5) client = Client() def stop_debugger(): # p.sendline("kill") read_data() p.terminate(True) def read_data(): try: while True: data = p.read_nonblocking(size=64, timeout=1) print(data.decode('UTF-8'), end='') except: pass def restart_debugger(do_break=True): stop_debugger() start_debugger(do_break) def test_bad_request(): req = client.create_request('version') req.request = 'xxx' res = client.send_request(req) assert res.is_error assert res.code == 0x1002 def test_version(): req = client.create_request('version') res = client.send_request(req) assert res.api_version == 1.1 assert 'gdb' in res.host_version def test_registers(): global registers read_data() res = client.perform_request('registers') registers = res.registers assert res.status == 'success' assert len(registers) > 0 assert registers['rip'] != 0 def test_memory(): res = client.perform_request('memory', address=registers['rip'], length=0x40) assert res.status == 'success' assert len(res.memory) > 0 def test_state_stopped(): res = client.perform_request('state') assert res.is_success assert res.state == "stopped" def test_targets(): res = client.perform_request('targets') assert res.is_success assert res.targets[0]['state'] == "stopped" assert res.targets[0]['arch'] == "x86_64" assert res.targets[0]['id'] == 0 assert res.targets[0]['file'].endswith('tests/inferior') def test_stack(): res = client.perform_request('stack', length=0x40) assert res.status == 'success' assert len(res.memory) > 0 def test_command(): res = client.perform_request('command', command="info reg") assert res.status == 'success' assert len(res.output) > 0 assert 'rax' in res.output def test_disassemble(): res = client.perform_request('disassemble', count=0x20) assert res.status == 'success' assert len(res.disassembly) > 0 assert 'DWORD' in res.disassembly def test_backtrace(): res = client.perform_request('backtrace') print(res) assert res.is_success assert res.frames[0]['name'] == "main" assert res.frames[0]['index'] == 0 voltron-0.1.4/tests/http_api_tests.py000066400000000000000000000101451270717212500177720ustar00rootroot00000000000000""" Tests that emulate the debugger adaptor and just test the interaction between the front end and back end API classes. HTTP edition! Tests: Server (via HTTP) """ import logging import sys import json import time import subprocess from nose.tools import * import voltron from voltron.core import * from voltron.api import * from voltron.plugin import * import platform if platform.system() == 'Darwin': sys.path.append("/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python") from .common import * import requests log = logging.getLogger('tests') class APIHostNotSupportedRequest(APIRequest): @server_side def dispatch(self): return APIDebuggerHostNotSupportedErrorResponse() class APIHostNotSupportedPlugin(APIPlugin): request = "host_not_supported" request_class = APIHostNotSupportedRequest response_class = APIResponse def setup(): global server, client, target, pm, adaptor, methods log.info("setting up API tests") # set up voltron voltron.setup_env() voltron.config['server'] = { "listen": { "tcp": ["127.0.0.1", 5555] } } pm = PluginManager() plugin = pm.debugger_plugin_for_host('mock') adaptor = plugin.adaptor_class() voltron.debugger = adaptor # inject mock methods inject_mock(adaptor) # start up a voltron server server = Server() server.start() time.sleep(2) def teardown(): server.stop() time.sleep(2) def test_disassemble(): data = requests.get('http://localhost:5555/api/disassemble?count=16').text res = APIResponse(data=data) assert res.is_success assert res.disassembly == disassemble_response def test_command(): data = requests.get('http://localhost:5555/api/command?command=reg%20read').text res = APIResponse(data=data) assert res.is_success assert res.output == command_response def test_targets(): data = requests.get('http://localhost:5555/api/targets').text res = api_response('targets', data=data) assert res.is_success assert res.targets == targets_response def test_memory(): data = requests.get('http://localhost:5555/api/registers').text res = api_response('registers', data=data) url = 'http://localhost:5555/api/memory?address={}&length=64'.format(res.registers['rip']) data = requests.get(url).text res = api_response('memory', data=data) assert res.is_success assert res.memory == memory_response def test_registers(): data = requests.get('http://localhost:5555/api/registers').text res = api_response('registers', data=data) assert res.is_success assert res.registers == registers_response def test_stack_length_missing(): data = requests.get('http://localhost:5555/api/stack').text res = APIErrorResponse(data=data) assert res.is_error assert res.message == 'length' def test_stack(): data = requests.get('http://localhost:5555/api/stack?length=64').text res = api_response('stack', data=data) assert res.is_success assert res.memory == stack_response def test_state(): data = requests.get('http://localhost:5555/api/state').text res = api_response('state', data=data) assert res.is_success assert res.state == state_response def test_version(): data = requests.get('http://localhost:5555/api/version').text res = api_response('version', data=data) assert res.is_success assert res.api_version == 1.1 assert res.host_version == 'lldb-something' def test_bad_json(): data = requests.post('http://localhost:5555/api/request', data='xxx').text res = APIResponse(data=data) assert res.is_error assert res.code == 0x1001 def test_bad_request(): data = requests.post('http://localhost:5555/api/request', data='{"type":"request","request":"no_such_request"}').text res = APIResponse(data=data) assert res.is_error assert res.code == 0x1002 def test_breakpoints(): data = requests.get('http://localhost:5555/api/breakpoints').text res = api_response('breakpoints', data=data) assert res.is_success assert res.breakpoints == breakpoints_response voltron-0.1.4/tests/inferior.c000066400000000000000000000016361270717212500163540ustar00rootroot00000000000000#include #include #include void test_function() { printf("*** test_function()\n"); } int main(int argc, char **argv) { int a = 0; int *b = NULL; int c=0,d=0,e=0; if (argc > 1 && strcmp(argv[1], "sleep") == 0) { printf("*** Sleeping for 5 seconds\n"); sleep(5); } else if (argc > 1 && strcmp(argv[1], "loop") == 0) { printf("*** Looping forever()\n"); while(1) { c++; d+=2; e+=3; } sleep(1); } else if (argc > 1 && strcmp(argv[1], "function") == 0) { printf("*** Calling test_function()\n"); test_function(); } else if (argc > 1 && strcmp(argv[1], "crash") == 0) { printf("*** Crashing\n"); a = *b; } else { printf("Usage: inferior < sleep | loop | function | crash >\n"); } return 0; } voltron-0.1.4/tests/lldb_api_tests.py000066400000000000000000000113241270717212500177300ustar00rootroot00000000000000""" Tests that exercise the LLDB backend directly by loading an inferior and then poking at it with the LLDBAdaptor class. Tests: LLDBAdaptor """ import tempfile import sys import json import time import logging import subprocess import threading from mock import Mock from nose.tools import * import voltron from voltron.core import * from voltron.api import * from voltron.plugin import PluginManager, DebuggerAdaptorPlugin import platform if platform.system() == 'Darwin': sys.path.append("/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python") try: import lldb from common import * voltron.setup_env() log = logging.getLogger('tests') def setup(): global adaptor, dbg, target log.info("setting up LLDB API tests") # create an LLDBAdaptor pm = PluginManager() plugin = pm.debugger_plugin_for_host('lldb') adaptor = plugin.adaptor_class() # compile and load the test inferior subprocess.call("cc -o tests/inferior tests/inferior.c", shell=True) target = adaptor.host.CreateTargetWithFileAndArch("tests/inferior", lldb.LLDB_ARCH_DEFAULT) main_bp = target.BreakpointCreateByName("main", target.GetExecutable().GetFilename()) def teardown(): time.sleep(2) def test_version(): assert 'lldb' in adaptor.version() def test_state_invalid(): try: adaptor.state() exception = False except NoSuchTargetException: exception = True except: exception = False assert exception def test_targets_not_running(): t = adaptor.targets()[0] assert t["state"] == "invalid" assert t["arch"] == "x86_64" assert t["id"] == 0 assert len(t["file"]) > 0 assert 'inferior' in t["file"] def test_targets_stopped(): process = target.LaunchSimple(None, None, os.getcwd()) t = adaptor.targets()[0] assert t["state"] == "stopped" process.Destroy() def test_registers(): process = target.LaunchSimple(None, None, os.getcwd()) regs = adaptor.registers() assert regs is not None assert len(regs) > 0 assert regs['rip'] != 0 process.Destroy() def test_stack_pointer(): process = target.LaunchSimple(None, None, os.getcwd()) sp = adaptor.stack_pointer() assert sp != 0 process.Destroy() def test_program_counter(): process = target.LaunchSimple(None, None, os.getcwd()) pc_name, pc = adaptor.program_counter() assert pc != 0 process.Destroy() def test_memory(): process = target.LaunchSimple(None, None, os.getcwd()) regs = adaptor.registers() mem = adaptor.memory(address=regs['rip'], length=0x40) assert len(mem) == 0x40 process.Destroy() def test_stack(): process = target.LaunchSimple(None, None, os.getcwd()) stack = adaptor.stack(length=0x40) assert len(stack) == 0x40 process.Destroy() def test_disassemble(): process = target.LaunchSimple(None, None, os.getcwd()) output = adaptor.disassemble(count=0x20) assert len(output) > 0 process.Destroy() def test_command(): process = target.LaunchSimple(None, None, os.getcwd()) output = adaptor.command("reg read") assert len(output) > 0 assert 'rax' in output process.Destroy() def test_dereference_main(): process = target.LaunchSimple(None, None, os.getcwd()) regs = adaptor.registers() output = adaptor.dereference(regs['rip']) assert ('symbol', 'main + 0x0') in output process.Destroy() def test_dereference_rsp(): process = target.LaunchSimple(None, None, os.getcwd()) regs = adaptor.registers() output = adaptor.dereference(regs['rsp']) assert ('symbol', 'start + 0x1') in output process.Destroy() def test_dereference_string(): process = target.LaunchSimple(None, None, os.getcwd()) regs = adaptor.registers() output = adaptor.dereference(regs['rsp'] + 0x20) assert 'inferior' in list(output[-1])[-1] process.Destroy() def test_breakpoints(): process = target.LaunchSimple(None, None, os.getcwd()) bps = adaptor.breakpoints() assert len(bps) == 1 assert bps[0]['one_shot'] == False assert bps[0]['enabled'] assert bps[0]['id'] == 1 assert bps[0]['hit_count'] > 0 assert bps[0]['locations'][0]['name'] == "inferior`main" process.Destroy() def test_capabilities(): assert adaptor.capabilities() == ['async'] except: print("No LLDB")voltron-0.1.4/tests/lldb_cli_tests.py000066400000000000000000000145171270717212500177350ustar00rootroot00000000000000""" Tests that test voltron in the lldb cli driver Tests: Client -> Server -> LLDBAdaptor Inside an LLDB CLI driver instance """ from __future__ import print_function import tempfile import sys import json import time import logging import pexpect import os import tempfile import six from mock import Mock from nose.tools import * import voltron from voltron.core import * from voltron.api import * from voltron.plugin import PluginManager, DebuggerAdaptorPlugin from .common import * log = logging.getLogger('tests') p = None client = None def setup(): global p, client, pm log.info("setting up LLDB CLI tests") voltron.setup_env() # compile test inferior pexpect.run("cc -o tests/inferior tests/inferior.c") # start debugger start_debugger() time.sleep(5) def teardown(): read_data() p.terminate(True) time.sleep(2) def start_debugger(do_break=True): global p, client if sys.platform == 'darwin': p = pexpect.spawn('lldb') else: p = pexpect.spawn('lldb-3.4') # for travis (f, tmpname) = tempfile.mkstemp('.py') os.write(f, six.b('\n'.join([ "import sys", "sys.path.append('/home/travis/virtualenv/python3.5.0/lib/python3.5/site-packages')", "sys.path.append('/home/travis/virtualenv/python3.4.3/lib/python3.4/site-packages')", "sys.path.append('/home/travis/virtualenv/python3.3.6/lib/python3.3/site-packages')", "sys.path.append('/home/travis/virtualenv/python2.7.10/lib/python2.7/site-packages')"]))) p.sendline("command script import {}".format(tmpname)) print("pid == {}".format(p.pid)) p.sendline("settings set target.x86-disassembly-flavor intel") p.sendline("command script import voltron/entry.py") time.sleep(2) p.sendline("file tests/inferior") time.sleep(2) p.sendline("voltron init") time.sleep(1) p.sendline("process kill") p.sendline("break delete 1") if do_break: p.sendline("b main") p.sendline("run loop") read_data() client = Client() def stop_debugger(): p.terminate(True) def read_data(): try: while True: data = p.read_nonblocking(size=64, timeout=1) print(data.decode('UTF-8'), end='') except: pass def restart(do_break=True): # stop_debugger() # start_debugger(do_break) p.sendline("process kill") p.sendline("break delete -f") if do_break: p.sendline("b main") p.sendline("run loop") def test_bad_request(): req = client.create_request('version') req.request = 'xxx' res = client.send_request(req) assert res.is_error assert res.code == 0x1002 time.sleep(2) def test_version(): req = client.create_request('version') res = client.send_request(req) assert res.api_version == 1.1 assert 'lldb' in res.host_version def test_registers(): global registers restart() time.sleep(1) read_data() res = client.perform_request('registers') registers = res.registers assert res.status == 'success' assert len(registers) > 0 if 'rip' in registers: assert registers['rip'] != 0 else: assert registers['eip'] != 0 def test_memory(): restart() time.sleep(1) res = client.perform_request('memory', address=registers['rip'], length=0x40) assert res.status == 'success' assert len(res.memory) > 0 def test_state_stopped(): restart() time.sleep(1) res = client.perform_request('state') assert res.is_success assert res.state == "stopped" def test_targets(): restart() time.sleep(1) res = client.perform_request('targets') assert res.is_success assert res.targets[0]['state'] == "stopped" assert res.targets[0]['arch'] == "x86_64" assert res.targets[0]['id'] == 0 assert res.targets[0]['file'].endswith('tests/inferior') def test_stack(): restart() time.sleep(1) res = client.perform_request('stack', length=0x40) assert res.status == 'success' assert len(res.memory) > 0 def test_command(): restart() time.sleep(1) res = client.perform_request('command', command="reg read") assert res.status == 'success' assert len(res.output) > 0 assert 'rax' in res.output def test_disassemble(): restart() time.sleep(1) res = client.perform_request('disassemble', count=0x20) assert res.status == 'success' assert len(res.disassembly) > 0 assert 'push' in res.disassembly def test_dereference(): restart() time.sleep(1) res = client.perform_request('registers') res = client.perform_request('dereference', pointer=res.registers['rsp']) assert res.status == 'success' assert res.output[0][0] == 'pointer' assert 'start' in res.output[-1][1] or 'main' in res.output[-1][1] def test_breakpoints(): restart(True) time.sleep(1) res = client.perform_request('breakpoints') assert res.status == 'success' # assert len(res.breakpoints) == 1 assert res.breakpoints[0]['one_shot'] == False assert res.breakpoints[0]['enabled'] # assert res.breakpoints[0]['id'] == 1 assert res.breakpoints[0]['hit_count'] > 0 assert res.breakpoints[0]['locations'][0]['name'] == "inferior`main" # def test_multi(): # global r1, r2 # restart(True) # time.sleep(1) # r1, r2 = None, None # def send_req(): # global r1, r2 # r1, r2 = client.send_requests(api_request('targets', block=True), api_request('registers', block=True)) # print "sent requests" # t = threading.Thread(target=send_req) # t.start() # time.sleep(5) # p.sendline("stepi") # time.sleep(5) # t.join() # print r1 # print r2 # assert r1.is_success # assert r1.targets[0]['state'] == "stopped" # assert r1.targets[0]['arch'] == "x86_64" # assert r1.targets[0]['id'] == 0 # assert r1.targets[0]['file'].endswith('tests/inferior') # assert r2.status == 'success' # assert len(r2.registers) > 0 # assert r2.registers['rip'] != 0 def test_capabilities(): restart(True) res = client.perform_request('version') assert res.capabilities == ['async'] def test_backtrace(): restart(True) time.sleep(1) res = client.perform_request('backtrace') print(res) assert res.frames[0]['name'] == "inferior`main + 0" assert res.frames[0]['index'] == 0 voltron-0.1.4/tests/test.c000066400000000000000000000001141270717212500155040ustar00rootroot00000000000000#include int main(void) { printf("hello\n"); return 0; }voltron-0.1.4/tests/testinit.lldb000066400000000000000000000000621270717212500170650ustar00rootroot00000000000000file tests/inferior voltron init b main run loop voltron-0.1.4/voltron/000077500000000000000000000000001270717212500147265ustar00rootroot00000000000000voltron-0.1.4/voltron/__init__.py000066400000000000000000000047731270717212500170520ustar00rootroot00000000000000import os import logging import logging.config import voltron from .main import main from scruffy import Environment, Directory, File, ConfigFile, PluginDirectory, PackageDirectory # scruffy environment containing config, plugins, etc env = None config = None debugger = None command = None commands = None server = None loaded = False def setup_env(): global env, config env = Environment(setup_logging=False, voltron_dir=Directory('~/.voltron', create=True, config=ConfigFile('config', defaults=File('config/default.cfg', parent=PackageDirectory()), apply_env=True), sock=File('{config:server.listen.domain}'), history=File('history'), user_plugins=PluginDirectory('plugins') ), pkg_plugins=PluginDirectory('plugins', parent=PackageDirectory()) ) config = env.config voltron.plugin.pm.register_plugins() LOGGER_DEFAULT = { 'handlers': ['null'], 'level': 'DEBUG', 'propagate': False } LOG_CONFIG = { 'version': 1, 'formatters': { 'standard': {'format': 'voltron: [%(levelname)s] %(message)s'} }, 'handlers': { 'default': { 'class': 'logging.StreamHandler', 'formatter': 'standard' }, 'null': { 'class': 'logging.NullHandler' } }, 'loggers': { '': LOGGER_DEFAULT, 'debugger': LOGGER_DEFAULT, 'core': LOGGER_DEFAULT, 'main': LOGGER_DEFAULT, 'api': LOGGER_DEFAULT, 'view': LOGGER_DEFAULT, 'plugin': LOGGER_DEFAULT, } } def setup_logging(logname=None): # configure logging logging.config.dictConfig(LOG_CONFIG) # enable the debug_file in all the loggers if the config says to if config and 'general' in config and config['general']['debug_logging']: if logname: filename = '{}.log'.format(logname) else: filename = 'voltron.log' for name in LOG_CONFIG['loggers']: h = logging.FileHandler(voltron.env.voltron_dir.path_to(filename), delay=True) h.setFormatter(logging.Formatter(fmt="%(asctime)s %(levelname)-7s %(filename)12s:%(lineno)-4s %(funcName)20s -- %(message)s")) logging.getLogger(name).addHandler(h) logging.info("======= VOLTRON - DEFENDER OF THE UNIVERSE [debug log] =======") return logging.getLogger(logname) # Python 3 shim if not hasattr(__builtins__, "xrange"): xrange = range # Setup the Voltron environment setup_env() voltron-0.1.4/voltron/__main__.py000066400000000000000000000000371270717212500170200ustar00rootroot00000000000000from .main import main main() voltron-0.1.4/voltron/api.py000066400000000000000000000254141270717212500160570ustar00rootroot00000000000000import os import logging import socket import select import threading import logging import logging.config import json import inspect import base64 import six from collections import defaultdict from scruffy.plugin import Plugin import voltron from .plugin import APIPlugin log = logging.getLogger('api') version = 1.1 class InvalidRequestTypeException(Exception): """ Exception raised when the client is requested to send an invalid request type. """ pass class InvalidDebuggerHostException(Exception): """ Exception raised when the debugger host is invalid. """ pass class InvalidViewNameException(Exception): """ Exception raised when an invalid view name is specified. """ pass class InvalidMessageException(Exception): """ Exception raised when an invalid API message is received. """ pass class ServerSideOnlyException(Exception): """ Exception raised when a server-side method is called on an APIMessage subclass that exists on the client-side. See @server_side decorator. """ pass class ClientSideOnlyException(Exception): """ Exception raised when a server-side method is called on an APIMessage subclass that exists on the client-side. See @client_side decorator. """ pass class DebuggerNotPresentException(Exception): """ Raised when an APIRequest is dispatched without a valid debugger present. """ pass class NoSuchTargetException(Exception): """ Raised when an APIRequest specifies an invalid target. """ pass class TargetBusyException(Exception): """ Raised when an APIRequest specifies a target that is currently busy and cannot be queried. """ pass class MissingFieldError(Exception): """ Raised when an APIMessage is validated and has a required field missing. """ pass class NoSuchThreadException(Exception): """ Raised when the specified thread ID or index does not exist. """ pass class UnknownArchitectureException(Exception): """ Raised when the debugger host is running in an unknown architecture. """ pass class BlockingNotSupportedError(Exception): """ Raised when a view that does not support blocking connects to a debugger host that does not support async mode. """ pass def server_side(func): """ Decorator to designate an API method applicable only to server-side instances. This allows us to use the same APIRequest and APIResponse subclasses on the client and server sides without too much confusion. """ def inner(*args, **kwargs): if args and hasattr(args[0], 'is_server') and not voltron.debugger: raise ServerSideOnlyException("This method can only be called on a server-side instance") return func(*args, **kwargs) return inner def client_side(func): """ Decorator to designate an API method applicable only to client-side instances. This allows us to use the same APIRequest and APIResponse subclasses on the client and server sides without too much confusion. """ def inner(*args, **kwargs): if args and hasattr(args[0], 'is_server') and voltron.debugger: raise ClientSideOnlyException("This method can only be called on a client-side instance") return func(*args, **kwargs) return inner def cast_b(val): if isinstance(val, six.binary_type): return val elif isinstance(val, six.text_type): return val.encode('latin1') return six.binary_type(val) def cast_s(val): if type(val) == six.text_type: return val elif type(val) == six.binary_type: return val.decode('latin1') return six.text_type(val) class APIMessage(object): """ Top-level API message class. """ _top_fields = ['type'] _fields = {} _encode_fields = [] type = None def __init__(self, data=None, *args, **kwargs): # process any data that was passed in if data: self.from_json(data) # any other kwargs are treated as field values for field in kwargs: setattr(self, field, kwargs[field]) def __str__(self): """ Return a string (JSON) representation of the API message properties. """ return self.to_json() def to_dict(self): """ Return a transmission-safe dictionary representation of the API message properties. """ d = {field: getattr(self, field) for field in self._top_fields if hasattr(self, field)} # set values of data fields d['data'] = {} for field in self._fields: if hasattr(self, field): # base64 encode the field for transmission if necessary if field in self._encode_fields: val = getattr(self, field) if val: val = cast_s(base64.b64encode(cast_b(val))) d['data'][field] = val else: d['data'][field] = getattr(self, field) return d def from_dict(self, d): """ Initialise an API message from a transmission-safe dictionary. """ for key in d: if key == 'data': for dkey in d['data']: if dkey in self._encode_fields: setattr(self, str(dkey), base64.b64decode(d['data'][dkey])) else: setattr(self, str(dkey), d['data'][dkey]) else: setattr(self, str(key), d[key]) def to_json(self): """ Return a JSON representation of the API message properties. """ return json.dumps(self.to_dict()) def from_json(self, data): """ Initialise an API message from a JSON representation. """ try: d = json.loads(data) except ValueError: raise InvalidMessageException() self.from_dict(d) def __getattr__(self, name): """ Attribute accessor. If a defined field is requested that doesn't have a value set, return None. """ if name in self._fields: return None def validate(self): """ Validate the message. Ensure all the required fields are present and not None. """ required_fields = list(filter(lambda x: self._fields[x], self._fields.keys())) for field in (self._top_fields + required_fields): if not hasattr(self, field) or hasattr(self, field) and getattr(self, field) == None: raise MissingFieldError(field) class APIRequest(APIMessage): """ An API request object. Contains functions and accessors common to all API request types. Subclasses of APIRequest are used on both the client and server sides. On the server side they are instantiated by Server's `handle_request()` method. On the client side they are instantiated by whatever class is doing the requesting (probably a view class). """ _top_fields = ['type', 'request', 'block', 'timeout'] _fields = {} type = 'request' request = None block = False timeout = 10 response = None wait_event = None timed_out = False @server_side def dispatch(self): """ In concrete subclasses this method will actually dispatch the request to the debugger host and return a response. In this case it raises an exception. """ raise NotImplementedError("Subclass APIRequest") @server_side def wait(self): """ Wait for the request to be dispatched. """ self.wait_event = threading.Event() timeout = int(self.timeout) if self.timeout else None self.timed_out = not self.wait_event.wait(timeout) def signal(self): """ Signal that the request has been dispatched and can return. """ self.wait_event.set() class APIBlockingRequest(APIRequest): """ An API request that blocks by default. """ block = True class APIResponse(APIMessage): """ An API response object. Contains functions and accessors common to all API response types. Subclasses of APIResponse are used on both the client and server sides. On the server side they are instantiated by the APIRequest's `dispatch` method in order to serialise and send to the client. On the client side they are instantiated by the Client class and returned by `send_request`. """ _top_fields = ['type', 'status'] _fields = {} type = 'response' status = None @property def is_success(self): return self.status == 'success' @property def is_error(self): return self.status == 'error' def __repr__(self): return "<%s: success = %s, error = %s, body: %s>" % ( str(self.__class__), self.is_success, self.is_error, {f: getattr(self, f) for f in self._top_fields + list(self._fields.keys())} ) class APISuccessResponse(APIResponse): """ A generic API success response. """ status = 'success' class APIErrorResponse(APIResponse): """ A generic API error response. """ _fields = {'code': True, 'message': True} status = 'error' @property def timed_out(self): return self.code == APITimedOutErrorResponse.code class APIGenericErrorResponse(APIErrorResponse): code = 0x1000 message = "An error occurred" def __init__(self, message=None): super(APIGenericErrorResponse, self).__init__() if message: self.message = message class APIInvalidRequestErrorResponse(APIErrorResponse): code = 0x1001 message = "Invalid API request" class APIPluginNotFoundErrorResponse(APIErrorResponse): code = 0x1002 message = "Plugin was not found for request" class APIDebuggerHostNotSupportedErrorResponse(APIErrorResponse): code = 0x1003 message = "The targeted debugger host is not supported by this plugin" class APITimedOutErrorResponse(APIErrorResponse): code = 0x1004 message = "The request timed out" class APIDebuggerNotPresentErrorResponse(APIErrorResponse): code = 0x1004 message = "No debugger host was found" class APINoSuchTargetErrorResponse(APIErrorResponse): code = 0x1005 message = "No such target" class APITargetBusyErrorResponse(APIErrorResponse): code = 0x1006 message = "Target busy" class APIMissingFieldErrorResponse(APIGenericErrorResponse): code = 0x1007 message = "Missing field" class APIEmptyResponseErrorResponse(APIGenericErrorResponse): code = 0x1008 message = "Empty response" class APIServerExitedErrorResponse(APIGenericErrorResponse): code = 0x1009 message = "Server exited" voltron-0.1.4/voltron/colour.py000066400000000000000000000015211270717212500166020ustar00rootroot00000000000000ESCAPES = { # reset 'reset': 0, # colours 'grey': 30, 'red': 31, 'green': 32, 'yellow': 33, 'blue': 34, 'magenta': 35, 'cyan': 36, 'white': 37, # background 'b_grey': 40, 'b_red': 41, 'b_green': 42, 'b_yellow': 43, 'b_blue': 44, 'b_magenta': 45, 'b_cyan': 46, 'b_white': 47, # attributes 'a_bold': 1, 'a_dark': 2, 'a_underline': 4, 'a_blink': 5, 'a_reverse': 7, 'a_concealed': 8 } ESC_TEMPLATE = '\033[{}m' def escapes(): return ESCAPES def get_esc(name): return ESCAPES[name] def fmt_esc(name): return ESC_TEMPLATE.format(escapes()[name]) FMT_ESCAPES = dict((k, fmt_esc(k)) for k in ESCAPES) voltron-0.1.4/voltron/config/000077500000000000000000000000001270717212500161735ustar00rootroot00000000000000voltron-0.1.4/voltron/config/default.cfg000066400000000000000000000162661270717212500203130ustar00rootroot00000000000000# # defaults.cfg # # Default config file. Unless you are snare, probably don't change this. # # To customise the configuration, create a file with the same format in ~/.voltron/ called "config". You don't need to # specify every option that is in the default config in your local config. Any options you do specify will override # those in the default. # # For example, if you only want to change the default header colours, the following config will suffice: # { # "view": { # "all_views": { # "header": { # "colour": "red", # "bg_colour": "green", # } # } # } # } # You can probably figure out more config options by reading the source. View-specific and named view settings override # the same settings in "all_views". # # Format is subject to change. # { "general": { "debug_logging": false }, "server": { "listen": { "domain": "~/.voltron/sock", "tcp": ["127.0.0.1", 5555] } }, "view": { #"api_url": "http+unix://~%2f.voltron%2fsock/api/request", "api_url": "http://localhost:5555/api/request", "reconnect": true, "all_views": { "clear": true, "update_on": "stop", "header": { "show": true, "pad": " ", "colour": "blue", "bg_colour": "grey", "attrs": [], "label_left": { "name": "info", "colour": "blue", "bg_colour": "grey", "attrs": [] }, "label_right": { "name": "title", "colour": "white", "bg_colour": "grey", "attrs": ["bold"] } }, "footer": { "show": false, "pad": " ", "colour": "blue", "bg_colour": "grey", "attrs": [], "label_left": { "name": null, "colour": "blue", "bg_colour": "grey", "attrs": [] }, "label_right": { "name": null, "colour": "blue", "bg_colour": "grey", "attrs": ["bold"] } }, "pad": { "pad_right": 0, "pad_bottom": 0 } }, "register_view": { "format": { "label_format": "{0}:", "label_func": "str_upper", "label_colour": "green", "label_colour_en": true, "value_format": "{0:0=16X}", "value_func": null, "value_colour": "reset", "value_colour_mod": "red", "value_colour_en": true, "format_name": null }, "sections": ["general"], "orientation": "vertical" }, "disassembly_view": { "header": { "show": true, "label_left": { "name": "title", "colour": "white", "bg_colour": "grey", "attrs": ["bold"] }, "label_right": { "name": null } } }, "stack_view": { "header": { "show": false }, "footer": { "show": true, "label_left": { "name": "title", "colour": "white", "bg_colour": "grey", "attrs": ["bold"] }, "label_right": { "name": "info", "colour": "blue", "bg_colour": "grey", "attrs": [] } }, "format": { "addr_format": "{0:0=16X}", "addr_colour": "blue", "divider_colour": "green", "string_colour": "white", "symbol_colour": "cyan" } }, "memory_view": { "header": { "show": false }, "footer": { "show": true, "label_left": { "name": "title", "colour": "white", "bg_colour": "grey", "attrs": ["bold"] }, "label_right": { "name": "info", "colour": "blue", "bg_colour": "grey", "attrs": [] } }, "format": { "addr_format": "{0:0=16X}", "addr_colour": "blue", "divider_colour": "green", "string_colour": "white", "symbol_colour": "cyan" } }, "backtrace_view": { "header": { "show": false }, "footer": { "show": true, "label_left": { "name": "title", "colour": "white", "bg_colour": "grey", "attrs": ["bold"] }, "label_right": { "name": "info", "colour": "blue", "bg_colour": "grey", "attrs": [] } } }, "breakpoints_view": { "format": { "row": "{disabled}{one_shot}{t.bold}{id}{t.normal} {hit}{t.blue}0x{address:0=16X}{t.normal} {t.green}h:{t.normal}{hit_count:<4} {t.cyan}{name}{t.normal}", "disabled": "{t.red}", "one_shot": "{t.underline}", "hit": "{t.standout}" } }, "some_named_stack_view": { "header": { "show": true, "label_left": { "name": "title", "colour": "red", "bg_colour": "grey", "attrs": ["bold"] }, "label_right": { "name": "info", "colour": "white", "bg_colour": "grey", "attrs": [] } }, "footer": { "show": false } } }, "console": { "prompt": { "format": "{red}voltron>{reset} " } } } voltron-0.1.4/voltron/core.py000066400000000000000000000356271270717212500162450ustar00rootroot00000000000000import os import sys import errno import logging import socket import select import threading import logging import logging.config import json try: import requests_unixsocket as requests except: import requests import threading import os.path import pkgutil from werkzeug.serving import WSGIRequestHandler, BaseWSGIServer, ThreadedWSGIServer from werkzeug.wsgi import SharedDataMiddleware, DispatcherMiddleware from flask import Flask, request, Response, render_template, make_response, redirect import voltron from .api import * from .plugin import * import six if six.PY2: if sys.platform == 'win32': from SocketServer import ThreadingMixIn else: from SocketServer import UnixStreamServer, ThreadingMixIn from BaseHTTPServer import HTTPServer else: if sys.platform == 'win32': from six.moves.socketserver import ThreadingMixIn else: from six.moves.socketserver import UnixStreamServer, ThreadingMixIn from six.moves.BaseHTTPServer import HTTPServer try: from voltron_web import app as ui_app except: ui_app = None def get_loader(name): try: return orig_get_loader(name) except AttributeError: pass orig_get_loader = pkgutil.get_loader pkgutil.get_loader = get_loader # make sure we use HTTP 1.1 for keep-alive WSGIRequestHandler.protocol_version = "HTTP/1.1" log = logging.getLogger("core") if sys.version_info.major == 2: STRTYPES = (str, unicode) elif sys.version_info.major == 3: STRTYPES = (str, bytes) else: raise RuntimeError("Not sure what strings look like on python %d" % sys.version_info.major) class APIFlaskApp(Flask): """ A Flask app for the API. """ def __init__(self, *args, **kwargs): if 'server' in kwargs: self.server = kwargs['server'] del kwargs['server'] super(APIFlaskApp, self).__init__('voltron_api', *args, **kwargs) def api_post(): res = self.server.handle_request(request.data.decode('UTF-8')) return Response(str(res), status=200, mimetype='application/json') def api_get(): res = self.server.handle_request(str(api_request(request.path.split('/')[-1], **request.args.to_dict()))) return Response(str(res), status=200, mimetype='application/json') # Handle API POST requests at /api/request api_post.methods = ["POST"] self.add_url_rule('/request', 'request', api_post) # Handle API GET requests at /api/ e.g. /api/version for plugin in voltron.plugin.pm.api_plugins: self.add_url_rule('/{}'.format(plugin), plugin, api_get) class RootFlaskApp(Flask): """ A Flask app for /. """ def __init__(self, *args, **kwargs): super(RootFlaskApp, self).__init__('voltron', *args, **kwargs) def index(): if ui_app: return redirect('/ui') else: return "The Voltron web interface is not installed. Install the voltron-web package with pip." self.add_url_rule('/', 'index', index) class Server(object): """ Main server class instantiated by the debugger host. Responsible for controlling the background thread that communicates with clients, and handling requests forwarded from that thread. """ def __init__(self): self.threads = [] self.listeners = [] self.is_running = False self.queue = [] self.queue_lock = threading.Lock() def start(self): """ Start the server. """ plugins = voltron.plugin.pm.web_plugins self.app = DispatcherMiddleware( RootFlaskApp(), { "/api": APIFlaskApp(server=self), "/view": SharedDataMiddleware( None, {'/{}'.format(n): os.path.join(p._dir, 'static') for (n, p) in six.iteritems(plugins)} ), "/ui": ui_app } ) def run_listener(name, cls, arg): log.debug("Starting listener for {} socket on {}".format(name, str(arg))) s = cls(*arg) t = threading.Thread(target=s.serve_forever) t.start() self.threads.append(t) self.listeners.append(s) if voltron.config.server.listen.tcp: run_listener('tcp', ThreadedVoltronWSGIServer, list(voltron.config.server.listen.tcp) + [self.app]) if voltron.config.server.listen.domain and sys.platform != 'win32': path = os.path.expanduser(str(voltron.config.server.listen.domain)) try: os.unlink(path) except: pass run_listener('domain', ThreadedUnixWSGIServer, [path, self.app]) self.is_running = True def stop(self): """ Stop the server. """ log.debug("Stopping listeners") for s in self.listeners: s.shutdown() s.socket.close() self.cancel_queue() for t in self.threads: t.join() self.listeners = [] self.threads = [] self.is_running = False log.debug("Listeners stopped and threads joined") def handle_request(self, data): req = None res = None # make sure we have a debugger, or we're gonna have a bad time if voltron.debugger: # parse incoming request with the top level APIRequest class so we can determine the request type try: req = APIRequest(data=data) except Exception as e: req = None log.exception("Exception raised while parsing API request: {} {}".format(type(e), e)) if req: # instantiate the request class try: log.debug("data = {}".format(data)) req = api_request(req.request, data=data) except Exception as e: log.exception("Exception raised while creating API request: {} {}".format(type(e), e)) req = None if not req: res = APIPluginNotFoundErrorResponse() else: res = APIInvalidRequestErrorResponse() else: res = APIDebuggerNotPresentErrorResponse() if not res: # no errors so far, queue the request and wait if req and req.block: self.queue_lock.acquire() self.queue.append(req) self.queue_lock.release() # When this returns the request will have been processed by the dispatch_queue method on the main # thread (or timed out). We have to do it this way because GDB sucks. req.wait() if req.timed_out: res = APITimedOutErrorResponse() else: res = req.response # Remove the request from the queue self.queue_lock.acquire() if req in self.queue: self.queue.remove(req) self.queue_lock.release() else: # non-blocking, dispatch request straight away res = self.dispatch_request(req) return res def cancel_queue(self): """ Cancel all requests in the queue so we can exit. """ self.queue_lock.acquire() q = list(self.queue) self.queue = [] self.queue_lock.release() log.debug("Canceling requests: {}".format(q)) for req in q: req.response = APIServerExitedErrorResponse() for req in q: req.signal() def dispatch_queue(self): """ Dispatch any queued requests. Called by the debugger when it stops. """ self.queue_lock.acquire() q = list(self.queue) self.queue = [] self.queue_lock.release() log.debug("Dispatching requests: {}".format(q)) for req in q: req.response = self.dispatch_request(req) for req in q: req.signal() def dispatch_request(self, req): """ Dispatch a request object. """ log.debug("Dispatching request: {}".format(str(req))) # make sure it's valid res = None try: req.validate() except MissingFieldError as e: res = APIMissingFieldErrorResponse(str(e)) # dispatch the request if not res: try: res = req.dispatch() except Exception as e: msg = "Exception raised while dispatching request: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) log.debug("Response: {}".format(str(res))) return res class VoltronWSGIServer(BaseWSGIServer): """ Custom version of the werkzeug WSGI server. This just needs to exist so we can swallow errors when clients disconnect. """ def finish_request(self, *args): try: super(VoltronWSGIServer, self).finish_request(*args) except socket.error as e: log.error("Error in finish_request: {}".format(e)) class ThreadedVoltronWSGIServer(ThreadingMixIn, VoltronWSGIServer): """ Threaded WSGI server to replace werkzeug's """ pass if sys.platform != 'win32': class UnixWSGIServer(UnixStreamServer, VoltronWSGIServer): """ A subclass of BaseWSGIServer that does sane things with Unix domain sockets. """ def __init__(self, sockfile=None, app=None): self.address_family = socket.AF_UNIX UnixStreamServer.__init__(self, sockfile, UnixWSGIRequestHandler) self.app = app self.passthrough_errors = None self.shutdown_signal = False self.ssl_context = None self.host = 'localhost' self.port = 0 class UnixWSGIRequestHandler(WSGIRequestHandler): """ A WSGIRequestHandler that does sane things with Unix domain sockets. """ def make_environ(self, *args, **kwargs): self.client_address = ('127.0.0.1', 0) return super(UnixWSGIRequestHandler, self).make_environ(*args, **kwargs) class ThreadedUnixWSGIServer(ThreadingMixIn, UnixWSGIServer): """ Threaded HTTP server that works over Unix domain sockets. Note: this intentionally does not inherit from HTTPServer. Go look at the source and you'll see why. """ multithread = True class ClientThread(threading.Thread): """ A thread that performs an API request with a client. """ def __init__(self, client, request, *args, **kwargs): self.request = request self.response = None self.exception = None self.client = client super(ClientThread, self).__init__(*args, **kwargs) def run(self): try: self.response = self.client.send_request(self.request) except Exception as e: self.exception = e class Client(object): """ Used by a client (ie. a view) to communicate with the server. """ def __init__(self, host='127.0.0.1', port=5555, sockfile=None, url=None): """ Initialise a new client """ self.session = requests.Session() if url: self.url = url elif sockfile: self.url = 'http+unix://{}/api/request'.format(sockfile.replace('/', '%2F')) else: self.url = 'http://{}:{}/api/request'.format(host, port) self.url = self.url.replace('~', os.path.expanduser('~').replace('/', '%2f')) def send_request(self, request): """ Send a request to the server. `request` is an APIRequest subclass. Returns an APIResponse or subclass instance. If an error occurred, it will be an APIErrorResponse, if the request was successful it will be the plugin's specified response class if one exists, otherwise it will be an APIResponse. """ # default to an empty response error res = APIEmptyResponseErrorResponse() # perform the request log.debug("Client sending request: " + str(request)) response = self.session.post(self.url, data=str(request)) data = response.text if response.status_code != 200: res = APIGenericErrorResponse(response.text) elif data and len(data) > 0: log.debug('Client received message: ' + data) try: # parse the response data generic_response = APIResponse(data=data) # if there's an error, return an error response if generic_response.is_error: res = APIErrorResponse(data=data) else: # success; generate a proper response plugin = voltron.plugin.pm.api_plugin_for_request(request.request) if plugin and plugin.response_class: # found a plugin for the request we sent, use its response type res = plugin.response_class(data=data) else: # didn't find a plugin, just return the generic APIResponse we already generated res = generic_response except Exception as e: log.exception('Exception parsing message: ' + str(e)) log.error('Invalid message: ' + data) else: res = APIEmptyResponseErrorResponse() return res def send_requests(self, *args): """ Send a set of requests. Each request is sent over its own connection and the function will return when all the requests have been fulfilled. """ threads = [ClientThread(self, req) for req in args] for t in threads: t.start() for t in threads: t.join() exceptions = [t.exception for t in threads if t.exception] if len(exceptions): raise exceptions[0] return [t.response for t in threads] def create_request(self, request_type, *args, **kwargs): """ Create a request. `request_type` is the request type (string). This is used to look up a plugin, whose request class is instantiated and passed the remaining arguments passed to this function. """ return api_request(request_type, *args, **kwargs) def perform_request(self, request_type, *args, **kwargs): """ Create and send a request. `request_type` is the request type (string). This is used to look up a plugin, whose request class is instantiated and passed the remaining arguments passed to this function. """ # create a request req = api_request(request_type, *args, **kwargs) # send it res = self.send_request(req) return res voltron-0.1.4/voltron/dbg.py000066400000000000000000000174261270717212500160460ustar00rootroot00000000000000try: import capstone except: capstone = None from voltron.api import * from voltron.plugin import * class InvalidPointerError(Exception): """ Raised when attempting to dereference an invalid pointer. """ pass def validate_target(func, *args, **kwargs): """ A decorator that ensures that the specified target_id exists and is valid. Expects the target ID to be either the 'target_id' param in kwargs, or the first positional parameter. Raises a NoSuchTargetException if the target does not exist. """ def inner(self, *args, **kwargs): # find the target param target_id = None if 'target_id' in kwargs and kwargs['target_id'] != None: target_id = kwargs['target_id'] else: target_id = 0 # if there was a target specified, check that it's valid if not self.target_is_valid(target_id): raise NoSuchTargetException() # call the function return func(self, *args, **kwargs) return inner def validate_busy(func, *args, **kwargs): """ A decorator that raises an exception if the specified target is busy. Expects the target ID to be either the 'target_id' param in kwargs, or the first positional parameter. Raises a TargetBusyException if the target does not exist. """ def inner(self, *args, **kwargs): # find the target param target_id = None if 'target_id' in kwargs and kwargs['target_id'] != None: target_id = kwargs['target_id'] else: target_id = 0 # if there was a target specified, ensure it's not busy if self.target_is_busy(target_id): raise TargetBusyException() # call the function return func(self, *args, **kwargs) return inner def lock_host(func, *args, **kwargs): """ A decorator that acquires a lock before accessing the debugger to avoid API locking related errors with the debugger host. """ def inner(self, *args, **kwargs): self.host_lock.acquire() try: res = func(self, *args, **kwargs) self.host_lock.release() except Exception as e: self.host_lock.release() raise e return res return inner class DebuggerAdaptor(object): """ Base debugger adaptor class. Debugger adaptors implemented in plugins for specific debuggers inherit from this. """ reg_names = { "x86": {"pc": "eip", "sp": "esp"}, "x86_64": {"pc": "rip", "sp": "rsp"}, "arm": {"pc": "pc", "sp": "sp"}, "armv6": {"pc": "pc", "sp": "sp"}, "armv7": {"pc": "pc", "sp": "sp"}, "armv7s": {"pc": "pc", "sp": "sp"}, "arm64": {"pc": "pc", "sp": "sp"}, "powerpc": {"pc": "pc", "sp": "r1"}, } cs_archs = {} if capstone: cs_archs = { "x86": (capstone.CS_ARCH_X86, capstone.CS_MODE_32), "x86_64": (capstone.CS_ARCH_X86, capstone.CS_MODE_64), "arm": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), "armv6": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), "armv7": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), "armv7s": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), "arm64": (capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM), "powerpc": (capstone.CS_ARCH_PPC, capstone.CS_MODE_32), } def __init__(self, *args, **kwargs): self.listeners = [] def target_exists(self, target_id=0): """ Returns True or False indicating whether or not the specified target is present and valid. `target_id` is a target ID (or None for the first target) """ try: target = self._target(target_id=target_id) except Exception as e: log.error("Exception checking if target exists: {} {}".format(type(e), e)) return False return target is not None def target_is_valid(self, target_id=0): """ Returns True or False indicating whether or not the specified target is present and valid. `target_id` is a target ID (or None for the first target) """ try: target = self._target(target_id=target_id) except: return False return target['state'] != "invalid" def target_is_busy(self, target_id=0): """ Returns True or False indicating whether or not the specified target is busy. `target_id` is a target ID (or None for the first target) """ try: target = self._target(target_id=target_id) except: raise NoSuchTargetException() return target['state'] == "running" def add_listener(self, callback, state_changes=["stopped"]): """ Add a listener for state changes. """ self.listeners.append({"callback": callback, "state_changes": state_changes}) def remove_listener(self, callback): """ Remove a listener. """ listeners = filter(lambda x: x['callback'] == callback, self.listeners) for l in listeners: self.listeners.remove(l) def update_state(self): """ Notify all the listeners (probably `wait` plugins) that the state has changed. This is called by the debugger's stop-hook. """ for listener in self.listeners: listener['callback']() def register_command_plugin(self, name, cls): pass def capabilities(self): """ Return a list of the debugger's capabilities. Thus far only the 'async' capability is supported. This indicates that the debugger host can be queried from a background thread, and that views can use non-blocking API requests without queueing requests to be dispatched next time the debugger stops. """ return [] def pc(self, target_id=0, thread_id=None): return self.program_counter(target_id, thread_id) def sp(self, target_id=0, thread_id=None): return self.stack_pointer(target_id, thread_id) def disassemble_capstone(self, target_id=0, address=None, count=None): """ Disassemble with capstone. """ target = self._target(target_id) if not address: pc_name, address = self.pc() mem = self.memory(address, count * 16, target_id=target_id) md = capstone.Cs(*self.cs_archs[target['arch']]) output = [] for idx, i in enumerate(md.disasm(mem, address)): if idx >= count: break output.append("0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str)) return '\n'.join(output) class DebuggerCommand (object): """ The `voltron` command in the debugger. """ def __init__(self, *args, **kwargs): super(DebuggerCommand, self).__init__(*args, **kwargs) self.adaptor = voltron.debugger self.registered = False def handle_command(self, command): global log if 'debug' in command: if 'enable' in command: log.setLevel(logging.DEBUG) print("Debug logging enabled") elif 'disable' in command: log.setLevel(logging.INFO) print("Debug logging disabled") else: enabled = "enabled" if log.getEffectiveLevel() == logging.DEBUG else "disabled" print("Debug logging is currently " + enabled) elif 'init' in command: self.register_hooks() elif 'stopped' in command or 'update' in command: self.adaptor.update_state() voltron.server.dispatch_queue() else: print("Usage: voltron ") voltron-0.1.4/voltron/entry.py000066400000000000000000000061251270717212500164450ustar00rootroot00000000000000""" This is the main entry point for Voltron from the debugger host's perspective. This file is loaded into the debugger through whatever means the given host supports. LLDB: (lldb) command script import /path/to/voltron/entry.py GDB: (gdb) source /path/to/voltron/entry.py VDB: (vdb) script /path/to/voltron/entry.py WinDbg/CDB (via PyKD): > .load pykd.pyd > !py --global C:\path\to\voltron\entry.py """ log = None try: # fix path if it's clobbered by brew import sys if sys.platform == 'darwin': py_base = '/System/Library/Frameworks/Python.framework/Versions/2.7/' new_path = ['lib/python27.zip', 'lib/python2.7', 'lib/python2.7/plat-darwin', 'lib/python2.7/plat-mac', 'lib/python2.7/plat-mac/lib-scriptpackages', 'Extras/lib/python', 'lib/python2.7/lib-tk', 'lib/python2.7/lib-old', 'lib/python2.7/lib-dynload'] sys.path = [p for p in sys.path if 'Cellar' not in p] + [py_base + p for p in new_path] except: pass try: import logging import os import sys blessed = None import blessed # add vtrace to the path so that dbg_vdb.py can import from vdb/vtrace. if "vtrace" in locals(): def parent_directory(the_path): return os.path.abspath(os.path.join(the_path, os.pardir)) def add_vdb_to_path(vtrace): sys.path.append(parent_directory(parent_directory(vtrace.__file__))) add_vdb_to_path(vtrace) else: pass import voltron from voltron.plugin import pm from voltron.core import Server log = voltron.setup_logging('debugger') # figure out in which debugger host we are running args = [] try: import lldb host = "lldb" def invoke(*args): voltron.command._invoke(*args) except ImportError: pass try: import gdb host = "gdb" except ImportError: pass try: import pykd host = "windbg" except: pass if "vtrace" in locals(): host = "vdb" args = [db] if not host: raise Exception("No debugger host is present") # register any plugins that were loaded pm.register_plugins() # get the debugger plugin for the host we're in plugin = pm.debugger_plugin_for_host(host) if not voltron.command: # set up command and adaptor instances voltron.debugger = plugin.adaptor_class(*args) voltron.command = plugin.command_class(*args) # create and start the voltron server voltron.server = Server() if host != "gdb": voltron.server.start() print(blessed.Terminal().bold_red("Voltron loaded.")) if host == 'lldb' and not voltron.command.registered: print("Run `voltron init` after you load a target.") except Exception as e: import traceback msg = "An error occurred while loading Voltron:\n\n{}".format(traceback.format_exc()) if blessed: msg = blessed.Terminal().bold_red(msg) if log: log.exception("Exception raised while loading Voltron") print(msg) voltron-0.1.4/voltron/lexers.py000066400000000000000000000131301270717212500166000ustar00rootroot00000000000000import re from pygments.lexer import RegexLexer, include, bygroups, using, DelegatingLexer from pygments.token import * class LLDBIntelLexer(RegexLexer): """ For Nasm (Intel) disassembly from LLDB. Based on the NasmLexer included with Pygments """ name = 'LLDBIntel' aliases = ['lldb_intel'] filenames = [] mimetypes = [] identifier = r'[]*' hexn = r'(?:0[xX][0-9a-f]+|$0[0-9a-f]*|[0-9]+[0-9a-f]*h)' octn = r'[0-7]+q' binn = r'[01]+b' decn = r'[0-9]+' floatn = decn + r'\.e?' + decn string = r'"(\\"|[^"\n])*"|' + r"'(\\'|[^'\n])*'|" + r"`(\\`|[^`\n])*`" declkw = r'(?:res|d)[bwdqt]|times' register = (r'r[0-9]+?[bwd]{0,1}|' r'[a-d][lh]|[er]?[a-d]x|[er]?[sbi]p|[er]?[sd]i|[c-gs]s|st[0-7]|' r'mm[0-7]|cr[0-4]|dr[0-367]|tr[3-7]|.mm\d*') wordop = r'seg|wrt|strict' type = r'byte|[dq]?word|ptr' flags = re.IGNORECASE | re.MULTILINE tokens = { 'root': [ include('whitespace'), (r'^\s*%', Comment.Preproc, 'preproc'), (identifier + ':', Name.Label), (r'(%s)(\s+)(equ)' % identifier, bygroups(Name.Constant, Keyword.Declaration, Keyword.Declaration), 'instruction-args'), (declkw, Keyword.Declaration, 'instruction-args'), (identifier, Name.Function, 'instruction-args'), (hexn, Number.Hex), (r'[:]', Text), (r'^->', Error), (r'[\r\n]+', Text) ], 'instruction-args': [ (string, String), (hexn, Number.Hex), (octn, Number.Oct), (binn, Number.Bin), (floatn, Number.Float), (decn, Number.Integer), include('punctuation'), (register, Name.Builtin), (identifier, Name.Variable), (r'[\r\n]+', Text, '#pop'), include('whitespace') ], 'preproc': [ (r'[^;\n]+', Comment.Preproc), (r';.*?\n', Comment.Single, '#pop'), (r'\n', Comment.Preproc, '#pop'), ], 'whitespace': [ (r'\n', Text), (r'[ \t]+', Text), (r';.*', Comment.Single), (r'#.*', Comment.Single) ], 'punctuation': [ (r'[,():\[\]]+', Punctuation), (r'[&|^<>+*/%~-]+', Operator), (r'[$]+', Keyword.Constant), (wordop, Operator.Word), (type, Keyword.Type) ], } class VDBIntelLexer(RegexLexer): """ For Nasm (Intel) disassembly from VDB. Based on the LLDBIntelLexer above. major difference is the raw instruction hex after the instruction address. example: rip 0x000000000056eb4f: 4885ff test rdi,rdi ;0x7f4f8740ca50,0x7f4f8740ca50 0x000000000056eb52: 740f jz 0x0056eb63 """ name = 'VDBIntel' aliases = ['vdb_intel'] filenames = [] mimetypes = [] space = r'[ \t]+' identifier = r'[]*' hexn = r'(?:0[xX][0-9a-f]+|$0[0-9a-f]*|[0-9]+[0-9a-f]*h)' # hex number hexr = r'(?:[0-9a-f]+)' # hex raw (no leader/trailer) octn = r'[0-7]+q' binn = r'[01]+b' decn = r'[0-9]+' floatn = decn + r'\.e?' + decn string = r'"(\\"|[^"\n])*"|' + r"'(\\'|[^'\n])*'|" + r"`(\\`|[^`\n])*`" register = (r'r[0-9]+[bwd]{0,1}|' r'[a-d][lh]|[er]?[a-d]x|[er]?[sbi]p|[er]?[sd]i|[c-gs]s|st[0-7]|' r'mm[0-7]|cr[0-4]|dr[0-367]|tr[3-7]|.mm\d*') wordop = r'seg|wrt|strict' type = r'byte|[dq]?word|ptr' flags = re.IGNORECASE | re.MULTILINE tokens = { 'root': [ (r'^(%s)(%s)(%s)(: )(%s)(%s)' % (register, space, hexn, hexr, space), bygroups(Name.Builtin, Text, Name.Label, Text, Number.Hex, Text), "instruction"), (r'^(%s)(%s)(: )(%s)(%s)' % (space, hexn, hexr, space), bygroups(Text, Name.Label, Text, Number.Hex, Text), "instruction") ], 'instruction': [ (space, Text), (r"(rep[a-z]*)( )", bygroups(Name.Function, Text)), (r"(%s)" % identifier, Name.Function, ("#pop", "instruction-args")), ], 'instruction-args': [ (space, Text), (string, String), (hexn, Number.Hex), (octn, Number.Oct), (binn, Number.Bin), (floatn, Number.Float), (decn, Number.Integer), include('punctuation'), (register, Name.Builtin), (identifier, Name.Variable), (r'[\r\n]+', Text, '#pop'), (r';', Text, ("#pop", 'comment')), ], 'comment': [ (space, Text), (string, Comment.Single), (hexn, Number.Hex), (octn, Number.Oct), (binn, Number.Bin), (floatn, Number.Float), (decn, Number.Integer), include('punctuation'), (register, Name.Builtin), (identifier, Name.Variable), (r'[\r\n]+', Text, '#pop'), ], 'punctuation': [ (r'[,():\[\]]+', Punctuation), (r'[&|^<>+*/%~-]+', Operator), (r'[$]+', Keyword.Constant), (wordop, Operator.Word), (type, Keyword.Type) ], } class WinDbgIntelLexer(VDBIntelLexer): name = 'WinDbgIntel' aliases = ['windbg_intel'] all_lexers = { 'lldb_intel': LLDBIntelLexer, 'gdb_intel': LLDBIntelLexer, 'vdb_intel': VDBIntelLexer, # 'windbg_intel': WinDbgIntelLexer, 'capstone_intel': LLDBIntelLexer } voltron-0.1.4/voltron/main.py000066400000000000000000000036761270717212500162400ustar00rootroot00000000000000import os import argparse import logging import traceback import logging import logging.config import voltron from .view import * from .core import * log = logging.getLogger('main') def main(debugger=None): voltron.setup_logging('main') # Set up command line arg parser parser = argparse.ArgumentParser() parser.register('action', 'parsers', AliasedSubParsersAction) parser.add_argument('--debug', '-d', action='store_true', help='print debug logging') parser.add_argument('-o', action='append', help='override config variable', default=[]) top_level_sp = parser.add_subparsers(title='subcommands', description='valid subcommands', dest='subcommand') top_level_sp.required = True view_parser = top_level_sp.add_parser('view', help='display a view', aliases=('v')) view_parser.register('action', 'parsers', AliasedSubParsersAction) view_sp = view_parser.add_subparsers(title='views', description='valid view types', help='additional help', dest='view') view_sp.required = True # Set up a subcommand for each view class pm = PluginManager() pm.register_plugins() for plugin in pm.view_plugins: pm.view_plugins[plugin].view_class.configure_subparser(view_sp) # Parse args args = parser.parse_args() if args.debug: voltron.config['general']['debug_logging'] = True voltron.setup_logging('main') voltron.config.update(options=dict((tuple(x.split('=')) for x in args.o))) # Instantiate and run the appropriate module inst = args.func(args, loaded_config=voltron.config) inst.pm = pm try: inst.run() except Exception as e: log.exception("Exception running module {}: {}".format(inst.__class__.__name__, traceback.format_exc())) print("Encountered an exception while running the view '{}':\n{}".format(inst.__class__.__name__, traceback.format_exc())) except KeyboardInterrupt: suppress_exit_log = True inst.cleanup() voltron-0.1.4/voltron/plugin.py000066400000000000000000000255601270717212500166060ustar00rootroot00000000000000import logging import inspect import os from collections import defaultdict from scruffy.plugin import Plugin import voltron log = logging.getLogger('plugin') class PluginManager(object): """ Collects and validates API, debugger and view plugins. Provides methods to access and search the plugin collection. Plugin loading itself is handled by scruffy, which is configured in the environment specification in `env.py`. """ def __init__(self): """ Initialise a new PluginManager. """ self._api_plugins = defaultdict(lambda: None) self._debugger_plugins = defaultdict(lambda: None) self._view_plugins = defaultdict(lambda: None) self._web_plugins = defaultdict(lambda: None) self._command_plugins = defaultdict(lambda: None) def register_plugins(self): for p in voltron.env.plugins: self.register_plugin(p) @property def api_plugins(self): return self._api_plugins @property def debugger_plugins(self): return self._debugger_plugins @property def view_plugins(self): return self._view_plugins @property def web_plugins(self): return self._web_plugins @property def command_plugins(self): return self._command_plugins def register_plugin(self, plugin): """ Register a new plugin with the PluginManager. `plugin` is a subclass of scruffy's Plugin class. This is called by __init__(), but may also be called by the debugger host to load a specific plugin at runtime. """ if hasattr(plugin, 'initialise'): plugin.initialise() if self.valid_api_plugin(plugin): log.debug("Registering API plugin: {}".format(plugin)) self._api_plugins[plugin.request] = plugin() elif self.valid_debugger_plugin(plugin): log.debug("Registering debugger plugin: {}".format(plugin)) self._debugger_plugins[plugin.host] = plugin() elif self.valid_view_plugin(plugin): log.debug("Registering view plugin: {}".format(plugin)) self._view_plugins[plugin.name] = plugin() elif self.valid_web_plugin(plugin): log.debug("Registering web plugin: {}".format(plugin)) self._web_plugins[plugin.name] = plugin() elif self.valid_command_plugin(plugin): log.debug("Registering command plugin: {}".format(plugin)) self._command_plugins[plugin.name] = plugin() if voltron.debugger: voltron.debugger.register_command_plugin(plugin.name, plugin.command_class) else: log.debug("Ignoring invalid plugin: {}".format(plugin)) def valid_api_plugin(self, plugin): """ Validate an API plugin, ensuring it is an API plugin and has the necessary fields present. `plugin` is a subclass of scruffy's Plugin class. """ if (issubclass(plugin, APIPlugin) and hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'api' and hasattr(plugin, 'request') and plugin.request != None and hasattr(plugin, 'request_class') and plugin.request_class != None and hasattr(plugin, 'response_class') and plugin.response_class != None): return True return False def valid_debugger_plugin(self, plugin): """ Validate a debugger plugin, ensuring it is a debugger plugin and has the necessary fields present. `plugin` is a subclass of scruffy's Plugin class. """ if (issubclass(plugin, DebuggerAdaptorPlugin) and hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'debugger' and hasattr(plugin, 'host') and plugin.host != None): return True return False def valid_view_plugin(self, plugin): """ Validate a view plugin, ensuring it is a view plugin and has the necessary fields present. `plugin` is a subclass of scruffy's Plugin class. """ if (issubclass(plugin, ViewPlugin) and hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'view' and hasattr(plugin, 'name') and plugin.name != None and hasattr(plugin, 'view_class') and plugin.view_class != None): return True return False def valid_web_plugin(self, plugin): """ Validate a web plugin, ensuring it is a web plugin and has the necessary fields present. `plugin` is a subclass of scruffy's Plugin class. """ if (issubclass(plugin, WebPlugin) and hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'web' and hasattr(plugin, 'name') and plugin.name != None): return True return False def valid_command_plugin(self, plugin): """ Validate a command plugin, ensuring it is a command plugin and has the necessary fields present. `plugin` is a subclass of scruffy's Plugin class. """ if (issubclass(plugin, CommandPlugin) and hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'command' and hasattr(plugin, 'name') and plugin.name != None): return True return False def api_plugin_for_request(self, request=None): """ Find an API plugin that supports the given request type. """ return self.api_plugins[request] def debugger_plugin_for_host(self, host=None): """ Find a debugger plugin that supports the debugger host. """ return self.debugger_plugins[host] def view_plugin_with_name(self, name=None): """ Find a view plugin that for the given view name. """ return self.view_plugins[name] def web_plugin_with_name(self, name=None): """ Find a web plugin that for the given view name. """ return self.web_plugins[name] def command_plugin_with_name(self, name=None): """ Find a command plugin that for the given view name. """ return self.command_plugins[name] class VoltronPlugin(Plugin): @classmethod def initialise(cls): pass class APIPlugin(VoltronPlugin): """ Abstract API plugin class. API plugins subclass this. `plugin_type` is 'api' `request` is the request type (e.g. 'version') `request_class` is the APIRequest subclass (e.g. APIVersionRequest) `response_class` is the APIResponse subclass (e.g. APIVersionResponse) `supported_hosts` is an array of debugger adaptor plugins that this plugin supports (e.g. 'lldb', 'gdb'). There is also a special host type called 'core' If the plugin requires only the 'core' debugger host, it can be used with any debugger adaptor plugin that implements the full interface (ie. the included 'lldb' and 'gdb' plugins). If it requires a specifically named debugger host plugin, then it will only work with those plugins specified. This allows developers to add custom API plugins that communicate directly with their chosen debugger host API, to do things that the standard debugger adaptor plugins don't support. See the core API plugins in voltron/plugins/api/ for examples. """ plugin_type = 'api' request = None request_class = None response_class = None supported_hosts = ['core'] @classmethod def initialise(cls): if cls.request_class: cls.request_class._plugin = cls cls.request_class.request = cls.request class DebuggerAdaptorPlugin(VoltronPlugin): """ Debugger adaptor plugin parent class. `plugin_type` is 'debugger' `host` is the name of the debugger host (e.g. 'lldb' or 'gdb') `adaptor_class` is the debugger adaptor class that can be queried See the core debugger plugins in voltron/plugins/debugger/ for examples. """ plugin_type = 'debugger' host = None adaptor_class = None supported_hosts = ['core'] @classmethod def initialise(cls): if cls.adaptor_class: cls.adaptor_class._plugin = cls class ViewPlugin(VoltronPlugin): """ View plugin parent class. `plugin_type` is 'view' `name` is the name of the view (e.g. 'register' or 'disassembly') `view_class` is the main view class See the core view plugins in voltron/plugins/view/ for examples. """ plugin_type = 'view' name = None view_class = None @classmethod def initialise(cls): if cls.view_class: cls.view_class._plugin = cls cls.view_class.view_type = cls.name class WebPlugin(VoltronPlugin): """ Web plugin parent class. `plugin_type` is 'web' `name` is the name of the web plugin (e.g. 'webview') `app` is a Flask app (or whatever, optional) """ _dir = None plugin_type = 'web' name = None app = None def __init__(self): self._dir = os.path.dirname(inspect.getfile(self.__class__)) class CommandPlugin(VoltronPlugin): """ Command plugin parent class. `plugin_type` is 'command' `name` is the name of the command plugin """ plugin_type = 'command' name = None # # Shared plugin manager and convenience methods # pm = PluginManager() def api_request(request, *args, **kwargs): """ Create an API request. `request_type` is the request type (string). This is used to look up a plugin, whose request class is instantiated and passed the remaining arguments passed to this function. """ plugin = pm.api_plugin_for_request(request) if plugin and plugin.request_class: req = plugin.request_class(*args, **kwargs) else: raise Exception("Invalid request type") return req def api_response(request, *args, **kwargs): plugin = pm.api_plugin_for_request(request) if plugin and plugin.response_class: req = plugin.response_class(*args, **kwargs) else: raise Exception("Invalid request type") return req def debugger_adaptor(host, *args, **kwargs): plugin = pm.debugger_plugin_for_host(host) if plugin and plugin.adaptor_class: adaptor = plugin.adaptor_class(*args, **kwargs) else: raise Exception("Invalid debugger host") return adaptor def view(name, *args, **kwargs): plugin = pm.view_plugin_with_name(name) if plugin and plugin.view_class: view = plugin.view_class(*args, **kwargs) else: raise Exception("Invalid view name") return view def command(name, *args, **kwargs): plugin = pm.command_plugin_with_name(name) if plugin and plugin.command_class: command = plugin.command_class(*args, **kwargs) else: raise Exception("Invalid command name") return command def web_plugins(): return pm.web_plugins voltron-0.1.4/voltron/plugins/000077500000000000000000000000001270717212500164075ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/__init__.py000066400000000000000000000000001270717212500205060ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/api/000077500000000000000000000000001270717212500171605ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/api/__init__.py000066400000000000000000000000001270717212500212570ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/api/backtrace.py000066400000000000000000000025201270717212500214500ustar00rootroot00000000000000import voltron from voltron.api import * import logging import voltron from voltron.api import * from scruffy.plugin import Plugin log = logging.getLogger('api') class APIBacktraceRequest(APIRequest): """ API backtrace request. { "type": "request", "request": "backtrace" } """ @server_side def dispatch(self): try: bt = voltron.debugger.backtrace() res = APIBacktraceResponse(frames=bt) except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception getting backtrace: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APIBacktraceResponse(APISuccessResponse): """ API backtrace response. { "type": "response", "status": "success", "data": { "frames": [ { "index": 0, "addr": 0xffff, "name": "inferior`main + 0" } ] } } """ _fields = {'frames': True} frames = [] class APIBacktracePlugin(APIPlugin): request = "backtrace" request_class = APIBacktraceRequest response_class = APIBacktraceResponse voltron-0.1.4/voltron/plugins/api/breakpoints.py000066400000000000000000000031761270717212500220620ustar00rootroot00000000000000import voltron from voltron.api import * import logging import voltron from voltron.api import * from scruffy.plugin import Plugin log = logging.getLogger('api') class APIBreakpointsRequest(APIRequest): """ API breakpoints request. { "type": "request", "request": "breakpoints" } """ @server_side def dispatch(self): try: bps = voltron.debugger.breakpoints() res = APIBreakpointsResponse(breakpoints=bps) except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception getting breakpoints: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APIBreakpointsResponse(APISuccessResponse): """ API breakpoints response. { "type": "response", "status": "success", "data": { "breakpoints": [ { "id": 1, "enabled": True, "one_shot": False, "hit_count": 5, "locations": [ { "address": 0x100000cf0, "name": 'main' } ] } ] } } """ _fields = {'breakpoints': True} breakpoints = [] class APIBreakpointsPlugin(APIPlugin): request = "breakpoints" request_class = APIBreakpointsRequest response_class = APIBreakpointsResponse voltron-0.1.4/voltron/plugins/api/command.py000066400000000000000000000024211270717212500211470ustar00rootroot00000000000000import logging import voltron from voltron.api import * from scruffy.plugin import Plugin log = logging.getLogger('api') class APICommandRequest(APIRequest): """ API execute command request. { "type": "request", "request": "command" "data": { "command": "break list" } } """ _fields = {'command': True} @server_side def dispatch(self): try: output = voltron.debugger.command(self.command) res = APICommandResponse() res.output = output except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception executing debugger command: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APICommandResponse(APISuccessResponse): """ API list targets response. { "type": "response", "status": "success", "data": { "output": "stuff" } } """ _fields = {'output': True} output = None class APICommandPlugin(APIPlugin): request = "command" request_class = APICommandRequest response_class = APICommandResponse voltron-0.1.4/voltron/plugins/api/dereference.py000066400000000000000000000025621270717212500220060ustar00rootroot00000000000000import logging import voltron from voltron.api import * from scruffy.plugin import Plugin log = logging.getLogger('api') class APIDerefRequest(APIRequest): """ API dereference pointer request. { "type": "request", "request": "dereference" "data": { "pointer": 0xffffff8012341234 } } """ _fields = {'pointer': True} @server_side def dispatch(self): try: output = voltron.debugger.dereference(self.pointer) log.debug('output: {}'.format(str(output))) res = APIDerefResponse() res.output = output except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception dereferencing pointer: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APIDerefResponse(APISuccessResponse): """ API dereference pointer response. { "type": "response", "status": "success", "data": { "output": [0xffffff8055555555, "main + 0x123"] } } """ _fields = {'output': True} output = None class APIDerefPlugin(APIPlugin): request = "dereference" request_class = APIDerefRequest response_class = APIDerefResponse voltron-0.1.4/voltron/plugins/api/disassemble.py000066400000000000000000000052331270717212500220300ustar00rootroot00000000000000import voltron import logging from voltron.api import * from voltron.plugin import * log = logging.getLogger('api') class APIDisassembleRequest(APIRequest): """ API disassemble request. { "type": "request", "request": "disassemble" "data": { "target_id": 0, "address": 0x12341234, "count": 16, "use_capstone": False } } `target_id` is optional. `address` is the address at which to start disassembling. Defaults to instruction pointer if not specified. `count` is the number of instructions to disassemble. `use_capstone` a flag to indicate whether or not Capstone should be used instead of the debugger's disassembler. """ _fields = {'target_id': False, 'address': False, 'count': True, 'use_capstone': False} target_id = 0 address = None count = 16 @server_side def dispatch(self): try: if self.address == None: pc_name, self.address = voltron.debugger.program_counter(target_id=self.target_id) if self.use_capstone: disasm = voltron.debugger.disassemble_capstone(target_id=self.target_id, address=self.address, count=self.count) else: disasm = voltron.debugger.disassemble(target_id=self.target_id, address=self.address, count=self.count) res = APIDisassembleResponse() res.disassembly = disasm try: res.flavor = voltron.debugger.disassembly_flavor() except: res.flavor = 'NA' res.host = voltron.debugger._plugin.host except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except TargetBusyException: res = APITargetBusyErrorResponse() except Exception as e: msg = "Unhandled exception {} disassembling: {}".format(type(e), e) log.exception(msg) res = APIErrorResponse(code=0, message=msg) return res class APIDisassembleResponse(APISuccessResponse): """ API disassemble response. { "type": "response", "status": "success", "data": { "disassembly": "mov blah blah" } } """ _fields = {'disassembly': True, 'formatted': False, 'flavor': False, 'host': False} disassembly = None formatted = None flavor = None host = None class APIDisassemblePlugin(APIPlugin): request = 'disassemble' request_class = APIDisassembleRequest response_class = APIDisassembleResponse voltron-0.1.4/voltron/plugins/api/memory.py000066400000000000000000000103761270717212500210510ustar00rootroot00000000000000import voltron import logging import base64 import six from voltron.api import * log = logging.getLogger('api') class APIMemoryRequest(APIRequest): """ API read memory request. { "type": "request", "request": "memory", "data": { "target_id":0, "address": 0x12341234, "length": 0x40 } } `target_id` is optional. If not present, the currently selected target will be used. `address` is the address at which to start reading. `length` is the number of bytes to read. `register` is the register containing the address at which to start reading. `command` is the debugger command to execute to calculate the address at which to start reading. `deref` is a flag indicating whether or not to dereference any pointers within the memory region read. """ _fields = { 'target_id': False, 'address': False, 'length': False, 'words': False, 'register': False, 'command': False, 'deref': False } target_id = 0 @server_side def dispatch(self): try: target = voltron.debugger.target(self.target_id) # if 'words' was specified, get the addr_size and calculate the length to read if self.words: self.length = self.words * target['addr_size'] # calculate the address at which to begin reading if self.address: addr = self.address elif self.command: output = voltron.debugger.command(self.args.command) if output: for item in reversed(output.split()): log.debug("checking item: {}".format(item)) try: addr = int(item) break except: try: addr = int(item, 16) break except: pass elif self.register: regs = voltron.debugger.registers(registers=[self.register]) addr = list(regs.values())[0] # read memory memory = voltron.debugger.memory(address=int(addr), length=int(self.length), target_id=int(self.target_id)) # deref pointers deref = None if self.deref: fmt = ('<' if target['byte_order'] == 'little' else '>') + {2: 'H', 4: 'L', 8: 'Q'}[target['addr_size']] deref = [] for chunk in zip(*[six.iterbytes(memory)] * target['addr_size']): chunk = ''.join([six.unichr(x) for x in chunk]).encode('latin1') p = list(struct.unpack(fmt, chunk))[0] if p > 0: try: deref.append(voltron.debugger.dereference(pointer=p)) except: deref.append([]) else: deref.append([]) res = APIMemoryResponse() res.address = addr res.memory = six.u(memory) res.bytes = len(memory) res.deref = deref except TargetBusyException: res = APITargetBusyErrorResponse() except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception getting memory from debugger: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APIMemoryResponse(APISuccessResponse): """ API read memory response. { "type": "response", "status": "success", "data": { "memory": "ABCDEF" # base64 encoded memory } } """ _fields = { 'address': True, 'memory': True, 'bytes': True, 'deref': False } _encode_fields = ['memory'] address = None memory = None bytes = None deref = None class APIReadMemoryPlugin(APIPlugin): request = 'memory' request_class = APIMemoryRequest response_class = APIMemoryResponse voltron-0.1.4/voltron/plugins/api/plugins.py000066400000000000000000000031111270717212500212070ustar00rootroot00000000000000import voltron import voltron.api from voltron.api import * from scruffy.plugin import Plugin class APIPluginsRequest(APIRequest): """ API plugins request. { "type": "request", "request": "plugins" } """ @server_side def dispatch(self): res = APIPluginsResponse() return res class APIPluginsResponse(APISuccessResponse): """ API plugins response. { "type": "response", "status": "success", "data": { "plugins": { "api": { "version": ["api_version", "host_version", "capabilities"] ... }, "debugger": { ... }, ... } } } """ _fields = { 'plugins': True } def __init__(self, *args, **kwargs): super(APIPluginsResponse, self).__init__(*args, **kwargs) self.plugins = { 'api': {n: {'request': p.request_class._fields, 'response': p.response_class._fields} for (n, p) in voltron.plugin.pm.api_plugins.iteritems()}, 'debugger': [n for n in voltron.plugin.pm.debugger_plugins], 'view': [n for n in voltron.plugin.pm.view_plugins], 'command': [n for n in voltron.plugin.pm.command_plugins], 'web': [n for n in voltron.plugin.pm.web_plugins], } class APIPluginsPlugin(APIPlugin): request = 'plugins' request_class = APIPluginsRequest response_class = APIPluginsResponse voltron-0.1.4/voltron/plugins/api/registers.py000066400000000000000000000033761270717212500215520ustar00rootroot00000000000000import voltron import logging from voltron.api import * log = logging.getLogger('api') class APIRegistersRequest(APIRequest): """ API state request. { "type": "request", "request": "registers", "data": { "target_id": 0, "thread_id": 123456, "registers": ['rsp'] } } `target_id` and `thread_id` are optional. If not present, the currently selected target and thread will be used. `registers` is optional. If it is not included all registers will be returned. """ _fields = {'target_id': False, 'thread_id': False, 'registers': False} target_id = 0 thread_id = None registers = [] @server_side def dispatch(self): try: regs = voltron.debugger.registers(target_id=self.target_id, thread_id=self.thread_id, registers=self.registers) res = APIRegistersResponse() res.registers = regs except TargetBusyException: res = APITargetBusyErrorResponse() except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception getting registers from debugger: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APIRegistersResponse(APISuccessResponse): """ API status response. { "type": "response", "status": "success", "data": { "registers": { "rip": 0x12341234, ... } } } """ _fields = {'registers': True} class APIRegistersPlugin(APIPlugin): request = 'registers' request_class = APIRegistersRequest response_class = APIRegistersResponse voltron-0.1.4/voltron/plugins/api/stack.py000066400000000000000000000036401270717212500206420ustar00rootroot00000000000000import voltron import logging import base64 from voltron.api import * log = logging.getLogger('api') class APIStackRequest(APIRequest): """ API read stack request. { "type": "request", "request": "stack" "data": { "target_id": 0, "thread_id": 123456, "length": 0x40 } } `target_id` and `thread_id` are optional. If not present, the currently selected target and thread will be used. `length` is the number of bytes to read. """ _fields = {'target_id': False, 'thread_id': False, 'length': True} target_id = 0 thread_id = None length = None @server_side def dispatch(self): try: sp_name, sp = voltron.debugger.stack_pointer(target_id=self.target_id) memory = voltron.debugger.stack(self.length, target_id=self.target_id) res = APIStackResponse() res.memory = memory res.stack_pointer = sp except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except TargetBusyException: res = APITargetBusyErrorResponse() except Exception as e: msg = "Unhandled exception {} reading stack: {}".format(type(e), e) log.exception(msg) res = APIErrorResponse(code=0, message=msg) return res class APIStackResponse(APISuccessResponse): """ API read stack response. { "type": "response", "status": "success", "data": { "memory": "\xff...", "stack_pointer": 0x12341234 } } """ _fields = {'memory': True, 'stack_pointer': True} _encode_fields = ['memory'] memory = None stack_pointer = None class APIStackPlugin(APIPlugin): request = 'stack' request_class = APIStackRequest response_class = APIStackResponse voltron-0.1.4/voltron/plugins/api/state.py000066400000000000000000000022331270717212500206520ustar00rootroot00000000000000import voltron import logging from voltron.api import * log = logging.getLogger('api') class APIStateRequest(APIRequest): """ API state request. { "type": "request", "request": "state", "data": { "target_id": 0 } } """ _fields = {'target_id': False} target_id = 0 @server_side def dispatch(self): try: state = voltron.debugger.state(target_id=self.target_id) log.debug("Got state from debugger: {}".format(state)) res = APIStateResponse() res.state = state except TargetBusyException: res = APITargetBusyErrorResponse() except NoSuchTargetException: res = APINoSuchTargetErrorResponse() return res class APIStateResponse(APISuccessResponse): """ API status response. { "type": "response", "data": { "state": "stopped" } } """ _fields = {'state': True} state = None class APIStatePlugin(APIPlugin): request = 'state' request_class = APIStateRequest response_class = APIStateResponse voltron-0.1.4/voltron/plugins/api/targets.py000066400000000000000000000026341270717212500212100ustar00rootroot00000000000000import logging import voltron from voltron.api import * from scruffy.plugin import Plugin log = logging.getLogger('api') class APITargetsRequest(APIRequest): """ API list targets request. { "type": "request", "request": "targets" } """ _fields = {} @server_side def dispatch(self): try: res = APITargetsResponse() res.targets = voltron.debugger.targets() except NoSuchTargetException: res = APINoSuchTargetErrorResponse() except Exception as e: msg = "Exception getting targets from debugger: {}".format(repr(e)) log.exception(msg) res = APIGenericErrorResponse(msg) return res class APITargetsResponse(APISuccessResponse): """ API list targets response. { "type": "response", "status": "success", "data": { "targets": [{ "id": 0, # ID that can be used in other funcs "file": "/bin/ls", # target's binary file "arch": "x86_64", # target's architecture "state: "stopped" # state }] } } """ _fields = {'targets': True} targets = [] class APITargetsPlugin(APIPlugin): request = 'targets' request_class = APITargetsRequest response_class = APITargetsResponse voltron-0.1.4/voltron/plugins/api/version.py000066400000000000000000000022011270717212500212120ustar00rootroot00000000000000import voltron import voltron.api from voltron.api import * from scruffy.plugin import Plugin class APIVersionRequest(APIRequest): """ API version request. { "type": "request", "request": "version" } """ @server_side def dispatch(self): res = APIVersionResponse() res.api_version = voltron.api.version res.host_version = voltron.debugger.version() res.capabilities = voltron.debugger.capabilities() return res class APIVersionResponse(APISuccessResponse): """ API version response. { "type": "response", "status": "success", "data": { "api_version": 1.0, "host_version": 'lldb-something', "capabilities": ["async"] } } """ _fields = { 'api_version': True, 'host_version': True, 'capabilities': False } api_version = None host_version = None capabilities = None class APIVersionPlugin(APIPlugin): request = 'version' request_class = APIVersionRequest response_class = APIVersionResponse voltron-0.1.4/voltron/plugins/debugger/000077500000000000000000000000001270717212500201735ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/debugger/__init__.py000066400000000000000000000000001270717212500222720ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/debugger/dbg_gdb.py000066400000000000000000000550031270717212500221200ustar00rootroot00000000000000from __future__ import print_function import logging import threading import re import struct import six from voltron.api import * from voltron.plugin import * from voltron.dbg import * try: import gdb HAVE_GDB = True except ImportError: HAVE_GDB = False log = logging.getLogger('debugger') if HAVE_GDB: class GDBAdaptor(DebuggerAdaptor): archs = { 'i386': 'x86', 'i386:intel': 'x86', 'i386:x64-32': 'x86', 'i386:x64-32:intel': 'x86', 'i8086': 'x86', 'i386:x86-64': 'x86_64', 'i386:x86-64:intel': 'x86_64', 'arm': 'arm', 'armv2': 'arm', 'armv2a': 'arm', 'armv3': 'arm', 'armv3m': 'arm', 'armv4': 'arm', 'armv4t': 'arm', 'armv5': 'arm', 'armv5t': 'arm', 'armv5te': 'arm', 'powerpc:common': 'powerpc' } sizes = { 'x86': 4, 'x86_64': 8, 'arm': 4, 'powerpc': 4, } max_frame = 64 max_string = 128 """ The interface with an instance of GDB """ def __init__(self, *args, **kwargs): self.listeners = [] self.host_lock = threading.RLock() self.host = gdb def version(self): """ Get the debugger's version. Returns a string containing the debugger's version (e.g. 'GNU gdb (GDB) 7.8') """ output = gdb.execute('show version', to_string=True) try: version = output.split('\n')[0] except: version = None return version def _target(self, target_id=0): """ Return information about the specified target. Returns data in the following structure: { "id": 0, # ID that can be used in other funcs "file": "/bin/ls", # target's binary file "arch": "x86_64", # target's architecture "state: "stopped" # state } """ # get target target = gdb.selected_inferior() # get target properties d = {} d["id"] = 0 d["num"] = target.num # get target state d["state"] = self._state() # get inferior file (doesn't seem to be available through the API) lines = list(filter(lambda x: x != '', gdb.execute('info inferiors', to_string=True).split('\n'))) if len(lines) > 1: info = list(filter(lambda x: '*' in x[0], map(lambda x: x.split(), lines[1:]))) d["file"] = info[0][-1] else: log.debug("No inferiors in `info inferiors`") raise NoSuchTargetException() # get arch d["arch"] = self.get_arch() d['byte_order'] = self.get_byte_order() d['addr_size'] = self.get_addr_size() return d @lock_host def target(self, target_id=0): """ Return information about the current inferior. GDB only supports querying the currently selected inferior, rather than an arbitrary target like LLDB, because the API kinda sucks. `target_id` is ignored. """ return self._target() @lock_host def targets(self, target_ids=None): """ Return information about the debugger's current targets. `target_ids` is ignored. Only the current target is returned. This method is only implemented to maintain API compatibility with the LLDBAdaptor. """ return [self._target()] @validate_target @lock_host def state(self, target_id=0): """ Get the state of a given target. """ return self._state() @validate_busy @validate_target @lock_host def registers(self, target_id=0, thread_id=None, registers=[]): """ Get the register values for a given target/thread. """ arch = self.get_arch() # if we got 'sp' or 'pc' in registers, change it to whatever the right name is for the current arch if arch in self.reg_names: if 'pc' in registers: registers.remove('pc') registers.append(self.reg_names[arch]['pc']) if 'sp' in registers: registers.remove('sp') registers.append(self.reg_names[arch]['sp']) else: raise Exception("Unsupported architecture: {}".format(target['arch'])) # get registers if registers != []: regs = {} for reg in registers: regs[reg] = self.get_register(reg) else: log.debug('Getting registers for arch {}'.format(arch)) if arch == "x86_64": regs = self.get_registers_x86_64() elif arch == "x86": regs = self.get_registers_x86() elif arch == "arm": regs = self.get_registers_arm() elif arch == "powerpc": regs = self.get_registers_powerpc() else: raise UnknownArchitectureException() return regs @validate_busy @validate_target @lock_host def stack_pointer(self, target_id=0, thread_id=None): """ Get the value of the stack pointer register. """ arch = self.get_arch() if arch in self.reg_names: sp_name = self.reg_names[arch]['sp'] sp = self.get_register(sp_name) else: raise UnknownArchitectureException() return sp_name, sp @validate_busy @validate_target @lock_host def program_counter(self, target_id=0, thread_id=None): """ Get the value of the program counter register. """ arch = self.get_arch() if arch in self.reg_names: pc_name = self.reg_names[arch]['pc'] pc = self.get_register(pc_name) else: raise UnknownArchitectureException() return pc_name, pc @validate_busy @validate_target @lock_host def memory(self, address, length, target_id=0): """ Get the register values for . `address` is the address at which to start reading `length` is the number of bytes to read """ # read memory log.debug('Reading 0x{:x} bytes of memory at 0x{:x}'.format(length, address)) memory = bytes(gdb.selected_inferior().read_memory(address, length)) return memory @validate_busy @validate_target @lock_host def stack(self, length, target_id=0, thread_id=None): """ Get the register values for . `length` is the number of bytes to read `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get the stack pointer sp_name, sp = self.stack_pointer(target_id=target_id, thread_id=thread_id) # read memory memory = self.memory(sp, length, target_id=target_id) return memory @validate_busy @validate_target @lock_host def disassemble(self, target_id=0, address=None, count=16): """ Get a disassembly of the instructions at the given address. `address` is the address at which to disassemble. If None, the current program counter is used. `count` is the number of instructions to disassemble. """ # make sure we have an address if address == None: pc_name, address = self.program_counter(target_id=target_id) # disassemble output = gdb.execute('x/{}i 0x{:x}'.format(count, address), to_string=True) return output @validate_busy @validate_target @lock_host def dereference(self, pointer, target_id=0): """ Recursively dereference a pointer for display """ fmt = ('<' if self.get_byte_order() == 'little' else '>') + {2: 'H', 4: 'L', 8: 'Q'}[self.get_addr_size()] addr = pointer chain = [] # recursively dereference while True: try: mem = gdb.selected_inferior().read_memory(addr, self.get_addr_size()) # log.debug("read mem: {}".format(mem)) (ptr,) = struct.unpack(fmt, mem) if ptr in chain: break chain.append(('pointer', addr)) addr = ptr except gdb.MemoryError: log.exception("Dereferencing pointer 0x{:X}".format(addr)) break # get some info for the last pointer # first try to resolve a symbol context for the address if len(chain): p, addr = chain[-1] output = gdb.execute('info symbol 0x{:x}'.format(addr), to_string=True) log.debug('output = {}'.format(output)) if 'No symbol matches' not in output: chain.append(('symbol', output.strip())) log.debug("symbol context: {}".format(str(chain[-1]))) else: log.debug("no symbol context, trying as a string") mem = gdb.selected_inferior().read_memory(addr, 2) if ord(mem[0]) <= 127 and ord(mem[0]) != 0: a = [] for i in range(0, self.max_string): mem = gdb.selected_inferior().read_memory(addr + i, 1) if ord(mem[0]) == 0 or ord(mem[0]) > 127: break if isinstance(mem, memoryview): a.append(mem.tobytes().decode('latin1')) else: a.append(str(mem)) chain.append(('string', ''.join(a))) log.debug("chain: {}".format(chain)) return chain @lock_host def command(self, command=None): """ Execute a command in the debugger. `command` is the command string to execute. """ if command: res = gdb.execute(command, to_string=True) else: raise Exception("No command specified") return res @lock_host def disassembly_flavor(self): """ Return the disassembly flavor setting for the debugger. Returns 'intel' or 'att' """ flavor = re.search('flavor is "(.*)"', gdb.execute("show disassembly-flavor", to_string=True)).group(1) return flavor @lock_host def breakpoints(self, target_id=0): """ Return a list of breakpoints. Returns data in the following structure: [ { "id": 1, "enabled": True, "one_shot": False, "hit_count": 5, "locations": [ { "address": 0x100000cf0, "name": 'main' } ] } ] """ breakpoints = [] # hahahahaha GDB sucks so much for b in gdb.breakpoints(): try: if b.location.startswith('*'): addr = int(b.location[1:], 16) else: output = gdb.execute('info addr {}'.format(b.location), to_string=True) m = re.match('.*is at ([^ ]*) .*', output) if not m: m = re.match('.*at address ([^ ]*)\..*', output) if m: addr = int(m.group(1), 16) else: addr = 0 except: addr = 0 breakpoints.append({ 'id': b.number, 'enabled': b.enabled, 'one_shot': b.temporary, 'hit_count': b.hit_count, 'locations': [{ "address": addr, "name": b.location }] }) return breakpoints @lock_host def backtrace(self, target_id=0, thread_id=None): """ Return a list of stack frames. """ frames = [] f = gdb.newest_frame() for i in range(self.max_frame): if not f: break frames.append({'index': i, 'addr': f.pc(), 'name': f.name()}) f = f.older() return frames def capabilities(self): """ Return a list of the debugger's capabilities. Thus far only the 'async' capability is supported. This indicates that the debugger host can be queried from a background thread, and that views can use non-blocking API requests without queueing requests to be dispatched next time the debugger stops. """ return [] # # Private functions # def _state(self): """ Get the state of a given target. Internal use. """ target = gdb.selected_inferior() if target.is_valid(): try: output = gdb.execute('info program', to_string=True) if "not being run" in output: state = "invalid" elif "stopped" in output: state = "stopped" except gdb.error as e: if 'Selected thread is running.' == str(e): state = "running" else: state = "invalid" return state def get_register(self, reg_name): arch = self.get_arch() if arch == "x86_64": reg = self.get_register_x86_64(reg_name) elif arch == "x86": reg = self.get_register_x86(reg_name) elif arch == "arm": reg = self.get_register_arm(reg_name) elif arch == "powerpc": reg = self.get_register_powerpc(reg_name) else: raise UnknownArchitectureException() return reg def get_registers_x86_64(self): # Get regular 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] = self.get_register_x86_64(reg) except: log.debug('Failed getting reg: ' + reg) vals[reg] = 'N/A' # Get flags try: vals['rflags'] = int(gdb.execute('info reg $eflags', to_string=True).split()[1], 16) except: log.debug('Failed getting reg: eflags') vals['rflags'] = 'N/A' # Get SSE registers try: sse = self.get_registers_sse(16) vals = dict(list(vals.items()) + list(sse.items())) except gdb.error: log.exception("Failed to get SSE registers") # Get FPU registers try: fpu = self.get_registers_fpu() vals = dict(list(vals.items()) + list(fpu.items())) except gdb.error: log.exception("Failed to get FPU registers") return vals def get_register_x86_64(self, reg): return int(gdb.parse_and_eval('(long long)$'+reg)) & 0xFFFFFFFFFFFFFFFF def get_registers_x86(self): # Get regular registers regs = ['eax','ebx','ecx','edx','ebp','esp','edi','esi','eip','cs','ds','es','fs','gs','ss'] vals = {} for reg in regs: try: vals[reg] = self.get_register_x86(reg) except: log.debug('Failed getting reg: ' + reg) vals[reg] = 'N/A' # Get flags try: vals['eflags'] = int(gdb.execute('info reg $eflags', to_string=True).split()[1], 16) except: log.debug('Failed getting reg: eflags') vals['eflags'] = 'N/A' # Get SSE registers try: sse = self.get_registers_sse(8) vals = dict(list(vals.items()) + list(sse.items())) except gdb.error: log.exception("Failed to get SSE registers") # Get FPU registers try: fpu = self.get_registers_fpu() vals = dict(list(vals.items()) + list(fpu.items())) except gdb.error: log.exception("Failed to get SSE registers") return vals def get_register_x86(self, reg): log.debug('Getting register: ' + reg) return int(gdb.parse_and_eval('(long)$'+reg)) & 0xFFFFFFFF def get_registers_sse(self, num=8): # the old way of doing this randomly crashed gdb or threw a python exception regs = {} for line in gdb.execute('info all-registers', to_string=True).split('\n'): m = re.match('^([xyz]mm\d+)\s.*uint128 = (0x[0-9a-f]+)\}', line) if m: regs[m.group(1)] = int(m.group(2), 16) return regs def get_registers_fpu(self): regs = {} for i in range(8): reg = 'st'+str(i) try: regs[reg] = int(gdb.execute('info reg '+reg, to_string=True).split()[-1][2:-1], 16) except: log.debug('Failed getting reg: ' + reg) regs[reg] = 'N/A' return regs def get_registers_arm(self): log.debug('Getting registers') regs = ['pc','sp','lr','cpsr','r0','r1','r2','r3','r4','r5','r6', 'r7','r8','r9','r10','r11','r12'] vals = {} for reg in regs: try: vals[reg] = self.get_register_arm(reg) except: log.debug('Failed getting reg: ' + reg) vals[reg] = 'N/A' return vals def get_register_arm(self, reg): log.debug('Getting register: ' + reg) return int(gdb.parse_and_eval('(long)$'+reg)) & 0xFFFFFFFF def get_registers_powerpc(self): log.debug('Getting registers') # TODO This could ideally pull from a single definition for the arch regs = ['pc','msr','cr','lr', 'ctr', 'r0','r1','r2','r3','r4','r5','r6', 'r7', 'r8','r9','r10','r11','r12','r13','r14', 'r15', 'r16','r17','r18','r19','r20','r21','r22', 'r23', 'r24','r25','r26','r27','r28','r29','r30', 'r31'] vals = {} for reg in regs: try: vals[reg] = self.get_register_powerpc(reg) except: log.debug('Failed getting reg: ' + reg) vals[reg] = 'N/A' return vals def get_register_powerpc(self, reg): log.debug('Getting register: ' + reg) return int(gdb.parse_and_eval('(long)$'+reg)) & 0xFFFFFFFF def get_next_instruction(self): return self.get_disasm().split('\n')[0].split(':')[1].strip() def get_arch(self): try: arch = gdb.selected_frame().architecture().name() except: arch = re.search('\(currently (.*)\)', gdb.execute('show architecture', to_string=True)).group(1) return self.archs[arch] def get_addr_size(self): arch = self.get_arch() return self.sizes[arch] def get_byte_order(self): return 'little' if 'little' in gdb.execute('show endian', to_string=True) else 'big' class GDBCommand(DebuggerCommand, gdb.Command): """ Debugger command class for GDB """ def __init__(self): super(GDBCommand, self).__init__("voltron", gdb.COMMAND_NONE, gdb.COMPLETE_NONE) self.adaptor = voltron.debugger self.registered = False self.register_hooks() def invoke(self, arg, from_tty): self.handle_command(arg) def register_hooks(self): if not self.registered: gdb.events.stop.connect(self.stop_handler) gdb.events.exited.connect(self.stop_and_exit_handler) gdb.events.cont.connect(self.cont_handler) def unregister_hooks(self): if self.registered: gdb.events.stop.disconnect(self.stop_handler) gdb.events.exited.disconnect(self.stop_and_exit_handler) gdb.events.cont.disconnect(self.cont_handler) self.registered = False def stop_handler(self, event): self.adaptor.update_state() voltron.server.dispatch_queue() log.debug('Inferior stopped') def exit_handler(self, event): log.debug('Inferior exited') voltron.server.stop() def stop_and_exit_handler(self, event): log.debug('Inferior stopped and exited') self.stop_handler(event) self.exit_handler(event) def cont_handler(self, event): log.debug('Inferior continued') if not voltron.server.is_running: voltron.server.start() class GDBAdaptorPlugin(DebuggerAdaptorPlugin): host = 'gdb' adaptor_class = GDBAdaptor command_class = GDBCommand voltron-0.1.4/voltron/plugins/debugger/dbg_lldb.py000066400000000000000000000502571270717212500223070ustar00rootroot00000000000000from __future__ import print_function import struct import logging import threading import codecs from collections import namedtuple from voltron.api import * from voltron.plugin import * from voltron.dbg import * try: import lldb HAVE_LLDB = True except ImportError: HAVE_LLDB = False log = logging.getLogger('debugger') MAX_DEREF = 16 if HAVE_LLDB: class LLDBAdaptor(DebuggerAdaptor): """ The interface with an instance of LLDB """ def __init__(self, host=None): self.listeners = [] self.host_lock = threading.RLock() if host: log.debug("Passed a debugger host") self.host = host elif lldb.debugger: log.debug("lldb.debugger is valid - probably running inside LLDB") self.host = lldb.debugger else: log.debug("No debugger host found - creating one") self.host = lldb.SBDebugger.Create() self.host.SetAsync(False) @property def host(self): """ Get the debugger host object that this adaptor talks to. Used by custom API plugins to talk directly to the debugger. """ return self._host @host.setter def host(self, value): self._host = value def normalize_triple(self, triple): """ Returns a (cpu, platform, abi) triple Returns None for any fields that can't be elided """ s = triple.split("-") arch, platform, abi = s[0], s[1], '-'.join(s[2:]) if arch == "x86_64h": arch = "x86_64" return (arch, platform, abi) def version(self): """ Get the debugger's version. Returns a string containing the debugger's version (e.g. 'lldb-310.2.37') """ return self.host.GetVersionString() def _target(self, target_id=0): """ Return information about the specified target. Returns data in the following structure: { "id": 0, # ID that can be used in other funcs "file": "/bin/ls", # target's binary file "arch": "x86_64", # target's architecture "state: "stopped" # state } """ # get target t = self.host.GetTargetAtIndex(target_id) # get target properties d = {} d["id"] = target_id d["state"] = self.host.StateAsCString(t.process.GetState()) d["file"] = t.GetExecutable().fullpath try: d["arch"], _, _ = self.normalize_triple(t.triple) except: d["arch"] = None if d["arch"] == 'i386': d["arch"] = 'x86' d["byte_order"] = 'little' if t.byte_order == lldb.eByteOrderLittle else 'big' d["addr_size"] = t.addr_size return d @lock_host def target(self, target_id=0): """ Return information about the specified target. """ return self._target(target_id=target_id) @lock_host def targets(self, target_ids=None): """ Return information about the debugger's current targets. `target_ids` is an array of target IDs (or None for all targets) Returns data in the following structure: [ { "id": 0, # ID that can be used in other funcs "file": "/bin/ls", # target's binary file "arch": "x86_64", # target's architecture "state: "stopped" # state } ] """ # initialise returned data targets = [] # if we didn't get any target IDs, get info for all targets if not target_ids: n = self.host.GetNumTargets() target_ids = range(n) # iterate through targets log.debug("Getting info for {} targets".format(len(target_ids))) for i in target_ids: targets.append(self._target(i)) return targets @validate_target @lock_host def state(self, target_id=0): """ Get the state of a given target. `target_id` is a target ID (or None for the first target) """ target = self.host.GetTargetAtIndex(target_id) state = self.host.StateAsCString(target.process.GetState()) return state @validate_busy @validate_target @lock_host def registers(self, target_id=0, thread_id=None, registers=[]): """ Get the register values for a given target/thread. `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get the target target = self.host.GetTargetAtIndex(target_id) t_info = self._target(target_id) # get the thread if not thread_id: thread_id = target.process.selected_thread.id try: thread = target.process.GetThreadByID(thread_id) except: raise NoSuchThreadException() # if we got 'sp' or 'pc' in registers, change it to whatever the right name is for the current arch if t_info['arch'] in self.reg_names: if 'pc' in registers: registers.remove('pc') registers.append(self.reg_names[t_info['arch']]['pc']) if 'sp' in registers: registers.remove('sp') registers.append(self.reg_names[t_info['arch']]['sp']) else: raise Exception("Unsupported architecture: {}".format(t_info['arch'])) # get the registers regs = thread.GetFrameAtIndex(0).GetRegisters() # extract the actual register values objs = [] for i in xrange(len(regs)): objs += regs[i] regs = {} for reg in objs: val = 'n/a' if reg.value is not None: try: val = reg.GetValueAsUnsigned() except: reg = None elif reg.num_children > 0: try: children = [] for i in xrange(reg.GetNumChildren()): children.append(int(reg.GetChildAtIndex(i, lldb.eNoDynamicValues, True).value, 16)) if t_info['byte_order'] == 'big': children = list(reversed(children)) val = int(codecs.encode(struct.pack('{}B'.format(len(children)), *children), 'hex'), 16) except: pass if registers == [] or reg.name in registers: regs[reg.name] = val return regs @validate_busy @validate_target @lock_host def stack_pointer(self, target_id=0, thread_id=None): """ Get the value of the stack pointer register. `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get registers and targets regs = self.registers(target_id=target_id, thread_id=thread_id) target = self._target(target_id=target_id) # get stack pointer register if target['arch'] in self.reg_names: sp_name = self.reg_names[target['arch']]['sp'] sp = regs[sp_name] else: raise Exception("Unsupported architecture: {}".format(target['arch'])) return (sp_name, sp) @validate_busy @validate_target @lock_host def program_counter(self, target_id=0, thread_id=None): """ Get the value of the program counter register. `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get registers and targets regs = self.registers(target_id=target_id, thread_id=thread_id) target = self._target(target_id=target_id) # get stack pointer register if target['arch'] in self.reg_names: pc_name = self.reg_names[target['arch']]['pc'] pc = regs[pc_name] else: raise Exception("Unsupported architecture: {}".format(target['arch'])) return (pc_name, pc) @validate_busy @validate_target @lock_host def memory(self, address, length, target_id=0): """ Get the register values for . `address` is the address at which to start reading `length` is the number of bytes to read `target_id` is a target ID (or None for the first target) """ # get the target target = self.host.GetTargetAtIndex(target_id) # read memory log.debug('Reading 0x{:x} bytes of memory at 0x{:x}'.format(length, address)) error = lldb.SBError() memory = target.process.ReadMemory(address, length, error) if not error.Success(): raise Exception("Failed reading memory: {}".format(error.GetCString())) return memory @validate_busy @validate_target @lock_host def stack(self, length, target_id=0, thread_id=None): """ Get the register values for . `length` is the number of bytes to read `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get the stack pointer sp_name, sp = self.stack_pointer(target_id=target_id, thread_id=thread_id) # read memory memory = self.memory(sp, length, target_id=target_id) return memory @validate_busy @validate_target @lock_host def disassemble(self, target_id=0, address=None, count=None): """ Get a disassembly of the instructions at the given address. `address` is the address at which to disassemble. If None, the current program counter is used. `count` is the number of instructions to disassemble. """ # make sure we have an address if address == None: pc_name, address = self.program_counter(target_id=target_id) # disassemble res = lldb.SBCommandReturnObject() output = self.command('disassemble -s {} -c {}'.format(address, count)) return output @validate_busy @validate_target @lock_host def dereference(self, pointer, target_id=0): """ Recursively dereference a pointer for display """ t = self.host.GetTargetAtIndex(target_id) error = lldb.SBError() addr = pointer chain = [] # recursively dereference for i in range(0, MAX_DEREF): ptr = t.process.ReadPointerFromMemory(addr, error) if error.Success(): if ptr in chain: chain.append(('circular', 'circular')) break chain.append(('pointer', addr)) addr = ptr else: break if len(chain) == 0: raise InvalidPointerError("0x{:X} is not a valid pointer".format(pointer)) # get some info for the last pointer # first try to resolve a symbol context for the address p, addr = chain[-1] sbaddr = lldb.SBAddress(addr, t) ctx = t.ResolveSymbolContextForAddress(sbaddr, lldb.eSymbolContextEverything) if ctx.IsValid() and ctx.GetSymbol().IsValid(): # found a symbol, store some info and we're done for this pointer fstart = ctx.GetSymbol().GetStartAddress().GetLoadAddress(t) offset = addr - fstart chain.append(('symbol', '{} + 0x{:X}'.format(ctx.GetSymbol().name, offset))) log.debug("symbol context: {}".format(str(chain[-1]))) else: # no symbol context found, see if it looks like a string log.debug("no symbol context") s = t.process.ReadCStringFromMemory(addr, 256, error) for i in range(0, len(s)): if ord(s[i]) >= 128: s = s[:i] break if len(s): chain.append(('string', s)) return chain @lock_host def command(self, command=None): """ Execute a command in the debugger. `command` is the command string to execute. """ # for some reason this doesn't work - figure it out if command: res = lldb.SBCommandReturnObject() ci = self.host.GetCommandInterpreter() ci.HandleCommand(str(command), res, False) if res.Succeeded(): return res.GetOutput().strip() else: raise Exception(res.GetError().strip()) else: raise Exception("No command specified") @lock_host def disassembly_flavor(self): """ Return the disassembly flavor setting for the debugger. Returns 'intel' or 'att' """ res = lldb.SBCommandReturnObject() ci = self.host.GetCommandInterpreter() ci.HandleCommand('settings show target.x86-disassembly-flavor', res) if res.Succeeded(): output = res.GetOutput().strip() flavor = output.split()[-1] if flavor == 'default': flavor = 'att' else: raise Exception(res.GetError().strip()) return flavor @validate_busy @validate_target @lock_host def breakpoints(self, target_id=0): """ Return a list of breakpoints. Returns data in the following structure: [ { "id": 1, "enabled": True, "one_shot": False, "hit_count": 5, "locations": [ { "address": 0x100000cf0, "name": 'main' } ] } ] """ breakpoints = [] t = self.host.GetTargetAtIndex(target_id) s = lldb.SBStream() for i in range(0, t.GetNumBreakpoints()): b = t.GetBreakpointAtIndex(i) locations = [] for j in range(0, b.GetNumLocations()): l = b.GetLocationAtIndex(j) s.Clear() l.GetAddress().GetDescription(s) desc = s.GetData() locations.append({ 'address': l.GetLoadAddress(), 'name': desc }) breakpoints.append({ 'id': b.id, 'enabled': b.enabled, 'one_shot': b.one_shot, 'hit_count': b.GetHitCount(), 'locations': locations }) return breakpoints @validate_busy @validate_target @lock_host def backtrace(self, target_id=0, thread_id=None): """ Return a list of stack frames. """ target = self.host.GetTargetAtIndex(target_id) if not thread_id: thread_id = target.process.selected_thread.id try: thread = target.process.GetThreadByID(thread_id) except: raise NoSuchThreadException() frames = [] for frame in thread: start_addr = frame.GetSymbol().GetStartAddress().GetFileAddress() offset = frame.addr.GetFileAddress() - start_addr ctx = frame.GetSymbolContext(lldb.eSymbolContextEverything) mod = ctx.GetModule() name = '{mod}`{symbol} + {offset}'.format(mod=os.path.basename(str(mod.file)), symbol=frame.name, offset=offset) frames.append({'index': frame.idx, 'addr': frame.addr.GetFileAddress(), 'name': name}) return frames def capabilities(self): """ Return a list of the debugger's capabilities. Thus far only the 'async' capability is supported. This indicates that the debugger host can be queried from a background thread, and that views can use non-blocking API requests without queueing requests to be dispatched next time the debugger stops. """ return ["async"] def register_command_plugin(self, name, cls): """ Register a command plugin with the LLDB adaptor. """ # make sure we have a commands object if not voltron.commands: voltron.commands = namedtuple('VoltronCommands', []) # method invocation creator def create_invocation(obj): def invoke(debugger, command, result, env_dict): obj.invoke(*command.split()) return invoke # store the invocation in `voltron.commands` to pass to LLDB setattr(voltron.commands, name, create_invocation(cls())) # register the invocation as a command script handler thing self.host.HandleCommand("command script add -f voltron.commands.{} {}".format(name, name)) class LLDBCommand(DebuggerCommand): """ Debugger command class for LLDB """ @staticmethod def _invoke(debugger, command, *args): voltron.command.handle_command(command) def __init__(self): super(LLDBCommand, self).__init__() self.hook_idx = None self.adaptor = voltron.debugger # install the voltron command handler self.adaptor.command("script import voltron") self.adaptor.command('command script add -f entry.invoke voltron') # try to register hooks automatically, as this works on new LLDB versions self.register_hooks(True) def invoke(self, debugger, command, result, dict): self.handle_command(command) def register_hooks(self, quiet=False): try: output = self.adaptor.command("target stop-hook list") if 'voltron' not in output: output = self.adaptor.command('target stop-hook add -o \'voltron stopped\'') try: # hahaha this sucks self.hook_idx = int(res.GetOutput().strip().split()[2][1:]) except: pass self.registered = True if not quiet: print("Registered stop-hook") except: if not quiet: print("No targets") def unregister_hooks(self): self.adaptor.command('target stop-hook delete {}'.format(self.hook_idx if self.hook_idx else '')) self.registered = False class LLDBAdaptorPlugin(DebuggerAdaptorPlugin): host = 'lldb' adaptor_class = LLDBAdaptor command_class = LLDBCommand voltron-0.1.4/voltron/plugins/debugger/dbg_mock.py000066400000000000000000000003121270717212500223060ustar00rootroot00000000000000from voltron.plugin import * from voltron.dbg import * class MockAdaptor(DebuggerAdaptor): pass class MockAdaptorPlugin(DebuggerAdaptorPlugin): host = 'mock' adaptor_class = MockAdaptor voltron-0.1.4/voltron/plugins/debugger/dbg_vdb.py000066400000000000000000000411451270717212500221410ustar00rootroot00000000000000from __future__ import print_function import re import shlex import struct import string import logging import threading from voltron.api import * from voltron.plugin import * from voltron.dbg import * try: import vtrace import vdb import envi HAVE_VDB = True except ImportError: HAVE_VDB = False log = logging.getLogger('debugger') if HAVE_VDB: class NotAStringError(Exception): pass class FailedToReadMemoryError(Exception): pass class VDBAdaptor(DebuggerAdaptor): """ The interface with an instance of VDB """ archs = { "i386": "x86", "amd64": "x86_64", "arm": "arm", } sizes = { 'x86': 4, 'x86_64': 8, 'arm': 4 } reg_names = { "x86_64": { "pc": "rip", "sp": "rsp", }, "x86": { "pc": "eip", "sp": "esp", } } def __init__(self, host, *args, **kwargs): self.listeners = [] self.host_lock = threading.RLock() self._vdb = host self._vtrace = vtrace def version(self): """ Get the debugger's version. Returns a string containing the debugger's version (e.g. 'GNU gdb (GDB) 7.8') """ return "VDB/version-unknown" def _target(self, target_id=0): """ Return information about the specified target. Returns data in the following structure: { "id": 0, # ID that can be used in other funcs "file": "/bin/ls", # target's binary file "arch": "x86_64", # target's architecture "state: "stopped" # state } """ d = {} d["id"] = 0 d["state"] = self._state() d["file"] = self._vdb.getTrace().metadata['ExeName'] d["arch"] = self.get_arch() d['byte_order'] = self.get_byte_order() d['addr_size'] = self.get_addr_size() return d @lock_host def target(self, target_id=0): """ Return information about the current inferior. `target_id` is ignored. """ return self._target() @lock_host def targets(self, target_ids=None): """ Return information about the debugger's current targets. `target_ids` is ignored. Only the current target is returned. This method is only implemented to maintain API compatibility with the LLDBAdaptor. """ return [self._target()] @validate_target @lock_host def state(self, target_id=0): """ Get the state of a given target. `target_id` is ignored. """ return self._state() @validate_busy @validate_target @lock_host def registers(self, target_id=0, thread_id=None, registers=[]): """ Get the register values for a given target/thread. `target_id` is ignored. """ arch = self.get_arch() if arch in self.reg_names: if 'pc' in registers: registers.remove('pc') registers.append(self.reg_names[arch]['pc']) if 'sp' in registers: registers.remove('sp') registers.append(self.reg_names[arch]['sp']) else: raise Exception("Unsupported architecture: {}".format(target['arch'])) if registers != []: regs = {} for reg in registers: regs[reg] = self.get_register(reg) else: log.debug('Getting registers for arch {}'.format(arch)) if arch == "x86_64": regs = self.get_registers_x86_64() elif arch == "x86": regs = self.get_registers_x86() elif arch == "arm": regs = self.get_registers_arm() else: raise UnknownArchitectureException() return regs @validate_busy @validate_target @lock_host def stack_pointer(self, target_id=0, thread_id=None): """ Get the value of the stack pointer register. `target_id` is ignored. """ arch = self.get_arch() if arch in self.reg_names: sp_name = self.reg_names[arch]['sp'] sp = self.get_register(sp_name) else: raise UnknownArchitectureException() return sp_name, sp @validate_busy @validate_target @lock_host def program_counter(self, target_id=0, thread_id=None): """ Get the value of the program counter register. `target_id` is ignored. """ arch = self.get_arch() if arch in self.reg_names: pc_name = self.reg_names[arch]['pc'] pc = self.get_register(pc_name) else: raise UnknownArchitectureException() return pc_name, pc @validate_busy @validate_target @lock_host def memory(self, address, length, target_id=0): """ Get the register values for . Raises `FailedToReadMemoryError` if... that happens. `address` is the address at which to start reading `length` is the number of bytes to read `target_id` is ignored. """ log.debug('Reading 0x{:x} bytes of memory at 0x{:x}'.format(length, address)) t = self._vdb.getTrace() try: return t.readMemory(address, length) except: raise FailedToReadMemoryError() @validate_busy @validate_target @lock_host def stack(self, length, target_id=0, thread_id=None): """ Get the register values for . `length` is the number of bytes to read `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get the stack pointer sp_name, sp = self.stack_pointer(target_id=target_id, thread_id=thread_id) # read memory memory = self.memory(sp, length, target_id=target_id) return memory def _get_n_opcodes_length(self, address, count): """ Get the number of bytes used to represent the `n` instructions at `address`. `address` is the starting address of the sequence of instructions. `count` is the number of instructions to decode. """ length = 0 t = self._vdb.getTrace() arch = self._vdb.arch.getArchId() for i in xrange(count): op = t.parseOpcode(address + length, arch=arch) length += op.size return length @validate_busy @validate_target @lock_host def disassemble(self, target_id=0, address=None, count=16): """ Get a disassembly of the instructions at the given address. `address` is the address at which to disassemble. If None, the current program counter is used. `count` is the number of instructions to disassemble. """ if address == None: pc_name, address = self.program_counter(target_id=target_id) length = self._get_n_opcodes_length(address, count) can = envi.memcanvas.StringMemoryCanvas(self._vdb.memobj, self._vdb.symobj) can.renderMemory(address, length, self._vdb.opcoderend) return str(can) def _get_ascii_string(self, address, min_length=4, max_length=32): """ Get the ASCII string of length at least `min_length`, but not more than `max_length` of it, or raise `NotAStringError` if it doesnt look like an ASCII string. """ cs = [] for i in xrange(max_length): try: c = self.memory(address + i, 1)[0] except FailedToReadMemoryError: break if ord(c) == 0: break elif c not in string.printable: break else: cs.append(c) if len(cs) >= min_length: return "".join(cs) else: raise NotAStringError() def _get_unicode_string(self, address, min_length=4, max_length=32): """ Get the *simple* Unicode string of length at least `min_length` characters, but not more than `max_length` characters of it, or raise `NotAStringError` if it doesnt look like a *simple* Unicode string. *simple* Unicode is ASCII with interspersed NULLs """ cs = [] for i in xrange(max_length): try: b = self.memory(address + (i * 2), 2) except FailedToReadMemoryError: break # need every other byte to be a NULL if ord(b[1]) != 0: break c = b[0] if ord(c) == 0: break elif c not in string.printable: break else: cs.append(c) if len(cs) >= min_length: return "".join(cs) else: raise NotAStringError() @validate_busy @validate_target @lock_host def dereference(self, pointer, target_id=0): """ Recursively dereference a pointer for display `target_id` is ignored. """ fmt = ('<' if self.get_byte_order() == 'little' else '>') + {2: 'H', 4: 'L', 8: 'Q'}[self.get_addr_size()] addr = pointer chain = [] # recursively dereference while True: try: mem = self.memory(addr, self.get_addr_size()) except FailedToReadMemoryError: break except Exception as e: print(e) print(type(e)) print(e.__class__.__name__) break log.debug("read mem: {}".format(mem)) (ptr,) = struct.unpack(fmt, mem) if ptr in chain: break chain.append(('pointer', addr)) addr = ptr # get some info for the last pointer # first try to resolve a symbol context for the address p, addr = chain[-1] output = self._vdb.reprPointer(addr) if "Who knows?!?!!?" not in output: chain.append(('symbol', output)) log.debug("symbol context: {}".format(str(chain[-1]))) else: log.debug("no symbol context") try: chain.append(("string", self._get_ascii_string(addr))) except NotAStringError: try: chain.append(("string", self._get_unicode_string(addr))) except NotAStringError: pass log.debug("chain: {}".format(chain)) return chain @lock_host def command(self, command=None): """ Execute a command in the debugger. `command` is the command string to execute. """ if command: # well, this is hacky... # hook the canvas to capture a command's output oldcan = self._vdb.canvas newcan = envi.memcanvas.StringMemoryCanvas(self._vdb.memobj, self._vdb.symobj) try: self._vdb.canvas = newcan self._vdb.onecmd(command) finally: self._vdb.canvas = oldcan return str(newcan).rstrip("\n") else: raise Exception("No command specified") return res @lock_host def disassembly_flavor(self): """ Return the disassembly flavor setting for the debugger. Returns 'intel' or 'att' """ return "intel" def capabilities(self): """ Return a list of the debugger's capabilities. Thus far only the 'async' capability is supported. This indicates that the debugger host can be queried from a background thread, and that views can use non-blocking API requests without queueing requests to be dispatched next time the debugger stops. """ return ['async'] # # Private functions # def _state(self): """ Get the state of a given target. Internal use. """ if not self._vdb.getTrace().isAttached(): state = "invalid" else: if self._vdb.getTrace().isRunning(): state = "running" else: state = "stopped" return state def get_registers(self): return self._vdb.getTrace().getRegisters() def get_register(self, reg_name): return self.get_registers()[reg_name] def get_registers_x86_64(self): return self.get_registers() def get_registers_x86(self): return self.get_registers() def get_registers_arm(self): return self.get_registers() def get_registers_sse(self, num=8): sse = {} for k, v in self.get_registers().items(): if k.startswith("xmm"): sse[k] = v return sse def get_registers_fpu(self): fpu = {} for k, v in self.get_registers().items(): if k.startswith("st"): fpu[k] = v return fpu def get_next_instruction(self): dis = self.disassemble(address=self.program_counter()[1], count=1) return dis.partition("\n")[0].strip() def get_arch(self): arch = self._vdb.getTrace().getMeta("Architecture") return self.archs[arch] def get_addr_size(self): arch = self.get_arch() return self.sizes[arch] def get_byte_order(self): return "little" class VDBCommand(DebuggerCommand, vtrace.Notifier): """ Debugger command class for VDB """ def __init__(self, host): """ vdb is the debugger instance vtrace is the vtrace module? """ super(VDBCommand, self).__init__() self._vdb = host self._vtrace = vtrace self.register_hooks() def invoke(self, arg, from_tty): self.handle_command(arg) def register_hooks(self): self._vdb.registerNotifier(vtrace.NOTIFY_ALL, self) def unregister_hooks(self): self._vdb.deregisterNotifier(vtrace.NOTIFY_ALL, self) def notify(self, event, trace): if event == self._vtrace.NOTIFY_DETACH: self.exit_handler(event) elif event == self._vtrace.NOTIFY_EXIT: self.exit_handler(event) elif event == self._vtrace.NOTIFY_BREAK: self.stop_handler(event) elif event == self._vtrace.NOTIFY_STEP: self.stop_handler(event) elif event == self._vtrace.NOTIFY_CONTINUE: self.cont_handler(event) def stop_handler(self, event): self.adaptor.update_state() voltron.server.dispatch_queue() log.debug('Inferior stopped') def exit_handler(self, event): log.debug('Inferior exited') voltron.server.cancel_queue() voltron.server.stop() def cont_handler(self, event): log.debug('Inferior continued') class VDBAdaptorPlugin(DebuggerAdaptorPlugin): host = 'vdb' adaptor_class = VDBAdaptor command_class = VDBCommand voltron-0.1.4/voltron/plugins/debugger/dbg_windbg.py000066400000000000000000000367771270717212500226570ustar00rootroot00000000000000from __future__ import print_function import logging import threading import re import struct import six import array from voltron.api import * from voltron.plugin import * from voltron.dbg import * try: in_windbg = False import pykd try: import vtrace except: in_windbg = True except ImportError: pass log = logging.getLogger('debugger') if in_windbg: class WinDbgAdaptor(DebuggerAdaptor): sizes = { 'x86': 4, 'x86_64': 8, } max_deref = 24 max_string = 128 def __init__(self, *args, **kwargs): self.listeners = [] self.host_lock = threading.RLock() self.host = pykd def version(self): """ Get the debugger's version. Returns a string containing the debugger's version (e.g. 'Microsoft (R) Windows Debugger Version whatever, pykd 0.3.0.38') """ try: [windbg] = [line for line in pykd.dbgCommand('version').split('\n') if 'Microsoft (R) Windows Debugger Version' in line] except: windbg = 'WinDbg ' return '{}, {}'.format(windbg, 'pykd {}'.format(pykd.version)) def _target(self, target_id=0): """ Return information about the specified target. Returns data in the following structure: { "id": 0, # ID that can be used in other funcs "file": "/bin/ls", # target's binary file "arch": "x86_64", # target's architecture "state: "stopped" # state } """ # get target properties d = {} d["id"] = pykd.getCurrentProcessId() d["num"] = d['id'] # get target state d["state"] = self._state() d["file"] = pykd.getProcessExeName() # get arch d["arch"] = self.get_arch() d['byte_order'] = self.get_byte_order() d['addr_size'] = self.get_addr_size() return d @lock_host def target(self, target_id=0): """ Return information about the current inferior. We only support querying the current inferior with WinDbg. `target_id` is ignored. """ return self._target() @lock_host def targets(self, target_ids=None): """ Return information about the debugger's current targets. `target_ids` is ignored. Only the current target is returned. This method is only implemented to maintain API compatibility with the LLDBAdaptor. """ return [self._target()] @validate_target @lock_host def state(self, target_id=0): """ Get the state of a given target. """ return self._state() @validate_busy @validate_target @lock_host def registers(self, target_id=0, thread_id=None, registers=[]): """ Get the register values for a given target/thread. """ arch = self.get_arch() # if we got 'sp' or 'pc' in registers, change it to whatever the right name is for the current arch if arch in self.reg_names: if 'pc' in registers: registers.remove('pc') registers.append(self.reg_names[arch]['pc']) if 'sp' in registers: registers.remove('sp') registers.append(self.reg_names[arch]['sp']) else: raise Exception("Unsupported architecture: {}".format(target['arch'])) # get registers if registers != []: vals = {} for reg in registers: vals[reg] = pykd.reg(reg) else: log.debug('Getting registers for arch {}'.format(arch)) if arch == "x86_64": reg_names = ['rax', 'rbx', 'rcx', 'rdx', 'rbp', 'rsp', 'rdi', 'rsi', 'rip', 'r8', 'r9', 'r10', 'r11', 'r12', 'r13', 'r14', 'r15', 'cs', 'ds', 'es', 'fs', 'gs', 'ss'] elif arch == "x86": reg_names = ['eax', 'ebx', 'ecx', 'edx', 'ebp', 'esp', 'edi', 'esi', 'eip', 'cs', 'ds', 'es', 'fs', 'gs', 'ss'] else: raise UnknownArchitectureException() vals = {} for reg in reg_names: try: vals[reg] = pykd.reg(reg) except: log.debug('Failed getting reg: ' + reg) vals[reg] = 'N/A' # Get flags try: vals['rflags'] = pykd.reg(reg) except: log.debug('Failed getting reg: eflags') vals['rflags'] = 'N/A' # Get SSE registers try: vals.update(self.get_registers_sse(16)) except: log.exception("Failed to get SSE registers") # Get FPU registers try: vals.update(self.get_registers_fpu()) except: log.exception("Failed to get FPU registers") return vals @validate_busy @validate_target @lock_host def stack_pointer(self, target_id=0, thread_id=None): """ Get the value of the stack pointer register. """ arch = self.get_arch() if arch in self.reg_names: sp_name = self.reg_names[arch]['sp'] sp = pykd.reg(sp_name) else: raise UnknownArchitectureException() return sp_name, sp @validate_busy @validate_target @lock_host def program_counter(self, target_id=0, thread_id=None): """ Get the value of the program counter register. """ arch = self.get_arch() if arch in self.reg_names: pc_name = self.reg_names[arch]['pc'] pc = pykd.reg(pc_name) else: raise UnknownArchitectureException() return pc_name, pc @validate_busy @validate_target @lock_host def memory(self, address, length, target_id=0): """ Get the register values for . `address` is the address at which to start reading `length` is the number of bytes to read """ # read memory log.debug('Reading 0x{:x} bytes of memory at 0x{:x}'.format(length, address)) memory = array.array('B', pykd.loadBytes(address, length)).tostring() return memory @validate_busy @validate_target @lock_host def stack(self, length, target_id=0, thread_id=None): """ Get the register values for . `length` is the number of bytes to read `target_id` is a target ID (or None for the first target) `thread_id` is a thread ID (or None for the selected thread) """ # get the stack pointer sp_name, sp = self.stack_pointer(target_id=target_id, thread_id=thread_id) # read memory memory = self.memory(sp, length, target_id=target_id) return memory @validate_busy @validate_target @lock_host def disassemble(self, target_id=0, address=None, count=16): """ Get a disassembly of the instructions at the given address. `address` is the address at which to disassemble. If None, the current program counter is used. `count` is the number of instructions to disassemble. """ # make sure we have an address if address is None: pc_name, address = self.program_counter(target_id=target_id) # disassemble output = pykd.dbgCommand('u 0x{:x} l{}'.format(address, count)) return output @validate_busy @validate_target @lock_host def dereference(self, pointer, target_id=0): """ Recursively dereference a pointer for display """ fmt = ('<' if self.get_byte_order() == 'little' else '>') + {2: 'H', 4: 'L', 8: 'Q'}[self.get_addr_size()] addr = pointer chain = [] # recursively dereference for i in range(0, self.max_deref): try: [ptr] = pykd.loadPtrs(addr, 1) if ptr in chain: break chain.append(('pointer', addr)) addr = ptr except: log.exception("Dereferencing pointer 0x{:X}".format(addr)) break # get some info for the last pointer # first try to resolve a symbol context for the address if len(chain): p, addr = chain[-1] output = pykd.findSymbol(addr) sym = True try: # if there's no symbol found, pykd returns a hex string of the address if int(output, 16) == addr: sym = False log.debug("no symbol context") except: pass if sym: chain.append(('symbol', output.strip())) else: log.debug("no symbol context") mem = pykd.loadBytes(addr, 2) if mem[0] < 127: if mem[1] == 0: a = [] for i in range(0, self.max_string, 2): mem = pykd.loadBytes(addr + i, 2) if mem == [0, 0]: break a.extend(mem) output = array.array('B', a).tostring().decode('UTF-16').encode('latin1') chain.append(('unicode', output)) else: output = pykd.loadCStr(addr) chain.append(('string', output)) log.debug("chain: {}".format(chain)) return chain @lock_host def command(self, command=None): """ Execute a command in the debugger. `command` is the command string to execute. """ if command: res = pykd.dbgCommand(command) else: raise Exception("No command specified") return res @lock_host def disassembly_flavor(self): """ Return the disassembly flavor setting for the debugger. Returns 'intel' or 'att' """ return 'intel' @lock_host def breakpoints(self, target_id=0): """ Return a list of breakpoints. Returns data in the following structure: [ { "id": 1, "enabled": True, "one_shot": False, "hit_count": 5, "locations": [ { "address": 0x100000cf0, "name": 'main' } ] } ] """ breakpoints = [] for i in range(0, pykd.getNumberBreakpoints()): b = pykd.getBp(i) addr = b.getOffset() name = hex(addr) try: name = pykd.findSymbol(addr) except: log.exception("No symbol found for address {}".format(addr)) pass breakpoints.append({ 'id': i, 'enabled': True, 'one_shot': False, 'hit_count': '-', 'locations': [{ "address": addr, "name": name }] }) return breakpoints def capabilities(self): """ Return a list of the debugger's capabilities. Thus far only the 'async' capability is supported. This indicates that the debugger host can be queried from a background thread, and that views can use non-blocking API requests without queueing requests to be dispatched next time the debugger stops. """ return ['async'] # # Private functions # def _state(self): """ Get the state of a given target. Internal use. """ s = pykd.getExecutionStatus() if s == pykd.executionStatus.Break: state = 'stopped' elif s == pykd.executionStatus.Go: state = 'running' else: state = 'invalid' return state def get_registers_sse(self, num=8): regs = {} for i in range(0, 16): try: reg = 'xmm{}'.format(i) regs[reg] = pykd.reg(reg) except: break return regs def get_registers_fpu(self): regs = {} for i in range(0, 8): try: reg = 'st{}'.format(i) regs[reg] = pykd.reg(reg) except: break return regs def get_next_instruction(self): return str(pykd.disasm()) def get_arch(self): t = pykd.getCPUType() if t == pykd.CPUType.I386: return 'x86' else: return 'x86_64' return arch def get_addr_size(self): arch = self.get_arch() return self.sizes[arch] def get_byte_order(self): return 'little' class EventHandler(pykd.eventHandler): """ Event handler for WinDbg/PyKD events. """ def __init__(self, adaptor, *args, **kwargs): super(EventHandler, self).__init__(*args, **kwargs) self.adaptor = adaptor def onExecutionStatusChange(self, status): if status == pykd.executionStatus.Break: self.adaptor.update_state() voltron.server.dispatch_queue() class WinDbgCommand(DebuggerCommand): """ Debugger command class for WinDbg """ def __init__(self): super(WinDbgCommand, self).__init__() self.register_hooks() def invoke(self, debugger, command, result, dict): self.handle_command(command) def register_hooks(self): self.handler = EventHandler(self.adaptor) def unregister_hooks(self): del self.handler self.handler = None class WinDbgAdaptorPlugin(DebuggerAdaptorPlugin): host = 'windbg' adaptor_class = WinDbgAdaptor command_class = WinDbgCommand voltron-0.1.4/voltron/plugins/view/000077500000000000000000000000001270717212500173615ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/view/__init__.py000066400000000000000000000000001270717212500214600ustar00rootroot00000000000000voltron-0.1.4/voltron/plugins/view/backtrace.py000066400000000000000000000020051270717212500216470ustar00rootroot00000000000000import logging from voltron.view import * from voltron.plugin import * from voltron.api import * log = logging.getLogger('view') class BacktraceView (TerminalView): def render(self): height, width = self.window_size() # Set up header and error message if applicable self.title = '[backtrace]' res = self.client.perform_request('command', block=self.block, command='bt') # don't render if it timed out, probably haven't stepped the debugger again if res.timed_out: return if res and res.is_success: # Get the command output self.body = res.output else: log.error("Error getting backtrace: {}".format(res.message)) self.body = self.colour(res.message, 'red') # Call parent's render method super(BacktraceView, self).render() class BacktraceViewPlugin(ViewPlugin): plugin_type = 'view' name = 'backtrace' aliases = ('t', 'bt', 'back') view_class = BacktraceView voltron-0.1.4/voltron/plugins/view/breakpoints.py000066400000000000000000000046371270717212500222660ustar00rootroot00000000000000import logging from blessed import Terminal from voltron.view import * from voltron.plugin import * from voltron.api import * log = logging.getLogger('view') class BreakpointsView (TerminalView): def render(self): self.title = '[breakpoints]' # get PC first so we can highlight a breakpoint we're at req = api_request('registers', registers=['pc']) res = self.client.send_request(req) if res and res.is_success and len(res.registers) > 0: pc = res.registers[list(res.registers.keys())[0]] else: pc = -1 # get breakpoints and render req = api_request('breakpoints', block=self.block) res = self.client.send_request(req) # don't render if it timed out, probably haven't stepped the debugger again if res.timed_out: return if res and res.is_success: fmtd = [] term = Terminal() for bp in res.breakpoints: # prepare formatting dictionary for the breakpoint d = bp.copy() d['locations'] = None d['t'] = term d['id'] = '#{:<2}'.format(d['id']) if d['one_shot']: d['one_shot'] = self.config.format.one_shot.format(t=term) else: d['one_shot'] = '' if d['enabled']: d['disabled'] = '' else: d['disabled'] = self.config.format.disabled.format(t=term) # add a row for each location for location in bp['locations']: # add location data to formatting dict and format the row d.update(location) if pc == d['address']: d['hit'] = self.config.format.hit.format(t=term) else: d['hit'] = '' f = self.config.format.row.format(**d) fmtd.append(f) d['id'] = ' ' self.body = '\n'.join(fmtd) else: log.error("Error getting breakpoints: {}".format(res.message)) self.body = self.colour(res.message, 'red') super(BreakpointsView, self).render() class BreakpointsViewPlugin(ViewPlugin): plugin_type = 'view' name = 'breakpoints' aliases = ('b', 'bp', 'break') view_class = BreakpointsView voltron-0.1.4/voltron/plugins/view/command.py000066400000000000000000000040611270717212500213520ustar00rootroot00000000000000import logging import importlib try: from pygments.lexers import get_lexer_by_name except: get_lexer_by_name = None from voltron.view import * from voltron.plugin import * from voltron.api import * log = logging.getLogger('view') class CommandView (TerminalView): @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('command', aliases=('c', 'cmd'), help='run a command each time the debugger host stops') VoltronView.add_generic_arguments(sp) sp.add_argument('command', action='store', help='command to run') sp.add_argument('--lexer', '-l', action='store', help='apply a Pygments lexer to the command output (e.g. "c")', default=None) sp.set_defaults(func=CommandView) def render(self): # Set up header and error message if applicable self.title = '[cmd:' + self.args.command + ']' # Get the command output res = self.client.perform_request('command', block=self.block, command=self.args.command) # don't render if it timed out, probably haven't stepped the debugger again if res.timed_out: return if res and res.is_success: if get_lexer_by_name and self.args.lexer: try: lexer = get_lexer_by_name(self.args.lexer, stripall=True) self.body = pygments.highlight(res.output, lexer, pygments.formatters.TerminalFormatter()) except Exception as e: log.warning('Failed to highlight view contents: ' + repr(e)) self.body = res.output else: self.body = res.output else: log.error("Error executing command: {}".format(res.message)) self.body = self.colour(res.message, 'red') # Call parent's render method super(CommandView, self).render() class CommandViewPlugin(ViewPlugin): plugin_type = 'view' name = 'command' view_class = CommandView voltron-0.1.4/voltron/plugins/view/disasm.py000066400000000000000000000041351270717212500212160ustar00rootroot00000000000000from voltron.view import * from voltron.plugin import * from voltron.api import * try: from voltron.lexers import * have_pygments = True except ImportError: have_pygments = False class DisasmView(TerminalView): @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('disasm', help='disassembly view', aliases=('d', 'dis', 'disasm')) VoltronView.add_generic_arguments(sp) sp.set_defaults(func=DisasmView) sp.add_argument('--use-capstone', '-c', action='store_true', default=False, help='use capstone') def render(self): height, width = self.window_size() # Set up header & error message if applicable self.title = '[disassembly]' # Request data req = api_request('disassemble', block=self.block, use_capstone=self.args.use_capstone) req.count = self.body_height() res = self.client.send_request(req) # don't render if it timed out, probably haven't stepped the debugger again if res.timed_out: return if res and res.is_success: # Get the disasm disasm = res.disassembly disasm = '\n'.join(disasm.split('\n')[:self.body_height()]) # Pygmentize output if have_pygments: try: host = 'capstone' if self.args.use_capstone else res.host lexer = all_lexers['{}_{}'.format(host, res.flavor)]() disasm = pygments.highlight(disasm, lexer, pygments.formatters.TerminalFormatter()) except Exception as e: log.warning('Failed to highlight disasm: ' + str(e)) # Build output self.body = disasm.rstrip() else: log.error("Error disassembling: {}".format(res.message)) self.body = self.colour(res.message, 'red') # Call parent's render method super(DisasmView, self).render() class DisasmViewPlugin(ViewPlugin): plugin_type = 'view' name = 'disassembly' aliases = ('d', 'dis', 'disasm') view_class = DisasmView voltron-0.1.4/voltron/plugins/view/memory.py000066400000000000000000000152121270717212500212440ustar00rootroot00000000000000import logging import struct import six from voltron.view import * from voltron.plugin import * from voltron.api import * log = logging.getLogger("view") class MemoryView (TerminalView): printable_filter = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) async = True @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('memory', help='display a chunk of memory', aliases=('m', 'mem')) VoltronView.add_generic_arguments(sp) group = sp.add_mutually_exclusive_group(required=False) group.add_argument('--deref', '-d', action='store_true', help=('display the data in a column one CPU word wide ' 'and dereference any valid pointers'), default=False) group.add_argument('--bytes', '-b', action='store', type=int, help='bytes per line (default 16)', default=16) sp.add_argument('--reverse', '-v', action='store_true', help='reverse the output', default=False) group = sp.add_mutually_exclusive_group(required=True) group.add_argument('--address', '-a', action='store', help='address (in hex) from which to start reading memory') group.add_argument('--command', '-c', action='store', help=('command to execute resulting in the address from ' 'which to start reading memory. voltron will do his almighty best to find an address in the output by ' 'splitting it on whitespace and searching from the end of the list of tokens. e.g. "print \$rip + 0x1234"'), default=None) group.add_argument('--register', '-r', action='store', help='register containing the address from which to start reading memory', default=None) sp.set_defaults(func=MemoryView) def render(self): height, width = self.window_size() target = None self.trunc_top = self.args.reverse # check args if self.args.register: args = {'register': self.args.register} elif self.args.command: args = {'command': self.args.command} else: args = {'address': self.args.address} if self.args.deref: args['words'] = height else: args['length'] = height*self.args.bytes # get memory and target info m_res, t_res = self.client.send_requests( api_request('memory', block=self.block, deref=self.args.deref is True, **args), api_request('targets', block=self.block)) # don't render if it timed out, probably haven't stepped the debugger again if t_res.timed_out: return if t_res and t_res.is_success and len(t_res.targets) > 0: target = t_res.targets[0] if self.args.deref: self.args.bytes = target['addr_size'] if m_res and m_res.is_success: lines = [] for c in range(0, m_res.bytes, self.args.bytes): chunk = m_res.memory[c:c+self.args.bytes] addr_str = self.colour(self.format_address(m_res.address + c, size=target['addr_size'], pad=False), self.config.format.addr_colour) if self.args.deref: fmt = ('<' if target['byte_order'] == 'little' else '>') + \ {2: 'H', 4: 'L', 8: 'Q'}[target['addr_size']] info_str = '' if len(chunk) == target['addr_size']: pointer = list(struct.unpack(fmt, chunk))[0] memory_str = ' '.join(["%02X" % x for x in six.iterbytes(chunk)]) info_str = self.format_deref(m_res.deref.pop(0)) else: memory_str = ' '.join(["%02X" % x for x in six.iterbytes(chunk)]) info_str = '' ascii_str = ''.join(["%s" % ((x <= 127 and self.printable_filter[x]) or '.') for x in six.iterbytes(chunk)]) divider = self.colour('|', self.config.format.divider_colour) lines.append('{}: {} {} {} {} {}'.format(addr_str, memory_str, divider, ascii_str, divider, info_str)) self.body = '\n'.join(reversed(lines)).strip() if self.args.reverse else '\n'.join(lines) self.info = '[0x{0:0=4x}:'.format(len(m_res.memory)) + self.config.format.addr_format.format(m_res.address) + ']' else: log.error("Error reading memory: {}".format(m_res.message)) self.body = self.colour(m_res.message, 'red') self.info = '' else: self.body = self.colour("Failed to get targets", 'red') if not self.title: self.title = "[memory]" super(MemoryView, self).render() def format_address(self, address, size=8, pad=True, prefix='0x'): fmt = '{:' + ('0=' + str(size*2) if pad else '') + 'X}' addr_str = fmt.format(address) if prefix: addr_str = prefix + addr_str return addr_str def format_deref(self, deref, size=8): fmtd = [] for t,item in deref: if t == "pointer": fmtd.append(self.format_address(item, size=size, pad=False)) elif t == "string": item = item.replace('\n', '\\n') fmtd.append(self.colour('"' + item + '"', self.config.format.string_colour)) elif t == "unicode": item = item.replace('\n', '\\n') fmtd.append(self.colour('u"' + item + '"', self.config.format.string_colour)) elif t == "symbol": fmtd.append(self.colour('`' + item + '`', self.config.format.symbol_colour)) elif t == "circular": fmtd.append(self.colour('(circular)', self.config.format.divider_colour)) return self.colour(' => ', self.config.format.divider_colour).join(fmtd) class MemoryViewPlugin(ViewPlugin): plugin_type = 'view' name = 'memory' view_class = MemoryView class StackView(MemoryView): @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('stack', help='display a chunk of stack memory', aliases=('s', 'st')) VoltronView.add_generic_arguments(sp) sp.set_defaults(func=StackView) def render(self): self.args.reverse = True self.args.deref = True self.args.register = 'sp' self.args.command = None self.args.address = None self.args.bytes = None self.title = '[stack]' super(StackView, self).render() class StackViewPlugin(ViewPlugin): plugin_type = 'view' name = 'stack' view_class = StackView voltron-0.1.4/voltron/plugins/view/register.py000066400000000000000000000620411270717212500215620ustar00rootroot00000000000000from numbers import Number from voltron.core import STRTYPES from voltron.view import * from voltron.plugin import * from voltron.api import * class RegisterView (TerminalView): FORMAT_INFO = { 'x86_64': [ { 'regs': ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip', 'r8','r9','r10','r11','r12','r13','r14','r15'], 'label_format': '{0:3s}:', 'category': 'general', }, { 'regs': ['cs','ds','es','fs','gs','ss'], 'value_format': SHORT_ADDR_FORMAT_16, 'category': 'general', }, { 'regs': ['rflags'], 'value_format': '{}', 'value_func': 'format_flags', 'value_colour_en': False, 'category': 'general', }, { 'regs': ['rflags'], 'value_format': '{}', 'value_func': 'format_jump', 'value_colour_en': False, 'category': 'general', 'format_name': 'jump' }, { 'regs': ['xmm0','xmm1','xmm2','xmm3','xmm4','xmm5','xmm6','xmm7','xmm8', 'xmm9','xmm10','xmm11','xmm12','xmm13','xmm14','xmm15'], 'value_format': SHORT_ADDR_FORMAT_128, 'value_func': 'format_xmm', 'category': 'sse', }, { 'regs': ['st0','st1','st2','st3','st4','st5','st6','st7'], 'value_format': '{0:0=20X}', 'value_func': 'format_fpu', 'category': 'fpu', }, ], 'x86': [ { 'regs': ['eax','ebx','ecx','edx','ebp','esp','edi','esi','eip'], 'label_format': '{0:3s}:', 'value_format': SHORT_ADDR_FORMAT_32, 'category': 'general', }, { 'regs': ['cs','ds','es','fs','gs','ss'], 'value_format': SHORT_ADDR_FORMAT_16, 'category': 'general', }, { 'regs': ['eflags'], 'value_format': '{}', 'value_func': 'format_flags', 'value_colour_en': False, 'category': 'general', }, { 'regs': ['eflags'], 'value_format': '{}', 'value_func': 'format_jump', 'value_colour_en': False, 'category': 'general', 'format_name': 'jump' }, { 'regs': ['xmm0','xmm1','xmm2','xmm3','xmm4','xmm5','xmm6','xmm7'], 'value_format': SHORT_ADDR_FORMAT_128, 'value_func': 'format_xmm', 'category': 'sse', }, { 'regs': ['st0','st1','st2','st3','st4','st5','st6','st7'], 'value_format': '{0:0=20X}', 'value_func': 'format_fpu', 'category': 'fpu', }, ], 'arm': [ { 'regs': ['pc','sp','lr','cpsr','r0','r1','r2','r3','r4','r5','r6', 'r7','r8','r9','r10','r11','r12'], 'label_format': '{0:>3s}:', 'value_format': SHORT_ADDR_FORMAT_32, 'category': 'general', } ], 'arm64': [ { 'regs': ['pc', 'sp', 'x0', 'x1', 'x2', 'x3', 'x4', 'x5', 'x6', 'x7', 'x8', 'x9', 'x10', 'x11', 'x12', 'x13', 'x14', 'x15', 'x16', 'x17', 'x18', 'x19', 'x20', 'x21', 'x22', 'x23', 'x24', 'x25', 'x26', 'x27', 'x28', 'x29', 'x30'], 'label_format': '{0:3s}:', 'value_format': SHORT_ADDR_FORMAT_64, 'category': 'general', }, ], 'powerpc': [ { 'regs': ['pc','msr','cr','lr', 'ctr', 'r0','r1','r2','r3','r4','r5','r6', 'r7', 'r8','r9','r10','r11','r12','r13','r14', 'r15', 'r16','r17','r18','r19','r20','r21','r22', 'r23', 'r24','r25','r26','r27','r28','r29','r30', 'r31'], 'label_format': '{0:>3s}:', 'value_format': SHORT_ADDR_FORMAT_32, 'category': 'general', } ], } TEMPLATES = { 'x86_64': { 'horizontal': { 'general': ( "{raxl} {rax} {rbxl} {rbx} {rbpl} {rbp} {rspl} {rsp} {rflags}\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} {jump}" ), 'sse': ( "{xmm0l} {xmm0} {xmm1l} {xmm1} {xmm2l} {xmm2}\n" "{xmm3l} {xmm3} {xmm4l} {xmm4} {xmm5l} {xmm5}\n" "{xmm6l} {xmm6} {xmm7l} {xmm7} {xmm8l} {xmm8}\n" "{xmm9l} {xmm9} {xmm10l} {xmm10} {xmm11l} {xmm11}\n" "{xmm12l} {xmm12} {xmm13l} {xmm13} {xmm14l} {xmm14}\n" "{xmm15l} {xmm15}\n" ), 'fpu': ( "{st0l} {st0} {st1l} {st1} {st2l} {st2} {st3l} {st2}\n" "{st4l} {st4} {st5l} {st5} {st6l} {st6} {st7l} {st7}\n" ) }, 'vertical': { 'general': ( "{rflags}\n{jump}\n" "{ripl} {rip}\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}" ), 'sse': ( "{xmm0l} {xmm0}\n{xmm1l} {xmm1}\n{xmm2l} {xmm2}\n{xmm3l} {xmm3}\n" "{xmm4l} {xmm4}\n{xmm5l} {xmm5}\n{xmm6l} {xmm6}\n{xmm7l} {xmm7}\n" "{xmm8l} {xmm8}\n{xmm9l} {xmm9}\n{xmm10l} {xmm10}\n{xmm11l} {xmm11}\n" "{xmm12l} {xmm12}\n{xmm13l} {xmm13}\n{xmm14l} {xmm14}\n{xmm15l} {xmm15}" ), 'fpu': ( "{st0l} {st0}\n{st1l} {st1}\n{st2l} {st2}\n{st3l} {st2}\n" "{st4l} {st4}\n{st5l} {st5}\n{st6l} {st6}\n{st7l} {st7}\n" ) } }, 'x86': { 'horizontal': { 'general': ( "{eaxl} {eax} {ebxl} {ebx} {ebpl} {ebp} {espl} {esp} {eflags}\n" "{edil} {edi} {esil} {esi} {edxl} {edx} {ecxl} {ecx} {eipl} {eip}\n" "{csl} {cs} {dsl} {ds} {esl} {es} {fsl} {fs} {gsl} {gs} {ssl} {ss} {jump}" ), 'sse': ( "{xmm0l} {xmm0} {xmm1l} {xmm1} {xmm2l} {xmm2}\n" "{xmm3l} {xmm3} {xmm4l} {xmm4} {xmm5l} {xmm5}\n" "{xmm6l} {xmm6} {xmm7l}" ), 'fpu': ( "{st0l} {st0} {st1l} {st1} {st2l} {st2} {st3l} {st2}\n" "{st4l} {st4} {st5l} {st5} {st6l} {st6} {st7l} {st7}\n" ) }, 'vertical': { 'general': ( "{eflags}\n{jump}\n" "{eipl} {eip}\n" "{eaxl} {eax}\n{ebxl} {ebx}\n{ebpl} {ebp}\n{espl} {esp}\n" "{edil} {edi}\n{esil} {esi}\n{edxl} {edx}\n{ecxl} {ecx}\n" "{csl} {cs}\n{dsl} {ds}\n{esl} {es}\n{fsl} {fs}\n{gsl} {gs}\n{ssl} {ss}" ), 'sse': ( "{xmm0l} {xmm0}\n{xmm1l} {xmm1}\n{xmm2l} {xmm2}\n{xmm3l} {xmm3}\n" "{xmm4l} {xmm4}\n{xmm5l} {xmm5}\n{xmm6l} {xmm6}\n{xmm7l} {xmm7}\n" ), 'fpu': ( "{st0l} {st0}\n{st1l} {st1}\n{st2l} {st2}\n{st3l} {st2}\n" "{st4l} {st4}\n{st5l} {st5}\n{st6l} {st6}\n{st7l} {st7}\n" ) } }, 'arm': { 'horizontal': { 'general': ( "{pcl} {pc} {spl} {sp} {lrl} {lr} {cpsrl} {cpsr}\n" "{r0l} {r0} {r1l} {r1} {r2l} {r2} {r3l} {r3} {r4l} {r4} {r5l} {r5} {r6l} {r6}\n" "{r7l} {r7} {r8l} {r8} {r9l} {r9} {r10l} {r10} {r11l} {r11} {r12l} {r12}" ), }, 'vertical': { 'general': ( "{pcl} {pc}\n{spl} {sp}\n{lrl} {lr}\n" "{r0l} {r0}\n{r1l} {r1}\n{r2l} {r2}\n{r3l} {r3}\n{r4l} {r4}\n{r5l} {r5}\n{r6l} {r6}\n{r7l} {r7}\n" "{r8l} {r8}\n{r9l} {r9}\n{r10l} {r10}\n{r11l} {r11}\n{r12l} {r12}\n{cpsrl}{cpsr}" ), } }, 'powerpc': { 'horizontal': { 'general': ( "{pcl} {pc} {crl} {cr} {lrl} {lr} {msrl} {msr} {ctrl} {ctr}\n" "{r0l} {r0} {r1l} {r1} {r2l} {r2} {r3l} {r3}\n" "{r4l} {r4} {r5l} {r5} {r6l} {r6} {r7l} {r7}\n" "{r8l} {r8} {r9l} {r9} {r10l} {r10} {r11l} {r11}\n" "{r12l} {r12} {r13l} {r13} {r14l} {r14} {r15l} {r15}\n" "{r16l} {r16} {r17l} {r17} {r18l} {r18} {r19l} {r19}\n" "{r20l} {r20} {r21l} {r21} {r22l} {r22} {r23l} {r23}\n" "{r24l} {r24} {r25l} {r25} {r26l} {r26} {r27l} {r27}\n" "{r28l} {r28} {r29l} {r29} {r30l} {r30} {r31l} {r31}" ), }, 'vertical': { 'general': ( "{pcl} {pc}\n{crl} {cr}\n{lrl} {lr}\n" "{msrl} {msr}\n{ctrl} {ctr}\n" "{r0l} {r0}\n{r1l} {r1}\n{r2l} {r2}\n{r3l} {r3}\n{r4l} {r4}\n{r5l} {r5}\n{r6l} {r6}\n{r7l} {r7}\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" "{r16l} {r16}\n{r17l} {r17}\n{r18l} {r18}\n{r19l} {r19}\n{r20l} {r20}\n{r21l} {r21}\n{r22l} {r22}\n{r23l} {r23}\n" "{r24l} {r24}\n{r25l} {r25}\n{r26l} {r26}\n{r27l} {r27}\n{r28l} {r28}\n{r29l} {r29}\n{r30l} {r30}\n{r31l} {r31}" ), } }, 'arm64': { 'horizontal': { 'general': ( "{pcl} {pc}\n{spl} {sp}\n" "{x0l} {x0}\n{x1l} {x1}\n{x2l} {x2}\n{x3l} {x3}\n{x4l} {x4}\n{x5l} {x5}\n{x6l} {x6}\n{x7l} {x7}\n" "{x8l} {x8}\n{x9l} {x9}\n{x10l} {x10}\n{x11l} {x11}\n{x12l} {x12}\n{x13l} {x13}\n{x14l} {x14}\n" "{x15l} {x15}\n{x16l} {x16}\n{x17l} {x17}\n{x18l} {x18}\n{x19l} {x19}\n{x20l} {x20}\n{x21l} {x21}\n" "{x22l} {x22}\n{x23l} {x23}\n{x24l} {x24}\n{x25l} {x25}\n{x26l} {x26}\n{x27l} {x27}\n{x28l} {x28}\n" "{x29l} {x29}\n{x30l} {x30}\n" ), }, 'vertical': { 'general': ( "{pcl} {pc}\n{spl} {sp}\n" "{x0l} {x0}\n{x1l} {x1}\n{x2l} {x2}\n{x3l} {x3}\n{x4l} {x4}\n{x5l} {x5}\n{x6l} {x6}\n{x7l} {x7}\n" "{x8l} {x8}\n{x9l} {x9}\n{x10l} {x10}\n{x11l} {x11}\n{x12l} {x12}\n{x13l} {x13}\n{x14l} {x14}\n" "{x15l} {x15}\n{x16l} {x16}\n{x17l} {x17}\n{x18l} {x18}\n{x19l} {x19}\n{x20l} {x20}\n{x21l} {x21}\n" "{x22l} {x22}\n{x23l} {x23}\n{x24l} {x24}\n{x25l} {x25}\n{x26l} {x26}\n{x27l} {x27}\n{x28l} {x28}\n" "{x29l} {x29}\n{x30l} {x30}" ), } } } FLAG_BITS = {'c': 0, 'p': 2, 'a': 4, 'z': 6, 's': 7, 't': 8, 'i': 9, 'd': 10, 'o': 11} FLAG_TEMPLATE = "[ {o} {d} {i} {t} {s} {z} {a} {p} {c} ]" XMM_INDENT = 7 last_regs = None last_flags = None @classmethod def configure_subparser(cls, subparsers): sp = subparsers.add_parser('register', help='register values', aliases=('r', 'reg')) VoltronView.add_generic_arguments(sp) sp.set_defaults(func=RegisterView) g = sp.add_mutually_exclusive_group() g.add_argument('--horizontal', '-o', dest="orientation", action='store_const', const="horizontal", help='horizontal orientation') g.add_argument('--vertical', '-v', dest="orientation", action='store_const', const="vertical", help='vertical orientation (default)') sp.add_argument('--general', '-g', dest="sections", action='append_const', const="general", help='show general registers') sp.add_argument('--no-general', '-G', dest="sections", action='append_const', const="no_general", help='show general registers') sp.add_argument('--sse', '-s', dest="sections", action='append_const', const="sse", help='show sse registers') sp.add_argument('--no-sse', '-S', dest="sections", action='append_const', const="no_sse", help='show sse registers') sp.add_argument('--fpu', '-p', dest="sections", action='append_const', const="fpu", help='show fpu registers') sp.add_argument('--no-fpu', '-P', dest="sections", action='append_const', const="no_fpu", help='show fpu registers') def __init__(self, *args, **kwargs): super(RegisterView, self).__init__(*args, **kwargs) self.str_upper = str.upper def apply_cli_config(self): super(RegisterView, self).apply_cli_config() if self.args.orientation != None: self.config.orientation = self.args.orientation if self.args.sections != None: a = filter(lambda x: 'no_'+x not in self.args.sections and not x.startswith('no_'), list(self.config.sections) + self.args.sections) config_sections = [] for sec in a: if sec not in config_sections: config_sections.append(sec) self.config.sections = config_sections def render(self): error = None # get target info (ie. arch) t_res, d_res, r_res = self.client.send_requests(api_request('targets', block=self.block), api_request('disassemble', count=1, block=self.block), api_request('registers', block=self.block)) # don't render if it timed out, probably haven't stepped the debugger again if t_res.timed_out: return if t_res and t_res.is_error or t_res is None or t_res and len(t_res.targets) == 0: error = "No such target" else: arch = t_res.targets[0]['arch'] self.curr_arch = arch # ensure the architecture is supported if arch not in self.FORMAT_INFO: error = "Architecture '{}' not supported".format(arch) else: # get next instruction try: self.curr_inst = d_res.disassembly.strip().split('\n')[-1].split(':')[1].strip() except: self.curr_inst = None # get registers for target if r_res.is_error: error = r_res.message # if everything is ok, render the view if not error: # Build template template = '\n'.join(map(lambda x: self.TEMPLATES[arch][self.config.orientation][x], self.config.sections)) # Process formatting settings data = defaultdict(lambda: 'n/a') data.update(r_res.registers) formats = self.FORMAT_INFO[arch] formatted = {} for fmt in formats: # Apply defaults where they're missing fmt = dict(list(self.config.format.items()) + list(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_func'] != None: formatted[reg+'l'] = getattr(self, fmt['label_func'])(str(label)) if fmt['label_colour_en']: formatted[reg+'l'] = self.colour(formatted[reg+'l'], fmt['label_colour']) # Format the value val = data[reg] if isinstance(val, STRTYPES): temp = fmt['value_format'].format(0) if len(val) < len(temp): val += (len(temp) - len(val))*' ' formatted_reg = self.colour(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'] formatted_reg = val if fmt['value_format'] != None and isinstance(formatted_reg, Number): formatted_reg = fmt['value_format'].format(formatted_reg) if fmt['value_func'] != None: if isinstance(fmt['value_func'], STRTYPES): formatted_reg = getattr(self, fmt['value_func'])(formatted_reg) else: formatted_reg = fmt['value_func'](formatted_reg) if fmt['value_colour_en']: formatted_reg = self.colour(formatted_reg, colour) if fmt['format_name'] == None: formatted[reg] = formatted_reg else: formatted[fmt['format_name']] = formatted_reg # Prepare output log.debug('Formatted: ' + str(formatted)) self.body = template.format(**formatted) # Store the regs self.last_regs = data else: # Set body to error message if appropriate self.body = self.colour(error, 'red') # Prepare headers and footers height, width = self.window_size() self.title = '[regs:{}]'.format('|'.join(self.config.sections)) if len(self.title) > width: self.title = '[regs]' # Call parent's render method super(RegisterView, self).render() def format_flags(self, val): values = {} # Get formatting info for flags if self.curr_arch == 'x86_64': reg = 'rflags' elif self.curr_arch == 'x86': reg = 'eflags' fmt = dict(list(self.config.format.items()) + list(list(filter(lambda x: reg in x['regs'], self.FORMAT_INFO[self.curr_arch]))[0].items())) # Handle each flag bit val = int(val, 10) formatted = {} for flag in self.FLAG_BITS.keys(): values[flag] = (val & (1 << self.FLAG_BITS[flag]) > 0) log.debug("Flag {} value {} (for flags 0x{})".format(flag, values[flag], val)) formatted[flag] = str.upper(flag) if values[flag] else flag if self.last_flags != None and self.last_flags[flag] != values[flag]: colour = fmt['value_colour_mod'] else: colour = fmt['value_colour'] formatted[flag] = self.colour(formatted[flag], colour) # Store the flag values for comparison self.last_flags = values # Format with template flags = self.FLAG_TEMPLATE.format(**formatted) return flags def format_jump(self, val): # Grab flag bits val = int(val, 10) values = {} for flag in self.FLAG_BITS.keys(): values[flag] = (val & (1 << self.FLAG_BITS[flag]) > 0) # If this is a jump instruction, see if it will be taken j = None if self.curr_inst: inst = self.curr_inst.split()[0] if inst in ['ja', 'jnbe']: if not values['c'] and not values['z']: j = (True, '!c && !z') else: j = (False, 'c || z') elif inst in ['jae', 'jnb', 'jnc']: if not values['c']: j = (True, '!c') else: j = (False, 'c') elif inst in ['jb', 'jc', 'jnae']: if values['c']: j = (True, 'c') else: j = (False, '!c') elif inst in ['jbe', 'jna']: if values['c'] or values['z']: j = (True, 'c || z') else: j = (False, '!c && !z') elif inst in ['jcxz', 'jecxz', 'jrcxz']: if self.get_arch() == 'x64': cx = regs['rcx'] elif self.get_arch() == 'x86': cx = regs['ecx'] if cx == 0: j = (True, cx+'==0') else: j = (False, cx+'!=0') elif inst in ['je', 'jz']: if values['z']: j = (True, 'z') else: j = (False, '!z') elif inst in ['jnle', 'jg']: if not values['z'] and values['s'] == values['o']: j = (True, '!z && s==o') else: j = (False, 'z || s!=o') elif inst in ['jge', 'jnl']: if values['s'] == values['o']: j = (True, 's==o') else: j = (False, 's!=o') elif inst in ['jl', 'jnge']: if values['s'] == values['o']: j = (False, 's==o') else: j = (True, 's!=o') elif inst in ['jle', 'jng']: if values['z'] or values['s'] == values['o']: j = (True, 'z || s==o') else: j = (False, '!z && s!=o') elif inst in ['jne', 'jnz']: if not values['z']: j = (True, '!z') else: j = (False, 'z') elif inst in ['jno']: if not values['o']: j = (True, '!o') else: j = (False, 'o') elif inst in ['jnp', 'jpo']: if not values['p']: j = (True, '!p') else: j = (False, 'p') elif inst in ['jns']: if not values['s']: j = (True, '!s') else: j = (False, 's') elif inst in ['jo']: if values['o']: j = (True, 'o') else: j = (False, '!o') elif inst in ['jp', 'jpe']: if values['p']: j = (True, 'p') else: j = (False, '!p') elif inst in ['js']: if values['s']: j = (True, 's') else: j = (False, '!s') # Construct message if j is not None: taken, reason = j if taken: jump = 'Jump ({})'.format(reason) else: jump = '!Jump ({})'.format(reason) else: jump = '' # Pad out jump = '{:^19}'.format(jump) # Colour if j is not None: jump = self.colour(jump, self.config.format.value_colour_mod) else: jump = self.colour(jump, self.config.format.value_colour) return '[' + jump + ']' def format_xmm(self, val): if self.config.orientation == 'vertical': height, width = self.window_size() if width < len(SHORT_ADDR_FORMAT_128.format(0)) + self.XMM_INDENT: return val[:16] + '\n' + ' '*self.XMM_INDENT + val[16:] else: return val[:16] + ':' + val[16:] else: return val def format_fpu(self, val): if self.config.orientation == 'vertical': return val else: return val class RegisterViewPlugin(ViewPlugin): plugin_type = 'view' name = 'register' view_class = RegisterView voltron-0.1.4/voltron/rdb.py000066400000000000000000000014661270717212500160560ustar00rootroot00000000000000import pdb import socket import sys # Trying to debug a quirk in some code that gets called async by {ll,g}db? # # from .rdb import Rdb # Rdb().set_trace() # # Then: telnet localhost 4444 socks = {} # Only bind the socket once def _sock(port): if port in socks: return socks[port] s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) s.bind(("127.0.0.1", port)) socks[port] = s return s class Rdb(pdb.Pdb): def __init__(self, port=4444): self.old_stdout = sys.stdout self.old_stdin = sys.stdin self.skt = _sock(port) self.skt.listen(1) (clientsocket, address) = self.skt.accept() handle = clientsocket.makefile('rw') pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle) sys.stdout = sys.stdin = handle voltron-0.1.4/voltron/view.py000066400000000000000000000333711270717212500162610ustar00rootroot00000000000000from __future__ import print_function import os import sys import logging import pprint import re import signal import time import argparse import traceback import subprocess from requests import ConnectionError from blessed import Terminal try: import pygments import pygments.lexers import pygments.formatters have_pygments = True except: have_pygments = False try: import cursor except: cursor = None from collections import defaultdict from scruffy import Config from .core import * from .colour import * from .plugin import * log = logging.getLogger("view") ADDR_FORMAT_128 = '0x{0:0=32X}' ADDR_FORMAT_64 = '0x{0:0=16X}' ADDR_FORMAT_32 = '0x{0:0=8X}' ADDR_FORMAT_16 = '0x{0:0=4X}' SHORT_ADDR_FORMAT_128 = '{0:0=32X}' SHORT_ADDR_FORMAT_64 = '{0:0=16X}' SHORT_ADDR_FORMAT_32 = '{0:0=8X}' SHORT_ADDR_FORMAT_16 = '{0:0=4X}' # https://gist.github.com/sampsyo/471779 class AliasedSubParsersAction(argparse._SubParsersAction): class _AliasedPseudoAction(argparse.Action): def __init__(self, name, aliases, help): dest = name if aliases: dest += ' (%s)' % ','.join(aliases) sup = super(AliasedSubParsersAction._AliasedPseudoAction, self) sup.__init__(option_strings=[], dest=dest, help=help) def add_parser(self, name, **kwargs): aliases = kwargs.pop('aliases', []) parser = super(AliasedSubParsersAction, self).add_parser(name, **kwargs) # Make the aliases work. for alias in aliases: self._name_parser_map[alias] = parser # Make the help text reflect them, first removing old help entry. if 'help' in kwargs: help = kwargs.pop('help') self._choices_actions.pop() pseudo_action = self._AliasedPseudoAction(name, aliases, help) self._choices_actions.append(pseudo_action) return parser class AnsiString(object): def __init__(self, string): chunks = string.split('\033') self.chars = [] chars = list(chunks[0]) if len(chunks) > 1: for chunk in chunks[1:]: if chunk == '(B': chars.append('\033'+chunk) else: p = chunk.find('m') if p > 0: chars.append('\033'+chunk[:p+1]) chars.extend(list(chunk[p+1:])) else: chars.extend(list(chunk)) # roll up ansi sequences ansi = [] for char in chars: if char[0] == '\033': ansi.append(char) else: self.chars.append(''.join(ansi) + char) ansi = [] if len(self.chars) > 2: if self.chars[-1][0] == '\033': self.chars[-2] = self.chars[-2] + self.chars[-1] self.chars = self.chars[:-1] def __getitem__(self, key): if isinstance(key, slice): return ''.join(self.chars[key.start:key.stop]) + '\033[0m' else: return self.chars[key] def __str__(self): return ''.join(self.chars) def __len__(self): return len(self.chars) def clean(self): return re.sub('\033\[.{1,2}m', '', str(self)) class VoltronView (object): """ Parent class for all views. Views may or may not support blocking mode. LLDB can be queried from a background thread, which means requests can (if it makes sense) be fulfilled as soon as they're received. GDB cannot be queried from a background thread, so requests have to be queued and dispatched by the main thread when the debugger stops. This means that GDB requires blocking mode. In blocking mode, the view's `render` method must make a single call to Client's send_request/send_request method, with the request(s) flagged as blocking (block=True). If a view (or, more likely, a more complex client) has multiple calls to send_request/send_requests, then it cannot support blocking mode and should be flagged as such by including `supports_blocking = False` (see below, views are flagged as supporting blocking by default). All of the included Voltron views support blocking mode, but this distinction has been made so that views can be written for LLDB only without making compromises to support GDB. """ view_type = None block = False supports_blocking = True @classmethod def add_generic_arguments(cls, sp): sp.add_argument('--show-header', '-e', dest="header", action='store_true', help='show header', default=None) sp.add_argument('--hide-header', '-E', dest="header", action='store_false', help='hide header') sp.add_argument('--show-footer', '-f', dest="footer", action='store_true', help='show footer', default=None) sp.add_argument('--hide-footer', '-F', dest="footer", action='store_false', help='hide footer') sp.add_argument('--name', '-n', action='store', help='named configuration to use', default=None) @classmethod def configure_subparser(cls, subparsers): if hasattr(cls._plugin, 'aliases'): sp = subparsers.add_parser(cls.view_type, aliases=cls._plugin.aliases, help='{} view'.format(cls.view_type)) else: sp = subparsers.add_parser(cls.view_type, help='{} view'.format(cls.view_type)) VoltronView.add_generic_arguments(sp) sp.set_defaults(func=cls) def __init__(self, args={}, loaded_config={}): log.debug('Loading view: ' + self.__class__.__name__) self.client = Client(url=voltron.config.view.api_url) self.pm = None self.args = args self.loaded_config = loaded_config self.server_version = None # Commonly set by render method for header and footer formatting self.title = '' self.info = '' self.body = '' # Build configuration self.build_config() log.debug("View config: " + pprint.pformat(self.config)) log.debug("Args: " + str(self.args)) # Let subclass do any setup it needs to do self.setup() # Override settings from command line args if self.args.header != None: self.config.header.show = self.args.header if self.args.footer != None: self.config.footer.show = self.args.footer # Setup a SIGWINCH handler so we do reasonable things on resize try: signal.signal(signal.SIGWINCH, self.sigwinch_handler) except: pass def build_config(self): # Start with all_views config self.config = self.loaded_config.view.all_views # Add view-specific config self.config.type = self.view_type name = self.view_type + '_view' if 'view' in self.loaded_config and name in self.loaded_config.view: self.config.update(self.loaded_config.view[name]) # Add named config if self.args.name != None: self.config.update(self.loaded_config[self.args.name]) # Apply view-specific command-line args self.apply_cli_config() def apply_cli_config(self): if self.args.header != None: self.config.header.show = self.args.header if self.args.footer != None: self.config.footer.show = self.args.footer def setup(self): log.debug('Base view class setup') def cleanup(self): log.debug('Base view class cleanup') def run(self): res = None os.system('clear') while True: try: # get the server version if not self.server_version: self.server_version = self.client.perform_request('version') # if the server supports async mode, use it, as some views may only work in async mode if self.server_version.capabilities and 'async' in self.server_version.capabilities: self.block = False elif self.supports_blocking: self.block = True else: raise BlockingNotSupportedError("Debugger requires blocking mode") # render the view. if this view is running in asynchronous mode, the view should return immediately. self.render() # if the view is not blocking (server supports async || view doesn't support sync), block until the # debugger stops again if not self.block: done = False while not done: res = self.client.perform_request('version', block=True) if res.is_success: done = True except ConnectionError as e: # what the hell, requests? a message is a message, not a fucking nested error object try: msg = e.message.args[1].strerror except: try: msg = e.message.args[0] except: msg = str(e) traceback.print_exc() # if we're not connected, render an error and try again in a second self.do_render(error='Error: {}'.format(msg)) self.server_version = None time.sleep(1) def render(self): log.warning('Might wanna implement render() in this view eh') def do_render(error=None): pass def should_reconnect(self): try: return self.loaded_config.view.reconnect except: return True def sigwinch_handler(self, sig, stack): pass class TerminalView (VoltronView): def __init__(self, *a, **kw): self.init_window() self.trunc_top = False super(TerminalView, self).__init__(*a, **kw) def init_window(self): self.t = Terminal() print(self.t.civis) if cursor: cursor.hide() def cleanup(self): log.debug('Cleaning up view') print(self.t.cnorm) if cursor: cursor.show() def clear(self): # blessed's clear doesn't work properly on windaz # maybe figure out the right way to do it some time os.system('clear') def render(self): self.do_render() def do_render(self, error=None): # Clear the screen self.clear() # If we got an error, we'll use that as the body if error: self.body = self.colour(error, 'red') # Refresh the formatted body self.fmt_body = self.body # Pad and truncate the body self.pad_body() self.truncate_body() # Print the header, body and footer try: if self.config.header.show: print(self.format_header_footer(self.config.header)) print(self.fmt_body, end='') if self.config.footer.show: print('\n' + self.format_header_footer(self.config.footer), end='') sys.stdout.flush() except IOError as e: # if we get an EINTR while printing, just do it again if e.errno == socket.EINTR: self.do_render() def sigwinch_handler(self, sig, stack): self.do_render() def window_size(self): height, width = subprocess.check_output(['stty','size']).split() height = int(height) - int(self.config.pad.pad_bottom) width = int(width) - int(self.config.pad.pad_right) return (height, width) def body_height(self): height, width = self.window_size() if self.config.header.show: height -= 1 if self.config.footer.show: height -= 1 return height def colour(self, text='', colour=None, background=None, attrs=[]): s = '' if colour != None: s += fmt_esc(colour) if background != None: s += fmt_esc('b_'+background) if attrs != []: s += ''.join(map(lambda x: fmt_esc('a_'+x), attrs)) s += text s += fmt_esc('reset') return s def format_header_footer(self, c): height, width = self.window_size() # Get values for labels l = getattr(self, c.label_left.name) if c.label_left.name != None else '' r = getattr(self, c.label_right.name) if c.label_right.name != None else '' p = c.pad llen = len(l) rlen = len(r) # Add colour l = self.colour(l, c.label_left.colour, c.label_left.bg_colour, c.label_left.attrs) r = self.colour(r, c.label_right.colour, c.label_right.bg_colour, c.label_right.attrs) p = self.colour(p, c.colour, c.bg_colour, c.attrs) # Build data = l + (width - llen - rlen)*p + r return data def pad_body(self): height, width = self.window_size() lines = self.fmt_body.split('\n') pad = self.body_height() - len(lines) if pad < 0: pad = 0 self.fmt_body += int(pad)*'\n' def truncate_body(self): height, width = self.window_size() # truncate lines horizontally lines = [] for line in self.fmt_body.split('\n'): s = AnsiString(line) if len(s) > width: line = s[:width-1] + self.colour('>', 'red') lines.append(line) # truncate body vertically if len(lines) > self.body_height(): if self.trunc_top: lines = lines[len(lines) - self.body_height():] else: lines = lines[:self.body_height()] self.fmt_body = '\n'.join(lines) def merge(d1, d2): for k1,v1 in d1.items(): if isinstance(v1, dict) and k1 in d2.keys() and isinstance(d2[k1], dict): merge(v1, d2[k1]) else: d2[k1] = v1 return d2