├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── angularview │ ├── angularview.py │ └── static │ │ ├── css │ │ ├── solarized.css │ │ └── style.css │ │ ├── index.html │ │ └── js │ │ ├── app.js │ │ ├── controllers.js │ │ └── services.js ├── client.py ├── command.py └── view.py ├── install.sh ├── setup.cfg ├── setup.py ├── tests ├── Vagrantfile ├── __init__.py ├── api_message_tests.py ├── common.py ├── frontend_tests.py ├── gdb_cli_tests.py ├── http_api_tests.py ├── inferior.c ├── lldb_api_tests.py ├── lldb_cli_tests.py ├── test.c └── testinit.lldb └── voltron ├── __init__.py ├── __main__.py ├── api.py ├── colour.py ├── config └── default.cfg ├── core.py ├── dbg.py ├── entry.py ├── lexers.py ├── main.py ├── plugin.py ├── plugins ├── __init__.py ├── api │ ├── __init__.py │ ├── backtrace.py │ ├── breakpoints.py │ ├── command.py │ ├── dereference.py │ ├── disassemble.py │ ├── memory.py │ ├── null.py │ ├── plugins.py │ ├── registers.py │ ├── stack.py │ ├── state.py │ ├── targets.py │ ├── version.py │ └── write_memory.py ├── debugger │ ├── __init__.py │ ├── dbg_gdb.py │ ├── dbg_lldb.py │ ├── dbg_mock.py │ ├── dbg_vdb.py │ └── dbg_windbg.py └── view │ ├── __init__.py │ ├── backtrace.py │ ├── breakpoints.py │ ├── command.py │ ├── disasm.py │ ├── memory.py │ └── register.py ├── rdb.py ├── repl.py ├── styles.py └── view.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### Python ### 2 | *.py[cod] 3 | 4 | # C extensions 5 | *.so 6 | 7 | # Packages 8 | *.egg 9 | *.egg-info 10 | dist 11 | build 12 | eggs 13 | parts 14 | bin 15 | var 16 | sdist 17 | develop-eggs 18 | .installed.cfg 19 | lib 20 | lib64 21 | __pycache__ 22 | 23 | # Installer logs 24 | pip-log.txt 25 | 26 | # Unit test / coverage reports 27 | .coverage 28 | .tox 29 | nosetests.xml 30 | 31 | # Translations 32 | *.mo 33 | 34 | # Mr Developer 35 | .mr.developer.cfg 36 | .project 37 | .pydevproject 38 | 39 | # Rope 40 | .ropeproject 41 | 42 | .DS_Store 43 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | matrix: 4 | include: 5 | - os: linux 6 | sudo: required 7 | dist: trusty 8 | python: "3.3" 9 | # - os: linux 10 | # sudo: required 11 | # dist: trusty 12 | # python: "3.4" 13 | # - os: linux 14 | # sudo: required 15 | # dist: trusty 16 | # python: "3.5" 17 | # - os: osx 18 | # osx_image: xcode7.2 19 | # language: generic 20 | 21 | addons: 22 | apt: 23 | packages: 24 | - build-essential 25 | - gdb 26 | 27 | before_install: 28 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; brew install python ; fi 29 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get update -qq ; sudo apt-get install lldb-3.4 ; fi 30 | 31 | install: 32 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install mock pexpect nose ; fi 33 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install mock pexpect nose --user; fi 34 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then pip install . ; fi 35 | - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then pip install . --user; fi 36 | 37 | script: 38 | - python --version 39 | - mkdir ~/.voltron 40 | - echo '{"general":{"debug_logging":true}}' >~/.voltron/config 41 | - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then python -m nose -sv tests/gdb_cli_tests.py ; fi 42 | - python -m nose -sv tests/lldb_cli_tests.py 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 snare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Voltron 2 | ======= 3 | 4 | [![build](https://travis-ci.org/snare/voltron.svg?branch=master)](https://travis-ci.org/snare/voltron/) 5 | 6 | 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. 7 | 8 | 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. 9 | 10 | Built-in views are provided for: 11 | 12 | - Registers 13 | - Disassembly 14 | - Stack 15 | - Memory 16 | - Breakpoints 17 | - Backtrace 18 | 19 | The author's setup looks something like this: 20 | 21 | ![voltron example LLDB](http://i.imgur.com/9nukztA.png) 22 | 23 | Any debugger command can be split off into a view and highlighted with a specified Pygments lexer: 24 | 25 | ![command views](http://i.imgur.com/RbYQYXp.png) 26 | 27 | More screenshots are [here](https://github.com/snare/voltron/wiki/Screenshots). 28 | 29 | Support 30 | ------- 31 | 32 | Voltron supports LLDB, GDB, VDB and WinDbg/CDB (via [PyKD](https://pykd.codeplex.com/)) and runs on macOS, Linux and Windows. 33 | 34 | WinDbg support is still fairly new, please [open an issue](https://github.com/snare/voltron/issues) if you have problems. 35 | 36 | The following architectures are supported: 37 | 38 | | | lldb | gdb | vdb | windbg | 39 | |---------|------|-----|-----|--------| 40 | | x86 | ✓ | ✓ | ✓ | ✓ | 41 | | x86_64 | ✓ | ✓ | ✓ | ✓ | 42 | | arm | ✓ | ✓ | ✓ | ✗ | 43 | | arm64 | ✓ | ✗ | ✗ | ✗ | 44 | | powerpc | ✗ | ✓ | ✗ | ✗ | 45 | 46 | Installation 47 | ------------ 48 | 49 | **Note:** Only macOS and Debian derivatives are fully supported by the install script. It should hopefully not fail on other Linux distros, but it won't try to install package dependencies. If you're using another distro, have a look at `install.sh` to work out what dependencies you might need to install before running it. 50 | 51 | Download the source and run the install script: 52 | 53 | $ git clone https://github.com/snare/voltron 54 | $ cd voltron 55 | $ ./install.sh 56 | 57 | By default, the install script will install into the user's `site-packages` directory. If you want to install into the system `site-packages`, use the `-s` flag: 58 | 59 | $ ./install.sh -s 60 | 61 | You can also install into a virtual environment (for LLDB only) like this: 62 | 63 | $ ./install.sh -v /path/to/venv -b lldb 64 | 65 | If you are on Windows without a shell, have problems installing, or would prefer to install manually, please see the [manual installation documentation](https://github.com/snare/voltron/wiki/Installation). 66 | 67 | Quick Start 68 | ----------- 69 | 70 | 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 macOS it might be */Library/Python/2.7/site-packages/voltron/entry.py*. The `install.sh` script will add this to your `.gdbinit` or `.lldbinit` file automatically if it detects GDB or LLDB in your path. 71 | 72 | LLDB: 73 | 74 | command script import /path/to/voltron/entry.py 75 | 76 | GDB: 77 | 78 | source /path/to/voltron/entry.py 79 | 80 | 2. Start your debugger and initialise Voltron manually if necessary. 81 | 82 | On recent versions of LLDB you do not need to initialise Voltron manually: 83 | 84 | $ lldb target_binary 85 | 86 | On older versions of LLDB you need to call `voltron init` after you load the inferior: 87 | 88 | $ lldb target_binary 89 | (lldb) voltron init 90 | 91 | GDB: 92 | 93 | $ gdb target_binary 94 | 95 | VDB: 96 | 97 | $ ./vdbbin target_binary 98 | > script /path/to/voltron/entry.py 99 | 100 | WinDbg/CDB is only supported run via Bash with a Linux userland. The author tests with [Git Bash](https://git-for-windows.github.io) and [ConEmu](http://conemu.github.io). PyKD and Voltron can be loaded in one command when launching the debugger: 101 | 102 | $ cdb -c '.load C:\path\to\pykd.pyd ; !py --global C:\path\to\voltron\entry.py' target_binary 103 | 104 | 3. In another terminal (I use iTerm panes) start one of the UI views. On LLDB, WinDbg and GDB the views will update immediately. On VDB they will not update until the inferior stops (at a breakpoint, after a step, etc): 105 | 106 | $ voltron view register 107 | $ voltron view stack 108 | $ voltron view disasm 109 | $ voltron view backtrace 110 | 111 | 4. Set a breakpoint and run your inferior. 112 | 113 | (*db) b main 114 | (*db) run 115 | 116 | 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. 117 | 118 | Documentation 119 | ------------- 120 | 121 | See the [wiki](https://github.com/snare/voltron/wiki) on github. 122 | 123 | FAQ 124 | --- 125 | 126 | **Q.** Why am I getting an `ImportError` loading Voltron? 127 | 128 | **A.** You might have multiple versions of Python installed and have installed Voltron using the wrong one. See the more detailed [installation instructions](https://github.com/snare/voltron/wiki/Installation). 129 | 130 | **Q.** [GEF](https://github.com/hugsy/gef)? [PEDA](https://github.com/longld/peda)? [PwnDbg](https://github.com/pwndbg/pwndbg)? [fG's gdbinit](https://github.com/gdbinit/gdbinit)? 131 | 132 | **A.** All super great extensions for GDB. These tools primarily provide sets of additional commands for exploitation tasks, but each also provides a "context" display with a view of registers, stack, code, etc, like Voltron. These tools print their context display in the debugger console each time the debugger stops. Voltron takes a different approach by embedding an RPC server implant in the debugger and enabling the attachment of views from other terminals (or even web browsers, or now [synchronising with Binary Ninja](https://github.com/snare/binja)), which allows the user to build a cleaner multi-window interface to their debugger. Voltron works great alongside all of these tools. You can just disable the context display in your GDB extension of choice and hook up some Voltron views, while still getting all the benefits of the useful commands added by these tools. 133 | 134 | Bugs and Errata 135 | --------------- 136 | 137 | See the [issue tracker](https://github.com/snare/voltron/issues) on github for more information or to submit issues. 138 | 139 | If you're experiencing an `ImportError` loading Voltron, please ensure you've followed the [installation instructions](https://github.com/snare/voltron/wiki/Installation) for your platform. 140 | 141 | ### LLDB 142 | 143 | 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. 144 | 145 | ### WinDbg 146 | 147 | More information about WinDbg/CDB support [here](https://github.com/snare/voltron/wiki/Installation#windbg). 148 | 149 | ### Misc 150 | 151 | The authors primarily use Voltron with the most recent version of LLDB on macOS. We will try to test everything on as many platforms and architectures as possible before releases, but LLDB/macOS/x64 is going to be by far the most frequently-used combination. Hopefully Voltron doesn't set your pets on fire, but YMMV. 152 | 153 | License 154 | ------- 155 | 156 | See the [LICENSE](https://github.com/snare/voltron/blob/master/LICENSE) file. 157 | 158 | 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. 159 | 160 | Credits 161 | ------- 162 | 163 | Thanks to my former employers Assurance and Azimuth Security for giving me time to spend working on this. 164 | 165 | Props to [richo](http://github.com/richo) for all his contributions to Voltron. 166 | 167 | [fG!](http://github.com/gdbinit)'s gdbinit was the original inspiration for this project. 168 | 169 | Thanks to [Willi](http://github.com/williballenthin) for implementing the VDB support. 170 | 171 | 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. 172 | 173 | Thanks to [grazfather](http://github.com/grazfather) for ongoing contributions. 174 | -------------------------------------------------------------------------------- /examples/angularview/angularview.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import pygments 3 | from voltron.plugin import * 4 | from voltron.lexers import * 5 | from voltron.api import * 6 | 7 | from flask import * 8 | 9 | log = logging.getLogger('api') 10 | 11 | 12 | class AngularViewPlugin(WebPlugin): 13 | name = 'angularview' 14 | 15 | 16 | class FormatDisassemblyRequest(APIRequest): 17 | _fields = {'disassembly': True} 18 | 19 | def dispatch(self): 20 | try: 21 | res = FormatDisassemblyResponse( 22 | disassembly=pygments.highlight(self.disassembly.strip(), LLDBIntelLexer(), pygments.formatters.HtmlFormatter())) 23 | except Exception as e: 24 | msg = "Exception formatting disassembly: {}".format(repr(e)) 25 | log.exception(msg) 26 | res = APIGenericErrorResponse(msg) 27 | 28 | return res 29 | 30 | 31 | class FormatDisassemblyResponse(APIResponse): 32 | _fields = {'disassembly': True} 33 | 34 | 35 | class FormatDisassemblyPlugin(APIPlugin): 36 | request = "format_disasm" 37 | request_class = FormatDisassemblyRequest 38 | response_class = FormatDisassemblyResponse 39 | -------------------------------------------------------------------------------- /examples/angularview/static/css/solarized.css: -------------------------------------------------------------------------------- 1 | /* Solarized Dark 2 | 3 | For use with Jekyll and Pygments 4 | 5 | http://ethanschoonover.com/solarized 6 | 7 | SOLARIZED HEX ROLE 8 | --------- -------- ------------------------------------------ 9 | base03 #002b36 background 10 | base01 #586e75 comments / secondary content 11 | base1 #93a1a1 body text / default code / primary content 12 | orange #cb4b16 constants 13 | red #dc322f regex, special keywords 14 | blue #268bd2 reserved keywords 15 | cyan #2aa198 strings, numbers 16 | green #859900 operators, other keywords 17 | */ 18 | 19 | /*.highlight { background-color: #002b36; color: #93a1a1 }*/ 20 | .highlight { background-color: #073642; color: #93a1a1 } 21 | .highlight .c { color: #586e75 } /* Comment */ 22 | .highlight .err { color: #93a1a1 } /* Error */ 23 | .highlight .g { color: #93a1a1 } /* Generic */ 24 | .highlight .k { color: #859900 } /* Keyword */ 25 | .highlight .l { color: #93a1a1 } /* Literal */ 26 | .highlight .n { color: #93a1a1 } /* Name */ 27 | .highlight .o { color: #859900 } /* Operator */ 28 | .highlight .x { color: #cb4b16 } /* Other */ 29 | .highlight .p { color: #93a1a1 } /* Punctuation */ 30 | .highlight .cm { color: #586e75 } /* Comment.Multiline */ 31 | .highlight .cp { color: #859900 } /* Comment.Preproc */ 32 | .highlight .c1 { color: #586e75 } /* Comment.Single */ 33 | .highlight .cs { color: #859900 } /* Comment.Special */ 34 | .highlight .gd { color: #2aa198 } /* Generic.Deleted */ 35 | .highlight .ge { color: #93a1a1; font-style: italic } /* Generic.Emph */ 36 | .highlight .gr { color: #dc322f } /* Generic.Error */ 37 | .highlight .gh { color: #cb4b16 } /* Generic.Heading */ 38 | .highlight .gi { color: #859900 } /* Generic.Inserted */ 39 | .highlight .go { color: #93a1a1 } /* Generic.Output */ 40 | .highlight .gp { color: #93a1a1 } /* Generic.Prompt */ 41 | .highlight .gs { color: #93a1a1; font-weight: bold } /* Generic.Strong */ 42 | .highlight .gu { color: #cb4b16 } /* Generic.Subheading */ 43 | .highlight .gt { color: #93a1a1 } /* Generic.Traceback */ 44 | .highlight .kc { color: #cb4b16 } /* Keyword.Constant */ 45 | .highlight .kd { color: #268bd2 } /* Keyword.Declaration */ 46 | .highlight .kn { color: #859900 } /* Keyword.Namespace */ 47 | .highlight .kp { color: #859900 } /* Keyword.Pseudo */ 48 | .highlight .kr { color: #268bd2 } /* Keyword.Reserved */ 49 | .highlight .kt { color: #dc322f } /* Keyword.Type */ 50 | .highlight .ld { color: #93a1a1 } /* Literal.Date */ 51 | .highlight .m { color: #2aa198 } /* Literal.Number */ 52 | .highlight .s { color: #2aa198 } /* Literal.String */ 53 | .highlight .na { color: #93a1a1 } /* Name.Attribute */ 54 | .highlight .nb { color: #B58900 } /* Name.Builtin */ 55 | .highlight .nc { color: #268bd2 } /* Name.Class */ 56 | .highlight .no { color: #cb4b16 } /* Name.Constant */ 57 | .highlight .nd { color: #268bd2 } /* Name.Decorator */ 58 | .highlight .ni { color: #cb4b16 } /* Name.Entity */ 59 | .highlight .ne { color: #cb4b16 } /* Name.Exception */ 60 | .highlight .nf { color: #268bd2 } /* Name.Function */ 61 | .highlight .nl { color: #93a1a1 } /* Name.Label */ 62 | .highlight .nn { color: #93a1a1 } /* Name.Namespace */ 63 | .highlight .nx { color: #93a1a1 } /* Name.Other */ 64 | .highlight .py { color: #93a1a1 } /* Name.Property */ 65 | .highlight .nt { color: #268bd2 } /* Name.Tag */ 66 | .highlight .nv { color: #268bd2 } /* Name.Variable */ 67 | .highlight .ow { color: #859900 } /* Operator.Word */ 68 | .highlight .w { color: #93a1a1 } /* Text.Whitespace */ 69 | .highlight .mf { color: #2aa198 } /* Literal.Number.Float */ 70 | .highlight .mh { color: #2aa198 } /* Literal.Number.Hex */ 71 | .highlight .mi { color: #2aa198 } /* Literal.Number.Integer */ 72 | .highlight .mo { color: #2aa198 } /* Literal.Number.Oct */ 73 | .highlight .sb { color: #586e75 } /* Literal.String.Backtick */ 74 | .highlight .sc { color: #2aa198 } /* Literal.String.Char */ 75 | .highlight .sd { color: #93a1a1 } /* Literal.String.Doc */ 76 | .highlight .s2 { color: #2aa198 } /* Literal.String.Double */ 77 | .highlight .se { color: #cb4b16 } /* Literal.String.Escape */ 78 | .highlight .sh { color: #93a1a1 } /* Literal.String.Heredoc */ 79 | .highlight .si { color: #2aa198 } /* Literal.String.Interpol */ 80 | .highlight .sx { color: #2aa198 } /* Literal.String.Other */ 81 | .highlight .sr { color: #dc322f } /* Literal.String.Regex */ 82 | .highlight .s1 { color: #2aa198 } /* Literal.String.Single */ 83 | .highlight .ss { color: #2aa198 } /* Literal.String.Symbol */ 84 | .highlight .bp { color: #268bd2 } /* Name.Builtin.Pseudo */ 85 | .highlight .vc { color: #268bd2 } /* Name.Variable.Class */ 86 | .highlight .vg { color: #268bd2 } /* Name.Variable.Global */ 87 | .highlight .vi { color: #268bd2 } /* Name.Variable.Instance */ 88 | .highlight .il { color: #2aa198 } /* Literal.Number.Integer.Long */ 89 | -------------------------------------------------------------------------------- /examples/angularview/static/css/style.css: -------------------------------------------------------------------------------- 1 | .reg_label { 2 | color: #b58900; 3 | font-weight: bold; 4 | } 5 | 6 | .reg_highlight { 7 | color: #dc322f; 8 | } 9 | 10 | .reg_normal { 11 | } 12 | 13 | #tile { 14 | display:inline-block; 15 | vertical-align: top; 16 | background-color: #073642; 17 | padding-left: 0.5em; 18 | padding-right: 0.5em; 19 | padding-top: 0.3em; 20 | padding-bottom: 0.3em; 21 | } 22 | 23 | pre { 24 | margin: 0; 25 | } 26 | 27 | #disasm { 28 | background-color: #073642; 29 | } -------------------------------------------------------------------------------- /examples/angularview/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | voltron 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
registers
15 |
16 | {{reg.name}} 17 | {{reg.value}} 18 |
19 |
20 |
21 |
disassembly
22 |
23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /examples/angularview/static/js/app.js: -------------------------------------------------------------------------------- 1 | angular.module('VoltronApp', [ 2 | 'VoltronApp.controllers', 3 | 'VoltronApp.services' 4 | ]) 5 | .config(function($sceProvider) { 6 | $sceProvider.enabled(false); 7 | }); 8 | -------------------------------------------------------------------------------- /examples/angularview/static/js/controllers.js: -------------------------------------------------------------------------------- 1 | function f_hex(number, pad) 2 | { 3 | return ("000000000000000000000000000000" + number.toString(16)).substr(-pad).toUpperCase(); 4 | } 5 | 6 | angular.module('VoltronApp.controllers', []). 7 | controller('voltronController', function($scope, voltronAPIservice) 8 | { 9 | $scope.version = null; 10 | $scope.registers = []; 11 | $scope.disassembly = null; 12 | var state = null; 13 | var new_regs = null; 14 | var old_regs = []; 15 | 16 | voltronAPIservice.version().success(function (response) { 17 | $scope.version = response.data; 18 | }); 19 | 20 | var format_registers = function(new_regs, old_regs, arch) { 21 | fmt = [] 22 | if (arch == 'x86_64') { 23 | regs = ['rip','rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','r8','r9','r10','r11','r12','r13','r14','r15'] 24 | for (i = 0; i < regs.length; i++) { 25 | fmt.push({ 26 | name: (regs[i].length == 2 ? String.fromCharCode(160) + regs[i] : regs[i]), 27 | value: f_hex(new_regs[regs[i]], 16), 28 | class: (new_regs[regs[i]] != old_regs[regs[i]] ? "reg_highlight" : "reg_normal") 29 | }) 30 | } 31 | } 32 | return fmt 33 | } 34 | 35 | var format_disasm = function(disasm) { 36 | lines = disasm.split(); 37 | 38 | // trim lldb's "inferior`main:" so hljs works 39 | if (lines[0].indexOf("`") > -1) { 40 | lines.splice(0, 1); 41 | } 42 | 43 | return lines.join('\n'); 44 | } 45 | 46 | var update = function() { 47 | // get target info 48 | voltronAPIservice.targets().success(function (response) { 49 | targets = response.data.targets; 50 | 51 | // make sure we have a target 52 | if (targets[0]['arch'] != null) { 53 | // update registers 54 | voltronAPIservice.registers().success(function (response) { 55 | // get new register values 56 | new_regs = response.data.registers 57 | 58 | // format registers 59 | $scope.registers = format_registers(new_regs, old_regs, targets[0]['arch']); 60 | 61 | // keep old registers 62 | old_regs = new_regs; 63 | }); 64 | 65 | // update disassembly 66 | voltronAPIservice.disassemble(null, 32).success(function (response) { 67 | $scope.disassembly = response.data.formatted; 68 | }); 69 | } 70 | }); 71 | } 72 | 73 | var poll = function() { 74 | // wait for the next state change 75 | response = voltronAPIservice.wait(30).success(function (response) { 76 | if (response.status == "success") { 77 | update(); 78 | } 79 | 80 | // wait for the next state change 81 | poll(); 82 | }); 83 | }; 84 | 85 | // initial update 86 | update(); 87 | 88 | // start waiting for debugger stops 89 | poll(); 90 | }); -------------------------------------------------------------------------------- /examples/angularview/static/js/services.js: -------------------------------------------------------------------------------- 1 | angular.module('VoltronApp.services', []). 2 | factory('voltronAPIservice', function($http) 3 | { 4 | var voltronAPI = {}; 5 | 6 | function createRequest(requestType, data) { 7 | return {type: "request", request: requestType, data: data} 8 | } 9 | 10 | voltronAPI.request = function(request) { 11 | return $http({ 12 | method: 'POST', 13 | url: '/api/request', 14 | data: request 15 | }); 16 | } 17 | 18 | voltronAPI.disassemble_format = function(data) { 19 | res = voltronAPI.request(createRequest('disassemble', {address: address, count: count})) 20 | return $http({ 21 | method: 'POST', 22 | url: '/api/request', 23 | data: createRequest('format_disasm', {disassembly: res.data.disassembly}) 24 | }); 25 | } 26 | 27 | voltronAPI.disassemble = function(address, count) { 28 | return voltronAPI.request(createRequest('disassemble', {address: address, count: count})) 29 | } 30 | 31 | voltronAPI.command = function(command) { 32 | return voltronAPI.request(createRequest('command', {command: command})) 33 | } 34 | 35 | voltronAPI.targets = function() { 36 | return voltronAPI.request(createRequest('targets', {})) 37 | } 38 | 39 | voltronAPI.memory = function(address, length) { 40 | return voltronAPI.request(createRequest('memory', {address: address, length: length})) 41 | } 42 | 43 | voltronAPI.registers = function() { 44 | return voltronAPI.request(createRequest('registers', {})) 45 | } 46 | 47 | voltronAPI.stack = function(length) { 48 | return voltronAPI.request(createRequest('stack', {length: length})) 49 | } 50 | 51 | voltronAPI.state = function() { 52 | return voltronAPI.request(createRequest('state', {})) 53 | } 54 | 55 | voltronAPI.version = function() { 56 | return voltronAPI.request(createRequest('version', {})) 57 | } 58 | 59 | return voltronAPI; 60 | }); 61 | -------------------------------------------------------------------------------- /examples/client.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Example Voltron client. 4 | 5 | Start your debugger as follows: 6 | 7 | $ lldb /tmp/inferior 8 | Voltron loaded. 9 | Run `voltron init` after you load a target. 10 | (lldb) target create "/tmp/inferior" 11 | Current executable set to '/tmp/inferior' (x86_64). 12 | (lldb) voltron init 13 | Registered stop-hook 14 | (lldb) b main 15 | Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0 16 | (lldb) run 17 | Process 13185 launched: '/Volumes/Data/Users/snare/code/voltron/repo/tests/inferior' (x86_64) 18 | Process 13185 stopped 19 | * thread #1: tid = 0x1ee63, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 20 | frame #0: 0x0000000100000cf0 inferior`main 21 | inferior`main: 22 | -> 0x100000cf0: push rbp 23 | 0x100000cf1: mov rbp, rsp 24 | 0x100000cf4: sub rsp, 0x50 25 | 0x100000cf8: mov dword ptr [rbp - 0x4], 0x0 26 | 27 | Run this client in another terminal. Each time you `stepi` in the debugger, 28 | the client will output the current RIP: 29 | 30 | $ python client.py 31 | Instruction pointer is: 0x100000CFF 32 | Instruction pointer is: 0x100000D02 33 | Instruction pointer is: 0x100000D06 34 | Instruction pointer is: 0x100000D0D 35 | Instruction pointer is: 0x100000D15 36 | Instruction pointer is: 0x100000D1C 37 | """ 38 | 39 | import voltron 40 | from voltron.core import Client 41 | 42 | 43 | def main(): 44 | # Create a client and connect to the server 45 | client = Client() 46 | 47 | # Main event loop 48 | while True: 49 | # Wait for the debugger to stop again 50 | res = client.perform_request('version', block=True) 51 | if res.is_success: 52 | # If nothing went wrong, get the instruction pointer and print it 53 | res = client.perform_request('registers', registers=['rip']) 54 | if res.is_success: 55 | print("Instruction pointer is: 0x{:X}".format(res.registers['rip'])) 56 | else: 57 | print("Failed to get registers: {}".format(res)) 58 | else: 59 | print("Error waiting for the debugger to stop: {}".format(res)) 60 | break 61 | 62 | 63 | if __name__ == "__main__": 64 | main() 65 | -------------------------------------------------------------------------------- /examples/command.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example command plugin 3 | 4 | Copy this to your ~/.voltron/plugins directory. It will be loaded when 5 | Voltron is initialised, and register a debugger command that works as follows: 6 | 7 | $ lldb /tmp/inferior 8 | Voltron loaded. 9 | Run `voltron init` after you load a target. 10 | (lldb) target create "/tmp/inferior" 11 | Current executable set to '/tmp/inferior' (x86_64). 12 | (lldb) b main 13 | Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0 14 | (lldb) run 15 | Process 12561 launched: '/tmp/inferior' (x86_64) 16 | Process 12561 stopped 17 | * thread #1: tid = 0x1d33f, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 18 | frame #0: 0x0000000100000cf0 inferior`main 19 | inferior`main: 20 | -> 0x100000cf0: push rbp 21 | 0x100000cf1: mov rbp, rsp 22 | 0x100000cf4: sub rsp, 0x50 23 | 0x100000cf8: mov dword ptr [rbp - 0x4], 0x0 24 | (lldb) example 25 | rax 0000000100000CF0 26 | rbx 0000000000000000 27 | rcx 00007FFF5FBFFA70 28 | rdx 00007FFF5FBFF978 29 | rbp 00007FFF5FBFF958 30 | rsp 00007FFF5FBFF948 31 | rdi 0000000000000001 32 | rsi 00007FFF5FBFF968 33 | rip 0000000100000CF0 34 | r8 0000000000000000 35 | r9 00007FFF5FBFEA08 36 | r10 0000000000000032 37 | r11 0000000000000246 38 | r12 0000000000000000 39 | r13 0000000000000000 40 | r14 0000000000000000 41 | r15 0000000000000000 42 | 43 | Provided you stick to the adaptor API that is implemented in every *DBAdaptor 44 | class, custom command plugins should work across all debugger hosts. 45 | 46 | This is a quick example that will only work on an x86_64 target. 47 | """ 48 | 49 | import blessed 50 | import voltron 51 | from voltron.plugin import CommandPlugin 52 | from voltron.command import VoltronCommand 53 | 54 | 55 | class ExampleCommand(VoltronCommand): 56 | def invoke(self, *args): 57 | regs = voltron.debugger.registers() 58 | reg_list = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip', 59 | 'r8','r9','r10','r11','r12','r13','r14','r15'] 60 | for name in reg_list: 61 | print("{t.bold}{:3} {t.normal}{:0=16X}".format(name, regs[name], t=blessed.Terminal())) 62 | 63 | 64 | class ExampleCommandPlugin(CommandPlugin): 65 | name = 'example' 66 | command_class = ExampleCommand -------------------------------------------------------------------------------- /examples/view.py: -------------------------------------------------------------------------------- 1 | """ 2 | Example Voltron view. 3 | 4 | Copy this to your ~/.voltron/plugins directory. When the `voltron view` command 5 | is executed, 'example' should be visible in the list of valid view names. 6 | 7 | Start your debugger as follows: 8 | 9 | $ lldb /tmp/inferior 10 | Voltron loaded. 11 | Run `voltron init` after you load a target. 12 | (lldb) target create "/tmp/inferior" 13 | Current executable set to '/tmp/inferior' (x86_64). 14 | (lldb) voltron init 15 | Registered stop-hook 16 | (lldb) b main 17 | Breakpoint 1: where = inferior`main, address = 0x0000000100000cf0 18 | (lldb) run 19 | Process 13185 launched: '/Volumes/Data/Users/snare/code/voltron/repo/tests/inferior' (x86_64) 20 | Process 13185 stopped 21 | * thread #1: tid = 0x1ee63, 0x0000000100000cf0 inferior`main, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1 22 | frame #0: 0x0000000100000cf0 inferior`main 23 | inferior`main: 24 | -> 0x100000cf0: push rbp 25 | 0x100000cf1: mov rbp, rsp 26 | 0x100000cf4: sub rsp, 0x50 27 | 0x100000cf8: mov dword ptr [rbp - 0x4], 0x0 28 | 29 | Run this view in another terminal (as follows). Each time you `stepi` in the 30 | debugger, the view will update and display the current register values. 31 | 32 | $ voltron view example 33 | """ 34 | 35 | from voltron.view import TerminalView 36 | from voltron.plugin import ViewPlugin 37 | 38 | 39 | class ExampleView(TerminalView): 40 | def render(self, *args, **kwargs): 41 | # Perform the request 42 | res = self.client.perform_request('registers') 43 | if res.is_success: 44 | # Process the registers and set the body to the formatted list 45 | reg_list = ['rax','rbx','rcx','rdx','rbp','rsp','rdi','rsi','rip', 46 | 'r8','r9','r10','r11','r12','r13','r14','r15'] 47 | lines = map(lambda x: '{:3}: {:016X}'.format(x, res.registers[x]), reg_list) 48 | self.body = '\n'.join(lines) 49 | else: 50 | self.body = "Failed to get registers: {}".format(res) 51 | 52 | # Set the title and info 53 | self.title = '[example]' 54 | self.info = 'some infoz' 55 | 56 | # Let the parent do the rendering 57 | super(ExampleView, self).render() 58 | 59 | 60 | class ExampleViewPlugin(ViewPlugin): 61 | name = 'example' 62 | view_class = ExampleView 63 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Install Voltron for whichever debuggers are detected (only GDB and LLDB so 4 | # far). 5 | # 6 | 7 | function usage { 8 | cat </dev/null 2>&1 156 | if [ $? -ne 0 ]; 157 | then 158 | # If not, attempt to install it using ensurepip 159 | echo "Attempting to install pip using 'ensurepip'." 160 | ${SUDO} ${LLDB_PYTHON} -m ensurepip $USER_MODE || return $? 161 | fi 162 | # Some really old pip installations default to the old pypi.python.org, which no longer works. 163 | echo "Attempting to upgrade pip." 164 | ${SUDO} ${LLDB_PYTHON} -m pip install "pip>=$PIP_MIN_VER" $USER_MODE -U --index-url "$PYPI_URL" 165 | if [ $? != 0 ]; 166 | then 167 | # We may still fail here due to TLS incompatibility 168 | # TLS 1.x got turned off 2018-04-11 169 | # https://status.python.org/incidents/hdx7w97m5hr8 170 | # Curl may be new enough to support TLS 1.2, so try to curl the pip installer from pypa.io 171 | # It's able to download and install pip without TLS errors somehow 172 | echo "Failed to upgrade pip." 173 | echo "Attempting to fall back to installation via curl." 174 | curl_get_pip || return $? 175 | fi 176 | } 177 | 178 | function get_lldb_python_exe { 179 | # Find the Python version used by LLDB 180 | local lldb_pyver=$(${LLDB} -Q -x -b --one-line 'script import platform; print(".".join(platform.python_version_tuple()[:2]))'|tail -1) 181 | local lldb_python=$(${LLDB} -Q -x -b --one-line 'script import sys; print(sys.executable)'|tail -1) 182 | 183 | lldb_python=$(${LLDB} -Q -x -b --one-line 'script import sys; print(sys.executable)'|tail -1) 184 | local lldb_python_basename=$(basename "${lldb_python}") 185 | if [ "python" = "$lldb_python_basename" ]; 186 | then 187 | lldb_python="${lldb_python/%$lldb_pyver/}${lldb_pyver}" 188 | elif [ "lldb" = "$lldb_python_basename" ]; 189 | then 190 | # newer lldb versions report sys.path as /path/to/lldb instead of python 191 | # sys.exec_path still appears to be the parent path of bin/python though 192 | local lldb_python_exec_prefix=$(${LLDB} -Q -x -b --one-line 'script import sys; print(sys.exec_prefix)'|tail -1) 193 | lldb_python="$lldb_python_exec_prefix/bin/python" 194 | lldb_python="${lldb_python/%$lldb_pyver/}${lldb_pyver}" 195 | fi 196 | 197 | echo "$lldb_python" 198 | 199 | } 200 | 201 | if [ "${BACKEND_GDB}" -eq 1 ]; then 202 | # Find the Python version used by GDB 203 | GDB_PYVER=$(${GDB} -batch -q --nx -ex 'pi import platform; print(".".join(platform.python_version_tuple()[:2]))') 204 | GDB_PYTHON=$(${GDB} -batch -q --nx -ex 'pi import sys; print(sys.executable)') 205 | GDB_PYTHON="${GDB_PYTHON/%$GDB_PYVER/}${GDB_PYVER}" 206 | 207 | install_packages 208 | 209 | if [ -z $USER_MODE ]; then 210 | GDB_SITE_PACKAGES=$(${GDB} -batch -q --nx -ex 'pi import site; print(site.getsitepackages()[0])') 211 | else 212 | GDB_SITE_PACKAGES=$(${GDB} -batch -q --nx -ex 'pi import site; print(site.getusersitepackages())') 213 | fi 214 | 215 | # Install Voltron and dependencies 216 | ${SUDO} ${GDB_PYTHON} -m pip install -U $USER_MODE $DEV_MODE . 217 | 218 | # Add Voltron to gdbinit 219 | GDB_INIT_FILE="${HOME}/.gdbinit" 220 | if [ -e ${GDB_INIT_FILE} ]; then 221 | sed -i.bak '/voltron/d' ${GDB_INIT_FILE} 222 | fi 223 | 224 | if [ -z $DEV_MODE ]; then 225 | GDB_ENTRY_FILE="$GDB_SITE_PACKAGES/voltron/entry.py" 226 | else 227 | GDB_ENTRY_FILE="$(pwd)/voltron/entry.py" 228 | fi 229 | echo "source $GDB_ENTRY_FILE" >> ${GDB_INIT_FILE} 230 | fi 231 | 232 | if [ "${BACKEND_LLDB}" -eq 1 ]; then 233 | 234 | LLDB_PYTHON=$(get_lldb_python_exe) || quit "Failed to locate python interpreter." 1 235 | ensure_pip || quit "Failed to install pip." 1 236 | ${LLDB_PYTHON} -m pip install --user --upgrade six || quit "Failed to install or upgrade 'six' python package." 1 237 | 238 | if [ -n "${VENV}" ]; then 239 | echo "Creating virtualenv..." 240 | ${LLDB_PYTHON} -m pip install --user virtualenv 241 | ${LLDB_PYTHON} -m virtualenv "${VENV}" 242 | LLDB_PYTHON="${VENV}/bin/python" 243 | LLDB_SITE_PACKAGES=$(find "${VENV}" -name site-packages) 244 | elif [ -z "${USER_MODE}" ]; then 245 | LLDB_SITE_PACKAGES=$(${LLDB} -Q -x -b --one-line 'script import site; print(site.getsitepackages()[0])'|tail -1) || quit "Failed to locate site-packages." 1 246 | else 247 | LLDB_SITE_PACKAGES=$(${LLDB} -Q -x -b --one-line 'script import site; print(site.getusersitepackages())'|tail -1) || quit "Failed to locate site-packages." 1 248 | fi 249 | 250 | install_packages || quit "Failed to install packages." 1 251 | 252 | if [ "$LLDB_SITE_PACKAGES" == "$GDB_SITE_PACKAGES" ]; then 253 | echo "Skipping installation for LLDB - same site-packages directory" 254 | else 255 | # Install Voltron and dependencies 256 | ${SUDO} ${LLDB_PYTHON} -m pip install -U $USER_MODE $DEV_MODE . || quit "Failed to install voltron." 1 257 | fi 258 | 259 | # Add Voltron to lldbinit 260 | LLDB_INIT_FILE="${HOME}/.lldbinit" 261 | if [ -e ${LLDB_INIT_FILE} ]; then 262 | sed -i.bak '/voltron/d' ${LLDB_INIT_FILE} 263 | fi 264 | 265 | if [ -z "${DEV_MODE}" ]; then 266 | LLDB_ENTRY_FILE="$LLDB_SITE_PACKAGES/voltron/entry.py" 267 | else 268 | LLDB_ENTRY_FILE="$(pwd)/voltron/entry.py" 269 | fi 270 | 271 | if [ -n "${VENV}" ]; then 272 | echo "script import sys;sys.path.append('${LLDB_SITE_PACKAGES}')" >> ${LLDB_INIT_FILE} 273 | fi 274 | echo "command script import $LLDB_ENTRY_FILE" >> ${LLDB_INIT_FILE} 275 | fi 276 | 277 | if [ "${BACKEND_GDB}" -ne 1 ] && [ "${BACKEND_LLDB}" -ne 1 ]; then 278 | # Find system Python 279 | PYTHON=$(command -v python) 280 | PYVER=$(${PYTHON} -c 'import platform; print(".".join(platform.python_version_tuple()[:2]))') 281 | if [ -z $USER_MODE ]; then 282 | PYTHON_SITE_PACKAGES=$(${PYTHON} -c 'import site; print(site.getsitepackages()[0])') 283 | else 284 | PYTHON_SITE_PACKAGES=$(${PYTHON} -c 'import site; print(site.getusersitepackages())') 285 | fi 286 | 287 | install_packages 288 | 289 | # Install Voltron and dependencies 290 | ${SUDO} ${PYTHON} -m pip install -U $USER_MODE $DEV_MODE . 291 | fi 292 | 293 | set +x 294 | echo "==============================================================" 295 | if [ "${BACKEND_GDB}" -eq 1 ]; then 296 | echo "Installed for GDB (${GDB}):" 297 | echo " Python: $GDB_PYTHON" 298 | echo " Packages directory: $GDB_SITE_PACKAGES" 299 | echo " Added voltron to: $GDB_INIT_FILE" 300 | echo " Entry point: $GDB_ENTRY_FILE" 301 | fi 302 | if [ "${BACKEND_LLDB}" -eq 1 ]; then 303 | echo "Installed for LLDB (${LLDB}):" 304 | echo " Python: $LLDB_PYTHON" 305 | echo " Packages directory: $LLDB_SITE_PACKAGES" 306 | echo " Added voltron to: $LLDB_INIT_FILE" 307 | echo " Entry point: $LLDB_ENTRY_FILE" 308 | fi 309 | if [ "${BACKEND_GDB}" -ne 1 ] && [ "${BACKEND_LLDB}" -ne 1 ]; then 310 | if [ -z "${GDB}" ] && [ -z "${LLDB}" ]; then 311 | echo -n "Couldn't find any debuggers. " 312 | else 313 | echo -n "No debuggers selected. " 314 | fi 315 | 316 | echo "Installed using the Python in your path:" 317 | echo " Python: $PYTHON" 318 | echo " Packages directory: $PYTHON_SITE_PACKAGES" 319 | echo " Did not add Voltron to any debugger init files." 320 | fi 321 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [flake8] 5 | per-file-ignores = 6 | dbg_lldb.py:E272,E722,E241 7 | disasm.py:E722 -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from __future__ import print_function 2 | 3 | import sys 4 | import platform 5 | import textwrap 6 | from subprocess import check_output 7 | from setuptools import setup, find_packages 8 | 9 | 10 | def check_install(): 11 | """ 12 | Try to detect the two most common installation errors: 13 | 14 | 1. Installing on macOS using a Homebrew version of Python 15 | 2. Installing on Linux using Python 2 when GDB is linked with Python 3 16 | """ 17 | if platform.system() == 'Darwin' and sys.executable != '/usr/bin/python': 18 | # send warning text to stderr so we can capture version string in a shell script like 19 | # version=$(python3 setup.py --version) 20 | print("*" * 79, file=sys.stderr) 21 | print(textwrap.fill( 22 | "WARNING: You are not using the version of Python included with " 23 | "macOS. If you intend to use Voltron with the LLDB included " 24 | "with Xcode, or GDB installed with Homebrew, it will not work " 25 | "unless it is installed using the system's default Python. If " 26 | "you intend to use Voltron with a debugger installed by some " 27 | "other method, it may be safe to ignore this warning. See the " 28 | "following documentation for more detailed installation " 29 | "instructions: " 30 | "https://github.com/snare/voltron/wiki/Installation", 79), file=sys.stderr) 31 | print("*" * 79, file=sys.stderr) 32 | elif platform.system() == 'Linux': 33 | try: 34 | output = check_output([ 35 | "gdb", "-batch", "-q", "--nx", "-ex", 36 | "pi print(sys.version_info.major)" 37 | ]).decode("utf-8") 38 | gdb_python = int(output) 39 | 40 | if gdb_python != sys.version_info.major: 41 | print("*" * 79, file=sys.stderr) 42 | print(textwrap.fill( 43 | "WARNING: You are installing Voltron using Python {0}.x " 44 | "and GDB is linked with Python {1}.x. GDB will not be " 45 | "able to load Voltron. Please install using Python {1} " 46 | "if you intend to use Voltron with the copy of GDB that " 47 | "is installed. See the following documentation for more " 48 | "detailed installation instructions: " 49 | "https://github.com/snare/voltron/wiki/Installation" 50 | .format(sys.version_info.major, gdb_python), 79), file=sys.stderr) 51 | print("*" * 79, file=sys.stderr) 52 | except: 53 | pass 54 | 55 | 56 | check_install() 57 | 58 | 59 | requirements = [ 60 | 'scruffington>=0.3.9', 61 | 'flask', 62 | 'flask_restful', 63 | 'blessed', 64 | 'pygments', 65 | 'requests', 66 | 'requests_unixsocket', 67 | 'six', 68 | 'pysigset', 69 | 'pygments', 70 | ] 71 | if sys.platform == 'win32': 72 | requirements.append('cursor') 73 | 74 | 75 | setup( 76 | name="voltron", 77 | version="0.1.8", 78 | author="snare", 79 | author_email="snare@ho.ax", 80 | description="A debugger UI", 81 | license="MIT", 82 | keywords="voltron debugger ui gdb lldb vdb " 83 | "vivisect vtrace windbg cdb pykd", 84 | url="https://github.com/snare/voltron", 85 | packages=find_packages(exclude=['tests', 'examples']), 86 | install_requires=requirements, 87 | package_data={'voltron': ['config/*']}, 88 | entry_points={ 89 | 'console_scripts': ['voltron=voltron:main'], 90 | 'pygments.lexers': [ 91 | 'lldb_intel = voltron.lexers:LLDBIntelLexer', 92 | 'lldb_att = voltron.lexers:LLDBATTLexer', 93 | 'gdb_intel = voltron.lexers:GDBIntelLexer', 94 | 'gdb_att = voltron.lexers:GDBATTLexer', 95 | 'vdb_intel = voltron.lexers:VDBIntelLexer', 96 | 'vdb_att = voltron.lexers:VDBATTLexer', 97 | 'windbg_intel = voltron.lexers:WinDbgIntelLexer', 98 | 'windbg_att = voltron.lexers:WinDbgATTLexer', 99 | 'capstone_intel = voltron.lexers:CapstoneIntelLexer', 100 | ], 101 | 'pygments.styles': [ 102 | 'volarized = voltron.styles:VolarizedStyle', 103 | ] 104 | }, 105 | zip_safe=False 106 | ) 107 | -------------------------------------------------------------------------------- /tests/Vagrantfile: -------------------------------------------------------------------------------- 1 | Vagrant.configure(2) do |config| 2 | config.vm.hostname = "voltron" 3 | config.vm.box = "ubuntu/trusty64" 4 | 5 | config.vm.provider :virtualbox do |v| 6 | v.memory = 1024 7 | v.linked_clone = true 8 | end 9 | 10 | config.vm.network "forwarded_port", guest: 5555, host: 5556 11 | 12 | config.vm.synced_folder "../", "/voltron" 13 | config.vm.synced_folder "~/shared", "/shared" 14 | 15 | config.vm.provision "shell", inline: <<-SHELL 16 | apt-get update 17 | apt-get upgrade -y 18 | apt-get install -y \ 19 | build-essential \ 20 | gdb \ 21 | zsh \ 22 | python3-pip \ 23 | lldb-3.8 24 | pip3 install six --upgrade 25 | su - vagrant -c "cd /voltron ; ./install.sh" 26 | mkdir -p /home/vagrant/.voltron 27 | chown vagrant:vagrant /home/vagrant/.voltron -R 28 | cat >/home/vagrant/.voltron/config < Server -> LLDBAdaptor 7 | 8 | Using an instantiated SBDebugger instance 9 | """ 10 | 11 | import tempfile 12 | import sys 13 | import json 14 | import time 15 | import logging 16 | import subprocess 17 | import time 18 | 19 | from mock import Mock 20 | from nose.tools import * 21 | 22 | import voltron 23 | from voltron.core import * 24 | from voltron.api import * 25 | from voltron.plugin import * 26 | 27 | import platform 28 | if platform.system() == 'Darwin': 29 | sys.path.append("/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python") 30 | 31 | try: 32 | import lldb 33 | 34 | from .common import * 35 | 36 | log = logging.getLogger("tests") 37 | 38 | def setup(): 39 | global server, client, target, pm, adaptor, methods 40 | 41 | log.info("setting up API tests") 42 | 43 | # set up voltron 44 | voltron.setup_env() 45 | pm = PluginManager() 46 | plugin = pm.debugger_plugin_for_host('lldb') 47 | adaptor = plugin.adaptor_class() 48 | voltron.debugger = adaptor 49 | 50 | # start up a voltron server 51 | server = Server() 52 | # import pdb;pdb.set_trace() 53 | server.start() 54 | 55 | time.sleep(0.1) 56 | 57 | # set up client 58 | client = Client() 59 | 60 | # compile and load the test inferior 61 | subprocess.call("cc -o tests/inferior tests/inferior.c", shell=True) 62 | target = adaptor.host.CreateTargetWithFileAndArch("tests/inferior", lldb.LLDB_ARCH_DEFAULT) 63 | main_bp = target.BreakpointCreateByName("main", target.GetExecutable().GetFilename()) 64 | 65 | def teardown(): 66 | server.stop() 67 | time.sleep(5) 68 | 69 | def test_state_no_target(): 70 | req = api_request('state') 71 | res = client.send_request(req) 72 | assert res.is_error 73 | assert res.code == 4101 74 | 75 | def test_state_stopped(): 76 | process = target.LaunchSimple(None, None, os.getcwd()) 77 | req = api_request('state') 78 | res = client.send_request(req) 79 | assert res.status == 'success' 80 | assert res.state == "stopped" 81 | target.process.Destroy() 82 | 83 | def test_targets(): 84 | req = api_request('targets') 85 | res = client.send_request(req) 86 | assert res.status == 'success' 87 | t = res.targets[0] 88 | assert t["id"] == 0 89 | assert t["arch"] == "x86_64" 90 | assert t["file"].endswith("inferior") 91 | 92 | def test_registers(): 93 | process = target.LaunchSimple(None, None, os.getcwd()) 94 | req = api_request('registers') 95 | res = client.send_request(req) 96 | assert res.status == 'success' 97 | assert len(res.registers) > 0 98 | assert res.registers['rip'] != 0 99 | target.process.Destroy() 100 | 101 | def test_memory(): 102 | process = target.LaunchSimple(None, None, os.getcwd()) 103 | res = client.perform_request('registers') 104 | rsp = res.registers['rsp'] 105 | res = client.perform_request('memory', address=rsp, length=0x40) 106 | assert res.status == 'success' 107 | assert len(res.memory) > 0 108 | res = client.perform_request('memory', address=rsp, length=0x40, deref=True) 109 | assert res.status == 'success' 110 | assert len(res.deref) > 0 111 | target.process.Destroy() 112 | 113 | def test_stack(): 114 | process = target.LaunchSimple(None, None, os.getcwd()) 115 | req = api_request('stack', length=0x40) 116 | res = client.send_request(req) 117 | assert res.status == 'success' 118 | assert len(res.memory) > 0 119 | target.process.Destroy() 120 | 121 | def test_command(): 122 | process = target.LaunchSimple(None, None, os.getcwd()) 123 | req = api_request('command', command="reg read") 124 | res = client.send_request(req) 125 | assert res.status == 'success' 126 | assert len(res.output) > 0 127 | assert 'rax' in res.output 128 | target.process.Destroy() 129 | 130 | def test_disassemble(): 131 | process = target.LaunchSimple(None, None, os.getcwd()) 132 | req = api_request('disassemble', count=16) 133 | res = client.send_request(req) 134 | assert res.status == 'success' 135 | assert len(res.disassembly) > 0 136 | assert 'push' in res.disassembly 137 | req = api_request('disassemble', count=16, use_capstone=True) 138 | res = client.send_request(req) 139 | assert res.status == 'success' 140 | assert len(res.disassembly) > 0 141 | assert 'push' in res.disassembly 142 | target.process.Destroy() 143 | 144 | def test_dereference(): 145 | process = target.LaunchSimple(None, None, os.getcwd()) 146 | res = client.perform_request('registers') 147 | res = client.perform_request('dereference', pointer=res.registers['rsp']) 148 | assert res.status == 'success' 149 | assert res.output[0][0] == 'pointer' 150 | assert res.output[-1][1] == 'start + 0x1' 151 | target.process.Destroy() 152 | 153 | def test_breakpoints(): 154 | process = target.LaunchSimple(None, None, os.getcwd()) 155 | res = client.perform_request('breakpoints') 156 | assert res.status == 'success' 157 | assert len(res.breakpoints) == 1 158 | assert res.breakpoints[0]['one_shot'] == False 159 | assert res.breakpoints[0]['enabled'] 160 | assert res.breakpoints[0]['id'] == 1 161 | assert res.breakpoints[0]['hit_count'] > 0 162 | assert res.breakpoints[0]['locations'][0]['name'] == "inferior`main" 163 | target.process.Destroy() 164 | 165 | def test_multi_request(): 166 | process = target.LaunchSimple(None, None, os.getcwd()) 167 | reg_res, dis_res = client.send_requests(api_request('registers'), 168 | api_request('disassemble', count=16)) 169 | assert reg_res.status == 'success' 170 | assert len(reg_res.registers) > 0 171 | assert reg_res.registers['rip'] != 0 172 | assert dis_res.status == 'success' 173 | assert len(dis_res.disassembly) > 0 174 | assert 'push' in dis_res.disassembly 175 | target.process.Destroy() 176 | 177 | except: 178 | print("No LLDB") 179 | -------------------------------------------------------------------------------- /tests/gdb_cli_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that test voltron in the gdb cli driver 3 | 4 | Tests: 5 | Client -> Server -> GDBAdaptor 6 | 7 | Inside a GDB instance 8 | """ 9 | 10 | from __future__ import print_function 11 | 12 | import tempfile 13 | import sys 14 | import json 15 | import time 16 | import logging 17 | import pexpect 18 | import os 19 | import six 20 | 21 | from mock import Mock 22 | from nose.tools import * 23 | 24 | import voltron 25 | from voltron.core import * 26 | from voltron.api import * 27 | from voltron.plugin import PluginManager, DebuggerAdaptorPlugin 28 | 29 | from .common import * 30 | 31 | log = logging.getLogger('tests') 32 | 33 | p = None 34 | client = None 35 | 36 | 37 | def setup(): 38 | global p, client, pm 39 | 40 | log.info("setting up GDB CLI tests") 41 | 42 | voltron.setup_env() 43 | 44 | # compile test inferior 45 | pexpect.run("cc -o tests/inferior tests/inferior.c") 46 | 47 | # start debugger 48 | start_debugger() 49 | 50 | 51 | def teardown(): 52 | read_data() 53 | p.terminate(True) 54 | 55 | 56 | def start_debugger(do_break=True): 57 | global p, client 58 | p = pexpect.spawn('gdb') 59 | p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python3.5.0/lib/python3.5/site-packages')") 60 | p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python3.4.3/lib/python3.4/site-packages')") 61 | p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python3.3.6/lib/python3.3/site-packages')") 62 | p.sendline("python import sys;sys.path.append('/home/travis/virtualenv/python2.7.10/lib/python2.7/site-packages')") 63 | p.sendline("source voltron/entry.py") 64 | p.sendline("file tests/inferior") 65 | p.sendline("set disassembly-flavor intel") 66 | p.sendline("voltron init") 67 | if do_break: 68 | p.sendline("b main") 69 | p.sendline("run loop") 70 | read_data() 71 | 72 | time.sleep(5) 73 | 74 | client = Client() 75 | 76 | 77 | def stop_debugger(): 78 | # p.sendline("kill") 79 | read_data() 80 | p.terminate(True) 81 | 82 | 83 | def read_data(): 84 | try: 85 | while True: 86 | data = p.read_nonblocking(size=64, timeout=1) 87 | print(data.decode('UTF-8'), end='') 88 | except: 89 | pass 90 | 91 | 92 | def restart_debugger(do_break=True): 93 | stop_debugger() 94 | start_debugger(do_break) 95 | 96 | 97 | def test_bad_request(): 98 | req = client.create_request('version') 99 | req.request = 'xxx' 100 | res = client.send_request(req) 101 | assert res.is_error 102 | assert res.code == 0x1002 103 | 104 | 105 | def test_version(): 106 | req = client.create_request('version') 107 | res = client.send_request(req) 108 | assert res.api_version == 1.1 109 | assert 'gdb' in res.host_version 110 | 111 | 112 | def test_registers(): 113 | global registers 114 | read_data() 115 | res = client.perform_request('registers') 116 | registers = res.registers 117 | assert res.status == 'success' 118 | assert len(registers) > 0 119 | assert registers['rip'] != 0 120 | 121 | 122 | def test_memory(): 123 | res = client.perform_request('memory', address=registers['rip'], length=0x40) 124 | assert res.status == 'success' 125 | assert len(res.memory) > 0 126 | 127 | 128 | def test_state_stopped(): 129 | res = client.perform_request('state') 130 | assert res.is_success 131 | assert res.state == "stopped" 132 | 133 | 134 | def test_targets(): 135 | res = client.perform_request('targets') 136 | assert res.is_success 137 | assert res.targets[0]['state'] == "stopped" 138 | assert res.targets[0]['arch'] == "x86_64" 139 | assert res.targets[0]['id'] == 0 140 | assert res.targets[0]['file'].endswith('tests/inferior') 141 | 142 | 143 | def test_stack(): 144 | res = client.perform_request('stack', length=0x40) 145 | assert res.status == 'success' 146 | assert len(res.memory) > 0 147 | 148 | 149 | def test_command(): 150 | res = client.perform_request('command', command="info reg") 151 | assert res.status == 'success' 152 | assert len(res.output) > 0 153 | assert 'rax' in res.output 154 | 155 | 156 | def test_disassemble(): 157 | res = client.perform_request('disassemble', count=0x20) 158 | assert res.status == 'success' 159 | assert len(res.disassembly) > 0 160 | assert 'DWORD' in res.disassembly 161 | 162 | 163 | def test_backtrace(): 164 | res = client.perform_request('backtrace') 165 | print(res) 166 | assert res.is_success 167 | assert res.frames[0]['name'] == "main" 168 | assert res.frames[0]['index'] == 0 169 | 170 | 171 | # def test_write_memory(): 172 | # value = six.b("AAAAAAAA") 173 | # res = client.perform_request('write_memory', address=registers['rsp'], value=value) 174 | # assert res.is_success 175 | # res = client.perform_request('memory', address=registers['rsp'], length=len(value)) 176 | # assert res.memory == value 177 | -------------------------------------------------------------------------------- /tests/http_api_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that emulate the debugger adaptor and just test the interaction between 3 | the front end and back end API classes. HTTP edition! 4 | 5 | Tests: 6 | Server (via HTTP) 7 | """ 8 | 9 | import logging 10 | import sys 11 | import json 12 | import time 13 | import subprocess 14 | 15 | from nose.tools import * 16 | 17 | import voltron 18 | from voltron.core import * 19 | from voltron.api import * 20 | from voltron.plugin import * 21 | 22 | import platform 23 | if platform.system() == 'Darwin': 24 | sys.path.append("/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python") 25 | 26 | from .common import * 27 | 28 | import requests 29 | 30 | 31 | log = logging.getLogger('tests') 32 | 33 | 34 | class APIHostNotSupportedRequest(APIRequest): 35 | @server_side 36 | def dispatch(self): 37 | return APIDebuggerHostNotSupportedErrorResponse() 38 | 39 | 40 | class APIHostNotSupportedPlugin(APIPlugin): 41 | request = "host_not_supported" 42 | request_class = APIHostNotSupportedRequest 43 | response_class = APIResponse 44 | 45 | 46 | def setup(): 47 | global server, client, target, pm, adaptor, methods 48 | 49 | log.info("setting up API tests") 50 | 51 | # set up voltron 52 | voltron.setup_env() 53 | voltron.config['server'] = { 54 | "listen": { 55 | "tcp": ["127.0.0.1", 5555] 56 | } 57 | } 58 | pm = PluginManager() 59 | plugin = pm.debugger_plugin_for_host('mock') 60 | adaptor = plugin.adaptor_class() 61 | voltron.debugger = adaptor 62 | 63 | # inject mock methods 64 | inject_mock(adaptor) 65 | 66 | # start up a voltron server 67 | server = Server() 68 | server.start() 69 | 70 | time.sleep(2) 71 | 72 | 73 | def teardown(): 74 | server.stop() 75 | time.sleep(2) 76 | 77 | 78 | def test_disassemble(): 79 | data = requests.get('http://localhost:5555/api/disassemble?count=16').text 80 | res = APIResponse(data=data) 81 | assert res.is_success 82 | assert res.disassembly == disassemble_response 83 | 84 | 85 | def test_command(): 86 | data = requests.get('http://localhost:5555/api/command?command=reg%20read').text 87 | res = APIResponse(data=data) 88 | assert res.is_success 89 | assert res.output == command_response 90 | 91 | 92 | def test_targets(): 93 | data = requests.get('http://localhost:5555/api/targets').text 94 | res = api_response('targets', data=data) 95 | assert res.is_success 96 | assert res.targets == targets_response 97 | 98 | 99 | def test_memory(): 100 | data = requests.get('http://localhost:5555/api/registers').text 101 | res = api_response('registers', data=data) 102 | url = 'http://localhost:5555/api/memory?address={}&length=64'.format(res.registers['rip']) 103 | data = requests.get(url).text 104 | res = api_response('memory', data=data) 105 | assert res.is_success 106 | assert res.memory == memory_response 107 | 108 | 109 | def test_registers(): 110 | data = requests.get('http://localhost:5555/api/registers').text 111 | res = api_response('registers', data=data) 112 | assert res.is_success 113 | assert res.registers == registers_response 114 | 115 | 116 | def test_stack_length_missing(): 117 | data = requests.get('http://localhost:5555/api/stack').text 118 | res = APIErrorResponse(data=data) 119 | assert res.is_error 120 | assert res.message == 'length' 121 | 122 | 123 | def test_stack(): 124 | data = requests.get('http://localhost:5555/api/stack?length=64').text 125 | res = api_response('stack', data=data) 126 | assert res.is_success 127 | assert res.memory == stack_response 128 | 129 | 130 | def test_state(): 131 | data = requests.get('http://localhost:5555/api/state').text 132 | res = api_response('state', data=data) 133 | assert res.is_success 134 | assert res.state == state_response 135 | 136 | 137 | def test_version(): 138 | data = requests.get('http://localhost:5555/api/version').text 139 | res = api_response('version', data=data) 140 | assert res.is_success 141 | assert res.api_version == 1.1 142 | assert res.host_version == 'lldb-something' 143 | 144 | 145 | def test_bad_json(): 146 | data = requests.post('http://localhost:5555/api/request', data='xxx').text 147 | res = APIResponse(data=data) 148 | assert res.is_error 149 | assert res.code == 0x1001 150 | 151 | 152 | def test_bad_request(): 153 | data = requests.post('http://localhost:5555/api/request', data='{"type":"request","request":"no_such_request"}').text 154 | res = APIResponse(data=data) 155 | assert res.is_error 156 | assert res.code == 0x1002 157 | 158 | 159 | def test_breakpoints(): 160 | data = requests.get('http://localhost:5555/api/breakpoints').text 161 | res = api_response('breakpoints', data=data) 162 | assert res.is_success 163 | assert res.breakpoints == breakpoints_response 164 | -------------------------------------------------------------------------------- /tests/inferior.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | void test_function() 6 | { 7 | printf("*** test_function()\n"); 8 | } 9 | 10 | int main(int argc, char **argv) 11 | { 12 | int a = 0; 13 | int *b = NULL; 14 | int c=0,d=0,e=0; 15 | 16 | if (argc > 1 && strcmp(argv[1], "sleep") == 0) 17 | { 18 | printf("*** Sleeping for 5 seconds\n"); 19 | sleep(5); 20 | } 21 | else if (argc > 1 && strcmp(argv[1], "loop") == 0) 22 | { 23 | printf("*** Looping forever()\n"); 24 | while(1) { 25 | c++; 26 | d+=2; 27 | e+=3; 28 | } 29 | sleep(1); 30 | } 31 | else if (argc > 1 && strcmp(argv[1], "function") == 0) 32 | { 33 | printf("*** Calling test_function()\n"); 34 | test_function(); 35 | } 36 | else if (argc > 1 && strcmp(argv[1], "crash") == 0) 37 | { 38 | printf("*** Crashing\n"); 39 | a = *b; 40 | } 41 | else 42 | { 43 | printf("Usage: inferior < sleep | loop | function | crash >\n"); 44 | } 45 | 46 | return 0; 47 | } 48 | -------------------------------------------------------------------------------- /tests/lldb_api_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that exercise the LLDB backend directly by loading an inferior and then 3 | poking at it with the LLDBAdaptor class. 4 | 5 | Tests: 6 | LLDBAdaptor 7 | """ 8 | 9 | import tempfile 10 | import sys 11 | import json 12 | import time 13 | import logging 14 | import subprocess 15 | import threading 16 | 17 | from mock import Mock 18 | from nose.tools import * 19 | 20 | import voltron 21 | from voltron.core import * 22 | from voltron.api import * 23 | from voltron.plugin import PluginManager, DebuggerAdaptorPlugin 24 | 25 | import platform 26 | if platform.system() == 'Darwin': 27 | sys.path.append("/Applications/Xcode.app/Contents/SharedFrameworks/LLDB.framework/Resources/Python") 28 | 29 | try: 30 | import lldb 31 | 32 | from common import * 33 | 34 | voltron.setup_env() 35 | 36 | log = logging.getLogger('tests') 37 | 38 | def setup(): 39 | global adaptor, dbg, target 40 | 41 | log.info("setting up LLDB API tests") 42 | 43 | # create an LLDBAdaptor 44 | pm = PluginManager() 45 | plugin = pm.debugger_plugin_for_host('lldb') 46 | adaptor = plugin.adaptor_class() 47 | 48 | # compile and load the test inferior 49 | subprocess.call("cc -o tests/inferior tests/inferior.c", shell=True) 50 | target = adaptor.host.CreateTargetWithFileAndArch("tests/inferior", lldb.LLDB_ARCH_DEFAULT) 51 | main_bp = target.BreakpointCreateByName("main", target.GetExecutable().GetFilename()) 52 | 53 | def teardown(): 54 | time.sleep(2) 55 | 56 | def test_version(): 57 | assert 'lldb' in adaptor.version() 58 | 59 | def test_state_invalid(): 60 | try: 61 | adaptor.state() 62 | exception = False 63 | except NoSuchTargetException: 64 | exception = True 65 | except: 66 | exception = False 67 | assert exception 68 | 69 | def test_targets_not_running(): 70 | t = adaptor.targets()[0] 71 | assert t["state"] == "invalid" 72 | assert t["arch"] == "x86_64" 73 | assert t["id"] == 0 74 | assert len(t["file"]) > 0 75 | assert 'inferior' in t["file"] 76 | 77 | def test_targets_stopped(): 78 | process = target.LaunchSimple(None, None, os.getcwd()) 79 | t = adaptor.targets()[0] 80 | assert t["state"] == "stopped" 81 | process.Destroy() 82 | 83 | def test_registers(): 84 | process = target.LaunchSimple(None, None, os.getcwd()) 85 | regs = adaptor.registers() 86 | assert regs is not None 87 | assert len(regs) > 0 88 | assert regs['rip'] != 0 89 | process.Destroy() 90 | 91 | def test_stack_pointer(): 92 | process = target.LaunchSimple(None, None, os.getcwd()) 93 | sp = adaptor.stack_pointer() 94 | assert sp != 0 95 | process.Destroy() 96 | 97 | def test_program_counter(): 98 | process = target.LaunchSimple(None, None, os.getcwd()) 99 | pc_name, pc = adaptor.program_counter() 100 | assert pc != 0 101 | process.Destroy() 102 | 103 | def test_memory(): 104 | process = target.LaunchSimple(None, None, os.getcwd()) 105 | regs = adaptor.registers() 106 | mem = adaptor.memory(address=regs['rip'], length=0x40) 107 | assert len(mem) == 0x40 108 | process.Destroy() 109 | 110 | def test_stack(): 111 | process = target.LaunchSimple(None, None, os.getcwd()) 112 | stack = adaptor.stack(length=0x40) 113 | assert len(stack) == 0x40 114 | process.Destroy() 115 | 116 | def test_disassemble(): 117 | process = target.LaunchSimple(None, None, os.getcwd()) 118 | output = adaptor.disassemble(count=0x20) 119 | assert len(output) > 0 120 | process.Destroy() 121 | 122 | def test_command(): 123 | process = target.LaunchSimple(None, None, os.getcwd()) 124 | output = adaptor.command("reg read") 125 | assert len(output) > 0 126 | assert 'rax' in output 127 | process.Destroy() 128 | 129 | def test_dereference_main(): 130 | process = target.LaunchSimple(None, None, os.getcwd()) 131 | regs = adaptor.registers() 132 | output = adaptor.dereference(regs['rip']) 133 | assert ('symbol', 'main + 0x0') in output 134 | process.Destroy() 135 | 136 | def test_dereference_rsp(): 137 | process = target.LaunchSimple(None, None, os.getcwd()) 138 | regs = adaptor.registers() 139 | output = adaptor.dereference(regs['rsp']) 140 | assert ('symbol', 'start + 0x1') in output 141 | process.Destroy() 142 | 143 | def test_dereference_string(): 144 | process = target.LaunchSimple(None, None, os.getcwd()) 145 | regs = adaptor.registers() 146 | output = adaptor.dereference(regs['rsp'] + 0x20) 147 | assert 'inferior' in list(output[-1])[-1] 148 | process.Destroy() 149 | 150 | def test_breakpoints(): 151 | process = target.LaunchSimple(None, None, os.getcwd()) 152 | bps = adaptor.breakpoints() 153 | assert len(bps) == 1 154 | assert bps[0]['one_shot'] == False 155 | assert bps[0]['enabled'] 156 | assert bps[0]['id'] == 1 157 | assert bps[0]['hit_count'] > 0 158 | assert bps[0]['locations'][0]['name'] == "inferior`main" 159 | process.Destroy() 160 | 161 | def test_capabilities(): 162 | assert adaptor.capabilities() == ['async'] 163 | 164 | except: 165 | print("No LLDB") -------------------------------------------------------------------------------- /tests/lldb_cli_tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | Tests that test voltron in the lldb cli driver 3 | 4 | Tests: 5 | Client -> Server -> LLDBAdaptor 6 | 7 | Inside an LLDB CLI driver instance 8 | """ 9 | from __future__ import print_function 10 | 11 | import tempfile 12 | import sys 13 | import json 14 | import time 15 | import logging 16 | import pexpect 17 | import os 18 | import tempfile 19 | import six 20 | 21 | from mock import Mock 22 | from nose.tools import * 23 | 24 | import voltron 25 | from voltron.core import * 26 | from voltron.api import * 27 | from voltron.plugin import PluginManager, DebuggerAdaptorPlugin 28 | 29 | from .common import * 30 | 31 | log = logging.getLogger('tests') 32 | 33 | p = None 34 | client = None 35 | 36 | 37 | def setup(): 38 | global p, client, pm 39 | 40 | log.info("setting up LLDB CLI tests") 41 | 42 | voltron.setup_env() 43 | 44 | # compile test inferior 45 | pexpect.run("cc -o tests/inferior tests/inferior.c") 46 | 47 | # start debugger 48 | start_debugger() 49 | time.sleep(10) 50 | 51 | 52 | def teardown(): 53 | read_data() 54 | p.terminate(True) 55 | time.sleep(2) 56 | 57 | 58 | def start_debugger(do_break=True): 59 | global p, client 60 | 61 | if sys.platform == 'darwin': 62 | p = pexpect.spawn('lldb') 63 | else: 64 | p = pexpect.spawn('lldb-3.4') 65 | 66 | # for travis 67 | (f, tmpname) = tempfile.mkstemp('.py') 68 | os.write(f, six.b('\n'.join([ 69 | "import sys", 70 | "sys.path.append('/home/travis/virtualenv/python3.5.0/lib/python3.5/site-packages')", 71 | "sys.path.append('/home/travis/virtualenv/python3.4.3/lib/python3.4/site-packages')", 72 | "sys.path.append('/home/travis/virtualenv/python3.3.6/lib/python3.3/site-packages')", 73 | "sys.path.append('/home/travis/virtualenv/python2.7.10/lib/python2.7/site-packages')"]))) 74 | p.sendline("command script import {}".format(tmpname)) 75 | 76 | print("pid == {}".format(p.pid)) 77 | p.sendline("settings set target.x86-disassembly-flavor intel") 78 | p.sendline("command script import voltron/entry.py") 79 | time.sleep(2) 80 | p.sendline("file tests/inferior") 81 | time.sleep(2) 82 | p.sendline("voltron init") 83 | time.sleep(1) 84 | p.sendline("process kill") 85 | p.sendline("break delete 1") 86 | if do_break: 87 | p.sendline("b main") 88 | p.sendline("run loop") 89 | read_data() 90 | client = Client() 91 | 92 | 93 | def stop_debugger(): 94 | p.terminate(True) 95 | 96 | 97 | def read_data(): 98 | try: 99 | while True: 100 | data = p.read_nonblocking(size=64, timeout=1) 101 | print(data.decode('UTF-8'), end='') 102 | except: 103 | pass 104 | 105 | 106 | def restart(do_break=True): 107 | # stop_debugger() 108 | # start_debugger(do_break) 109 | p.sendline("process kill") 110 | p.sendline("break delete -f") 111 | if do_break: 112 | p.sendline("b main") 113 | p.sendline("run loop") 114 | 115 | 116 | def test_bad_request(): 117 | req = client.create_request('version') 118 | req.request = 'xxx' 119 | res = client.send_request(req) 120 | assert res.is_error 121 | assert res.code == 0x1002 122 | time.sleep(2) 123 | 124 | 125 | def test_version(): 126 | req = client.create_request('version') 127 | res = client.send_request(req) 128 | assert res.api_version == 1.1 129 | assert 'lldb' in res.host_version 130 | 131 | 132 | def test_registers(): 133 | global registers 134 | restart() 135 | time.sleep(1) 136 | read_data() 137 | res = client.perform_request('registers') 138 | registers = res.registers 139 | assert res.status == 'success' 140 | assert len(registers) > 0 141 | if 'rip' in registers: 142 | assert registers['rip'] != 0 143 | else: 144 | assert registers['eip'] != 0 145 | 146 | 147 | def test_memory(): 148 | restart() 149 | time.sleep(1) 150 | res = client.perform_request('memory', address=registers['rip'], length=0x40) 151 | assert res.status == 'success' 152 | assert len(res.memory) > 0 153 | 154 | 155 | def test_state_stopped(): 156 | restart() 157 | time.sleep(1) 158 | res = client.perform_request('state') 159 | assert res.is_success 160 | assert res.state == "stopped" 161 | 162 | 163 | def test_targets(): 164 | restart() 165 | time.sleep(1) 166 | res = client.perform_request('targets') 167 | assert res.is_success 168 | assert res.targets[0]['state'] == "stopped" 169 | assert res.targets[0]['arch'] == "x86_64" 170 | assert res.targets[0]['id'] == 0 171 | assert res.targets[0]['file'].endswith('tests/inferior') 172 | 173 | 174 | def test_stack(): 175 | restart() 176 | time.sleep(1) 177 | res = client.perform_request('stack', length=0x40) 178 | assert res.status == 'success' 179 | assert len(res.memory) > 0 180 | 181 | 182 | def test_command(): 183 | restart() 184 | time.sleep(1) 185 | res = client.perform_request('command', command="reg read") 186 | assert res.status == 'success' 187 | assert len(res.output) > 0 188 | assert 'rax' in res.output 189 | 190 | 191 | def test_disassemble(): 192 | restart() 193 | time.sleep(1) 194 | res = client.perform_request('disassemble', count=0x20) 195 | assert res.status == 'success' 196 | assert len(res.disassembly) > 0 197 | assert 'push' in res.disassembly 198 | 199 | 200 | def test_dereference(): 201 | restart() 202 | time.sleep(1) 203 | res = client.perform_request('registers') 204 | res = client.perform_request('dereference', pointer=res.registers['rsp']) 205 | assert res.status == 'success' 206 | assert res.output[0][0] == 'pointer' 207 | assert 'start' in res.output[-1][1] or 'main' in res.output[-1][1] 208 | 209 | 210 | def test_breakpoints(): 211 | restart(True) 212 | time.sleep(1) 213 | res = client.perform_request('breakpoints') 214 | assert res.status == 'success' 215 | # assert len(res.breakpoints) == 1 216 | assert res.breakpoints[0]['one_shot'] == False 217 | assert res.breakpoints[0]['enabled'] 218 | # assert res.breakpoints[0]['id'] == 1 219 | assert res.breakpoints[0]['hit_count'] > 0 220 | assert res.breakpoints[0]['locations'][0]['name'] == "inferior`main" 221 | 222 | 223 | # def test_multi(): 224 | # global r1, r2 225 | # restart(True) 226 | # time.sleep(1) 227 | # r1, r2 = None, None 228 | 229 | # def send_req(): 230 | # global r1, r2 231 | # r1, r2 = client.send_requests(api_request('targets', block=True), api_request('registers', block=True)) 232 | # print "sent requests" 233 | 234 | # t = threading.Thread(target=send_req) 235 | # t.start() 236 | # time.sleep(5) 237 | # p.sendline("stepi") 238 | # time.sleep(5) 239 | # t.join() 240 | # print r1 241 | # print r2 242 | # assert r1.is_success 243 | # assert r1.targets[0]['state'] == "stopped" 244 | # assert r1.targets[0]['arch'] == "x86_64" 245 | # assert r1.targets[0]['id'] == 0 246 | # assert r1.targets[0]['file'].endswith('tests/inferior') 247 | # assert r2.status == 'success' 248 | # assert len(r2.registers) > 0 249 | # assert r2.registers['rip'] != 0 250 | 251 | 252 | def test_capabilities(): 253 | restart(True) 254 | res = client.perform_request('version') 255 | assert res.capabilities == ['async'] 256 | 257 | 258 | def test_backtrace(): 259 | restart(True) 260 | time.sleep(1) 261 | res = client.perform_request('backtrace') 262 | print(res) 263 | assert res.frames[0]['name'] == "inferior`main + 0" 264 | assert res.frames[0]['index'] == 0 265 | -------------------------------------------------------------------------------- /tests/test.c: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | int main(void) 4 | { 5 | printf("hello\n"); 6 | 7 | return 0; 8 | } -------------------------------------------------------------------------------- /tests/testinit.lldb: -------------------------------------------------------------------------------- 1 | file tests/inferior 2 | voltron init 3 | b main 4 | run loop 5 | 6 | -------------------------------------------------------------------------------- /voltron/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import logging.config 4 | 5 | import voltron 6 | from .main import main 7 | 8 | from scruffy import Environment, Directory, File, ConfigFile, PluginDirectory, PackageDirectory 9 | 10 | # scruffy environment containing config, plugins, etc 11 | env = None 12 | config = None 13 | 14 | debugger = None 15 | command = None 16 | commands = None 17 | server = None 18 | 19 | loaded = False 20 | 21 | 22 | def setup_env(): 23 | global env, config 24 | env = Environment(setup_logging=False, 25 | voltron_dir=Directory('~/.voltron', create=True, 26 | config=ConfigFile('config', defaults=File('config/default.cfg', parent=PackageDirectory()), apply_env=True), 27 | sock=File('{config:server.listen.domain}'), 28 | history=File('history'), 29 | user_plugins=PluginDirectory('plugins') 30 | ), 31 | pkg_plugins=PluginDirectory('plugins', parent=PackageDirectory()) 32 | ) 33 | config = env.config 34 | 35 | voltron.plugin.pm.register_plugins() 36 | 37 | LOGGER_DEFAULT = { 38 | 'handlers': ['null'], 39 | 'level': 'DEBUG', 40 | 'propagate': False 41 | } 42 | 43 | LOG_CONFIG = { 44 | 'version': 1, 45 | 'formatters': { 46 | 'standard': {'format': 'voltron: [%(levelname)s] %(message)s'} 47 | }, 48 | 'handlers': { 49 | 'default': { 50 | 'class': 'logging.StreamHandler', 51 | 'formatter': 'standard' 52 | }, 53 | 'null': { 54 | 'class': 'logging.NullHandler' 55 | } 56 | }, 57 | 'loggers': { 58 | '': LOGGER_DEFAULT, 59 | 'debugger': LOGGER_DEFAULT, 60 | 'core': LOGGER_DEFAULT, 61 | 'main': LOGGER_DEFAULT, 62 | 'api': LOGGER_DEFAULT, 63 | 'view': LOGGER_DEFAULT, 64 | 'plugin': LOGGER_DEFAULT, 65 | } 66 | } 67 | 68 | 69 | def setup_logging(logname=None): 70 | # configure logging 71 | logging.config.dictConfig(LOG_CONFIG) 72 | 73 | # enable the debug_file in all the loggers if the config says to 74 | if config and 'general' in config and config['general']['debug_logging']: 75 | if logname: 76 | filename = '{}.log'.format(logname) 77 | else: 78 | filename = 'voltron.log' 79 | for name in LOG_CONFIG['loggers']: 80 | h = logging.FileHandler(voltron.env.voltron_dir.path_to(filename), delay=True) 81 | h.setFormatter(logging.Formatter(fmt="%(asctime)s %(levelname)-7s %(filename)12s:%(lineno)-4s %(funcName)20s -- %(message)s")) 82 | logging.getLogger(name).addHandler(h) 83 | 84 | logging.info("======= VOLTRON - DEFENDER OF THE UNIVERSE [debug log] =======") 85 | 86 | return logging.getLogger(logname) 87 | 88 | 89 | # Python 3 shim 90 | if not hasattr(__builtins__, "xrange"): 91 | xrange = range 92 | 93 | 94 | # Setup the Voltron environment 95 | setup_env() 96 | -------------------------------------------------------------------------------- /voltron/__main__.py: -------------------------------------------------------------------------------- 1 | try: 2 | # for some reason the relative import doesn't work in VSCode's 3 | # interactive debugger, but this does but I'm not sure if there's a 4 | # chance a different voltron could be somewhere in sys.path, so 5 | # let's try the relative import first 6 | from .main import main 7 | except ImportError: 8 | from voltron.main import main 9 | 10 | main() 11 | -------------------------------------------------------------------------------- /voltron/api.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import socket 4 | import select 5 | import threading 6 | import logging 7 | import logging.config 8 | import json 9 | import inspect 10 | import base64 11 | import six 12 | 13 | from collections import defaultdict 14 | 15 | from scruffy.plugin import Plugin 16 | 17 | import voltron 18 | from .plugin import APIPlugin 19 | 20 | log = logging.getLogger('api') 21 | 22 | version = 1.1 23 | 24 | 25 | class InvalidRequestTypeException(Exception): 26 | """ 27 | Exception raised when the client is requested to send an invalid request type. 28 | """ 29 | pass 30 | 31 | 32 | class InvalidDebuggerHostException(Exception): 33 | """ 34 | Exception raised when the debugger host is invalid. 35 | """ 36 | pass 37 | 38 | 39 | class InvalidViewNameException(Exception): 40 | """ 41 | Exception raised when an invalid view name is specified. 42 | """ 43 | pass 44 | 45 | 46 | class InvalidMessageException(Exception): 47 | """ 48 | Exception raised when an invalid API message is received. 49 | """ 50 | pass 51 | 52 | 53 | class ServerSideOnlyException(Exception): 54 | """ 55 | Exception raised when a server-side method is called on an APIMessage 56 | subclass that exists on the client-side. 57 | 58 | See @server_side decorator. 59 | """ 60 | pass 61 | 62 | 63 | class ClientSideOnlyException(Exception): 64 | """ 65 | Exception raised when a server-side method is called on an APIMessage 66 | subclass that exists on the client-side. 67 | 68 | See @client_side decorator. 69 | """ 70 | pass 71 | 72 | 73 | class DebuggerNotPresentException(Exception): 74 | """ 75 | Raised when an APIRequest is dispatched without a valid debugger present. 76 | """ 77 | pass 78 | 79 | 80 | class NoSuchTargetException(Exception): 81 | """ 82 | Raised when an APIRequest specifies an invalid target. 83 | """ 84 | pass 85 | 86 | 87 | class TargetBusyException(Exception): 88 | """ 89 | Raised when an APIRequest specifies a target that is currently busy and 90 | cannot be queried. 91 | """ 92 | pass 93 | 94 | 95 | class MissingFieldError(Exception): 96 | """ 97 | Raised when an APIMessage is validated and has a required field missing. 98 | """ 99 | pass 100 | 101 | 102 | class NoSuchThreadException(Exception): 103 | """ 104 | Raised when the specified thread ID or index does not exist. 105 | """ 106 | pass 107 | 108 | 109 | class UnknownArchitectureException(Exception): 110 | """ 111 | Raised when the debugger host is running in an unknown architecture. 112 | """ 113 | pass 114 | 115 | 116 | class BlockingNotSupportedError(Exception): 117 | """ 118 | Raised when a view that does not support blocking connects to a debugger 119 | host that does not support async mode. 120 | """ 121 | pass 122 | 123 | 124 | def server_side(func): 125 | """ 126 | Decorator to designate an API method applicable only to server-side 127 | instances. 128 | 129 | This allows us to use the same APIRequest and APIResponse subclasses on the 130 | client and server sides without too much confusion. 131 | """ 132 | def inner(*args, **kwargs): 133 | if args and hasattr(args[0], 'is_server') and not voltron.debugger: 134 | raise ServerSideOnlyException("This method can only be called on a server-side instance") 135 | return func(*args, **kwargs) 136 | return inner 137 | 138 | 139 | def client_side(func): 140 | """ 141 | Decorator to designate an API method applicable only to client-side 142 | instances. 143 | 144 | This allows us to use the same APIRequest and APIResponse subclasses on the 145 | client and server sides without too much confusion. 146 | """ 147 | def inner(*args, **kwargs): 148 | if args and hasattr(args[0], 'is_server') and voltron.debugger: 149 | raise ClientSideOnlyException("This method can only be called on a client-side instance") 150 | return func(*args, **kwargs) 151 | return inner 152 | 153 | 154 | def cast_b(val): 155 | if isinstance(val, six.binary_type): 156 | return val 157 | elif isinstance(val, six.text_type): 158 | return val.encode('latin1') 159 | return six.binary_type(val) 160 | 161 | 162 | def cast_s(val): 163 | if type(val) == six.text_type: 164 | return val 165 | elif type(val) == six.binary_type: 166 | return val.decode('latin1') 167 | return six.text_type(val) 168 | 169 | 170 | class APIMessage(object): 171 | """ 172 | Top-level API message class. 173 | """ 174 | _top_fields = ['type'] 175 | _fields = {} 176 | _encode_fields = [] 177 | 178 | type = None 179 | 180 | def __init__(self, data=None, *args, **kwargs): 181 | # process any data that was passed in 182 | if data: 183 | self.from_json(data) 184 | 185 | # any other kwargs are treated as field values 186 | for field in kwargs: 187 | setattr(self, field, kwargs[field]) 188 | 189 | def __str__(self): 190 | """ 191 | Return a string (JSON) representation of the API message properties. 192 | """ 193 | return self.to_json() 194 | 195 | def to_dict(self): 196 | """ 197 | Return a transmission-safe dictionary representation of the API message properties. 198 | """ 199 | d = {field: getattr(self, field) for field in self._top_fields if hasattr(self, field)} 200 | 201 | # set values of data fields 202 | d['data'] = {} 203 | for field in self._fields: 204 | if hasattr(self, field): 205 | # base64 encode the field for transmission if necessary 206 | if field in self._encode_fields: 207 | val = getattr(self, field) 208 | if val: 209 | val = cast_s(base64.b64encode(cast_b(val))) 210 | d['data'][field] = val 211 | else: 212 | d['data'][field] = getattr(self, field) 213 | 214 | return d 215 | 216 | def from_dict(self, d): 217 | """ 218 | Initialise an API message from a transmission-safe dictionary. 219 | """ 220 | for key in d: 221 | if key == 'data': 222 | for dkey in d['data']: 223 | if dkey in self._encode_fields: 224 | setattr(self, str(dkey), base64.b64decode(d['data'][dkey])) 225 | else: 226 | setattr(self, str(dkey), d['data'][dkey]) 227 | else: 228 | setattr(self, str(key), d[key]) 229 | 230 | def to_json(self): 231 | """ 232 | Return a JSON representation of the API message properties. 233 | """ 234 | return json.dumps(self.to_dict()) 235 | 236 | def from_json(self, data): 237 | """ 238 | Initialise an API message from a JSON representation. 239 | """ 240 | try: 241 | d = json.loads(data) 242 | except ValueError: 243 | raise InvalidMessageException() 244 | self.from_dict(d) 245 | 246 | def __getattr__(self, name): 247 | """ 248 | Attribute accessor. 249 | 250 | If a defined field is requested that doesn't have a value set, 251 | return None. 252 | """ 253 | if name in self._fields: 254 | return None 255 | 256 | def validate(self): 257 | """ 258 | Validate the message. 259 | 260 | Ensure all the required fields are present and not None. 261 | """ 262 | required_fields = list(filter(lambda x: self._fields[x], self._fields.keys())) 263 | for field in (self._top_fields + required_fields): 264 | if not hasattr(self, field) or hasattr(self, field) and getattr(self, field) == None: 265 | raise MissingFieldError(field) 266 | 267 | 268 | class APIRequest(APIMessage): 269 | """ 270 | An API request object. Contains functions and accessors common to all API 271 | request types. 272 | 273 | Subclasses of APIRequest are used on both the client and server sides. On 274 | the server side they are instantiated by Server's `handle_request()` 275 | method. On the client side they are instantiated by whatever class is doing 276 | the requesting (probably a view class). 277 | """ 278 | _top_fields = ['type', 'request', 'block', 'timeout'] 279 | _fields = {} 280 | 281 | type = 'request' 282 | request = None 283 | block = False 284 | timeout = 10 285 | 286 | response = None 287 | wait_event = None 288 | timed_out = False 289 | 290 | @server_side 291 | def dispatch(self): 292 | """ 293 | In concrete subclasses this method will actually dispatch the request 294 | to the debugger host and return a response. In this case it raises an 295 | exception. 296 | """ 297 | raise NotImplementedError("Subclass APIRequest") 298 | 299 | @server_side 300 | def wait(self): 301 | """ 302 | Wait for the request to be dispatched. 303 | """ 304 | self.wait_event = threading.Event() 305 | timeout = int(self.timeout) if self.timeout else None 306 | self.timed_out = not self.wait_event.wait(timeout) 307 | 308 | def signal(self): 309 | """ 310 | Signal that the request has been dispatched and can return. 311 | """ 312 | self.wait_event.set() 313 | 314 | 315 | class APIBlockingRequest(APIRequest): 316 | """ 317 | An API request that blocks by default. 318 | """ 319 | block = True 320 | 321 | 322 | class APIResponse(APIMessage): 323 | """ 324 | An API response object. Contains functions and accessors common to all API 325 | response types. 326 | 327 | Subclasses of APIResponse are used on both the client and server sides. On 328 | the server side they are instantiated by the APIRequest's `dispatch` method 329 | in order to serialise and send to the client. On the client side they are 330 | instantiated by the Client class and returned by `send_request`. 331 | """ 332 | _top_fields = ['type', 'status'] 333 | _fields = {} 334 | 335 | type = 'response' 336 | status = None 337 | 338 | @property 339 | def is_success(self): 340 | return self.status == 'success' 341 | 342 | @property 343 | def is_error(self): 344 | return self.status == 'error' 345 | 346 | def __repr__(self): 347 | return "<%s: success = %s, error = %s, body: %s>" % ( 348 | str(self.__class__), 349 | self.is_success, 350 | self.is_error, 351 | {f: getattr(self, f) for f in self._top_fields + list(self._fields.keys())} 352 | ) 353 | 354 | 355 | class APISuccessResponse(APIResponse): 356 | """ 357 | A generic API success response. 358 | """ 359 | status = 'success' 360 | 361 | 362 | class APIErrorResponse(APIResponse): 363 | """ 364 | A generic API error response. 365 | """ 366 | _fields = {'code': True, 'message': True} 367 | 368 | status = 'error' 369 | 370 | @property 371 | def timed_out(self): 372 | return self.code == APITimedOutErrorResponse.code 373 | 374 | 375 | class APIGenericErrorResponse(APIErrorResponse): 376 | code = 0x1000 377 | message = "An error occurred" 378 | 379 | def __init__(self, message=None): 380 | super(APIGenericErrorResponse, self).__init__() 381 | if message: 382 | self.message = message 383 | 384 | 385 | class APIInvalidRequestErrorResponse(APIErrorResponse): 386 | code = 0x1001 387 | message = "Invalid API request" 388 | 389 | 390 | class APIPluginNotFoundErrorResponse(APIErrorResponse): 391 | code = 0x1002 392 | message = "Plugin was not found for request" 393 | 394 | 395 | class APIDebuggerHostNotSupportedErrorResponse(APIErrorResponse): 396 | code = 0x1003 397 | message = "The targeted debugger host is not supported by this plugin" 398 | 399 | 400 | class APITimedOutErrorResponse(APIErrorResponse): 401 | code = 0x1004 402 | message = "The request timed out" 403 | 404 | 405 | class APIDebuggerNotPresentErrorResponse(APIErrorResponse): 406 | code = 0x1004 407 | message = "No debugger host was found" 408 | 409 | 410 | class APINoSuchTargetErrorResponse(APIErrorResponse): 411 | code = 0x1005 412 | message = "No such target" 413 | 414 | 415 | class APITargetBusyErrorResponse(APIErrorResponse): 416 | code = 0x1006 417 | message = "Target busy" 418 | 419 | 420 | class APIMissingFieldErrorResponse(APIGenericErrorResponse): 421 | code = 0x1007 422 | message = "Missing field" 423 | 424 | 425 | class APIEmptyResponseErrorResponse(APIGenericErrorResponse): 426 | code = 0x1008 427 | message = "Empty response" 428 | 429 | 430 | class APIServerNotRunningErrorResponse(APIGenericErrorResponse): 431 | code = 0x1009 432 | message = "Server is not running" 433 | -------------------------------------------------------------------------------- /voltron/colour.py: -------------------------------------------------------------------------------- 1 | import re 2 | ESCAPES = { 3 | # reset 4 | 'reset': 0, 5 | 6 | # colours 7 | 'grey': 30, 8 | 'red': 31, 9 | 'green': 32, 10 | 'yellow': 33, 11 | 'blue': 34, 12 | 'magenta': 35, 13 | 'cyan': 36, 14 | 'white': 37, 15 | 16 | # background 17 | 'b_grey': 40, 18 | 'b_red': 41, 19 | 'b_green': 42, 20 | 'b_yellow': 43, 21 | 'b_blue': 44, 22 | 'b_magenta': 45, 23 | 'b_cyan': 46, 24 | 'b_white': 47, 25 | 26 | # attributes 27 | 'a_bold': 1, 28 | 'a_dark': 2, 29 | 'a_underline': 4, 30 | 'a_blink': 5, 31 | 'a_reverse': 7, 32 | 'a_concealed': 8 33 | } 34 | ESC_TEMPLATE = '\033[{}m' 35 | 36 | def escapes(): 37 | return ESCAPES 38 | 39 | def get_esc(name): 40 | return ESCAPES[name] 41 | 42 | def fmt_esc(name): 43 | return ESC_TEMPLATE.format(escapes()[name]) 44 | 45 | FMT_ESCAPES = dict((k, fmt_esc(k)) for k in ESCAPES) 46 | 47 | 48 | def uncolour(text): 49 | """ 50 | Remove ANSI color/style sequences from a string. The set of all possible 51 | ANSI sequences is large, so does not try to strip every possible one. But 52 | does strip some outliers by other ANSI colorizers in the wild. Those 53 | include `\x1b[K` (aka EL or erase to end of line) and `\x1b[m`, a terse 54 | version of the more common `\x1b[0m`. 55 | 56 | Stolen from: https://github.com/jonathaneunice/colors/blob/master/colors/colors.py 57 | """ 58 | text = re.sub('\x1b\\[(K|.*?m)', '', text) 59 | return text 60 | -------------------------------------------------------------------------------- /voltron/config/default.cfg: -------------------------------------------------------------------------------- 1 | general: 2 | debug_logging: false 3 | server: 4 | listen: 5 | domain: "~/.voltron/sock" 6 | tcp: 7 | - 127.0.0.1 8 | - 5555 9 | view: 10 | #api_url: "http+unix://~%2f.voltron%2fsock/api/request", 11 | api_url: http://localhost:5555/api/request 12 | reconnect: true 13 | all_views: 14 | clear: true 15 | update_on: stop 16 | format: 17 | pygments_style: volarized 18 | pygments_formatter: terminal256 19 | header: 20 | show: true 21 | pad: " " 22 | colour: blue 23 | bg_colour: grey 24 | attrs: [] 25 | label_left: 26 | name: info 27 | colour: blue 28 | bg_colour: grey 29 | attrs: [] 30 | label_right: 31 | name: title 32 | colour: white 33 | bg_colour: grey 34 | attrs: 35 | - bold 36 | footer: 37 | show: false 38 | pad: " " 39 | colour: blue 40 | bg_colour: grey 41 | attrs: [] 42 | label_left: 43 | name: 44 | colour: blue 45 | bg_colour: grey 46 | attrs: [] 47 | label_right: 48 | name: 49 | colour: blue 50 | bg_colour: grey 51 | attrs: 52 | - bold 53 | pad: 54 | pad_right: 0 55 | pad_bottom: 0 56 | keymap: 57 | q: exit 58 | p: page_up 59 | n: page_down 60 | KEY_PPAGE: page_up 61 | KEY_NPAGE: page_down 62 | KEY_UP: line_up 63 | KEY_DOWN: line_down 64 | KEY_ENTER: reset 65 | register_view: 66 | format: 67 | label_format: "{0}" 68 | label_func: str_upper 69 | label_colour: green 70 | label_colour_en: true 71 | value_format: "{0:0=16X}" 72 | value_func: 73 | value_colour: reset 74 | value_colour_mod: red 75 | value_colour_en: true 76 | format_name: 77 | addr_colour: blue 78 | divider_colour: green 79 | string_colour: white 80 | symbol_colour: cyan 81 | sections: 82 | - general 83 | orientation: vertical 84 | disassembly_view: 85 | header: 86 | show: true 87 | label_left: 88 | name: title 89 | colour: white 90 | bg_colour: grey 91 | attrs: 92 | - bold 93 | label_right: 94 | name: 95 | stack_view: 96 | header: 97 | show: false 98 | footer: 99 | show: true 100 | label_left: 101 | name: title 102 | colour: white 103 | bg_colour: grey 104 | attrs: 105 | - bold 106 | label_right: 107 | name: info 108 | colour: blue 109 | bg_colour: grey 110 | attrs: [] 111 | format: 112 | addr_format: "{0:0=16X}" 113 | memory_view: 114 | header: 115 | show: false 116 | footer: 117 | show: true 118 | label_left: 119 | name: title 120 | colour: white 121 | bg_colour: grey 122 | attrs: 123 | - bold 124 | label_right: 125 | name: info 126 | colour: blue 127 | bg_colour: grey 128 | attrs: [] 129 | format: 130 | addr_format: "{0:0=16X}" 131 | backtrace_view: 132 | header: 133 | show: false 134 | footer: 135 | show: true 136 | label_left: 137 | name: title 138 | colour: white 139 | bg_colour: grey 140 | attrs: 141 | - bold 142 | label_right: 143 | name: info 144 | colour: blue 145 | bg_colour: grey 146 | attrs: [] 147 | breakpoints_view: 148 | format: 149 | row: "{disabled}{one_shot}{t.bold}{id}{t.normal} {hit}{t.blue}0x{address:0=16X}{t.normal} 150 | {t.green}h:{t.normal}{hit_count:<4} {t.cyan}{name}{t.normal}" 151 | disabled: "{t.red}" 152 | one_shot: "{t.underline}" 153 | hit: "{t.standout}" 154 | some_named_stack_view: 155 | header: 156 | show: true 157 | label_left: 158 | name: title 159 | colour: red 160 | bg_colour: grey 161 | attrs: 162 | - bold 163 | label_right: 164 | name: info 165 | colour: white 166 | bg_colour: grey 167 | attrs: [] 168 | footer: 169 | show: false 170 | console: 171 | prompt: 172 | format: "{red}voltron>{reset} " 173 | -------------------------------------------------------------------------------- /voltron/dbg.py: -------------------------------------------------------------------------------- 1 | try: 2 | import capstone 3 | except: 4 | capstone = None 5 | 6 | from voltron.api import * 7 | from voltron.plugin import * 8 | 9 | 10 | class InvalidPointerError(Exception): 11 | """ 12 | Raised when attempting to dereference an invalid pointer. 13 | """ 14 | pass 15 | 16 | 17 | def validate_target(func, *args, **kwargs): 18 | """ 19 | A decorator that ensures that the specified target_id exists and 20 | is valid. 21 | 22 | Expects the target ID to be either the 'target_id' param in kwargs, 23 | or the first positional parameter. 24 | 25 | Raises a NoSuchTargetException if the target does not exist. 26 | """ 27 | def inner(self, *args, **kwargs): 28 | # find the target param 29 | target_id = None 30 | if 'target_id' in kwargs and kwargs['target_id'] != None: 31 | target_id = kwargs['target_id'] 32 | else: 33 | target_id = 0 34 | 35 | # if there was a target specified, check that it's valid 36 | if not self.target_is_valid(target_id): 37 | raise NoSuchTargetException() 38 | 39 | # call the function 40 | return func(self, *args, **kwargs) 41 | return inner 42 | 43 | 44 | def validate_busy(func, *args, **kwargs): 45 | """ 46 | A decorator that raises an exception if the specified target is busy. 47 | 48 | Expects the target ID to be either the 'target_id' param in kwargs, 49 | or the first positional parameter. 50 | 51 | Raises a TargetBusyException if the target does not exist. 52 | """ 53 | def inner(self, *args, **kwargs): 54 | # find the target param 55 | target_id = None 56 | if 'target_id' in kwargs and kwargs['target_id'] != None: 57 | target_id = kwargs['target_id'] 58 | else: 59 | target_id = 0 60 | 61 | # if there was a target specified, ensure it's not busy 62 | if self.target_is_busy(target_id): 63 | raise TargetBusyException() 64 | 65 | # call the function 66 | return func(self, *args, **kwargs) 67 | return inner 68 | 69 | 70 | def lock_host(func, *args, **kwargs): 71 | """ 72 | A decorator that acquires a lock before accessing the debugger to 73 | avoid API locking related errors with the debugger host. 74 | """ 75 | def inner(self, *args, **kwargs): 76 | self.host_lock.acquire() 77 | try: 78 | res = func(self, *args, **kwargs) 79 | self.host_lock.release() 80 | except Exception as e: 81 | self.host_lock.release() 82 | raise e 83 | return res 84 | return inner 85 | 86 | 87 | class DebuggerAdaptor(object): 88 | """ 89 | Base debugger adaptor class. Debugger adaptors implemented in plugins for 90 | specific debuggers inherit from this. 91 | """ 92 | 93 | reg_names = { 94 | "x86": {"pc": "eip", "sp": "esp"}, 95 | "x86_64": {"pc": "rip", "sp": "rsp"}, 96 | "arm": {"pc": "pc", "sp": "sp"}, 97 | "armv6": {"pc": "pc", "sp": "sp"}, 98 | "armv7": {"pc": "pc", "sp": "sp"}, 99 | "armv7s": {"pc": "pc", "sp": "sp"}, 100 | "arm64": {"pc": "pc", "sp": "sp"}, 101 | "powerpc": {"pc": "pc", "sp": "r1"}, 102 | } 103 | cs_archs = {} 104 | if capstone: 105 | cs_archs = { 106 | "x86": (capstone.CS_ARCH_X86, capstone.CS_MODE_32), 107 | "x86_64": (capstone.CS_ARCH_X86, capstone.CS_MODE_64), 108 | "arm": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), 109 | "armv6": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), 110 | "armv7": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), 111 | "armv7s": (capstone.CS_ARCH_ARM, capstone.CS_MODE_ARM), 112 | "arm64": (capstone.CS_ARCH_ARM64, capstone.CS_MODE_ARM), 113 | "powerpc": (capstone.CS_ARCH_PPC, capstone.CS_MODE_32), 114 | } 115 | 116 | def __init__(self, *args, **kwargs): 117 | self.listeners = [] 118 | 119 | def target_exists(self, target_id=0): 120 | """ 121 | Returns True or False indicating whether or not the specified 122 | target is present and valid. 123 | 124 | `target_id` is a target ID (or None for the first target) 125 | """ 126 | try: 127 | target = self.target(target_id=target_id) 128 | except Exception as e: 129 | log.error("Exception checking if target exists: {} {}".format(type(e), e)) 130 | return False 131 | return target is not None 132 | 133 | def target_is_valid(self, target_id=0): 134 | """ 135 | Returns True or False indicating whether or not the specified 136 | target is present and valid. 137 | 138 | `target_id` is a target ID (or None for the first target) 139 | """ 140 | try: 141 | target = self.target(target_id=target_id) 142 | except: 143 | return False 144 | return target['state'] != "invalid" 145 | 146 | def target_is_busy(self, target_id=0): 147 | """ 148 | Returns True or False indicating whether or not the specified 149 | target is busy. 150 | 151 | `target_id` is a target ID (or None for the first target) 152 | """ 153 | try: 154 | target = self.target(target_id=target_id) 155 | except: 156 | raise NoSuchTargetException() 157 | return target['state'] == "running" 158 | 159 | def add_listener(self, callback, state_changes=["stopped"]): 160 | """ 161 | Add a listener for state changes. 162 | """ 163 | self.listeners.append({"callback": callback, "state_changes": state_changes}) 164 | 165 | def remove_listener(self, callback): 166 | """ 167 | Remove a listener. 168 | """ 169 | listeners = filter(lambda x: x['callback'] == callback, self.listeners) 170 | for l in listeners: 171 | self.listeners.remove(l) 172 | 173 | def update_state(self): 174 | """ 175 | Notify all the listeners (probably `wait` plugins) that the state 176 | has changed. 177 | 178 | This is called by the debugger's stop-hook. 179 | """ 180 | for listener in self.listeners: 181 | listener['callback']() 182 | 183 | def register_command_plugin(self, name, cls): 184 | pass 185 | 186 | def capabilities(self): 187 | """ 188 | Return a list of the debugger's capabilities. 189 | 190 | Thus far only the 'async' capability is supported. This indicates 191 | that the debugger host can be queried from a background thread, 192 | and that views can use non-blocking API requests without queueing 193 | requests to be dispatched next time the debugger stops. 194 | """ 195 | return [] 196 | 197 | def pc(self, target_id=0, thread_id=None): 198 | return self.program_counter(target_id, thread_id) 199 | 200 | def sp(self, target_id=0, thread_id=None): 201 | return self.stack_pointer(target_id, thread_id) 202 | 203 | def disassemble_capstone(self, target_id=0, address=None, count=None): 204 | """ 205 | Disassemble with capstone. 206 | """ 207 | target = self.target(target_id) 208 | if not address: 209 | pc_name, address = self.pc() 210 | 211 | mem = self.memory(address, count * 16, target_id=target_id) 212 | 213 | md = capstone.Cs(*self.cs_archs[target['arch']]) 214 | output = [] 215 | for idx, i in enumerate(md.disasm(mem, address)): 216 | if idx >= count: 217 | break 218 | output.append("0x%x:\t%s\t%s" % (i.address, i.mnemonic, i.op_str)) 219 | 220 | return '\n'.join(output) 221 | 222 | 223 | class DebuggerCommand (object): 224 | """ 225 | The `voltron` command in the debugger. 226 | """ 227 | def __init__(self, *args, **kwargs): 228 | super(DebuggerCommand, self).__init__(*args, **kwargs) 229 | self.adaptor = voltron.debugger 230 | self.registered = False 231 | 232 | def handle_command(self, command): 233 | global log 234 | if 'debug' in command: 235 | if 'enable' in command: 236 | log.setLevel(logging.DEBUG) 237 | print("Debug logging enabled") 238 | elif 'disable' in command: 239 | log.setLevel(logging.INFO) 240 | print("Debug logging disabled") 241 | else: 242 | enabled = "enabled" if log.getEffectiveLevel() == logging.DEBUG else "disabled" 243 | print("Debug logging is currently " + enabled) 244 | elif 'init' in command: 245 | self.register_hooks() 246 | elif 'stopped' in command or 'update' in command: 247 | self.adaptor.update_state() 248 | voltron.server.dispatch_queue() 249 | else: 250 | print("Usage: voltron ") 251 | -------------------------------------------------------------------------------- /voltron/entry.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is the main entry point for Voltron from the debugger host's perspective. 3 | This file is loaded into the debugger through whatever means the given host 4 | supports. 5 | 6 | LLDB: 7 | 8 | (lldb) command script import /path/to/voltron/entry.py 9 | 10 | GDB: 11 | 12 | (gdb) source /path/to/voltron/entry.py 13 | 14 | VDB: 15 | 16 | (vdb) script /path/to/voltron/entry.py 17 | 18 | WinDbg/CDB (via PyKD): 19 | 20 | > .load pykd.pyd 21 | > !py --global C:\path\to\voltron\entry.py 22 | """ 23 | 24 | log = None 25 | 26 | try: 27 | # fix path if it's clobbered by brew 28 | import sys 29 | if sys.platform == 'darwin': 30 | py_base = '/System/Library/Frameworks/Python.framework/Versions/2.7/' 31 | new_path = ['lib/python27.zip', 'lib/python2.7', 'lib/python2.7/plat-darwin', 'lib/python2.7/plat-mac', 32 | 'lib/python2.7/plat-mac/lib-scriptpackages', 'Extras/lib/python', 'lib/python2.7/lib-tk', 33 | 'lib/python2.7/lib-old', 'lib/python2.7/lib-dynload'] 34 | sys.path = [p for p in sys.path if 'Cellar' not in p] + [py_base + p for p in new_path] 35 | except: 36 | pass 37 | 38 | try: 39 | import logging 40 | import os 41 | import sys 42 | blessed = None 43 | import blessed 44 | 45 | # add vtrace to the path so that dbg_vdb.py can import from vdb/vtrace. 46 | if "vtrace" in locals(): 47 | def parent_directory(the_path): 48 | return os.path.abspath(os.path.join(the_path, os.pardir)) 49 | 50 | def add_vdb_to_path(vtrace): 51 | sys.path.append(parent_directory(parent_directory(vtrace.__file__))) 52 | 53 | add_vdb_to_path(vtrace) 54 | else: 55 | pass 56 | 57 | import voltron 58 | from voltron.plugin import pm 59 | from voltron.core import Server 60 | 61 | log = voltron.setup_logging('debugger') 62 | 63 | # figure out in which debugger host we are running 64 | args = [] 65 | host = None 66 | try: 67 | import lldb 68 | host = "lldb" 69 | 70 | def invoke(*args): 71 | voltron.command._invoke(*args) 72 | except ImportError: 73 | pass 74 | try: 75 | import gdb 76 | host = "gdb" 77 | except ImportError: 78 | pass 79 | try: 80 | import pykd 81 | host = "windbg" 82 | except: 83 | pass 84 | if "vtrace" in locals(): 85 | host = "vdb" 86 | args = [db] 87 | if not host: 88 | raise Exception("No debugger host is present") 89 | 90 | # register any plugins that were loaded 91 | pm.register_plugins() 92 | 93 | # get the debugger plugin for the host we're in 94 | plugin = pm.debugger_plugin_for_host(host) 95 | 96 | if not voltron.server: 97 | # set up command and adaptor instances 98 | voltron.debugger = plugin.adaptor_class(*args) 99 | voltron.command = plugin.command_class(*args) 100 | 101 | # register command plugins now that we have a debugger host loaded 102 | pm.register_command_plugins() 103 | 104 | # create and start the voltron server 105 | voltron.server = Server() 106 | voltron.server.start() 107 | 108 | print(blessed.Terminal().bold_red("Voltron loaded.")) 109 | if host == 'lldb' and not voltron.command.registered: 110 | print("Run `voltron init` after you load a target.") 111 | except Exception as e: 112 | import traceback 113 | msg = ("An error occurred while loading Voltron:\n\n{}" 114 | "\nPlease ensure Voltron is installed correctly per the documentation: " 115 | "https://github.com/snare/voltron/wiki/Installation").format(traceback.format_exc()) 116 | if blessed: 117 | msg = blessed.Terminal().bold_red(msg) 118 | if log: 119 | log.exception("Exception raised while loading Voltron") 120 | print(msg) 121 | -------------------------------------------------------------------------------- /voltron/lexers.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | from pygments.lexer import RegexLexer, include, bygroups, using, DelegatingLexer 4 | from pygments.lexers import get_lexer_by_name 5 | from pygments.token import * 6 | 7 | 8 | class DisassemblyLexer(RegexLexer): 9 | """ 10 | For Nasm (Intel) disassembly from LLDB. 11 | 12 | Based on the NasmLexer included with Pygments 13 | """ 14 | name = 'LLDB Intel syntax disassembly' 15 | aliases = ['lldb_intel'] 16 | filenames = [] 17 | mimetypes = [] 18 | 19 | identifier = r'[]*' 20 | hexn = r'(?:0[xX][0-9a-f]+|$0[0-9a-f]*|[0-9]+[0-9a-f]*h)' 21 | octn = r'[0-7]+q' 22 | binn = r'[01]+b' 23 | decn = r'[0-9]+' 24 | floatn = decn + r'\.e?' + decn 25 | string = r'"(\\"|[^"\n])*"|' + r"'(\\'|[^'\n])*'|" + r"`(\\`|[^`\n])*`" 26 | declkw = r'(?:res|d)[bwdqt]|times' 27 | register = (r'r[0-9]+[bwd]{0,1}|' 28 | r'[a-d][lh]|[er]?[a-d]x|[er]?[sbi]p|[er]?[sd]i|[c-gs]s|st[0-7]|' 29 | r'mm[0-7]|cr[0-4]|dr[0-367]|tr[3-7]|.mm\d+') 30 | wordop = r'seg|wrt|strict' 31 | type = r'byte|[dq]?word|ptr|xmmword|opaque' 32 | 33 | flags = re.IGNORECASE | re.MULTILINE 34 | tokens = { 35 | 'root': [ 36 | (identifier + '`' + identifier, Name.Function), 37 | ('->', Generic.Prompt), 38 | include('whitespace'), 39 | (r'^\s*%', Comment.Preproc, 'preproc'), 40 | (identifier + ':', Name.Label), 41 | (r'(%s)(\s+)(equ)' % identifier, 42 | bygroups(Name.Constant, Keyword.Declaration, Keyword.Declaration), 43 | 'instruction-args'), 44 | (declkw, Keyword.Declaration, 'instruction-args'), 45 | (identifier, Keyword.Declaration, 'instruction-args'), 46 | (r' *' + hexn, Name.Label), 47 | (r'[:]', Text), 48 | (r'^->', Error), 49 | (r'[\r\n]+', Text) 50 | ], 51 | 'instruction-args': [ 52 | (register, Name.Builtin), 53 | (string, String), 54 | (hexn, Number.Hex), 55 | (octn, Number.Oct), 56 | (binn, Number.Bin), 57 | (floatn, Number.Float), 58 | (decn, Number.Integer), 59 | include('punctuation'), 60 | (identifier, Name.Variable), 61 | (r'[\r\n]+', Text, '#pop'), 62 | include('whitespace') 63 | ], 64 | 'preproc': [ 65 | (r'[^;\n]+', Comment.Preproc), 66 | (r';.*?\n', Comment.Single, '#pop'), 67 | (r'\n', Comment.Preproc, '#pop'), 68 | ], 69 | 'whitespace': [ 70 | (r'\n', Text), 71 | (r'[ \t]+', Text), 72 | (r';.*', Comment.Single), 73 | (r'#.*', Comment.Single) 74 | ], 75 | 'punctuation': [ 76 | (r'[,():\[\]]+', Punctuation), 77 | (r'[&|^<>+*/%~-]+', Operator), 78 | (r'[$]+', Keyword.Constant), 79 | (wordop, Operator.Word), 80 | (type, Keyword.Type) 81 | ], 82 | } 83 | 84 | 85 | class LLDBIntelLexer(DisassemblyLexer): 86 | name = 'LLDB Intel syntax disassembly' 87 | aliases = ['lldb_intel'] 88 | 89 | 90 | class LLDBATTLexer(DisassemblyLexer): 91 | name = 'LLDB AT&T syntax disassembly' 92 | aliases = ['lldb_att'] 93 | 94 | 95 | class GDBATTLexer(DisassemblyLexer): 96 | name = 'GDB AT&T syntax disassembly' 97 | aliases = ['gdb_att'] 98 | 99 | 100 | class GDBIntelLexer(DisassemblyLexer): 101 | name = 'GDB Intel syntax disassembly' 102 | aliases = ['gdb_intel'] 103 | 104 | 105 | class VDBATTLexer(DisassemblyLexer): 106 | name = 'VDB AT&T syntax disassembly' 107 | aliases = ['vdb_att'] 108 | 109 | 110 | class CapstoneIntelLexer(DisassemblyLexer): 111 | name = 'Capstone Intel syntax disassembly' 112 | aliases = ['capstone_intel'] 113 | 114 | 115 | class VDBIntelLexer(RegexLexer): 116 | """ 117 | For Nasm (Intel) disassembly from VDB. 118 | 119 | Based on the LLDBIntelLexer above. 120 | major difference is the raw instruction hex after the instruction address. 121 | 122 | example: 123 | rip 0x000000000056eb4f: 4885ff test rdi,rdi ;0x7f4f8740ca50,0x7f4f8740ca50 124 | 0x000000000056eb52: 740f jz 0x0056eb63 125 | """ 126 | name = 'VDB Intel syntax disassembly' 127 | aliases = ['vdb_intel'] 128 | filenames = [] 129 | mimetypes = [] 130 | 131 | space = r'[ \t]+' 132 | identifier = r'[]*' 133 | hexn = r'(?:0[xX][0-9a-f]+|$0[0-9a-f]*|[0-9]+[0-9a-f]*h)' # hex number 134 | hexr = r'(?:[0-9a-f]+)' # hex raw (no leader/trailer) 135 | octn = r'[0-7]+q' 136 | binn = r'[01]+b' 137 | decn = r'[0-9]+' 138 | floatn = decn + r'\.e?' + decn 139 | string = r'"(\\"|[^"\n])*"|' + r"'(\\'|[^'\n])*'|" + r"`(\\`|[^`\n])*`" 140 | register = (r'r[0-9]+[bwd]{0,1}|' 141 | r'[a-d][lh]|[er]?[a-d]x|[er]?[sbi]p|[er]?[sd]i|[c-gs]s|st[0-7]|' 142 | r'mm[0-7]|cr[0-4]|dr[0-367]|tr[3-7]|.mm\d*') 143 | wordop = r'seg|wrt|strict' 144 | type = r'byte|[dq]?word|ptr' 145 | 146 | flags = re.IGNORECASE | re.MULTILINE 147 | tokens = { 148 | 'root': [ 149 | (r'^(%s)(%s)(%s)(: )(%s)(%s)' % (register, space, hexn, hexr, space), 150 | bygroups(Name.Builtin, Text, Name.Label, Text, Number.Hex, Text), 151 | "instruction"), 152 | (r'^(%s)(%s)(: )(%s)(%s)' % (space, hexn, hexr, space), 153 | bygroups(Text, Name.Label, Text, Number.Hex, Text), 154 | "instruction") 155 | ], 156 | 'instruction': [ 157 | (space, Text), 158 | (r"(rep[a-z]*)( )", bygroups(Name.Function, Text)), 159 | (r"(%s)" % identifier, Name.Function, ("#pop", "instruction-args")), 160 | ], 161 | 'instruction-args': [ 162 | (space, Text), 163 | (string, String), 164 | (hexn, Number.Hex), 165 | (octn, Number.Oct), 166 | (binn, Number.Bin), 167 | (floatn, Number.Float), 168 | (decn, Number.Integer), 169 | include('punctuation'), 170 | (register, Name.Builtin), 171 | (identifier, Name.Variable), 172 | (r'[\r\n]+', Text, '#pop'), 173 | (r';', Text, ("#pop", 'comment')), 174 | ], 175 | 'comment': [ 176 | (space, Text), 177 | (string, Comment.Single), 178 | (hexn, Number.Hex), 179 | (octn, Number.Oct), 180 | (binn, Number.Bin), 181 | (floatn, Number.Float), 182 | (decn, Number.Integer), 183 | include('punctuation'), 184 | (register, Name.Builtin), 185 | (identifier, Name.Variable), 186 | (r'[\r\n]+', Text, '#pop'), 187 | ], 188 | 'punctuation': [ 189 | (r'[,():\[\]]+', Punctuation), 190 | (r'[&|^<>+*/%~-]+', Operator), 191 | (r'[$]+', Keyword.Constant), 192 | (wordop, Operator.Word), 193 | (type, Keyword.Type) 194 | ], 195 | } 196 | 197 | 198 | class WinDbgIntelLexer(RegexLexer): 199 | name = 'WinDbg Intel syntax disassembly' 200 | aliases = ['windbg_intel'] 201 | filenames = [] 202 | mimetypes = [] 203 | 204 | identifier = r'[]*' 205 | hexn = r'(0[xX])?([0-9a-f]+|$0[0-9a-f`]*|[0-9]+[0-9a-f]*h)' 206 | addr = r'(0[xX])?([0-9a-f`]+|$0[0-9a-f`]*|[0-9]+[0-9a-f`]*h)' 207 | octn = r'[0-7]+q' 208 | binn = r'[01]+b' 209 | decn = r'[0-9]+' 210 | floatn = decn + r'\.e?' + decn 211 | string = r'"(\\"|[^"\n])*"|' + r"'(\\'|[^'\n])*'|" + r"`(\\`|[^`\n])*`" 212 | declkw = r'(?:res|d)[bwdqt]|times' 213 | register = (r'r[0-9]+?[bwd]{0,1}|' 214 | r'[a-d][lh]|[er]?[a-d]x|[er]?[sbi]p|[er]?[sd]i|[c-gs]s|st[0-7]|' 215 | r'mm[0-7]|cr[0-4]|dr[0-367]|tr[3-7]|.mm\d*') 216 | wordop = r'seg|wrt|strict' 217 | type = r'byte|[dq]?word|ptr' 218 | func = r'[a-zA-Z]*\!?' 219 | 220 | flags = re.IGNORECASE | re.MULTILINE 221 | tokens = { 222 | 'root': [ 223 | (addr, Number.Hex, 'instruction-line'), 224 | include('whitespace'), 225 | (identifier, Name.Class, 'label'), 226 | (r'[:]', Text), 227 | (r'[\r\n]+', Text) 228 | ], 229 | 'instruction-line': [ 230 | (r' ', Text), 231 | (hexn, Text, 'instruction'), 232 | ], 233 | 'instruction': [ 234 | include('whitespace'), 235 | (r'(%s)(\s+)(equ)' % identifier, 236 | bygroups(Name.Constant, Keyword.Declaration, Keyword.Declaration), 237 | 'instruction-args'), 238 | (declkw, Keyword.Declaration, 'instruction-args'), 239 | (identifier, Name.Function, 'instruction-args'), 240 | ], 241 | 'label': [ 242 | (r'[!+]', Operator), 243 | (identifier, Name.Function), 244 | (hexn, Number.Hex), 245 | (r'[:]', Text, '#pop'), 246 | 247 | ], 248 | 'instruction-args': [ 249 | (string, String), 250 | include('punctuation'), 251 | (register, Name.Builtin), 252 | include('label'), 253 | (identifier, Name.Variable), 254 | (r'[\r\n]+', Text, '#pop:3'), 255 | include('whitespace'), 256 | (hexn, Number.Hex), 257 | (addr, Number.Hex), 258 | (octn, Number.Oct), 259 | (binn, Number.Bin), 260 | (floatn, Number.Float), 261 | (decn, Number.Integer), 262 | ], 263 | 'preproc': [ 264 | (r'[^;\n]+', Comment.Preproc), 265 | (r';.*?\n', Comment.Single, '#pop'), 266 | (r'\n', Comment.Preproc, '#pop'), 267 | ], 268 | 'whitespace': [ 269 | (r'\n', Text), 270 | (r'[ \t]+', Text), 271 | (r';.*', Comment.Single), 272 | (r'#.*', Comment.Single) 273 | ], 274 | 'punctuation': [ 275 | (r'[,():\[\]]+', Punctuation), 276 | (r'[&|^<>+*/%~-]+', Operator), 277 | (r'[$]+', Keyword.Constant), 278 | (wordop, Operator.Word), 279 | (type, Keyword.Type) 280 | ], 281 | } 282 | 283 | 284 | class WinDbgATTLexer(WinDbgIntelLexer): 285 | name = 'WinDbg ATT syntax disassembly' 286 | aliases = ['windbg_att'] 287 | -------------------------------------------------------------------------------- /voltron/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import argparse 3 | import logging 4 | import traceback 5 | import logging 6 | import logging.config 7 | 8 | import voltron 9 | from .view import * 10 | from .core import * 11 | 12 | log = logging.getLogger('main') 13 | 14 | 15 | def main(debugger=None): 16 | voltron.setup_logging('main') 17 | 18 | # Set up command line arg parser 19 | parser = argparse.ArgumentParser() 20 | parser.register('action', 'parsers', AliasedSubParsersAction) 21 | parser.add_argument('--debug', '-d', action='store_true', help='print debug logging') 22 | parser.add_argument('-o', action='append', help='override config variable', default=[]) 23 | top_level_sp = parser.add_subparsers(title='subcommands', description='valid subcommands', dest='subcommand') 24 | top_level_sp.required = True 25 | view_parser = top_level_sp.add_parser('view', help='display a view', aliases=('v')) 26 | view_parser.register('action', 'parsers', AliasedSubParsersAction) 27 | view_sp = view_parser.add_subparsers(title='views', description='valid view types', help='additional help', dest='view') 28 | view_sp.required = True 29 | 30 | # Set up a subcommand for each view class 31 | pm = PluginManager() 32 | pm.register_plugins() 33 | for plugin in pm.view_plugins: 34 | pm.view_plugins[plugin].view_class.configure_subparser(view_sp) 35 | 36 | # Parse args 37 | args = parser.parse_args() 38 | if args.debug: 39 | voltron.config['general']['debug_logging'] = True 40 | voltron.setup_logging('main') 41 | voltron.config.update(options=dict((tuple(x.split('=')) for x in args.o))) 42 | 43 | # Instantiate and run the appropriate module 44 | inst = args.func(args, loaded_config=voltron.config) 45 | inst.pm = pm 46 | try: 47 | inst.run() 48 | except Exception as e: 49 | log.exception("Exception running module {}: {}".format(inst.__class__.__name__, traceback.format_exc())) 50 | print("Encountered an exception while running the view '{}':\n{}".format(inst.__class__.__name__, traceback.format_exc())) 51 | except KeyboardInterrupt: 52 | suppress_exit_log = True 53 | inst.cleanup() 54 | -------------------------------------------------------------------------------- /voltron/plugin.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import inspect 3 | import os 4 | from collections import defaultdict 5 | 6 | from scruffy.plugin import Plugin 7 | 8 | import voltron 9 | 10 | log = logging.getLogger('plugin') 11 | 12 | 13 | class PluginManager(object): 14 | """ 15 | Collects and validates API, debugger and view plugins. Provides methods to 16 | access and search the plugin collection. 17 | 18 | Plugin loading itself is handled by scruffy, which is configured in the 19 | environment specification in `env.py`. 20 | """ 21 | def __init__(self): 22 | """ 23 | Initialise a new PluginManager. 24 | """ 25 | self._api_plugins = defaultdict(lambda: None) 26 | self._debugger_plugins = defaultdict(lambda: None) 27 | self._view_plugins = defaultdict(lambda: None) 28 | self._web_plugins = defaultdict(lambda: None) 29 | self._command_plugins = defaultdict(lambda: None) 30 | 31 | def register_plugins(self): 32 | for p in voltron.env.plugins: 33 | self.register_plugin(p) 34 | 35 | def register_command_plugins(self): 36 | for p in voltron.env.plugins: 37 | if issubclass(p, CommandPlugin): 38 | self.register_plugin(p) 39 | 40 | @property 41 | def api_plugins(self): 42 | return self._api_plugins 43 | 44 | @property 45 | def debugger_plugins(self): 46 | return self._debugger_plugins 47 | 48 | @property 49 | def view_plugins(self): 50 | return self._view_plugins 51 | 52 | @property 53 | def web_plugins(self): 54 | return self._web_plugins 55 | 56 | @property 57 | def command_plugins(self): 58 | return self._command_plugins 59 | 60 | def register_plugin(self, plugin): 61 | """ 62 | Register a new plugin with the PluginManager. 63 | 64 | `plugin` is a subclass of scruffy's Plugin class. 65 | 66 | This is called by __init__(), but may also be called by the debugger 67 | host to load a specific plugin at runtime. 68 | """ 69 | if hasattr(plugin, 'initialise'): 70 | plugin.initialise() 71 | if self.valid_api_plugin(plugin): 72 | log.debug("Registering API plugin: {}".format(plugin)) 73 | self._api_plugins[plugin.request] = plugin() 74 | elif self.valid_debugger_plugin(plugin): 75 | log.debug("Registering debugger plugin: {}".format(plugin)) 76 | self._debugger_plugins[plugin.host] = plugin() 77 | elif self.valid_view_plugin(plugin): 78 | log.debug("Registering view plugin: {}".format(plugin)) 79 | self._view_plugins[plugin.name] = plugin() 80 | elif self.valid_web_plugin(plugin): 81 | log.debug("Registering web plugin: {}".format(plugin)) 82 | self._web_plugins[plugin.name] = plugin() 83 | elif self.valid_command_plugin(plugin): 84 | log.debug("Registering command plugin: {}".format(plugin)) 85 | self._command_plugins[plugin.name] = plugin() 86 | if voltron.debugger: 87 | voltron.debugger.register_command_plugin(plugin.name, plugin.command_class) 88 | else: 89 | log.debug("Ignoring invalid plugin: {}".format(plugin)) 90 | 91 | def valid_api_plugin(self, plugin): 92 | """ 93 | Validate an API plugin, ensuring it is an API plugin and has the 94 | necessary fields present. 95 | 96 | `plugin` is a subclass of scruffy's Plugin class. 97 | """ 98 | if (issubclass(plugin, APIPlugin) and 99 | hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'api' and 100 | hasattr(plugin, 'request') and plugin.request != None and 101 | hasattr(plugin, 'request_class') and plugin.request_class != None and 102 | hasattr(plugin, 'response_class') and plugin.response_class != None): 103 | return True 104 | return False 105 | 106 | def valid_debugger_plugin(self, plugin): 107 | """ 108 | Validate a debugger plugin, ensuring it is a debugger plugin and has 109 | the necessary fields present. 110 | 111 | `plugin` is a subclass of scruffy's Plugin class. 112 | """ 113 | if (issubclass(plugin, DebuggerAdaptorPlugin) and 114 | hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'debugger' and 115 | hasattr(plugin, 'host') and plugin.host != None): 116 | return True 117 | return False 118 | 119 | def valid_view_plugin(self, plugin): 120 | """ 121 | Validate a view plugin, ensuring it is a view plugin and has the 122 | necessary fields present. 123 | 124 | `plugin` is a subclass of scruffy's Plugin class. 125 | """ 126 | if (issubclass(plugin, ViewPlugin) and 127 | hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'view' and 128 | hasattr(plugin, 'name') and plugin.name != None and 129 | hasattr(plugin, 'view_class') and plugin.view_class != None): 130 | return True 131 | return False 132 | 133 | def valid_web_plugin(self, plugin): 134 | """ 135 | Validate a web plugin, ensuring it is a web plugin and has the 136 | necessary fields present. 137 | 138 | `plugin` is a subclass of scruffy's Plugin class. 139 | """ 140 | if (issubclass(plugin, WebPlugin) and 141 | hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'web' and 142 | hasattr(plugin, 'name') and plugin.name != None): 143 | return True 144 | return False 145 | 146 | def valid_command_plugin(self, plugin): 147 | """ 148 | Validate a command plugin, ensuring it is a command plugin and has the 149 | necessary fields present. 150 | 151 | `plugin` is a subclass of scruffy's Plugin class. 152 | """ 153 | if (issubclass(plugin, CommandPlugin) and 154 | hasattr(plugin, 'plugin_type') and plugin.plugin_type == 'command' and 155 | hasattr(plugin, 'name') and plugin.name != None): 156 | return True 157 | return False 158 | 159 | def api_plugin_for_request(self, request=None): 160 | """ 161 | Find an API plugin that supports the given request type. 162 | """ 163 | return self.api_plugins[request] 164 | 165 | def debugger_plugin_for_host(self, host=None): 166 | """ 167 | Find a debugger plugin that supports the debugger host. 168 | """ 169 | return self.debugger_plugins[host] 170 | 171 | def view_plugin_with_name(self, name=None): 172 | """ 173 | Find a view plugin that for the given view name. 174 | """ 175 | return self.view_plugins[name] 176 | 177 | def web_plugin_with_name(self, name=None): 178 | """ 179 | Find a web plugin that for the given view name. 180 | """ 181 | return self.web_plugins[name] 182 | 183 | def command_plugin_with_name(self, name=None): 184 | """ 185 | Find a command plugin that for the given view name. 186 | """ 187 | return self.command_plugins[name] 188 | 189 | 190 | class VoltronPlugin(Plugin): 191 | @classmethod 192 | def initialise(cls): 193 | pass 194 | 195 | 196 | class APIPlugin(VoltronPlugin): 197 | """ 198 | Abstract API plugin class. API plugins subclass this. 199 | 200 | `plugin_type` is 'api' 201 | `request` is the request type (e.g. 'version') 202 | `request_class` is the APIRequest subclass (e.g. APIVersionRequest) 203 | `response_class` is the APIResponse subclass (e.g. APIVersionResponse) 204 | `supported_hosts` is an array of debugger adaptor plugins that this plugin 205 | supports (e.g. 'lldb', 'gdb'). There is also a special host type called 206 | 'core' If the plugin requires only the 'core' debugger host, it can be used 207 | with any debugger adaptor plugin that implements the full interface (ie. 208 | the included 'lldb' and 'gdb' plugins). If it requires a specifically named 209 | debugger host plugin, then it will only work with those plugins specified. 210 | This allows developers to add custom API plugins that communicate directly 211 | with their chosen debugger host API, to do things that the standard 212 | debugger adaptor plugins don't support. 213 | 214 | See the core API plugins in voltron/plugins/api/ for examples. 215 | """ 216 | plugin_type = 'api' 217 | request = None 218 | request_class = None 219 | response_class = None 220 | supported_hosts = ['core'] 221 | 222 | @classmethod 223 | def initialise(cls): 224 | if cls.request_class: 225 | cls.request_class._plugin = cls 226 | cls.request_class.request = cls.request 227 | 228 | 229 | class DebuggerAdaptorPlugin(VoltronPlugin): 230 | """ 231 | Debugger adaptor plugin parent class. 232 | 233 | `plugin_type` is 'debugger' 234 | `host` is the name of the debugger host (e.g. 'lldb' or 'gdb') 235 | `adaptor_class` is the debugger adaptor class that can be queried 236 | See the core debugger plugins in voltron/plugins/debugger/ for examples. 237 | """ 238 | plugin_type = 'debugger' 239 | host = None 240 | adaptor_class = None 241 | supported_hosts = ['core'] 242 | 243 | @classmethod 244 | def initialise(cls): 245 | if cls.adaptor_class: 246 | cls.adaptor_class._plugin = cls 247 | 248 | 249 | class ViewPlugin(VoltronPlugin): 250 | """ 251 | View plugin parent class. 252 | 253 | `plugin_type` is 'view' 254 | `name` is the name of the view (e.g. 'register' or 'disassembly') 255 | `view_class` is the main view class 256 | 257 | See the core view plugins in voltron/plugins/view/ for examples. 258 | """ 259 | plugin_type = 'view' 260 | name = None 261 | view_class = None 262 | 263 | @classmethod 264 | def initialise(cls): 265 | if cls.view_class: 266 | cls.view_class._plugin = cls 267 | cls.view_class.view_type = cls.name 268 | 269 | 270 | class WebPlugin(VoltronPlugin): 271 | """ 272 | Web plugin parent class. 273 | 274 | `plugin_type` is 'web' 275 | `name` is the name of the web plugin (e.g. 'webview') 276 | `app` is a Flask app (or whatever, optional) 277 | """ 278 | _dir = None 279 | 280 | plugin_type = 'web' 281 | name = None 282 | app = None 283 | 284 | def __init__(self): 285 | self._dir = os.path.dirname(inspect.getfile(self.__class__)) 286 | 287 | 288 | class CommandPlugin(VoltronPlugin): 289 | """ 290 | Command plugin parent class. 291 | 292 | `plugin_type` is 'command' 293 | `name` is the name of the command plugin 294 | """ 295 | plugin_type = 'command' 296 | name = None 297 | 298 | 299 | class VoltronCommand(object): 300 | pass 301 | 302 | 303 | # 304 | # Shared plugin manager and convenience methods 305 | # 306 | 307 | pm = PluginManager() 308 | 309 | 310 | def api_request(request, *args, **kwargs): 311 | """ 312 | Create an API request. 313 | 314 | `request_type` is the request type (string). This is used to look up a 315 | plugin, whose request class is instantiated and passed the remaining 316 | arguments passed to this function. 317 | """ 318 | plugin = pm.api_plugin_for_request(request) 319 | if plugin and plugin.request_class: 320 | req = plugin.request_class(*args, **kwargs) 321 | else: 322 | raise Exception("Invalid request type") 323 | return req 324 | 325 | 326 | def api_response(request, *args, **kwargs): 327 | plugin = pm.api_plugin_for_request(request) 328 | if plugin and plugin.response_class: 329 | req = plugin.response_class(*args, **kwargs) 330 | else: 331 | raise Exception("Invalid request type") 332 | return req 333 | 334 | 335 | def debugger_adaptor(host, *args, **kwargs): 336 | plugin = pm.debugger_plugin_for_host(host) 337 | if plugin and plugin.adaptor_class: 338 | adaptor = plugin.adaptor_class(*args, **kwargs) 339 | else: 340 | raise Exception("Invalid debugger host") 341 | return adaptor 342 | 343 | 344 | def view(name, *args, **kwargs): 345 | plugin = pm.view_plugin_with_name(name) 346 | if plugin and plugin.view_class: 347 | view = plugin.view_class(*args, **kwargs) 348 | else: 349 | raise Exception("Invalid view name") 350 | return view 351 | 352 | 353 | def command(name, *args, **kwargs): 354 | plugin = pm.command_plugin_with_name(name) 355 | if plugin and plugin.command_class: 356 | command = plugin.command_class(*args, **kwargs) 357 | else: 358 | raise Exception("Invalid command name") 359 | return command 360 | 361 | 362 | def web_plugins(): 363 | return pm.web_plugins 364 | -------------------------------------------------------------------------------- /voltron/plugins/__init__.py: -------------------------------------------------------------------------------- 1 | if not hasattr(__builtins__, "xrange"): 2 | xrange = range 3 | -------------------------------------------------------------------------------- /voltron/plugins/api/__init__.py: -------------------------------------------------------------------------------- 1 | if not hasattr(__builtins__, "xrange"): 2 | xrange = range 3 | -------------------------------------------------------------------------------- /voltron/plugins/api/backtrace.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | from voltron.api import * 3 | 4 | 5 | import logging 6 | 7 | import voltron 8 | from voltron.api import * 9 | 10 | from scruffy.plugin import Plugin 11 | 12 | log = logging.getLogger('api') 13 | 14 | 15 | class APIBacktraceRequest(APIRequest): 16 | """ 17 | API backtrace request. 18 | 19 | { 20 | "type": "request", 21 | "request": "backtrace" 22 | } 23 | """ 24 | @server_side 25 | def dispatch(self): 26 | try: 27 | bt = voltron.debugger.backtrace() 28 | res = APIBacktraceResponse(frames=bt) 29 | except NoSuchTargetException: 30 | res = APINoSuchTargetErrorResponse() 31 | except Exception as e: 32 | msg = "Exception getting backtrace: {}".format(repr(e)) 33 | log.exception(msg) 34 | res = APIGenericErrorResponse(msg) 35 | 36 | return res 37 | 38 | 39 | class APIBacktraceResponse(APISuccessResponse): 40 | """ 41 | API backtrace response. 42 | 43 | { 44 | "type": "response", 45 | "status": "success", 46 | "data": { 47 | "frames": [ 48 | { 49 | "index": 0, 50 | "addr": 0xffff, 51 | "name": "inferior`main + 0" 52 | } 53 | ] 54 | } 55 | } 56 | """ 57 | _fields = {'frames': True} 58 | 59 | frames = [] 60 | 61 | 62 | class APIBacktracePlugin(APIPlugin): 63 | request = "backtrace" 64 | request_class = APIBacktraceRequest 65 | response_class = APIBacktraceResponse 66 | -------------------------------------------------------------------------------- /voltron/plugins/api/breakpoints.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | from voltron.api import * 3 | 4 | 5 | import logging 6 | 7 | import voltron 8 | from voltron.api import * 9 | 10 | from scruffy.plugin import Plugin 11 | 12 | log = logging.getLogger('api') 13 | 14 | 15 | class APIBreakpointsRequest(APIRequest): 16 | """ 17 | API breakpoints request. 18 | 19 | { 20 | "type": "request", 21 | "request": "breakpoints" 22 | } 23 | """ 24 | @server_side 25 | def dispatch(self): 26 | try: 27 | bps = voltron.debugger.breakpoints() 28 | res = APIBreakpointsResponse(breakpoints=bps) 29 | except NoSuchTargetException: 30 | res = APINoSuchTargetErrorResponse() 31 | except Exception as e: 32 | msg = "Exception getting breakpoints: {}".format(repr(e)) 33 | log.exception(msg) 34 | res = APIGenericErrorResponse(msg) 35 | 36 | return res 37 | 38 | 39 | class APIBreakpointsResponse(APISuccessResponse): 40 | """ 41 | API breakpoints response. 42 | 43 | { 44 | "type": "response", 45 | "status": "success", 46 | "data": { 47 | "breakpoints": [ 48 | { 49 | "id": 1, 50 | "enabled": True, 51 | "one_shot": False, 52 | "hit_count": 5, 53 | "locations": [ 54 | { 55 | "address": 0x100000cf0, 56 | "name": 'main' 57 | } 58 | ] 59 | } 60 | ] 61 | } 62 | } 63 | """ 64 | _fields = {'breakpoints': True} 65 | 66 | breakpoints = [] 67 | 68 | 69 | class APIBreakpointsPlugin(APIPlugin): 70 | request = "breakpoints" 71 | request_class = APIBreakpointsRequest 72 | response_class = APIBreakpointsResponse 73 | -------------------------------------------------------------------------------- /voltron/plugins/api/command.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import voltron 4 | from voltron.api import * 5 | 6 | from scruffy.plugin import Plugin 7 | 8 | log = logging.getLogger('api') 9 | 10 | class APICommandRequest(APIRequest): 11 | """ 12 | API execute command request. 13 | 14 | { 15 | "type": "request", 16 | "request": "command" 17 | "data": { 18 | "command": "break list" 19 | } 20 | } 21 | """ 22 | _fields = {'command': True} 23 | 24 | @server_side 25 | def dispatch(self): 26 | try: 27 | output = voltron.debugger.command(self.command) 28 | res = APICommandResponse() 29 | res.output = output 30 | except NoSuchTargetException: 31 | res = APINoSuchTargetErrorResponse() 32 | except Exception as e: 33 | msg = "Exception executing debugger command: {}".format(repr(e)) 34 | log.exception(msg) 35 | res = APIGenericErrorResponse(msg) 36 | 37 | return res 38 | 39 | 40 | class APICommandResponse(APISuccessResponse): 41 | """ 42 | API list targets response. 43 | 44 | { 45 | "type": "response", 46 | "status": "success", 47 | "data": { 48 | "output": "stuff" 49 | } 50 | } 51 | """ 52 | _fields = {'output': True} 53 | 54 | output = None 55 | 56 | 57 | class APICommandPlugin(APIPlugin): 58 | request = "command" 59 | request_class = APICommandRequest 60 | response_class = APICommandResponse 61 | -------------------------------------------------------------------------------- /voltron/plugins/api/dereference.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import voltron 4 | from voltron.api import * 5 | 6 | from scruffy.plugin import Plugin 7 | 8 | log = logging.getLogger('api') 9 | 10 | class APIDerefRequest(APIRequest): 11 | """ 12 | API dereference pointer request. 13 | 14 | { 15 | "type": "request", 16 | "request": "dereference" 17 | "data": { 18 | "pointer": 0xffffff8012341234 19 | } 20 | } 21 | """ 22 | _fields = {'pointer': True} 23 | 24 | @server_side 25 | def dispatch(self): 26 | try: 27 | output = voltron.debugger.dereference(self.pointer) 28 | log.debug('output: {}'.format(str(output))) 29 | res = APIDerefResponse() 30 | res.output = output 31 | except NoSuchTargetException: 32 | res = APINoSuchTargetErrorResponse() 33 | except Exception as e: 34 | msg = "Exception dereferencing pointer: {}".format(repr(e)) 35 | log.exception(msg) 36 | res = APIGenericErrorResponse(msg) 37 | 38 | return res 39 | 40 | 41 | class APIDerefResponse(APISuccessResponse): 42 | """ 43 | API dereference pointer response. 44 | 45 | { 46 | "type": "response", 47 | "status": "success", 48 | "data": { 49 | "output": [0xffffff8055555555, "main + 0x123"] 50 | } 51 | } 52 | """ 53 | _fields = {'output': True} 54 | 55 | output = None 56 | 57 | 58 | class APIDerefPlugin(APIPlugin): 59 | request = "dereference" 60 | request_class = APIDerefRequest 61 | response_class = APIDerefResponse 62 | -------------------------------------------------------------------------------- /voltron/plugins/api/disassemble.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import logging 3 | 4 | from voltron.api import * 5 | from voltron.plugin import * 6 | 7 | log = logging.getLogger('api') 8 | 9 | class APIDisassembleRequest(APIRequest): 10 | """ 11 | API disassemble request. 12 | 13 | { 14 | "type": "request", 15 | "request": "disassemble" 16 | "data": { 17 | "target_id": 0, 18 | "address": 0x12341234, 19 | "count": 16, 20 | "use_capstone": False 21 | } 22 | } 23 | 24 | `target_id` is optional. 25 | `address` is the address at which to start disassembling. Defaults to 26 | instruction pointer if not specified. 27 | `count` is the number of instructions to disassemble. 28 | `use_capstone` a flag to indicate whether or not Capstone should be used 29 | instead of the debugger's disassembler. 30 | """ 31 | _fields = {'target_id': False, 'address': False, 'count': True, 'use_capstone': False} 32 | 33 | target_id = 0 34 | address = None 35 | count = 16 36 | 37 | @server_side 38 | def dispatch(self): 39 | try: 40 | if self.address == None: 41 | pc_name, self.address = voltron.debugger.program_counter(target_id=self.target_id) 42 | if self.use_capstone: 43 | disasm = voltron.debugger.disassemble_capstone(target_id=self.target_id, address=self.address, 44 | count=self.count) 45 | else: 46 | disasm = voltron.debugger.disassemble(target_id=self.target_id, address=self.address, count=self.count) 47 | res = APIDisassembleResponse() 48 | res.disassembly = disasm 49 | try: 50 | res.flavor = voltron.debugger.disassembly_flavor() 51 | except: 52 | res.flavor = 'NA' 53 | res.host = voltron.debugger._plugin.host 54 | except NoSuchTargetException: 55 | res = APINoSuchTargetErrorResponse() 56 | except TargetBusyException: 57 | res = APITargetBusyErrorResponse() 58 | except Exception as e: 59 | msg = "Unhandled exception {} disassembling: {}".format(type(e), e) 60 | log.exception(msg) 61 | res = APIErrorResponse(code=0, message=msg) 62 | 63 | return res 64 | 65 | 66 | class APIDisassembleResponse(APISuccessResponse): 67 | """ 68 | API disassemble response. 69 | 70 | { 71 | "type": "response", 72 | "status": "success", 73 | "data": { 74 | "disassembly": "mov blah blah" 75 | } 76 | } 77 | """ 78 | _fields = {'disassembly': True, 'formatted': False, 'flavor': False, 'host': False} 79 | 80 | disassembly = None 81 | formatted = None 82 | flavor = None 83 | host = None 84 | 85 | 86 | class APIDisassemblePlugin(APIPlugin): 87 | request = 'disassemble' 88 | request_class = APIDisassembleRequest 89 | response_class = APIDisassembleResponse 90 | -------------------------------------------------------------------------------- /voltron/plugins/api/memory.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import logging 3 | import six 4 | import struct 5 | 6 | from voltron.api import * 7 | 8 | log = logging.getLogger('api') 9 | 10 | 11 | class APIMemoryRequest(APIRequest): 12 | """ 13 | API read memory request. 14 | 15 | { 16 | "type": "request", 17 | "request": "memory", 18 | "data": { 19 | "target_id":0, 20 | "address": 0x12341234, 21 | "length": 0x40 22 | } 23 | } 24 | 25 | `target_id` is optional. If not present, the currently selected target 26 | will be used. 27 | 28 | `address` is the address at which to start reading. 29 | 30 | `length` is the number of bytes to read. 31 | 32 | `register` is the register containing the address at which to start reading. 33 | 34 | `command` is the debugger command to execute to calculate the address at 35 | which to start reading. 36 | 37 | `deref` is a flag indicating whether or not to dereference any pointers 38 | within the memory region read. 39 | 40 | `offset` is an offset to add to the address at which to start reading. 41 | """ 42 | _fields = { 43 | 'target_id': False, 44 | 'address': False, 45 | 'length': False, 46 | 'words': False, 47 | 'register': False, 48 | 'command': False, 49 | 'deref': False, 50 | 'offset': False 51 | } 52 | 53 | target_id = 0 54 | 55 | @server_side 56 | def dispatch(self): 57 | try: 58 | target = voltron.debugger.target(self.target_id) 59 | 60 | # if 'words' was specified, get the addr_size and calculate the length to read 61 | if self.words: 62 | self.length = self.words * target['addr_size'] 63 | 64 | # calculate the address at which to begin reading 65 | if self.address: 66 | addr = self.address 67 | elif self.command: 68 | output = voltron.debugger.command(self.command) 69 | if output: 70 | for item in reversed(output.split()): 71 | log.debug("checking item: {}".format(item)) 72 | try: 73 | addr = int(item) 74 | break 75 | except: 76 | try: 77 | addr = int(item, 16) 78 | break 79 | except: 80 | pass 81 | elif self.register: 82 | regs = voltron.debugger.registers(registers=[self.register]) 83 | addr = list(regs.values())[0] 84 | if self.offset: 85 | if self.words: 86 | addr += self.offset * target['addr_size'] 87 | else: 88 | addr += self.offset 89 | 90 | # read memory 91 | memory = voltron.debugger.memory(address=int(addr), length=int(self.length), target_id=int(self.target_id)) 92 | 93 | # deref pointers 94 | deref = None 95 | if self.deref: 96 | fmt = ('<' if target['byte_order'] == 'little' else '>') + {2: 'H', 4: 'L', 8: 'Q'}[target['addr_size']] 97 | deref = [] 98 | for chunk in zip(*[six.iterbytes(memory)] * target['addr_size']): 99 | chunk = ''.join([six.unichr(x) for x in chunk]).encode('latin1') 100 | p = list(struct.unpack(fmt, chunk))[0] 101 | if p > 0: 102 | try: 103 | deref.append(voltron.debugger.dereference(pointer=p)) 104 | except: 105 | deref.append([]) 106 | else: 107 | deref.append([]) 108 | 109 | res = APIMemoryResponse() 110 | res.address = addr 111 | res.memory = six.u(memory) 112 | res.bytes = len(memory) 113 | res.deref = deref 114 | except TargetBusyException: 115 | res = APITargetBusyErrorResponse() 116 | except NoSuchTargetException: 117 | res = APINoSuchTargetErrorResponse() 118 | except Exception as e: 119 | msg = "Exception getting memory from debugger: {}".format(repr(e)) 120 | log.exception(msg) 121 | res = APIGenericErrorResponse(msg) 122 | 123 | return res 124 | 125 | 126 | class APIMemoryResponse(APISuccessResponse): 127 | """ 128 | API read memory response. 129 | 130 | { 131 | "type": "response", 132 | "status": "success", 133 | "data": { 134 | "memory": "ABCDEF" # base64 encoded memory 135 | } 136 | } 137 | """ 138 | _fields = { 139 | 'address': True, 140 | 'memory': True, 141 | 'bytes': True, 142 | 'deref': False 143 | } 144 | _encode_fields = ['memory'] 145 | 146 | address = None 147 | memory = None 148 | bytes = None 149 | deref = None 150 | 151 | 152 | class APIReadMemoryPlugin(APIPlugin): 153 | request = 'memory' 154 | request_class = APIMemoryRequest 155 | response_class = APIMemoryResponse 156 | -------------------------------------------------------------------------------- /voltron/plugins/api/null.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import voltron.api 3 | from voltron.api import * 4 | 5 | from scruffy.plugin import Plugin 6 | 7 | 8 | class APINullRequest(APIRequest): 9 | """ 10 | API null request. 11 | 12 | { 13 | "type": "request", 14 | "request": "null" 15 | } 16 | """ 17 | @server_side 18 | def dispatch(self): 19 | return APINullResponse() 20 | 21 | 22 | class APINullResponse(APISuccessResponse): 23 | """ 24 | API null response. 25 | 26 | { 27 | "type": "response", 28 | "status": "success" 29 | } 30 | """ 31 | 32 | 33 | class APINullPlugin(APIPlugin): 34 | request = 'null' 35 | request_class = APINullRequest 36 | response_class = APINullResponse 37 | -------------------------------------------------------------------------------- /voltron/plugins/api/plugins.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import voltron.api 3 | from voltron.api import * 4 | 5 | from scruffy.plugin import Plugin 6 | 7 | 8 | class APIPluginsRequest(APIRequest): 9 | """ 10 | API plugins request. 11 | 12 | { 13 | "type": "request", 14 | "request": "plugins" 15 | } 16 | """ 17 | @server_side 18 | def dispatch(self): 19 | res = APIPluginsResponse() 20 | return res 21 | 22 | 23 | class APIPluginsResponse(APISuccessResponse): 24 | """ 25 | API plugins response. 26 | 27 | { 28 | "type": "response", 29 | "status": "success", 30 | "data": { 31 | "plugins": { 32 | "api": { 33 | "version": ["api_version", "host_version", "capabilities"] 34 | ... 35 | }, 36 | "debugger": { 37 | ... 38 | }, 39 | ... 40 | } 41 | } 42 | } 43 | """ 44 | _fields = { 45 | 'plugins': True 46 | } 47 | 48 | def __init__(self, *args, **kwargs): 49 | super(APIPluginsResponse, self).__init__(*args, **kwargs) 50 | self.plugins = { 51 | 'api': {n: {'request': p.request_class._fields, 'response': p.response_class._fields} 52 | for (n, p) in voltron.plugin.pm.api_plugins.iteritems()}, 53 | 'debugger': [n for n in voltron.plugin.pm.debugger_plugins], 54 | 'view': [n for n in voltron.plugin.pm.view_plugins], 55 | 'command': [n for n in voltron.plugin.pm.command_plugins], 56 | 'web': [n for n in voltron.plugin.pm.web_plugins], 57 | } 58 | 59 | 60 | class APIPluginsPlugin(APIPlugin): 61 | request = 'plugins' 62 | request_class = APIPluginsRequest 63 | response_class = APIPluginsResponse 64 | -------------------------------------------------------------------------------- /voltron/plugins/api/registers.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import logging 3 | 4 | from voltron.api import * 5 | 6 | log = logging.getLogger('api') 7 | 8 | 9 | class APIRegistersRequest(APIRequest): 10 | """ 11 | API state request. 12 | 13 | { 14 | "type": "request", 15 | "request": "registers", 16 | "data": { 17 | "target_id": 0, 18 | "thread_id": 123456, 19 | "registers": ['rsp'] 20 | } 21 | } 22 | 23 | `target_id` and `thread_id` are optional. If not present, the currently 24 | selected target and thread will be used. 25 | 26 | `registers` is optional. If it is not included all registers will be 27 | returned. 28 | """ 29 | _fields = {'target_id': False, 'thread_id': False, 'registers': False} 30 | 31 | target_id = 0 32 | thread_id = None 33 | registers = [] 34 | 35 | @server_side 36 | def dispatch(self): 37 | try: 38 | regs = voltron.debugger.registers(target_id=self.target_id, thread_id=self.thread_id, registers=self.registers) 39 | res = APIRegistersResponse() 40 | res.registers = regs 41 | res.deref = {} 42 | for reg, val in regs.items(): 43 | try: 44 | if val > 0: 45 | try: 46 | res.deref[reg] = voltron.debugger.dereference(pointer=val) 47 | except: 48 | res.deref[reg] = [] 49 | else: 50 | res.deref[reg] = [] 51 | except TypeError: 52 | res.deref[reg] = [] 53 | except TargetBusyException: 54 | res = APITargetBusyErrorResponse() 55 | except NoSuchTargetException: 56 | res = APINoSuchTargetErrorResponse() 57 | except Exception as e: 58 | msg = "Exception getting registers from debugger: {}".format(repr(e)) 59 | log.exception(msg) 60 | res = APIGenericErrorResponse(msg) 61 | 62 | return res 63 | 64 | 65 | class APIRegistersResponse(APISuccessResponse): 66 | """ 67 | API status response. 68 | 69 | { 70 | "type": "response", 71 | "status": "success", 72 | "data": { 73 | "registers": { "rip": 0x12341234, ... }, 74 | "deref": {"rip": [(pointer, 0x12341234), ...]} 75 | } 76 | } 77 | """ 78 | _fields = {'registers': True, 'deref': False} 79 | 80 | 81 | class APIRegistersPlugin(APIPlugin): 82 | request = 'registers' 83 | request_class = APIRegistersRequest 84 | response_class = APIRegistersResponse 85 | -------------------------------------------------------------------------------- /voltron/plugins/api/stack.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import logging 3 | import base64 4 | 5 | from voltron.api import * 6 | 7 | log = logging.getLogger('api') 8 | 9 | class APIStackRequest(APIRequest): 10 | """ 11 | API read stack request. 12 | 13 | { 14 | "type": "request", 15 | "request": "stack" 16 | "data": { 17 | "target_id": 0, 18 | "thread_id": 123456, 19 | "length": 0x40 20 | } 21 | } 22 | 23 | `target_id` and `thread_id` are optional. If not present, the currently 24 | selected target and thread will be used. 25 | 26 | `length` is the number of bytes to read. 27 | """ 28 | _fields = {'target_id': False, 'thread_id': False, 'length': True} 29 | 30 | target_id = 0 31 | thread_id = None 32 | length = None 33 | 34 | @server_side 35 | def dispatch(self): 36 | try: 37 | sp_name, sp = voltron.debugger.stack_pointer(target_id=self.target_id) 38 | memory = voltron.debugger.memory(address=sp, length=self.length, target_id=self.target_id) 39 | res = APIStackResponse() 40 | res.memory = memory 41 | res.stack_pointer = sp 42 | except NoSuchTargetException: 43 | res = APINoSuchTargetErrorResponse() 44 | except TargetBusyException: 45 | res = APITargetBusyErrorResponse() 46 | except Exception as e: 47 | msg = "Unhandled exception {} reading stack: {}".format(type(e), e) 48 | log.exception(msg) 49 | res = APIErrorResponse(code=0, message=msg) 50 | 51 | return res 52 | 53 | 54 | class APIStackResponse(APISuccessResponse): 55 | """ 56 | API read stack response. 57 | 58 | { 59 | "type": "response", 60 | "status": "success", 61 | "data": { 62 | "memory": "\xff...", 63 | "stack_pointer": 0x12341234 64 | } 65 | } 66 | """ 67 | _fields = {'memory': True, 'stack_pointer': True} 68 | 69 | _encode_fields = ['memory'] 70 | 71 | memory = None 72 | stack_pointer = None 73 | 74 | 75 | class APIStackPlugin(APIPlugin): 76 | request = 'stack' 77 | request_class = APIStackRequest 78 | response_class = APIStackResponse 79 | -------------------------------------------------------------------------------- /voltron/plugins/api/state.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import logging 3 | 4 | from voltron.api import * 5 | 6 | log = logging.getLogger('api') 7 | 8 | class APIStateRequest(APIRequest): 9 | """ 10 | API state request. 11 | 12 | { 13 | "type": "request", 14 | "request": "state", 15 | "data": { 16 | "target_id": 0 17 | } 18 | } 19 | """ 20 | _fields = {'target_id': False} 21 | 22 | target_id = 0 23 | 24 | @server_side 25 | def dispatch(self): 26 | try: 27 | state = voltron.debugger.state(target_id=self.target_id) 28 | log.debug("Got state from debugger: {}".format(state)) 29 | res = APIStateResponse() 30 | res.state = state 31 | except TargetBusyException: 32 | res = APITargetBusyErrorResponse() 33 | except NoSuchTargetException: 34 | res = APINoSuchTargetErrorResponse() 35 | 36 | return res 37 | 38 | 39 | class APIStateResponse(APISuccessResponse): 40 | """ 41 | API status response. 42 | 43 | { 44 | "type": "response", 45 | "data": { 46 | "state": "stopped" 47 | } 48 | } 49 | """ 50 | _fields = {'state': True} 51 | 52 | state = None 53 | 54 | 55 | class APIStatePlugin(APIPlugin): 56 | request = 'state' 57 | request_class = APIStateRequest 58 | response_class = APIStateResponse 59 | -------------------------------------------------------------------------------- /voltron/plugins/api/targets.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import voltron 4 | from voltron.api import * 5 | 6 | from scruffy.plugin import Plugin 7 | 8 | log = logging.getLogger('api') 9 | 10 | class APITargetsRequest(APIRequest): 11 | """ 12 | API list targets request. 13 | 14 | { 15 | "type": "request", 16 | "request": "targets" 17 | } 18 | """ 19 | _fields = {} 20 | 21 | @server_side 22 | def dispatch(self): 23 | try: 24 | res = APITargetsResponse() 25 | res.targets = voltron.debugger.targets() 26 | except NoSuchTargetException: 27 | res = APINoSuchTargetErrorResponse() 28 | except Exception as e: 29 | msg = "Exception getting targets from debugger: {}".format(repr(e)) 30 | log.exception(msg) 31 | res = APIGenericErrorResponse(msg) 32 | 33 | return res 34 | 35 | 36 | class APITargetsResponse(APISuccessResponse): 37 | """ 38 | API list targets response. 39 | 40 | { 41 | "type": "response", 42 | "status": "success", 43 | "data": { 44 | "targets": [{ 45 | "id": 0, # ID that can be used in other funcs 46 | "file": "/bin/ls", # target's binary file 47 | "arch": "x86_64", # target's architecture 48 | "state: "stopped" # state 49 | }] 50 | } 51 | } 52 | """ 53 | _fields = {'targets': True} 54 | 55 | targets = [] 56 | 57 | 58 | class APITargetsPlugin(APIPlugin): 59 | request = 'targets' 60 | request_class = APITargetsRequest 61 | response_class = APITargetsResponse 62 | 63 | -------------------------------------------------------------------------------- /voltron/plugins/api/version.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import voltron.api 3 | from voltron.api import * 4 | 5 | from scruffy.plugin import Plugin 6 | 7 | 8 | class APIVersionRequest(APIRequest): 9 | """ 10 | API version request. 11 | 12 | { 13 | "type": "request", 14 | "request": "version" 15 | } 16 | """ 17 | @server_side 18 | def dispatch(self): 19 | res = APIVersionResponse() 20 | res.api_version = voltron.api.version 21 | res.host_version = voltron.debugger.version() 22 | res.capabilities = voltron.debugger.capabilities() 23 | return res 24 | 25 | 26 | class APIVersionResponse(APISuccessResponse): 27 | """ 28 | API version response. 29 | 30 | { 31 | "type": "response", 32 | "status": "success", 33 | "data": { 34 | "api_version": 1.0, 35 | "host_version": 'lldb-something', 36 | "capabilities": ["async"] 37 | } 38 | } 39 | """ 40 | _fields = { 41 | 'api_version': True, 42 | 'host_version': True, 43 | 'capabilities': False 44 | } 45 | 46 | api_version = None 47 | host_version = None 48 | capabilities = None 49 | 50 | 51 | class APIVersionPlugin(APIPlugin): 52 | request = 'version' 53 | request_class = APIVersionRequest 54 | response_class = APIVersionResponse 55 | 56 | -------------------------------------------------------------------------------- /voltron/plugins/api/write_memory.py: -------------------------------------------------------------------------------- 1 | import voltron 2 | import logging 3 | import six 4 | import struct 5 | 6 | from voltron.api import * 7 | 8 | log = logging.getLogger('api') 9 | 10 | 11 | class APIWriteMemoryRequest(APIRequest): 12 | """ 13 | API write memory request. 14 | 15 | { 16 | "type": "request", 17 | "request": "write_memory", 18 | "data": { 19 | "target_id":0, 20 | "address": 0x12341234, 21 | "data": "\xcc" 22 | } 23 | } 24 | 25 | `target_id` is optional. If not present, the currently selected target 26 | will be used. 27 | 28 | `address` is the address at which to start writing. 29 | 30 | `data` is the data to write. 31 | """ 32 | _fields = { 33 | 'target_id': False, 34 | 'address': True, 35 | 'value': True 36 | } 37 | _encode_fields = ['value'] 38 | 39 | target_id = 0 40 | 41 | @server_side 42 | def dispatch(self): 43 | try: 44 | target = voltron.debugger.target(self.target_id) 45 | 46 | voltron.debugger.write_memory(address=int(self.address), data=self.value, target_id=int(self.target_id)) 47 | 48 | res = APISuccessResponse() 49 | except TargetBusyException: 50 | res = APITargetBusyErrorResponse() 51 | except NoSuchTargetException: 52 | res = APINoSuchTargetErrorResponse() 53 | except Exception as e: 54 | msg = "Exception writing memory in debugger: {}".format(repr(e)) 55 | log.exception(msg) 56 | res = APIGenericErrorResponse(msg) 57 | 58 | return res 59 | 60 | 61 | class APIWriteMemoryPlugin(APIPlugin): 62 | request = 'write_memory' 63 | request_class = APIWriteMemoryRequest 64 | response_class = APISuccessResponse 65 | -------------------------------------------------------------------------------- /voltron/plugins/debugger/__init__.py: -------------------------------------------------------------------------------- 1 | if not hasattr(__builtins__, "xrange"): 2 | xrange = range 3 | -------------------------------------------------------------------------------- /voltron/plugins/debugger/dbg_mock.py: -------------------------------------------------------------------------------- 1 | from voltron.plugin import * 2 | from voltron.dbg import * 3 | 4 | 5 | class MockAdaptor(DebuggerAdaptor): 6 | pass 7 | 8 | 9 | class MockAdaptorPlugin(DebuggerAdaptorPlugin): 10 | host = 'mock' 11 | adaptor_class = MockAdaptor 12 | -------------------------------------------------------------------------------- /voltron/plugins/view/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snare/voltron/be9e54b109380a21e4ec7ec9b3ecc9d4a7fd6529/voltron/plugins/view/__init__.py -------------------------------------------------------------------------------- /voltron/plugins/view/backtrace.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from voltron.view import * 4 | from voltron.plugin import * 5 | from voltron.api import * 6 | 7 | log = logging.getLogger('view') 8 | 9 | 10 | class BacktraceView (TerminalView): 11 | def build_requests(self): 12 | return [api_request('command', block=self.block, command='bt')] 13 | 14 | def render(self, results): 15 | [res] = results 16 | 17 | # Set up header and error message if applicable 18 | self.title = '[backtrace]' 19 | 20 | # don't render if it timed out, probably haven't stepped the debugger again 21 | if res.timed_out: 22 | return 23 | 24 | if res and res.is_success: 25 | # Get the command output 26 | self.body = res.output 27 | else: 28 | log.error("Error getting backtrace: {}".format(res.message)) 29 | self.body = self.colour(res.message, 'red') 30 | 31 | # Call parent's render method 32 | super(BacktraceView, self).render(results) 33 | 34 | 35 | class BacktraceViewPlugin(ViewPlugin): 36 | plugin_type = 'view' 37 | name = 'backtrace' 38 | aliases = ('t', 'bt', 'back') 39 | view_class = BacktraceView 40 | -------------------------------------------------------------------------------- /voltron/plugins/view/breakpoints.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from blessed import Terminal 4 | 5 | from voltron.view import * 6 | from voltron.plugin import * 7 | from voltron.api import * 8 | 9 | log = logging.getLogger('view') 10 | 11 | 12 | class BreakpointsView (TerminalView): 13 | def build_requests(self): 14 | return [ 15 | api_request('registers', registers=['pc']), 16 | api_request('breakpoints', block=self.block) 17 | ] 18 | 19 | def render(self, results): 20 | self.title = '[breakpoints]' 21 | 22 | r_res, b_res = results 23 | 24 | # get PC first so we can highlight a breakpoint we're at 25 | if r_res and r_res.is_success and len(r_res.registers) > 0: 26 | pc = r_res.registers[list(r_res.registers.keys())[0]] 27 | else: 28 | pc = -1 29 | 30 | if b_res and b_res.is_success: 31 | fmtd = [] 32 | term = Terminal() 33 | for bp in b_res.breakpoints: 34 | # prepare formatting dictionary for the breakpoint 35 | d = bp.copy() 36 | d['locations'] = None 37 | d['t'] = term 38 | d['id'] = '#{:<2}'.format(d['id']) 39 | if d['one_shot']: 40 | d['one_shot'] = self.config.format.one_shot.format(t=term) 41 | else: 42 | d['one_shot'] = '' 43 | if d['enabled']: 44 | d['disabled'] = '' 45 | else: 46 | d['disabled'] = self.config.format.disabled.format(t=term) 47 | 48 | # add a row for each location 49 | for location in bp['locations']: 50 | # add location data to formatting dict and format the row 51 | d.update(location) 52 | if pc == d['address']: 53 | d['hit'] = self.config.format.hit.format(t=term) 54 | else: 55 | d['hit'] = '' 56 | f = self.config.format.row.format(**d) 57 | fmtd.append(f) 58 | d['id'] = ' ' 59 | 60 | self.body = '\n'.join(fmtd) 61 | else: 62 | log.error("Error getting breakpoints: {}".format(b_res.message)) 63 | self.body = self.colour(b_res.message, 'red') 64 | 65 | super(BreakpointsView, self).render(results) 66 | 67 | 68 | class BreakpointsViewPlugin(ViewPlugin): 69 | plugin_type = 'view' 70 | name = 'breakpoints' 71 | aliases = ('b', 'bp', 'break') 72 | view_class = BreakpointsView 73 | -------------------------------------------------------------------------------- /voltron/plugins/view/command.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pygments 4 | from pygments.lexers import get_lexer_by_name 5 | 6 | from voltron.view import * 7 | from voltron.plugin import * 8 | from voltron.api import * 9 | 10 | log = logging.getLogger('view') 11 | 12 | 13 | class CommandView (TerminalView): 14 | @classmethod 15 | def configure_subparser(cls, subparsers): 16 | sp = subparsers.add_parser('command', aliases=('c', 'cmd'), 17 | help='run a command each time the debugger host stops') 18 | VoltronView.add_generic_arguments(sp) 19 | sp.add_argument('command', action='store', help='command to run') 20 | sp.add_argument('--lexer', '-l', action='store', 21 | help='apply a Pygments lexer to the command output (e.g. "c")', 22 | default=None) 23 | sp.set_defaults(func=CommandView) 24 | 25 | def build_requests(self): 26 | return [api_request('command', block=self.block, command=self.args.command)] 27 | 28 | def render(self, results): 29 | [res] = results 30 | 31 | # Set up header and error message if applicable 32 | self.title = '[cmd:' + self.args.command + ']' 33 | 34 | if res and res.is_success: 35 | if get_lexer_by_name and self.args.lexer: 36 | try: 37 | lexer = get_lexer_by_name(self.args.lexer, stripall=True) 38 | self.body = pygments.highlight(res.output, lexer, pygments.formatters.TerminalFormatter()) 39 | except Exception as e: 40 | log.warning('Failed to highlight view contents: ' + repr(e)) 41 | self.body = res.output 42 | else: 43 | self.body = res.output 44 | else: 45 | log.error("Error executing command: {}".format(res.message)) 46 | self.body = self.colour(res.message, 'red') 47 | 48 | # Call parent's render method 49 | super(CommandView, self).render(results) 50 | 51 | 52 | class CommandViewPlugin(ViewPlugin): 53 | plugin_type = 'view' 54 | name = 'command' 55 | view_class = CommandView 56 | -------------------------------------------------------------------------------- /voltron/plugins/view/disasm.py: -------------------------------------------------------------------------------- 1 | from voltron.view import TerminalView, VoltronView, log 2 | from voltron.plugin import api_request, ViewPlugin 3 | from voltron.lexers import get_lexer_by_name 4 | import pygments 5 | import pygments.formatters 6 | 7 | 8 | class DisasmView(TerminalView): 9 | @classmethod 10 | def configure_subparser(cls, subparsers): 11 | sp = subparsers.add_parser('disasm', help='disassembly view', aliases=('d', 'dis')) 12 | VoltronView.add_generic_arguments(sp) 13 | sp.set_defaults(func=DisasmView) 14 | sp.add_argument('--use-capstone', '-c', action='store_true', default=False, help='use capstone') 15 | sp.add_argument('--address', '-a', action='store', default=None, 16 | help='address (in hex or decimal) from which to start disassembly') 17 | 18 | def build_requests(self): 19 | if self.args.address: 20 | if self.args.address.startswith('0x'): 21 | addr = int(self.args.address, 16) 22 | else: 23 | try: 24 | addr = int(self.args.address, 10) 25 | except: 26 | addr = int(self.args.address, 16) 27 | else: 28 | addr = None 29 | req = api_request('disassemble', block=self.block, use_capstone=self.args.use_capstone, 30 | offset=self.scroll_offset, address=addr) 31 | req.count = self.body_height() 32 | return [req] 33 | 34 | def render(self, results): 35 | [res] = results 36 | 37 | # Set up header & error message if applicable 38 | self.title = '[disassembly]' 39 | 40 | # don't render if it timed out, probably haven't stepped the debugger again 41 | if res.timed_out: 42 | return 43 | 44 | if res and res.is_success: 45 | # Get the disasm 46 | disasm = res.disassembly 47 | disasm = '\n'.join(disasm.split('\n')[:self.body_height()]) 48 | 49 | # Highlight output 50 | try: 51 | host = 'capstone' if self.args.use_capstone else res.host 52 | lexer = get_lexer_by_name('{}_{}'.format(host, res.flavor)) 53 | formatter = pygments.formatters.get_formatter_by_name( 54 | self.config.format.pygments_formatter, 55 | style=self.config.format.pygments_style) 56 | disasm = pygments.highlight(disasm, lexer, formatter) 57 | except Exception as e: 58 | log.warning('Failed to highlight disasm: ' + str(e)) 59 | log.info(self.config.format) 60 | 61 | # Build output 62 | self.body = disasm.rstrip() 63 | else: 64 | log.error("Error disassembling: {}".format(res.message)) 65 | self.body = self.colour(res.message, 'red') 66 | 67 | # Call parent's render method 68 | super(DisasmView, self).render(results) 69 | 70 | 71 | class DisasmViewPlugin(ViewPlugin): 72 | plugin_type = 'view' 73 | name = 'disassembly' 74 | aliases = ('d', 'dis', 'disasm') 75 | view_class = DisasmView 76 | -------------------------------------------------------------------------------- /voltron/plugins/view/memory.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import six 3 | import pygments 4 | import pygments.formatters 5 | from pygments.token import * 6 | 7 | from voltron.view import TerminalView, VoltronView 8 | from voltron.plugin import ViewPlugin, api_request 9 | 10 | log = logging.getLogger("view") 11 | 12 | 13 | class MemoryView(TerminalView): 14 | printable_filter = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) 15 | 16 | asynchronous = True 17 | last_memory = None 18 | last_address = 0 19 | 20 | @classmethod 21 | def configure_subparser(cls, subparsers): 22 | sp = subparsers.add_parser('memory', help='display a chunk of memory', aliases=('m', 'mem')) 23 | VoltronView.add_generic_arguments(sp) 24 | group = sp.add_mutually_exclusive_group(required=False) 25 | group.add_argument('--deref', '-d', action='store_true', 26 | help='display the data in a column one CPU word wide and dereference any valid pointers', 27 | default=False) 28 | group.add_argument('--bytes', '-b', action='store', type=int, help='bytes per line (default 16)', default=16) 29 | group.add_argument('--words', '-w', action='store', type=int, help='machine words per line', default=0) 30 | sp.add_argument('--reverse', '-v', action='store_true', help='reverse the output', default=False) 31 | sp.add_argument('--track', '-t', action='store_true', help='track and highlight changes', default=True) 32 | sp.add_argument('--no-track', '-T', action='store_false', help='don\'t track and highlight changes') 33 | 34 | group = sp.add_mutually_exclusive_group(required=False) 35 | group.add_argument('--address', '-a', action='store', 36 | help='address (in hex or decimal) from which to start reading memory') 37 | group.add_argument('--command', '-c', action='store', 38 | help='command to execute resulting in the address from which to start reading memory. ' 39 | 'voltron will do his almighty best to find an address. e.g. "print \$rip + 0x1234"', 40 | default=None) 41 | group.add_argument('--register', '-r', action='store', 42 | help='register containing the address from which to start reading memory', default=None) 43 | sp.set_defaults(func=MemoryView) 44 | 45 | def build_requests(self): 46 | height, width = self.window_size() 47 | 48 | # check args 49 | if self.args.register: 50 | args = {'register': self.args.register} 51 | elif self.args.command: 52 | args = {'command': self.args.command} 53 | elif self.args.address: 54 | if self.args.address.startswith('0x'): 55 | addr = int(self.args.address, 16) 56 | else: 57 | try: 58 | addr = int(self.args.address, 10) 59 | except: 60 | addr = int(self.args.address, 16) 61 | args = {'address': addr} 62 | else: 63 | args = {'register': 'sp'} 64 | 65 | if self.args.deref: 66 | args['words'] = height 67 | args['offset'] = self.scroll_offset if self.args.reverse else -self.scroll_offset 68 | else: 69 | args['length'] = height * self.args.bytes * 2 70 | args['offset'] = self.scroll_offset * self.args.bytes * (1 if self.args.reverse else -1) 71 | 72 | # get memory and target info 73 | return [ 74 | api_request('targets'), 75 | api_request('memory', deref=self.args.deref is True, **args) 76 | ] 77 | 78 | def generate_tokens(self, results): 79 | t_res, m_res = results 80 | 81 | if t_res and t_res.is_success and len(t_res.targets) > 0: 82 | target = t_res.targets[0] 83 | 84 | if m_res and m_res.is_success: 85 | bytes_per_chunk = self.args.words*target['addr_size'] if self.args.words else self.args.bytes 86 | for c in range(0, m_res.bytes, bytes_per_chunk): 87 | chunk = m_res.memory[c:c + bytes_per_chunk] 88 | yield (Name.Label, self.format_address(m_res.address + c, size=target['addr_size'], pad=False)) 89 | yield (Name.Label, ': ') 90 | 91 | # Hex bytes 92 | byte_array = [] 93 | for i, x in enumerate(six.iterbytes(chunk)): 94 | n = "%02X" % x 95 | token = Text if x else Comment 96 | if self.args.track and self.last_memory and self.last_address == m_res.address: 97 | if x != six.indexbytes(self.last_memory, c + i): 98 | token = Error 99 | byte_array.append((token, n)) 100 | 101 | if self.args.words: 102 | if target['byte_order'] =='little': 103 | byte_array_words = [byte_array[i:i+ target['addr_size']] for i in range(0, bytes_per_chunk, target['addr_size'])] 104 | for word in byte_array_words: 105 | word.reverse() 106 | for x in word: 107 | yield x 108 | yield (Text, ' ') 109 | else: 110 | for x in byte_array: 111 | yield x 112 | yield (Text, ' ') 113 | 114 | # ASCII representation 115 | yield (Punctuation, '| ') 116 | for i, x in enumerate(six.iterbytes(chunk)): 117 | token = String.Char if x else Comment 118 | if self.args.track and self.last_memory and self.last_address == m_res.address: 119 | if x != six.indexbytes(self.last_memory, c + i): 120 | token = Error 121 | yield (token, ((x <= 127 and self.printable_filter[x]) or '.')) 122 | yield (Punctuation, ' | ') 123 | 124 | # Deref chain 125 | if self.args.deref: 126 | chain = m_res.deref.pop(0) 127 | for i, (t, item) in enumerate(chain): 128 | if t == "pointer": 129 | yield (Number.Hex, self.format_address(item, size=target['addr_size'], pad=False)) 130 | elif t == "string": 131 | for r in ['\n', '\r', '\v']: 132 | item = item.replace(r, '\\{:x}'.format(ord(r))) 133 | yield (String.Double, '"' + item + '"') 134 | elif t == "unicode": 135 | for r in ['\n', '\r', '\v']: 136 | item = item.replace(r, '\\{:x}'.format(ord(r))) 137 | yield (String.Double, 'u"' + item + '"') 138 | elif t == "symbol": 139 | yield (Name.Function, '`' + item + '`') 140 | elif t == "circular": 141 | yield (Text, '(circular)') 142 | if i < len(chain) - 1: 143 | yield (Punctuation, ' => ') 144 | 145 | yield (Text, '\n') 146 | 147 | def render(self, results): 148 | target = None 149 | self.trunc_top = self.args.reverse 150 | 151 | t_res, m_res = results 152 | 153 | if t_res and t_res.is_success and len(t_res.targets) > 0: 154 | target = t_res.targets[0] 155 | 156 | if self.args.deref or self.args.words: 157 | self.args.bytes = target['addr_size'] 158 | 159 | f = pygments.formatters.get_formatter_by_name(self.config.format.pygments_formatter, 160 | style=self.config.format.pygments_style) 161 | 162 | if m_res and m_res.is_success: 163 | lines = pygments.format(self.generate_tokens(results), f).split('\n') 164 | self.body = '\n'.join(reversed(lines)).strip() if self.args.reverse else '\n'.join(lines) 165 | self.info = '[0x{0:0=4x}:'.format(len(m_res.memory)) + self.config.format.addr_format.format(m_res.address) + ']' 166 | else: 167 | log.error("Error reading memory: {}".format(m_res.message)) 168 | self.body = pygments.format([(Error, m_res.message)], f) 169 | self.info = '' 170 | 171 | # Store the memory 172 | if self.args.track: 173 | self.last_address = m_res.address 174 | self.last_memory = m_res.memory 175 | else: 176 | self.body = self.colour("Failed to get targets", 'red') 177 | 178 | if not self.title: 179 | self.title = "[memory]" 180 | 181 | super(MemoryView, self).render(results) 182 | 183 | def format_address(self, address, size=8, pad=True, prefix='0x'): 184 | fmt = '{:' + ('0=' + str(size * 2) if pad else '') + 'X}' 185 | addr_str = fmt.format(address) 186 | if prefix: 187 | addr_str = prefix + addr_str 188 | return addr_str 189 | 190 | 191 | class MemoryViewPlugin(ViewPlugin): 192 | plugin_type = 'view' 193 | name = 'memory' 194 | view_class = MemoryView 195 | 196 | 197 | class StackView(MemoryView): 198 | @classmethod 199 | def configure_subparser(cls, subparsers): 200 | sp = subparsers.add_parser('stack', help='display a chunk of stack memory', aliases=('s', 'st')) 201 | VoltronView.add_generic_arguments(sp) 202 | sp.set_defaults(func=StackView) 203 | sp.add_argument('--reverse', '-v', action='store_false', help='(un)reverse the output', default=True) 204 | sp.add_argument('--track', '-t', action='store_true', help='track and highlight changes', default=True) 205 | sp.add_argument('--no-track', '-T', action='store_false', help='don\'t track and highlight changes') 206 | 207 | def build_requests(self): 208 | self.args.deref = True 209 | self.args.words = False 210 | self.args.register = 'sp' 211 | self.args.command = None 212 | self.args.address = None 213 | self.args.bytes = None 214 | 215 | return super(StackView, self).build_requests() 216 | 217 | def render(self, results): 218 | self.title = '[stack]' 219 | 220 | super(StackView, self).render(results) 221 | 222 | 223 | class StackViewPlugin(ViewPlugin): 224 | plugin_type = 'view' 225 | name = 'stack' 226 | view_class = StackView 227 | -------------------------------------------------------------------------------- /voltron/rdb.py: -------------------------------------------------------------------------------- 1 | import pdb 2 | import socket 3 | import sys 4 | 5 | # Trying to debug a quirk in some code that gets called async by {ll,g}db? 6 | # 7 | # from .rdb import Rdb 8 | # Rdb().set_trace() 9 | # 10 | # Then: telnet localhost 4444 11 | 12 | 13 | socks = {} 14 | # Only bind the socket once 15 | def _sock(port): 16 | if port in socks: 17 | return socks[port] 18 | 19 | s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 20 | s.bind(("127.0.0.1", port)) 21 | socks[port] = s 22 | return s 23 | 24 | class Rdb(pdb.Pdb): 25 | def __init__(self, port=4444): 26 | self.old_stdout = sys.stdout 27 | self.old_stdin = sys.stdin 28 | self.skt = _sock(port) 29 | self.skt.listen(1) 30 | (clientsocket, address) = self.skt.accept() 31 | handle = clientsocket.makefile('rw') 32 | pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle) 33 | sys.stdout = sys.stdin = handle 34 | -------------------------------------------------------------------------------- /voltron/repl.py: -------------------------------------------------------------------------------- 1 | from .core import Client 2 | 3 | 4 | class REPLClient(Client): 5 | """ 6 | A Voltron client for use in the Python REPL (e.g. Calculon). 7 | """ 8 | def __getattr__(self, key): 9 | try: 10 | res = self.perform_request('registers', registers=[key]) 11 | if res.is_success: 12 | return res.registers[key] 13 | else: 14 | print("Error getting register: {}".format(res.message)) 15 | except Exception as e: 16 | print("Exception getting register: {}".format(repr(e))) 17 | 18 | def __getitem__(self, key): 19 | try: 20 | d = {} 21 | if isinstance(key, slice): 22 | d['address'] = key.start 23 | d['length'] = key.stop - key.start 24 | else: 25 | d['address'] = key 26 | d['length'] = 1 27 | 28 | res = self.perform_request('memory', **d) 29 | 30 | if res.is_success: 31 | return res.memory 32 | else: 33 | print("Error reading memory: {}".format(res.message)) 34 | except Exception as e: 35 | print("Exception reading memory: {}".format(repr(e))) 36 | 37 | def __setitem__(self, key, value): 38 | try: 39 | d = {} 40 | if isinstance(key, slice): 41 | d['address'] = key.start 42 | d['value'] = ((key.stop - key.start) * value)[:key.stop - key.start] 43 | else: 44 | d['address'] = key 45 | d['value'] = value 46 | 47 | res = self.perform_request('write_memory', **d) 48 | 49 | if res.is_success: 50 | return None 51 | else: 52 | print("Error writing memory: {}".format(res.message)) 53 | except Exception as e: 54 | print("Exception writing memory: {}".format(repr(e))) 55 | 56 | def __call__(self, command): 57 | try: 58 | res = self.perform_request('command', command=command) 59 | if res.is_success: 60 | return res.output 61 | else: 62 | print("Error executing command: {}".format(res.message)) 63 | except Exception as e: 64 | print("Exception executing command: {}".format(repr(e))) 65 | 66 | 67 | V = REPLClient() 68 | -------------------------------------------------------------------------------- /voltron/styles.py: -------------------------------------------------------------------------------- 1 | from pygments.style import Style 2 | from pygments.token import Token, Comment, Name, Keyword, Generic, Number, Operator, String, Punctuation, Error 3 | 4 | BASE03 = '#002b36' 5 | BASE02 = '#073642' 6 | BASE01 = '#586e75' 7 | BASE00 = '#657b83' 8 | BASE0 = '#839496' 9 | BASE1 = '#93a1a1' 10 | BASE2 = '#eee8d5' 11 | BASE3 = '#fdf6e3' 12 | YELLOW = '#b58900' 13 | ORANGE = '#cb4b16' 14 | RED = '#dc322f' 15 | MAGENTA = '#d33682' 16 | VIOLET = '#6c71c4' 17 | BLUE = '#268bd2' 18 | CYAN = '#2aa198' 19 | GREEN = '#859900' 20 | 21 | 22 | class VolarizedStyle(Style): 23 | background_color = BASE03 24 | styles = { 25 | Keyword: GREEN, 26 | Keyword.Constant: ORANGE, 27 | Keyword.Declaration: BASE1, 28 | Keyword.Namespace: ORANGE, 29 | # Keyword.Pseudo 30 | Keyword.Reserved: BLUE, 31 | Keyword.Type: VIOLET, 32 | 33 | Name: BASE1, 34 | Name.Attribute: BASE1, 35 | Name.Builtin: YELLOW, 36 | Name.Builtin.Pseudo: YELLOW, 37 | Name.Class: BLUE, 38 | Name.Constant: ORANGE, 39 | Name.Decorator: BLUE, 40 | Name.Entity: ORANGE, 41 | Name.Exception: YELLOW, 42 | Name.Function: BLUE, 43 | Name.Label: BASE01, 44 | # Name.Namespace 45 | # Name.Other 46 | Name.Tag: BLUE, 47 | Name.Variable: BLUE, 48 | # Name.Variable.Class 49 | # Name.Variable.Global 50 | # Name.Variable.Instance 51 | 52 | # Literal 53 | # Literal.Date 54 | String: BASE1, 55 | String.Backtick: BASE01, 56 | String.Char: BASE1, 57 | String.Doc: CYAN, 58 | # String.Double 59 | String.Escape: RED, 60 | String.Heredoc: CYAN, 61 | # String.Interpol 62 | # String.Other 63 | String.Regex: RED, 64 | # String.Single 65 | # String.Symbol 66 | Number: CYAN, 67 | # Number.Float 68 | # Number.Hex 69 | # Number.Integer 70 | # Number.Integer.Long 71 | # Number.Oct 72 | 73 | Operator: GREEN, 74 | Operator.Word: GREEN, 75 | 76 | Punctuation: BASE00, 77 | 78 | Comment: BASE00, 79 | # Comment.Multiline 80 | Comment.Preproc: GREEN, 81 | # Comment.Single 82 | Comment.Special: GREEN, 83 | 84 | # Generic 85 | Generic.Deleted: CYAN, 86 | Generic.Emph: 'italic', 87 | Generic.Error: RED, 88 | Generic.Heading: ORANGE, 89 | Generic.Inserted: GREEN, 90 | # Generic.Output 91 | Generic.Prompt: RED, 92 | Generic.Strong: 'bold', 93 | Generic.Subheading: ORANGE, 94 | # Generic.Traceback 95 | 96 | Token: BASE1, 97 | Token.Other: ORANGE, 98 | 99 | Error: RED 100 | } 101 | --------------------------------------------------------------------------------