├── LICENSE ├── README.md ├── dsync.py └── rsrc ├── hint.gif └── sync.gif /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Dennis Elser 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 | # dsync 2 | 3 | IDAPython plugin that synchronizes decompiled and disassembled code views. 4 | 5 | Please refer to comments in source code for more details. 6 | 7 | Requires IDA Pro 7.3 8 | 9 | ![dsync animated gif](/rsrc/sync.gif?raw=true) 10 | 11 | ![dsync hint animated gif](/rsrc/hint.gif?raw=true) 12 | 13 | N.B.: You may want to use the official synchronization feature introduced with [IDA 7.3](https://www.hex-rays.com/products/ida/news/7_3/) instead. 14 | -------------------------------------------------------------------------------- /dsync.py: -------------------------------------------------------------------------------- 1 | from idaapi import * 2 | 3 | """ 4 | This plugin for IDA synchronizes Hexrays decompiler views and disassembly views (from 5 | decompiled code to disassembly by default - use the TAB key for synchronizing from 6 | disassembly to decompiled code). 7 | 8 | It also highlights all addresses (code and data definitions) involved. Pressing the 9 | hotkey Ctrl-Shift-S switches synchronization on and off. Hovering over pseudocode 10 | items will display corresponding disassembled code in a hint window. The item that 11 | belongs to the item that is located under the cursor will be highlighted and pointed 12 | to by an arrow. 13 | 14 | The plugin requires IDA 7.3. 15 | """ 16 | 17 | __author__ = 'https://github.com/patois' 18 | 19 | HL_COLOR = 0xAD8044 20 | 21 | # ----------------------------------------------------------------------- 22 | class idb_hook_t(IDB_Hooks): 23 | def __init__(self, hxehook): 24 | self.hxehook = hxehook 25 | IDB_Hooks.__init__(self) 26 | 27 | def savebase(self): 28 | self.hxehook._reset_all_colors() 29 | return 0 30 | 31 | # ----------------------------------------------------------------------- 32 | class hxe_hook_t(Hexrays_Hooks): 33 | def __init__(self): 34 | Hexrays_Hooks.__init__(self) 35 | self.idbhook = idb_hook_t(self) 36 | self.idbhook.hook() 37 | self.pseudocode_instances = {} 38 | self.n_spaces = 40 39 | 40 | def close_pseudocode(self, vd): 41 | self._reset_colors(vd.view_idx, ignore_vd=True) 42 | refresh_idaview_anyway() 43 | return 0 44 | 45 | def create_hint(self, vd): 46 | result = self._get_vd_context(vd) 47 | if result: 48 | _, _, _, item_ea_list = result 49 | 50 | if len(item_ea_list): 51 | if vd.get_current_item(USE_MOUSE): 52 | cur_item_ea = vd.item.it.ea 53 | else: 54 | cur_item_ea = BADADDR 55 | 56 | lines = [] 57 | for ea in item_ea_list: 58 | disasm_line = generate_disasm_line(ea, 0) 59 | if disasm_line: 60 | addr = "0x%x: " % ea 61 | 62 | if cur_item_ea == ea: 63 | prefix = COLSTR("==> %s" % addr, SCOLOR_INSN) 64 | else: 65 | prefix = " " + addr 66 | 67 | lines.append(prefix+disasm_line) 68 | 69 | lines.append("") 70 | lines.append(self.n_spaces * "-") 71 | lines.append("") 72 | custom_hints = "\n".join(lines) 73 | # ask decompiler to append default hints 74 | return (2, custom_hints, len(lines)) 75 | return 0 76 | 77 | def curpos(self, vd): 78 | self._reset_all_colors() 79 | self._apply_colors(vd) 80 | refresh_idaview_anyway() 81 | return 0 82 | 83 | def refresh_pseudocode(self, vd): 84 | self._reset_all_colors(ignore_vd=True) 85 | return 0 86 | 87 | def cleanup(self): 88 | self._reset_all_colors() 89 | refresh_idaview_anyway() 90 | 91 | if self.idbhook: 92 | self.idbhook.unhook() 93 | self.idbhook = None 94 | return 95 | 96 | def _reset_colors(self, idx, ignore_vd=False): 97 | v = self.pseudocode_instances[idx] 98 | if v: 99 | pseudocode, lineno, color, disasm_lines = v 100 | if not ignore_vd and pseudocode: 101 | try: 102 | pseudocode[lineno].bgcolor = color 103 | except: # wtf 104 | pass 105 | for ea, color in disasm_lines: 106 | set_item_color(ea, color) 107 | self.pseudocode_instances.pop(idx) 108 | return 109 | 110 | def _reset_all_colors(self, ignore_vd=False): 111 | # restore colors 112 | if self.pseudocode_instances: 113 | pi = list(self.pseudocode_instances) 114 | for k in pi: 115 | self._reset_colors(k, ignore_vd) 116 | self.pseudocode_instances = {} 117 | return 118 | 119 | def _apply_colors(self, vd): 120 | result = self._get_vd_context(vd) 121 | if result: 122 | pseudocode, lineno, col, item_ea_list = result 123 | disasm_lines = [(ea, get_item_color(ea)) for ea in item_ea_list] 124 | if len(item_ea_list): 125 | jumpto(item_ea_list[0], -1, UIJMP_IDAVIEW | UIJMP_DONTPUSH) 126 | self.pseudocode_instances[vd.view_idx] = (pseudocode, lineno, col, disasm_lines) 127 | 128 | if pseudocode: 129 | try: 130 | pseudocode[lineno].bgcolor = HL_COLOR 131 | except: # wtf 132 | pass 133 | for ea, _ in disasm_lines: 134 | set_item_color(ea, HL_COLOR) 135 | return 136 | 137 | def _get_item_indexes(self, line): 138 | indexes = [] 139 | tag = COLOR_ON + chr(COLOR_ADDR) 140 | pos = line.find(tag) 141 | while pos != -1 and len(line[pos+len(tag):]) >= COLOR_ADDR_SIZE: 142 | addr = line[pos+len(tag):pos+len(tag)+COLOR_ADDR_SIZE] 143 | idx = int(addr, 16) 144 | a = ctree_anchor_t() 145 | a.value = idx 146 | if a.is_valid_anchor() and a.is_citem_anchor(): 147 | """ 148 | print "a.value %s %d lvar %s citem %s itp %s blkcmt %s" % ( 149 | a.is_valid_anchor(), 150 | a.get_index(), 151 | a.is_lvar_anchor(), 152 | a.is_citem_anchor(), 153 | a.is_itp_anchor(), 154 | a.is_blkcmt_anchor()) 155 | """ 156 | indexes.append(a.get_index()) 157 | pos = line.find(tag, pos+len(tag)+COLOR_ADDR_SIZE) 158 | return indexes 159 | 160 | def _get_vd_context(self, vd): 161 | if vd: 162 | lineno = vd.cpos.lnnum 163 | pseudocode = vd.cfunc.get_pseudocode() 164 | 165 | if pseudocode and lineno != -1: 166 | try: 167 | color = pseudocode[lineno].bgcolor 168 | line = pseudocode[lineno].line 169 | 170 | item_idxs = self._get_item_indexes(line) 171 | ea_list = {} 172 | for i in item_idxs: 173 | try: 174 | item = vd.cfunc.treeitems.at(i) 175 | if item and item.ea != BADADDR: 176 | ea_list[item.ea] = None 177 | except: 178 | pass 179 | return (pseudocode, lineno, color, sorted(ea_list.keys())) 180 | except: 181 | pass 182 | return None 183 | 184 | # ----------------------------------------------------------------------- 185 | def is_ida_version(min_ver_required): 186 | return IDA_SDK_VERSION >= min_ver_required 187 | 188 | # ----------------------------------------------------------------------- 189 | class Dsync(ida_idaapi.plugin_t): 190 | comment = '' 191 | help = '' 192 | flags = PLUGIN_MOD 193 | wanted_name = 'dsync' 194 | wanted_hotkey = 'Ctrl-Shift-S' 195 | hxehook = None 196 | 197 | def init(self): 198 | required_ver = 730 199 | if not is_ida_version(required_ver) or not init_hexrays_plugin(): 200 | msg ("[!] '%s' is inactive (IDA v%d and decompiler required).\n" % (Dsync.wanted_name, required_ver)) 201 | return PLUGIN_SKIP 202 | 203 | msg("[+] '%s' loaded. %s activates/deactivates synchronization.\n" % (Dsync.wanted_name, Dsync.wanted_hotkey)) 204 | return PLUGIN_KEEP 205 | 206 | def run(self, arg): 207 | if not Dsync.hxehook: 208 | Dsync.hxehook = hxe_hook_t() 209 | Dsync.hxehook.hook() 210 | else: 211 | Dsync.hxehook.unhook() 212 | Dsync.hxehook.cleanup() 213 | Dsync.hxehook = None 214 | 215 | msg("[+] %s is %sabled now.\n" % (Dsync.wanted_name, "en" if Dsync.hxehook else "dis")) 216 | 217 | def term(self): 218 | msg("[+] %s unloaded.\n" % (Dsync.wanted_name)) 219 | if Dsync.hxehook: 220 | Dsync.hxehook.unhook() 221 | Dsync.hxehook.cleanup() 222 | Dsync.hxehook = None 223 | 224 | # ----------------------------------------------------------------------- 225 | def PLUGIN_ENTRY(): 226 | return Dsync() 227 | -------------------------------------------------------------------------------- /rsrc/hint.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patois/dsync/af8467e6eeea60b0a2e4fcc133e8d5e198d4ab9f/rsrc/hint.gif -------------------------------------------------------------------------------- /rsrc/sync.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patois/dsync/af8467e6eeea60b0a2e4fcc133e8d5e198d4ab9f/rsrc/sync.gif --------------------------------------------------------------------------------