├── .gitignore ├── LICENSE ├── README.md ├── binch ├── binch3 ├── binchlib ├── __init__.py ├── disassemble.py ├── main.py ├── signals.py ├── statusbar.py └── view.py ├── setup.py └── tests ├── test-arm ├── test-heavy ├── test-x64 └── test-x86 /.gitignore: -------------------------------------------------------------------------------- 1 | a.out 2 | *.pyc 3 | *.txt 4 | .* 5 | 6 | # Distribution / packaging 7 | bin/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | eggs/ 12 | lib/ 13 | lib64/ 14 | parts/ 15 | sdist/ 16 | var/ 17 | *.egg-info/ 18 | .installed.cfg 19 | *.egg 20 | MANIFEST 21 | 22 | # Installer logs 23 | pip-log.txt 24 | pip-delete-this-directory.txt 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Choongwoo Han (tunz) 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 | **NOTE** 2 | 3 | This repository is not maintained. Please use [binch-go](https://github.com/tunz/binch-go) instead. I just rewrote this in Go since python is slow and keystone python library is not maintained well. 4 | 5 | --- 6 | 7 | # a light BINary patCH tool 8 | A light ELF binary patch tool in python urwid. It helps to patch a ELF binary in a few steps. 9 | 10 | ![capture](https://cloud.githubusercontent.com/assets/7830853/9346653/9d0fb980-465a-11e5-9908-541ae4c80492.png) 11 | 12 | Now, it only supports x86, x86_64, and ARM(experimental). 13 | 14 | ## Usage 15 | 16 | ``` 17 | $ ./binch [binary name] 18 | ``` 19 | 20 | #### Shortcuts 21 | ``` 22 | g: Go to a specific address. (if not exists, jump to nearest address) 23 | d: Remove a current line. (Fill with nop) 24 | q: Quit. 25 | s: Save a modified binary to a file. 26 | enter: Modify a current line. 27 | h: Modify hex bytes of a current line. 28 | f: follow the address of jmp or call instructions. 29 | ``` 30 | -------------------------------------------------------------------------------- /binch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from binchlib.main import binch 3 | binch() 4 | -------------------------------------------------------------------------------- /binch3: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from binchlib.main import binch 3 | binch() 4 | -------------------------------------------------------------------------------- /binchlib/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunz/binch/b3a0558b4f0468e1fd7fa58c3559f7dff14b1e34/binchlib/__init__.py -------------------------------------------------------------------------------- /binchlib/disassemble.py: -------------------------------------------------------------------------------- 1 | from capstone import * 2 | from keystone import * 3 | from elftools.elf.elffile import ELFFile 4 | from elftools.elf.sections import SymbolTableSection 5 | import sys, os 6 | from . import signals 7 | 8 | class Disassembler(): 9 | 10 | memory = [] 11 | 12 | def __init__(self, filename): 13 | self.filename = filename 14 | self.loadELF(filename) 15 | self.init_disasmblr() 16 | self.init_asmblr() 17 | 18 | def read_memory(self, address, size): 19 | for vaddr, foffset, memsize, mem in self.memory: 20 | if address >= vaddr and address <= vaddr + memsize: 21 | if size: 22 | return mem[address - vaddr : address - vaddr + size] 23 | else: 24 | return mem[address - vaddr:] 25 | return "" 26 | 27 | def write_memory(self, address, data): 28 | offset = self.addr2offset(address) 29 | for idx, (vaddr, foffset, memsize, mem) in enumerate(self.memory): 30 | if offset >= foffset and offset <= foffset + memsize: 31 | mem=list(mem) 32 | for i in range(0, len(data)): 33 | if offset - foffset + i < len(mem): 34 | mem[offset - foffset + i] = data[i] 35 | else: 36 | mem.append(data[i]) 37 | memsize+=1 38 | self.memory[idx] = (vaddr, foffset, memsize, ''.join(mem)) 39 | 40 | def addr2offset(self, address): 41 | for vaddr, foffset, memsize, mem in self.memory: 42 | if address >= vaddr and address <= vaddr + memsize: 43 | return address - vaddr + foffset 44 | return -1 45 | 46 | def load_code_segments(self, segments, filename): 47 | memory = [] 48 | for elf_segment in segments: 49 | if elf_segment.header.p_type != 'PT_LOAD': 50 | continue 51 | 52 | align = 0x1000 53 | ELF_PAGEOFFSET = elf_segment.header.p_vaddr & (align-1) 54 | 55 | memsz = elf_segment.header.p_memsz + ELF_PAGEOFFSET 56 | offset = elf_segment.header.p_offset - ELF_PAGEOFFSET 57 | filesz = elf_segment.header.p_filesz + ELF_PAGEOFFSET 58 | vaddr = elf_segment.header.p_vaddr - ELF_PAGEOFFSET 59 | memsz = (memsz + align ) & ~(align-1) 60 | 61 | with open(filename, 'rb') as f: 62 | f.seek(offset, 0) 63 | data = f.read(filesz) 64 | memory.append((vaddr, offset, memsz, data)) 65 | return memory 66 | 67 | def load_symbol_table(self, symbols): 68 | syms = dict() 69 | thumbs = list() 70 | for symbol in symbols: 71 | if symbol['st_info']['type'] == 'STT_FUNC': 72 | if self.is_thumb_addr(symbol['st_value']): 73 | syms[symbol['st_value'] - 1] = symbol.name 74 | else: 75 | syms[symbol['st_value']] = symbol.name 76 | elif self.arch == 'ARM' and symbol['st_info']['type'] == 'STT_NOTYPE': 77 | if symbol.name == '$t': # Thumb 78 | thumbs.append((symbol['st_value'], True)) 79 | elif symbol.name == '$a': #ARM 80 | thumbs.append((symbol['st_value'], False)) 81 | return syms, thumbs 82 | 83 | def load_section_info(self, sections): 84 | symtab = dict() 85 | thumbtab = list() 86 | code_addrs = [] 87 | 88 | for section in sections: 89 | if isinstance(section, SymbolTableSection): 90 | syms, thumbs = self.load_symbol_table(section.iter_symbols()) 91 | symtab.update(syms) 92 | thumbtab.extend(thumbs) 93 | elif section['sh_flags'] == 6: # Assumption: Code section's flag is AX (ALLOC=2 & EXEC=4) 94 | code_addrs.append({'address': section['sh_addr'], 'size': section['sh_size']}) 95 | return symtab, thumbtab, code_addrs 96 | 97 | def loadELF(self, filename): 98 | try: 99 | elf = ELFFile(open(filename, 'rb')) 100 | except: 101 | raise Exception("[-] This file is not an ELF file: %s" % filename) 102 | 103 | self.arch = elf.get_machine_arch() 104 | self.entry = elf.header.e_entry 105 | self.memory = self.load_code_segments(elf.iter_segments(), filename) 106 | self.symtab, self.thumbtab, self.code_addrs = self.load_section_info(elf.iter_sections()) 107 | 108 | self.thumbtab.sort(key=lambda tup: tup[0]) 109 | self.code_addrs = sorted(self.code_addrs, key=lambda k: k['address']) 110 | 111 | def init_asmblr(self): 112 | arch = {'x86':KS_ARCH_X86,'x64':KS_ARCH_X86, 'ARM':KS_ARCH_ARM}[self.arch] 113 | mode = {'x86':KS_MODE_32, 'x64':KS_MODE_64, 'ARM':KS_MODE_ARM}[self.arch] 114 | self.ks = Ks(arch, mode) 115 | if self.arch == 'ARM': 116 | self.t_ks = Ks(arch, CS_MODE_THUMB) 117 | 118 | def init_disasmblr(self): 119 | arch = {'x86':CS_ARCH_X86,'x64':CS_ARCH_X86, 'ARM':CS_ARCH_ARM}[self.arch] 120 | mode = {'x86':CS_MODE_32, 'x64':CS_MODE_64, 'ARM':CS_MODE_ARM}[self.arch] 121 | self.md = Cs(arch, mode) 122 | self.md.detail = True 123 | if self.arch == 'ARM': 124 | self.t_md = Cs(arch, CS_MODE_THUMB) 125 | self.t_md.detail = True 126 | 127 | def disasm(self, address, size=None): 128 | if self.arch == 'ARM' and self.thumbtab: 129 | disasms = [] 130 | thumb = bool(address & 1) 131 | address = address & 0xfffffffe 132 | for addr, isthumb in self.thumbtab: 133 | if address < addr: 134 | md = self.md if not thumb else self.t_md 135 | disasms.extend([i for i in md.disasm(self.read_memory(address, addr-address), address)]) 136 | address = addr 137 | thumb = isthumb 138 | return disasms 139 | else: 140 | return [i for i in self.md.disasm(self.read_memory(address, size), address)] 141 | 142 | def asm(self, asmcode, thumb=False): 143 | ks = self.ks if not thumb else self.t_ks 144 | try: 145 | encoding, count = ks.asm(asmcode) 146 | except KsError as err: 147 | msg = "Error: %s" % err 148 | signals.set_message.send(0, message=msg, expire=2) 149 | return "" 150 | return ''.join(map(chr, encoding)) 151 | 152 | def is_thumb_instr(self, instr): 153 | return instr._cs.mode == CS_MODE_THUMB 154 | 155 | def save(self): 156 | def save_binary(filename): 157 | def save_binary_yes(yn, filename): 158 | if yn == 'y': 159 | try: 160 | original_binary = open(self.filename, 'rb').read() 161 | f = open(filename, 'wb') 162 | f.write(original_binary) 163 | for vaddr, foffset, memsize, mem in self.memory: 164 | f.seek(foffset, 0) 165 | f.write(mem) 166 | f.close() 167 | os.chmod(filename, 0o755) 168 | return "Successfully save to '%s'" % filename 169 | except Exception as e: 170 | return "Fail to save binary: "+str(e) 171 | 172 | return "Fail to save binary" 173 | 174 | if filename == "": 175 | filename = self.filename 176 | 177 | if os.path.exists(filename): 178 | return (filename+" already exists, Overwrite?", save_binary_yes, filename) 179 | else: 180 | return save_binary_yes('y', filename) 181 | 182 | signals.set_prompt.send(self, text="Save to (filename): ", callback=save_binary) 183 | 184 | def is_thumb_addr(self, address): 185 | return self.arch == 'ARM' and (address & 1) == 1 186 | -------------------------------------------------------------------------------- /binchlib/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from .view import DisassembleView 4 | import argparse 5 | import sys 6 | import os 7 | 8 | def binch(args=None): 9 | parser = argparse.ArgumentParser(description='A light ELF binary patch tool.') 10 | parser.add_argument('filename', metavar='filename', type=str, 11 | help='a binary filename to patch') 12 | args = parser.parse_args() 13 | 14 | filepath = os.path.abspath(args.filename) 15 | 16 | if os.path.isfile(filepath): 17 | DisassembleView(filepath).main() 18 | else: 19 | print("[-] There is no file: %s" % (filepath)) 20 | -------------------------------------------------------------------------------- /binchlib/signals.py: -------------------------------------------------------------------------------- 1 | import blinker 2 | 3 | focus = blinker.Signal() 4 | set_prompt = blinker.Signal() 5 | set_prompt_yn = blinker.Signal() 6 | set_message = blinker.Signal() 7 | redraw_status = blinker.Signal() 8 | call_delay = blinker.Signal() 9 | -------------------------------------------------------------------------------- /binchlib/statusbar.py: -------------------------------------------------------------------------------- 1 | from . import signals 2 | import view 3 | import urwid 4 | 5 | class CommandLine(urwid.WidgetWrap): 6 | def __init__(self): 7 | urwid.WidgetWrap.__init__(self, None) 8 | self.clear() 9 | signals.set_prompt.connect(self.sig_prompt) 10 | signals.set_prompt_yn.connect(self.sig_prompt_yn) 11 | signals.set_message.connect(self.sig_message) 12 | self.prompt_callback = False 13 | self.prompt_yn_callback = False 14 | 15 | def clear(self): 16 | self._w = urwid.Text("") 17 | 18 | def sig_message(self, sender, message, expire=None): 19 | w = urwid.Text(message) 20 | self._w = w 21 | if expire: 22 | def cb(*args): 23 | if w == self._w: 24 | self.clear() 25 | signals.call_delay.send(seconds=expire, callback=cb) 26 | 27 | def sig_prompt(self, sender, text, callback): 28 | self.prompt_yn_callback = False 29 | signals.focus.send(self, section='footer') 30 | self._w = urwid.Edit(text, "") 31 | self.prompt_callback = callback 32 | 33 | def sig_prompt_yn(self, sender, text, callback, arg): 34 | self.prompt_callback = False 35 | signals.focus.send(self, section='footer') 36 | self.ask_yn(text, callback, arg) 37 | 38 | def ask_yn(self, text, callback, arg): 39 | self._w = urwid.Edit(text + " (y/n):", '') 40 | self.prompt_yn_callback = (callback, arg) 41 | 42 | def prompt(self, text): 43 | msg = self.prompt_callback(text) 44 | self.prompt_callback = False 45 | if isinstance(msg, tuple): 46 | msg, callback, arg = msg 47 | self.ask_yn(msg, callback, arg) 48 | else: 49 | signals.focus.send(self, section='body') 50 | if isinstance(msg, str): 51 | signals.set_message.send(self, message=msg, expire=1) 52 | 53 | def prompt_yn(self, yn): 54 | func, arg = self.prompt_yn_callback 55 | msg = func(yn, arg) 56 | signals.focus.send(self, section='body') 57 | self.prompt_yn_callback = False 58 | if msg: 59 | signals.set_message.send(self, message=msg, expire=1) 60 | else: 61 | self.clear() 62 | 63 | def prompt_clear(self): 64 | self.prompt_callback = False 65 | self.prompt_yn_callback = False 66 | signals.focus.send(self, section='body') 67 | self.clear() 68 | 69 | def selectable(self): 70 | return True 71 | 72 | def keypress(self, size, k): 73 | if self.prompt_callback: 74 | if k == "esc": 75 | self.prompt_clear() 76 | elif k == "enter": 77 | self.prompt(self._w.get_edit_text()) 78 | elif isinstance(k, basestring): 79 | self._w.keypress(size, k) 80 | else: 81 | return k 82 | elif self.prompt_yn_callback: 83 | if k == "esc": 84 | self.prompt_clear() 85 | elif k == "y" or k == "Y": 86 | self.prompt_yn('y') 87 | elif k == "n" or k == "N": 88 | self.prompt_yn('n') 89 | 90 | class StatusBar(urwid.WidgetWrap): 91 | def __init__(self, text, view): 92 | urwid.WidgetWrap.__init__(self, None) 93 | self.view = view 94 | self.commandline = CommandLine() 95 | self.default_text = text 96 | self.update_status() 97 | signals.redraw_status.connect(self.sig_redraw_status) 98 | 99 | def sig_redraw_status(self, sender): 100 | self.update_status() 101 | 102 | def update_status(self): 103 | if self.view.disasmblr.arch == 'ARM' and isinstance(self.view.disasmlist._w.focus, view.DisassembleInstruction): 104 | if self.view.disasmlist._w.focus.isthumb: 105 | mode = "[Thumb]" 106 | else: 107 | mode = "[ ARM ]" 108 | self.status = urwid.Columns([ 109 | urwid.WidgetWrap(urwid.Text(self.default_text)), 110 | ('fixed', 20, urwid.WidgetWrap(urwid.Text(mode))) 111 | ]) 112 | else: 113 | self.status = urwid.WidgetWrap(urwid.Text(self.default_text)) 114 | self.status = urwid.AttrMap(self.status, 'status') 115 | self._w = urwid.Pile([self.status, self.commandline]) 116 | 117 | def keypress(self, *args, **kwargs): 118 | return self.commandline.keypress(*args, **kwargs) 119 | 120 | def selectable(self): 121 | return True 122 | -------------------------------------------------------------------------------- /binchlib/view.py: -------------------------------------------------------------------------------- 1 | import urwid 2 | from .disassemble import * 3 | from .statusbar import * 4 | from . import signals 5 | from capstone.x86 import X86_OP_IMM 6 | from capstone.arm import ARM_OP_IMM 7 | import traceback 8 | import progressbar 9 | import sys 10 | import re 11 | 12 | class DisassembleText(urwid.Text): 13 | 14 | def selectable(self): 15 | return False 16 | 17 | def keypress(self, size, key): 18 | return key 19 | 20 | class DisassembleInstruction(urwid.WidgetWrap): 21 | def __init__(self, instr, disasmblr, view): 22 | urwid.WidgetWrap.__init__(self, None) 23 | self.instruction = instr 24 | self.hexcode = list(self.instruction.bytes) 25 | self.isthumb = disasmblr.is_thumb_instr(instr) 26 | self._editbox = None 27 | self._hexeditbox = None 28 | self.edit_mode = False 29 | self.hex_edit_mode = False 30 | self.disasmblr = disasmblr 31 | self.view = view 32 | self.repeat = 1 33 | self.mode_plain() 34 | 35 | def selectable(self): 36 | return True 37 | 38 | def mode_plain(self): 39 | repeat_str = "" 40 | if self.repeat > 1: 41 | repeat_str = " .. (repeat %d times)" % self.repeat 42 | self._w = urwid.Columns([('fixed', 102, urwid.Text("%s%s%s%s" % ( 43 | hex(self.instruction.address).rstrip('L').ljust(11, ' ')+' ', 44 | ' '.join(["%02x" % j for j in self.hexcode*self.repeat]).ljust(27, ' ')+' ', 45 | self.instruction.mnemonic.ljust(7, ' ')+' ', 46 | self.instruction.op_str + repeat_str)) 47 | )]) 48 | self._w = urwid.AttrMap(self._w, 'bg', 'reveal focus') 49 | 50 | def mode_edit1(self): 51 | self.address = urwid.Text(hex(self.instruction.address).rstrip('L')) 52 | self.opcode = urwid.Text(' '.join(["%02x" % j for j in self.hexcode*self.repeat])) 53 | self._w = urwid.Columns([ 54 | ('fixed', 12, self.address), 55 | ('fixed', 28, self.opcode), 56 | ('fixed', 62, self._editbox) 57 | ]) 58 | self._w = urwid.AttrMap(self._w, 'bg', 'reveal focus') 59 | 60 | def mode_edit2(self): 61 | self.address = urwid.Text(hex(self.instruction.address).rstrip('L')) 62 | self.instr = urwid.Text("%s%s" % (self.instruction.mnemonic.ljust(8, ' '), self.instruction.op_str)) 63 | self._w = urwid.Columns([ 64 | ('fixed', 12, self.address), 65 | ('fixed', 28, self._hexeditbox), 66 | ('fixed', 62, self.instr) 67 | ]) 68 | self._w = urwid.AttrMap(self._w, 'bg', 'reveal focus') 69 | 70 | def modify_opcode(self, opcode, original_opcode=None): 71 | if opcode == "": 72 | self.mode_plain() 73 | return 74 | 75 | if original_opcode == None: 76 | original_opcode = ''.join(map(chr, self.hexcode*self.repeat)) 77 | 78 | original_opcode_len = len(original_opcode) 79 | 80 | if len(opcode) < original_opcode_len: 81 | if self.disasmblr.arch == 'ARM': 82 | opcode = opcode.ljust(original_opcode_len, "\x00") # Fill with nop 83 | else: 84 | opcode = opcode.ljust(original_opcode_len, "\x90") # Fill with nop 85 | elif len(opcode) > original_opcode_len: 86 | safe_opcode_len = 0 87 | opcode_data = self.disasmblr.read_memory(self.instruction.address, 0x20) 88 | md = self.disasmblr.md if not self.isthumb else self.disasmblr.t_md 89 | disasm_code = md.disasm(opcode_data, 0x20) 90 | for i in disasm_code: 91 | if len(opcode) > safe_opcode_len: 92 | safe_opcode_len += len(i.bytes) 93 | if self.disasmblr.arch == 'ARM': 94 | opcode = opcode.ljust(safe_opcode_len, "\x00") # Fill with nop 95 | else: 96 | opcode = opcode.ljust(safe_opcode_len, "\x90") # Fill with nop 97 | 98 | self.disasmblr.write_memory(self.instruction.address, opcode) 99 | 100 | repeat = 0 101 | 102 | if self.isthumb: 103 | codes = [i for i in self.disasmblr.t_md.disasm(opcode, self.instruction.address)] 104 | else: 105 | codes = [i for i in self.disasmblr.md.disasm(opcode, self.instruction.address)] 106 | 107 | if self.disasmblr.arch in ['x86','x64']: 108 | NOPCODE = [0x90] 109 | elif self.disasmblr.arch == 'ARM': 110 | NOPCODE = [0x00, 0x00] 111 | 112 | nopcode_repeat = True 113 | for c in codes: 114 | repeat += 1 115 | if list(c.bytes) != NOPCODE: 116 | nopcode_repeat = False 117 | break 118 | 119 | if nopcode_repeat: 120 | codes = codes[:1] 121 | self.repeat = repeat 122 | else: 123 | self.repeat = 1 124 | 125 | if original_opcode_len == len(opcode) and len(codes) == 1: 126 | code = codes[0] 127 | 128 | if (len(code.operands) == 1 and 129 | ((self.disasmblr.arch in ['x86','x64'] and code.operands[0].type == X86_OP_IMM) or 130 | (self.disasmblr.arch == 'ARM' and code.operands[0].type == ARM_OP_IMM))): 131 | self.view.update_list(self.view.disasmlist._w.focus_position) 132 | 133 | self.instruction = code 134 | self.hexcode = list(self.instruction.bytes) 135 | self.mode_plain() 136 | else: 137 | def update_all(yn, arg): 138 | if yn == "y": 139 | self.view.update_list(self.view.disasmlist._w.focus_position) 140 | else: 141 | self.modify_opcode(original_opcode) 142 | 143 | signals.set_prompt_yn.send(self, 144 | text="This operation will break following codes, is it okey?", 145 | callback=update_all, 146 | arg=None 147 | ) 148 | 149 | def repeat_inc(self): 150 | self.repeat += 1 151 | self.mode_plain() 152 | 153 | def keypress(self, size, key): 154 | if self.edit_mode: 155 | if key == "esc": 156 | self.edit_mode = False 157 | self.mode_plain() 158 | elif key == "enter": 159 | self.edit_mode = False 160 | asmcode = self._editbox.get_edit_text() 161 | is_thumb_code = True if self.disasmblr.arch == 'ARM' and self.isthumb else False 162 | opcode = self.disasmblr.asm(asmcode, thumb=is_thumb_code) 163 | self.modify_opcode(opcode) 164 | elif isinstance(key, basestring): 165 | self._w.keypress(size, key) 166 | else: 167 | return key 168 | elif self.hex_edit_mode: 169 | if key == "esc": 170 | self.hex_edit_mode = False 171 | self.mode_plain() 172 | elif key == "enter": 173 | self.hex_edit_mode = False 174 | hexcode = self._hexeditbox.get_edit_text() 175 | original_hexcode = ''.join(map(chr, self.hexcode*self.repeat)) 176 | try: 177 | opcode = hexcode.replace(' ','').decode('hex') 178 | self.modify_opcode(opcode, original_hexcode) 179 | except Exception as e: 180 | msg = "Error: "+str(e) 181 | self.modify_opcode(original_hexcode, original_hexcode) 182 | signals.set_message.send(0, message=msg, expire=2) 183 | self.mode_plain() 184 | 185 | elif isinstance(key, basestring): 186 | self._w.keypress(size, key) 187 | else: 188 | return key 189 | else: 190 | if key == "enter": 191 | self._editbox = urwid.Edit("", "%s%s" % (self.instruction.mnemonic.ljust(8, ' '), 192 | self.instruction.op_str)) 193 | self.mode_edit1() 194 | self.edit_mode = True 195 | elif key == "h": 196 | self._hexeditbox = urwid.Edit("", ' '.join(["%02x" % j for j in self.instruction.bytes])) 197 | self.mode_edit2() 198 | self.hex_edit_mode = True 199 | elif key == "f": 200 | followAddress = False 201 | mnemonic = self.instruction.mnemonic 202 | if self.disasmblr.arch in ['x86', 'x64'] and (mnemonic[0] == 'j' or mnemonic == 'call'): 203 | if self.instruction.operands[0].type == X86_OP_IMM: 204 | followAddress = True 205 | elif self.disasmblr.arch == 'ARM' and mnemonic[0] == 'b': 206 | if self.instruction.operands[0].type == ARM_OP_IMM: 207 | followAddress = True 208 | if followAddress: 209 | address = int(self.instruction.op_str.lstrip('#'), 16) 210 | try: 211 | self.view.disasmlist.set_focus(self.view.index_map[address]) 212 | self.view.history.append(self.instruction.address) 213 | msg = "Jump to "+hex(address) 214 | signals.set_message.send(0, message=msg, expire=1) 215 | except: 216 | msg = "Error: Fail to jump... please report it" 217 | signals.set_message.send(0, message=msg, expire=2) 218 | elif key == "d" or key == "D": 219 | def fill_with_nop(yn, arg): 220 | if yn == 'y': 221 | if self.disasmblr.arch == 'ARM': 222 | self.modify_opcode("\x00") 223 | else: 224 | self.modify_opcode("\x90") 225 | signals.set_prompt_yn.send(self, text="Remove this line?", callback=fill_with_nop, arg=None) 226 | else: 227 | if key == "j" or key == "J": 228 | key = "down" 229 | elif key == "k" or key == "K": 230 | key = "up" 231 | return key 232 | 233 | class SymbolText(urwid.Text): 234 | 235 | def selectable(self): 236 | return False 237 | 238 | def keypress(self, size, key): 239 | return key 240 | 241 | class DisassembleList(urwid.WidgetWrap): 242 | def __init__(self, dList): 243 | urwid.WidgetWrap.__init__(self, None) 244 | self.update_list(dList) 245 | 246 | def set_focus(self, idx): 247 | self._w.set_focus(idx) 248 | 249 | def update_list(self, dList, focus=0): 250 | self._w = urwid.ListBox(urwid.SimpleListWalker(dList)) 251 | if focus: 252 | self._w.set_focus(focus) 253 | 254 | def selectable(self): 255 | return True 256 | 257 | def keypress(self, size, key): 258 | key = super(self.__class__, self).keypress(size, key) 259 | if key == "j": 260 | key = "down" 261 | elif key == "k": 262 | key = "up" 263 | return key 264 | 265 | class DisassembleWindow(urwid.Frame): 266 | def __init__(self, view, body, header, footer): 267 | urwid.Frame.__init__( 268 | self, body, 269 | header if header else None, 270 | footer if footer else None 271 | ) 272 | self.view = view 273 | signals.focus.connect(self.sig_focus) 274 | 275 | def sig_focus(self, sender, section): 276 | self.focus_position = section 277 | 278 | def keypress(self, size, key): 279 | key = super(self.__class__, self).keypress(size, key) 280 | return key 281 | 282 | class DisassembleView: 283 | palette = [('header', 'white', 'black'), 284 | ('reveal focus', 'black', 'light gray', 'standout'), 285 | ('status', 'white', 'dark blue', 'standout')] 286 | 287 | def __init__(self, filename): 288 | self.header = urwid.Text(" BINCH: %s" % (filename)) 289 | 290 | self.disasmblr = Disassembler(filename) 291 | 292 | items = self.setup_list(True) 293 | self.disasmlist = DisassembleList(items) 294 | start_index = self.find_index(self.disasmblr.entry) 295 | if start_index != -1: 296 | self.disasmlist.set_focus(start_index) 297 | 298 | self.history = list() 299 | 300 | self.body = urwid.Padding(self.disasmlist, 'center', 105) 301 | self.body = urwid.Filler(self.body, ('fixed top',1), ('fixed bottom',1)) 302 | 303 | self.footer = StatusBar("HotKeys -> g: Go to a address | s: Save | d: Remove | enter: Modify | q: Quit", self) 304 | self.view = DisassembleWindow(self, 305 | urwid.AttrWrap(self.body, 'body'), 306 | urwid.AttrWrap(self.header, 'head'), 307 | self.footer) 308 | 309 | signals.call_delay.connect(self.sig_call_delay) 310 | 311 | def find_index(self, address): 312 | try: 313 | if self.disasmblr.is_thumb_addr(address): 314 | return self.index_map[address & -2] 315 | else: 316 | return self.index_map[address] 317 | except KeyError: 318 | return -1 319 | 320 | def setup_list(self, show_progressbar = False): 321 | if self.disasmblr.arch in ['x86','x64']: 322 | NOPCODE = [0x90] 323 | elif self.disasmblr.arch == 'ARM': 324 | NOPCODE = [0x00, 0x00] 325 | 326 | body = [] 327 | for code in self.disasmblr.code_addrs: 328 | body.extend(self.disasmblr.disasm(code['address'], code['size'])) 329 | 330 | items = [] 331 | idx = 0 332 | self.index_map = dict() 333 | 334 | if show_progressbar: 335 | instr_list = progressbar.ProgressBar(widgets=[progressbar.Percentage(), ' ', 336 | progressbar.Bar(), ' ', progressbar.ETA()])(body) 337 | else: 338 | instr_list = body 339 | 340 | for i in instr_list: 341 | address = i.address 342 | symbol = None 343 | try: symbol = self.disasmblr.symtab[address] 344 | except: 345 | if self.disasmblr.is_thumb_instr(i): 346 | try: symbol = self.disasmblr.symtab[address - 1] 347 | except: pass 348 | 349 | if symbol: 350 | items.append(SymbolText(" ")) 351 | items.append(SymbolText(" < %s >" % symbol)) 352 | idx+=2 353 | hexcode = list(i.bytes) 354 | if hexcode == NOPCODE and (isinstance(items[-1], DisassembleInstruction) and items[-1].hexcode == NOPCODE): 355 | items[-1].repeat_inc() 356 | else: 357 | items.append(DisassembleInstruction(i, self.disasmblr, self)) 358 | self.index_map[address] = idx 359 | idx+=1 360 | sys.stdout.write("\033[F") 361 | 362 | return items 363 | 364 | def update_list(self, focus=0): 365 | items = self.setup_list() 366 | self.disasmlist.update_list(items, focus) 367 | 368 | def update_status(self, *arg): 369 | signals.redraw_status.send(self) 370 | self.loop.set_alarm_in(0.03, self.update_status) 371 | 372 | def main(self): 373 | self.loop = urwid.MainLoop(self.view, self.palette, 374 | handle_mouse=False, 375 | unhandled_input=self.unhandled_input) 376 | 377 | self.loop.set_alarm_in(0.03, self.update_status) 378 | 379 | try: 380 | self.loop.run() 381 | except: 382 | self.loop.stop() 383 | print(traceback.format_exc()) 384 | 385 | def unhandled_input(self, k): 386 | def goto(text): 387 | try: 388 | if bool(re.match(r'^([0-9]|0x[0-9a-fA-F]+|\+|\-| )+$',text)): 389 | address = eval(text) 390 | else: 391 | return "It is invalid number: "+text 392 | except: 393 | return "Fail to calculate address: "+text 394 | 395 | if address in self.index_map: 396 | self.history.append(self.disasmlist._w.body[self.disasmlist._w.focus_position].instruction.address) 397 | self.disasmlist.set_focus(self.index_map[address]) 398 | return "Jump to "+hex(address) 399 | else: 400 | for i in range(1, 0x10): 401 | if address - i in self.index_map: 402 | self.history.append(self.disasmlist._w.body[self.disasmlist._w.focus_position].instruction.address) 403 | self.disasmlist.set_focus(self.index_map[address - i]) 404 | return "Jump to "+hex(address - i) 405 | elif address + i in self.index_map: 406 | self.history.append(self.disasmlist._w.body[self.disasmlist._w.focus_position].instruction.address) 407 | self.disasmlist.set_focus(self.index_map[address + i]) 408 | return "Jump to "+hex(address + i) 409 | 410 | return "Invalid address: "+hex(address) 411 | 412 | if k in ('q', 'Q'): 413 | def ask_quit(yn, arg): 414 | if yn == 'y': 415 | raise urwid.ExitMainLoop() 416 | signals.set_prompt_yn.send(self, text="Quit?", callback=ask_quit, arg=None) 417 | elif k in ('g', 'G'): 418 | signals.set_prompt.send(self, text="Goto: ", callback=goto) 419 | elif k in ('s', 'S'): 420 | self.disasmblr.save() 421 | elif k == "esc": 422 | if len(self.history) > 0: 423 | address = self.history[-1] 424 | del self.history[-1] 425 | self.disasmlist.set_focus(self.index_map[address]) 426 | 427 | def sig_call_delay(self, sender, seconds, callback): 428 | def cb(*_): 429 | return callback() 430 | self.loop.set_alarm_in(seconds, cb) 431 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | py_modules = [ 4 | 'pyelftools', 5 | 'capstone', 6 | 'keystone-engine', 7 | 'urwid', 8 | 'blinker', 9 | 'progressbar2' 10 | ] 11 | 12 | setup ( 13 | name = 'binch', 14 | version = '0.3.1', 15 | description = 'a light ELF binary patch tool', 16 | author = 'Choongwoo Han', 17 | author_email = 'cwhan.tunz@gmail.com', 18 | url = 'https://github.com/tunz/binch', 19 | license = 'MIT', 20 | classifiers=[ 21 | 'Development Status :: 3 - Alpha', 22 | 'Intended Audience :: Developers', 23 | 'Topic :: Security', 24 | 'Topic :: Software Development', 25 | 'Programming Language :: Python :: 2.7', 26 | 'Programming Language :: Python :: 3.4', 27 | 'License :: OSI Approved :: MIT License', 28 | 'Environment :: Console', 29 | 'Environment :: Console :: Curses', 30 | 'Operating System :: MacOS', 31 | 'Operating System :: POSIX :: Linux' 32 | ], 33 | keywords = 'disassemble binary patch', 34 | packages = find_packages(), 35 | install_requires = py_modules, 36 | entry_points = { 37 | 'console_scripts': ['binch = binchlib.main:binch'] 38 | } 39 | ) 40 | -------------------------------------------------------------------------------- /tests/test-arm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunz/binch/b3a0558b4f0468e1fd7fa58c3559f7dff14b1e34/tests/test-arm -------------------------------------------------------------------------------- /tests/test-heavy: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunz/binch/b3a0558b4f0468e1fd7fa58c3559f7dff14b1e34/tests/test-heavy -------------------------------------------------------------------------------- /tests/test-x64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunz/binch/b3a0558b4f0468e1fd7fa58c3559f7dff14b1e34/tests/test-x64 -------------------------------------------------------------------------------- /tests/test-x86: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tunz/binch/b3a0558b4f0468e1fd7fa58c3559f7dff14b1e34/tests/test-x86 --------------------------------------------------------------------------------