├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── cfgs ├── hyperpwn-gef.yml ├── hyperpwn-peda.yml └── hyperpwn-pwndbg.yml ├── index.js ├── package-lock.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | { 2 | "language": "node_js", 3 | "node_js": "node" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Bet4 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 | # Hyperpwn 2 | 3 | [![Build Status](https://travis-ci.org/bet4it/hyperpwn.svg?branch=master)](https://travis-ci.org/bet4it/hyperpwn) [![XO code style](https://img.shields.io/badge/code_style-XO-5ed9c7.svg)](https://github.com/sindresorhus/xo) 4 | 5 | Hyperpwn is a [Hyper](https://hyper.is) plugin to improve the display when debugging with GDB. 6 | 7 | Hyperpwn needs [GEF](https://github.com/hugsy/gef), [pwndbg](https://github.com/pwndbg/pwndbg) or [peda](https://github.com/bet4it/peda) to be loaded in GDB as a backend. 8 | Hyperpwn handles with its context data, seperates them to different windows to get a clearer display and can easily replay previous states. 9 | 10 | Hyperpwn can be used on Windows, Linux and macOS. 11 | 12 | * Use hyperpwn on GEF. Theme: [hyper-chesterish](https://github.com/henrikdahl/hyper-chesterish): 13 | ![](https://user-images.githubusercontent.com/16643669/61991945-25f06e00-b08a-11e9-95b2-a9eb32e0bfad.gif) 14 | 15 | * Use hyperpwn together with [hyper-pane](https://github.com/chabou/hyper-pane) on pwndbg. Theme: [hyper-material-theme](https://github.com/equinusocio/hyper-material-theme): 16 | ![](https://user-images.githubusercontent.com/16643669/61991962-5df7b100-b08a-11e9-9b9e-e811da4b8d11.gif) 17 | 18 | * If you want to use hyperpwn together with other tools such as pwntools and VS Code, you can check for [this tutorial](https://github.com/bet4it/build-an-efficient-pwn-environment) 19 | 20 | # Install 21 | Firstly, you need to [install the latest release of Hyper](https://hyper.is/#installation) on your computer. 22 | 23 | Hyperpwn relies on [hyperinator](https://github.com/bet4it/hyperinator). You need to install both `hyperinator` and `hyperpwn` plugins to use hyperpwn. 24 | 25 | You can install them by command line: 26 | ```sh 27 | $ hyper i hyperinator 28 | $ hyper i hyperpwn 29 | ``` 30 | Or edit `~/.hyper.js` manually and add them to plugins: 31 | ``` 32 | plugins: [ 33 | "hyperinator", "hyperpwn" 34 | ], 35 | ``` 36 | 37 | # Usage 38 | Just run `gdb` in Hyper terminal. 39 | 40 | If the backend is loaded, hyperpwn will automatically create a config file in `~/.hyperinator`, load it and handle with the context data. 41 | 42 | You can edit the config file to change the layout and parts to display. 43 | 44 | ## Shortcuts 45 | * stepi: `F7` 46 | * nexti: `F8` 47 | * display previous state: `ctrl+shift+pageup` 48 | * display next state: `ctrl+shift+pagedown` 49 | 50 | # Configuration 51 | ## Default configuration: 52 | ``` js 53 | module.exports = { 54 | config: { 55 | // other configs... 56 | hyperpwn: { 57 | hotkeys: { 58 | prev: 'ctrl+shift+pageup', 59 | next: 'ctrl+shift+pagedown', 60 | cmd: { 61 | stepi: 'f7', 62 | nexti: 'f8' 63 | } 64 | }, 65 | autoClean: false, 66 | autoLayout: true, 67 | showHeaders: true, 68 | headerStyle: { 69 | position: 'absolute', 70 | top: 0, 71 | right: 0, 72 | fontSize: '10px' 73 | } 74 | } 75 | } 76 | //... 77 | }; 78 | ``` 79 | 80 | # Notice 81 | * If you want to use hyperpwn on peda, please use my fork of [peda](https://github.com/bet4it/peda) or [peda-arm](https://github.com/bet4it/peda-arm). 82 | * Make sure you have enabled the display of `legend` in the backend (which is the default behavior). 83 | * You can try to change configs like `context.nb_lines_code`, `context.nb_lines_code_prev` in GEF, or `context-code-lines` in pwndbg, to get a better display. 84 | -------------------------------------------------------------------------------- /cfgs/hyperpwn-gef.yml: -------------------------------------------------------------------------------- 1 | session_name: gef 2 | global_options: 3 | default-shell: /bin/sleep 4 | default-shell-args: 100000000 5 | windows: 6 | - layout: 1c02,237x62,0,0{139x62,0,0[139x34,0,0,1,139x8,0,35,2,139x18,0,44,3],97x62,140,0[97x36,140,0,4,97x4,140,37,6,97x20,140,42,5]} 7 | panes: 8 | - shell_command: 9 | - hyperpwn reg 10 | - shell_command: 11 | - hyperpwn trace 12 | - shell_command: 13 | - hyperpwn code 14 | - shell_command: 15 | - 16 | reuse: True 17 | focus: True 18 | - shell_command: 19 | - hyperpwn thread 20 | - shell_command: 21 | - hyperpwn stack 22 | -------------------------------------------------------------------------------- /cfgs/hyperpwn-peda.yml: -------------------------------------------------------------------------------- 1 | session_name: peda 2 | global_options: 3 | default-shell: /bin/sleep 4 | default-shell-args: 100000000 5 | windows: 6 | - layout: 9a99,237x62,0,0{141x62,0,0[141x25,0,0,6,141x19,0,26,8,141x12,0,46,9,141x3,0,59,12],95x62,142,0,7} 7 | panes: 8 | - shell_command: 9 | - hyperpwn code 10 | - shell_command: 11 | - 12 | reuse: True 13 | focus: True 14 | - shell_command: 15 | - hyperpwn stack 16 | - shell_command: 17 | - hyperpwn legend 18 | - shell_command: 19 | - hyperpwn reg 20 | -------------------------------------------------------------------------------- /cfgs/hyperpwn-pwndbg.yml: -------------------------------------------------------------------------------- 1 | session_name: pwndbg 2 | global_options: 3 | default-shell: /bin/sleep 4 | default-shell-args: 100000000 5 | windows: 6 | - layout: ea21,237x62,0,0{139x62,0,0[139x34,0,0,1,139x8,0,35,2,139x18,0,44,3],97x62,140,0[97x35,140,0,4,97x4,140,36,6,97x21,140,41,5]} 7 | panes: 8 | - shell_command: 9 | - hyperpwn REG 10 | - shell_command: 11 | - hyperpwn TRACE 12 | - shell_command: 13 | - hyperpwn DISASM 14 | - shell_command: 15 | - 16 | reuse: True 17 | focus: True 18 | - shell_command: 19 | - hyperpwn LEGEND 20 | - shell_command: 21 | - hyperpwn STACK 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const {homedir} = require('os') 2 | const fs = require('fs-extra') 3 | const path = require('path') 4 | const yaml = require('yaml') 5 | const merge = require('lodash.merge') 6 | const stripAnsi = require('strip-ansi') 7 | const expandTabs = require('expandtabs') 8 | const cliTruncate = require('cli-truncate') 9 | const ansiEscapes = require('ansi-escapes') 10 | 11 | let hyperpwn 12 | let contextStart 13 | let contextData 14 | let legendFix 15 | 16 | const defaultConfig = { 17 | hotkeys: { 18 | prev: 'ctrl+shift+pageup', 19 | next: 'ctrl+shift+pagedown', 20 | cmd: { 21 | stepi: 'f7', 22 | nexti: 'f8' 23 | } 24 | }, 25 | autoClean: false, 26 | autoLayout: true, 27 | showHeaders: true, 28 | headerStyle: { 29 | position: 'absolute', 30 | top: 0, 31 | right: 0, 32 | fontSize: '10px' 33 | } 34 | } 35 | 36 | const defaultShell = 37 | { 38 | win32: { 39 | 'default-shell': 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe', 40 | 'default-shell-args': '& {Clear-Host; While($true) {Read-Host | Out-Null}}' 41 | }, 42 | linux: { 43 | 'default-shell': '/bin/sleep', 44 | 'default-shell-args': 'infinity' 45 | }, 46 | darwin: { 47 | 'default-shell': '/bin/sleep', 48 | 'default-shell-args': '100000000' 49 | } 50 | } 51 | 52 | let config = defaultConfig 53 | 54 | class Hyperpwn { 55 | constructor() { 56 | this.index = null 57 | this.mainUid = null 58 | this.records = {} 59 | this.recordLen = 0 60 | this.legend = {uid: null, data: null, header: null} 61 | this.replayPrev = this.replayPrev.bind(this) 62 | this.replayNext = this.replayNext.bind(this) 63 | this.sendCmd = this.sendCmd.bind(this) 64 | } 65 | 66 | uids() { 67 | return Object.keys(this.records) 68 | } 69 | 70 | addUid(uid, name) { 71 | if (!this.records.hasOwnProperty(uid)) { 72 | this.records[uid] = [] 73 | } 74 | this.records[uid].name = name 75 | } 76 | 77 | delUid(uid) { 78 | if (uid === this.legend.uid) { 79 | this.legend = {uid: null, data: null, header: null} 80 | } else { 81 | delete this.records[uid] 82 | if (this.uids().length === 0) { 83 | this.index = null 84 | this.recordLen = 0 85 | } 86 | } 87 | } 88 | 89 | addData(title, data) { 90 | return this.uids().some(uid => { 91 | if (title.toLowerCase().includes(this.records[uid].name.toLowerCase())) { 92 | this.records[uid].push(data) 93 | return true 94 | } 95 | return false 96 | }) 97 | } 98 | 99 | alignData() { 100 | for (const uid of this.uids()) { 101 | if (this.records[uid].length === this.recordLen) { 102 | this.records[uid].push('') 103 | } 104 | } 105 | this.recordLen += 1 106 | } 107 | 108 | cleanData() { 109 | for (const uid of this.uids()) { 110 | const {name} = this.records[uid] 111 | this.records[uid] = [] 112 | this.records[uid].name = name 113 | } 114 | this.index = null 115 | this.recordLen = 0 116 | } 117 | 118 | addLegend(data) { 119 | if (this.legend.data !== data) { 120 | this.legend.data = data 121 | this.store.dispatch({ 122 | type: 'SESSION_PTY_DATA', 123 | uid: this.legend.uid, 124 | data: ansiEscapes.clearTerminal + data 125 | }) 126 | } 127 | } 128 | 129 | uidHeader(uid) { 130 | if (uid === this.legend.uid) { 131 | return this.legend.header 132 | } 133 | if (uid in this.records) { 134 | return `[${this.records[uid].name}]` 135 | } 136 | } 137 | 138 | initSession(store, uid, backend) { 139 | this.store = store 140 | this.mainUid = uid 141 | const {termGroups} = store.getState().termGroups 142 | if (config.autoLayout && Object.keys(termGroups).length === 1) { 143 | this.loadLayout(backend) 144 | } 145 | if (config.autoClean) { 146 | this.cleanData() 147 | } 148 | } 149 | 150 | loadLayout(name) { 151 | if (this.uids().length === 0) { 152 | const cfgName = `hyperpwn-${name}.yml` 153 | const cfgPath = path.resolve(homedir(), '.hyperinator', cfgName) 154 | const libPath = path.resolve(__dirname, 'cfgs', cfgName) 155 | if (!fs.existsSync(cfgPath)) { 156 | const cfg = yaml.parse(fs.readFileSync(libPath, 'utf8')) 157 | cfg['global_options'] = defaultShell[process.platform] 158 | fs.outputFileSync(cfgPath, yaml.stringify(cfg)) 159 | } 160 | 161 | this.store.dispatch({ 162 | type: 'HYPERINATOR_LOAD', 163 | data: cfgPath 164 | }) 165 | } 166 | } 167 | 168 | replayUid(uid, cols) { 169 | if (uid in this.records && Number.isInteger(this.index)) { 170 | let data = this.records[uid][this.index] 171 | data = data.replace(/^.*$/gm, line => cliTruncate(expandTabs(line), cols)) 172 | data = ansiEscapes.clearTerminal + data 173 | this.store.dispatch({ 174 | type: 'SESSION_PTY_DATA', 175 | uid, 176 | data 177 | }) 178 | } 179 | } 180 | 181 | replay() { 182 | for (const uid of this.uids()) { 183 | const {cols} = this.store.getState().sessions.sessions[uid] 184 | this.replayUid(uid, cols) 185 | } 186 | } 187 | 188 | replayPrev() { 189 | if (this.index > 0) { 190 | this.index -= 1 191 | this.replay() 192 | } 193 | } 194 | 195 | replayNext() { 196 | if (this.index < this.recordLen - 1) { 197 | this.index += 1 198 | this.replay() 199 | } 200 | } 201 | 202 | replayLast() { 203 | if (this.recordLen) { 204 | this.index = this.recordLen - 1 205 | this.replay() 206 | } 207 | } 208 | 209 | sendCmd(cmd) { 210 | return () => { 211 | if (this.mainUid) { 212 | window.rpc.emit('data', { 213 | uid: hyperpwn.mainUid, 214 | data: '\b'.repeat(1000) + cmd + '\n' 215 | }) 216 | } 217 | } 218 | } 219 | } 220 | 221 | exports.middleware = store => next => action => { 222 | const {type} = action 223 | 224 | if ((type === 'CONFIG_LOAD' || type === 'CONFIG_RELOAD') && action.config.hyperpwn) { 225 | config = merge(JSON.parse(JSON.stringify(defaultConfig)), action.config.hyperpwn) 226 | } 227 | 228 | if (type === 'SESSION_USER_DATA') { 229 | const {activeUid} = store.getState().sessions 230 | if (hyperpwn.uids().includes(activeUid)) { 231 | window.rpc.emit('data', { 232 | uid: hyperpwn.mainUid, 233 | data: action.data 234 | }) 235 | store.dispatch({ 236 | type: 'SESSION_SET_ACTIVE', 237 | uid: hyperpwn.mainUid 238 | }) 239 | return 240 | } 241 | } 242 | 243 | if (type === 'SESSION_PTY_DATA') { 244 | const {uid} = action 245 | let data = action.data.replace(/\u0007{2,}/, '') 246 | action.data = data 247 | const strippedData = stripAnsi(data) 248 | if (strippedData.includes('Init PEDA')) { 249 | hyperpwn.initSession(store, uid, 'peda') 250 | } 251 | if (/GEF for (linux|darwin) ready/.test(strippedData)) { 252 | hyperpwn.initSession(store, uid, 'gef') 253 | } 254 | if (strippedData.includes('pwndbg: loaded ')) { 255 | hyperpwn.initSession(store, uid, 'pwndbg') 256 | } 257 | 258 | const view = /^.* hyperpwn (.*)[\r\n]+$/.exec(strippedData) 259 | if (view) { 260 | if (view[1].toLowerCase() === 'legend') { 261 | hyperpwn.legend.uid = uid 262 | hyperpwn.legend.header = `[${view[1]}]` 263 | } else { 264 | hyperpwn.addUid(uid, view[1]) 265 | } 266 | action.data = ansiEscapes.cursorHide 267 | } 268 | 269 | if (uid !== hyperpwn.mainUid) { 270 | next(action) 271 | return 272 | } 273 | 274 | if (legendFix) { 275 | data = data.slice(2) 276 | legendFix = false 277 | } 278 | 279 | if (contextStart) { 280 | action.data = '' 281 | contextData += data 282 | } 283 | 284 | const legend = /(?:\[ )?legend: (.*?)]?$/gim.exec(data) 285 | if (legend) { 286 | contextStart = true 287 | hyperpwn.addLegend(legend[0]) 288 | action.data = data.slice(0, legend.index) 289 | contextData = data.slice(legend.index + legend[0].length) 290 | if (contextData.length > 0) { 291 | contextData = contextData.slice(2) 292 | } else { 293 | legendFix = true 294 | } 295 | } 296 | 297 | if (contextStart && contextData.length > 0) { 298 | contextData = contextData.replace(/\r((?:\u001B\[[^m]*m)*)\n/g, '$1\r\n') 299 | const firstTitle = /^(?:\u001B\[[^m]*m)*\[?[-─]/.exec(contextData) 300 | if (!firstTitle) { 301 | contextStart = false 302 | action.data += contextData 303 | contextData = '' 304 | } 305 | 306 | const end = /\r\n(?:\u001B\[[^m]*m)*\[?[-─]+(?:\u001B\[[^m]*m)*[-─]+]?(?:\u001B\[[^m]*m)*\r\n/.exec(contextData) 307 | if (end) { 308 | let endDisp = false 309 | let dataAdded = false 310 | contextStart = false 311 | const tailData = contextData.slice(end.index + end[0].length) 312 | const partRegex = /^((?:\u001B\[[^m]*m)*\[?[-─]+.*[-─]+]?(?:\u001B\[[^m]*m)*)$/gm 313 | const parts = contextData.slice(0, end.index + 2).split(partRegex).slice(1) 314 | contextData = '' 315 | for (let i = 0; i < parts.length; i += 2) { 316 | if (hyperpwn.addData(parts[i], parts[i + 1].slice(2, -2))) { 317 | dataAdded = true 318 | } else { 319 | action.data += parts[i] + parts[i + 1] 320 | endDisp = true 321 | } 322 | } 323 | if (dataAdded) { 324 | hyperpwn.alignData() 325 | } 326 | hyperpwn.replayLast() 327 | 328 | if (endDisp) { 329 | action.data += end[0].slice(2) 330 | } 331 | setTimeout(() => { 332 | store.dispatch({ 333 | type: 'SESSION_PTY_DATA', 334 | uid: hyperpwn.mainUid, 335 | data: tailData 336 | }) 337 | }, 0) 338 | } 339 | } 340 | if (!action.data) { 341 | return 342 | } 343 | } 344 | 345 | if (type === 'SESSION_RESIZE') { 346 | hyperpwn.replayUid(action.uid, action.cols) 347 | } 348 | 349 | if (type === 'SESSION_PTY_EXIT') { 350 | hyperpwn.delUid(action.uid) 351 | } 352 | 353 | next(action) 354 | } 355 | 356 | exports.decorateConfig = mainConfig => { 357 | if (mainConfig.hyperpwn) { 358 | config = merge(JSON.parse(JSON.stringify(defaultConfig)), mainConfig.hyperpwn) 359 | } 360 | return mainConfig 361 | } 362 | 363 | exports.decorateKeymaps = keymaps => { 364 | const newKeymaps = { 365 | 'pwn:replayprev': config.hotkeys.prev, 366 | 'pwn:replaynext': config.hotkeys.next 367 | } 368 | for (const [k, v] of Object.entries(config.hotkeys.cmd)) { 369 | newKeymaps['pwn:cmd:' + k] = v 370 | } 371 | return Object.assign({}, keymaps, newKeymaps) 372 | } 373 | 374 | exports.decorateTerms = (Terms, {React}) => { 375 | return class extends React.Component { 376 | constructor(props, context) { 377 | super(props, context) 378 | this.onDecorated = this.onDecorated.bind(this) 379 | this.terms = null 380 | 381 | hyperpwn = new Hyperpwn() 382 | } 383 | 384 | onDecorated(terms) { 385 | this.terms = terms 386 | if (this.props.onDecorated) { 387 | this.props.onDecorated(terms) 388 | } 389 | 390 | if (this.terms) { 391 | const commands = { 392 | 'pwn:replayprev': hyperpwn.replayPrev, 393 | 'pwn:replaynext': hyperpwn.replayNext 394 | } 395 | for (const cmd of Object.keys(config.hotkeys.cmd)) { 396 | commands['pwn:cmd:' + cmd] = hyperpwn.sendCmd(cmd) 397 | } 398 | terms.registerCommands(commands) 399 | } 400 | } 401 | 402 | render() { 403 | return React.createElement( 404 | Terms, 405 | Object.assign({}, this.props, { 406 | onDecorated: this.onDecorated 407 | }) 408 | ) 409 | } 410 | } 411 | } 412 | 413 | exports.decorateTerm = (Term, {React}) => { 414 | return class extends React.Component { 415 | render() { 416 | const props = {} 417 | 418 | let header 419 | if (config.showHeaders) { 420 | header = hyperpwn.uidHeader(this.props.uid) 421 | } 422 | 423 | if (!header) { 424 | return React.createElement(Term, Object.assign({}, this.props, props)) 425 | } 426 | 427 | const myCustomChildrenBefore = React.createElement( 428 | 'div', 429 | { 430 | key: 'pwn', 431 | style: config.headerStyle 432 | }, 433 | header 434 | ) 435 | const customChildrenBefore = this.props.customChildrenBefore ? 436 | [this.props.customChildrenBefore].concat(myCustomChildrenBefore) : 437 | myCustomChildrenBefore 438 | props.customChildrenBefore = customChildrenBefore 439 | return React.createElement(Term, Object.assign({}, this.props, props)) 440 | } 441 | } 442 | } 443 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperpwn", 3 | "version": "0.5.1", 4 | "description": "A hyper plugin to provide GDB with a flexible GUI", 5 | "repository": "bet4it/hyperpwn", 6 | "author": "Bet4 <0xbet4@gmail.com>", 7 | "license": "MIT", 8 | "main": "index.js", 9 | "files": [ 10 | "cfgs" 11 | ], 12 | "bin": {}, 13 | "keywords": [ 14 | "hyper" 15 | ], 16 | "scripts": { 17 | "test": "xo" 18 | }, 19 | "xo": { 20 | "esnext": true, 21 | "space": true, 22 | "semicolon": false, 23 | "rules": { 24 | "max-depth": "off", 25 | "complexity": "off", 26 | "dot-notation": "off", 27 | "no-control-regex": "off", 28 | "padding-line-between-statements": [ 29 | "error", 30 | { 31 | "blankLine": "any", 32 | "prev": "multiline-block-like", 33 | "next": "*" 34 | } 35 | ] 36 | }, 37 | "env": [ 38 | "node", 39 | "browser" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "xo": "^0.39.1" 44 | }, 45 | "dependencies": { 46 | "ansi-escapes": "^4.3.2", 47 | "cli-truncate": "^2.1.0", 48 | "expandtabs": "^1.0.0", 49 | "fs-extra": "^10.0.0", 50 | "yaml": "^2.7.1", 51 | "lodash.merge": "^4.6.2", 52 | "strip-ansi": "^6.0.0" 53 | } 54 | } 55 | --------------------------------------------------------------------------------