├── abyss_filters ├── __init__.py ├── experimental │ ├── lvars_info.py │ ├── item_index.py │ ├── item_ctype.py │ ├── lvars_alias.py │ └── item_sync.py ├── token_colorizer.py ├── signed_ops.py └── hierarchy.py ├── rsrc ├── func.gif └── signedops.gif ├── README.md ├── LICENSE └── abyss.py /abyss_filters/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /rsrc/func.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patois/abyss/HEAD/rsrc/func.gif -------------------------------------------------------------------------------- /rsrc/signedops.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/patois/abyss/HEAD/rsrc/signedops.gif -------------------------------------------------------------------------------- /abyss_filters/experimental/lvars_info.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_hexrays, ida_lines 3 | 4 | class lvars_info_t(abyss_filter_t): 5 | """appends a postfix to local variables that indicates 6 | each variable's type (*r*egister or *s*tack) and its size 7 | in bytes.""" 8 | 9 | def maturity_ev(self, cfunc, new_maturity): 10 | if new_maturity == ida_hexrays.CMAT_FINAL: 11 | lvars = cfunc.get_lvars() 12 | for lvar in lvars: 13 | if lvar.has_nice_name and not lvar.has_user_name: 14 | vtype = "s" if lvar.is_stk_var() else "r" if lvar.is_reg_var() else "u" 15 | suffix = "_%s%d" % (vtype, lvar.width) 16 | lvar.name += ida_lines.COLSTR(suffix, ida_lines.SCOLOR_AUTOCMT) 17 | lvar.set_user_name() 18 | return 0 19 | 20 | def FILTER_INIT(): 21 | return lvars_info_t() -------------------------------------------------------------------------------- /abyss_filters/experimental/item_index.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_lines as il 3 | import re 4 | 5 | def replace_addr_tags(s): 6 | tag = "%c%c" % (il.COLOR_ON, il.COLOR_ADDR) 7 | tag_size = len(tag) 8 | ti = {} 9 | p = s.find(tag) 10 | while p != -1: 11 | ti[s[p+tag_size:p+tag_size+il.COLOR_ADDR_SIZE]] = 0 12 | p = s.find(tag, p+tag_size+il.COLOR_ADDR_SIZE) 13 | for addr in ti.keys(): 14 | s = s.replace(tag+addr, il.COLSTR("<%s>" % addr, il.SCOLOR_AUTOCMT)+tag+addr) 15 | return s 16 | 17 | class color_addr_info_t(abyss_filter_t): 18 | """This filter makes COLOR_ADDR tags visible in the 19 | decompiled code (useful only to developers).""" 20 | 21 | def func_printed_ev(self, cfunc): 22 | pc = cfunc.get_pseudocode() 23 | for sl in pc: 24 | sl.line = replace_addr_tags(sl.line) 25 | return 0 26 | 27 | def FILTER_INIT(): 28 | return color_addr_info_t() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # abyss - Postprocess Hexrays Decompiler Output 2 | 3 | ## Installation 4 | Copy abyss.py and abyss_filters to IDA plugins directory 5 | 6 | ## Usage 7 | Right-click within a decompiler view, pick a filter 8 | from the abyss context menu. 9 | 10 | Per-filter default settings can be changed by editing 11 | the config file: "%APPDATA%/Hex-Rays/IDA Pro/cfg/abyss.cfg" 12 | 13 | ## Disclaimer 14 | Experimental/WIP code, use at your own risk :) 15 | 16 | ## Developers 17 | Create a fresh Python module within "abyss_filters", make sure 18 | to inherit from the abyss_filter_t class (see abyss.py). 19 | 20 | Re-running the plugin from the plugins menu or by pressing 21 | the Ctrl-Alt-R keycombo reloads all filters dynamically. 22 | This allows for development of filters without having to 23 | restart IDA. 24 | 25 | ## Example filters (incomplete list) 26 | 27 | ### signed_ops.py 28 | ![abyss signedops gif](/rsrc/signedops.gif?raw=true) 29 | 30 | ### token_colorizer.py 31 | ![abyss func gif](/rsrc/func.gif?raw=true) 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 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 | -------------------------------------------------------------------------------- /abyss_filters/experimental/item_ctype.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_lines as il 3 | import ida_hexrays 4 | import re 5 | 6 | def replace_addr_tags(cfunc, s): 7 | tag = "%c%c" % (il.COLOR_ON, il.COLOR_ADDR) 8 | tag_size = len(tag) 9 | ti = {} 10 | p = s.find(tag) 11 | while p != -1: 12 | ti[s[p+tag_size:p+tag_size+il.COLOR_ADDR_SIZE]] = 0 13 | p = s.find(tag, p+tag_size+il.COLOR_ADDR_SIZE) 14 | for addr in ti.keys(): 15 | idx = int(addr, 16) 16 | a = ida_hexrays.ctree_anchor_t() 17 | a.value = idx 18 | if a.is_valid_anchor() and a.is_citem_anchor(): 19 | item = cfunc.treeitems.at(a.get_index()) 20 | if item: 21 | ctype_name = ida_hexrays.get_ctype_name(item.op) 22 | s = s.replace(tag+addr, il.COLSTR("<%s>" % ctype_name, il.SCOLOR_AUTOCMT)+tag+addr) 23 | return s 24 | 25 | class item_ctype_info_t(abyss_filter_t): 26 | """This filter prepends ctype names (useful only to developers).""" 27 | 28 | def func_printed_ev(self, cfunc): 29 | pc = cfunc.get_pseudocode() 30 | for sl in pc: 31 | sl.line = replace_addr_tags(cfunc, sl.line) 32 | return 0 33 | 34 | def FILTER_INIT(): 35 | return item_ctype_info_t() -------------------------------------------------------------------------------- /abyss_filters/token_colorizer.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_lines 3 | import ida_diskio 4 | import os 5 | import configparser 6 | 7 | CFG_TOKENS = {} 8 | CFG_COLOR_DEFAULT = ida_lines.SCOLOR_ERROR 9 | 10 | # ---------------------------------------------------------------------------- 11 | def get_self_filename(): 12 | mod, _ = os.path.splitext(os.path.basename(__file__)) 13 | return mod 14 | 15 | # ---------------------------------------------------------------------------- 16 | def get_cfg_filename(): 17 | """returns full path of config file name.""" 18 | return os.path.join( 19 | ida_diskio.get_user_idadir(), 20 | "cfg", 21 | "abyss_%s.cfg" % get_self_filename()) 22 | 23 | # ---------------------------------------------------------------------------- 24 | def create_cfg_file(): 25 | config = configparser.ConfigParser() 26 | config["init"] = { 27 | "token_color1":"SCOLOR_MACRO", 28 | "token1":"return", 29 | "token_color2":"SCOLOR_CHAR", 30 | "token2":"sprintf", 31 | "token3":"memcpy", 32 | "token4":"malloc", 33 | "token5":"free"} 34 | with open(get_cfg_filename(), "w") as cfg_file: 35 | config.write(cfg_file) 36 | return 37 | 38 | # ---------------------------------------------------------------------------- 39 | def read_cfg(): 40 | global CFG_COLOR 41 | global CFG_TOKENS 42 | 43 | cfg_file = get_cfg_filename() 44 | if not os.path.isfile(cfg_file): 45 | create_cfg_file() 46 | read_cfg() 47 | return 48 | config = configparser.ConfigParser() 49 | config.read(cfg_file) 50 | 51 | sectname = "init" 52 | if sectname in config: 53 | section = config[sectname] 54 | color = CFG_COLOR_DEFAULT 55 | for key in section: 56 | val = section[key] 57 | if key.startswith("token_color"): 58 | color = getattr(globals()["ida_lines"], val) 59 | else: 60 | if color not in CFG_TOKENS: 61 | CFG_TOKENS[color] = [] 62 | CFG_TOKENS[color].append(val) 63 | return 64 | 65 | # ---------------------------------------------------------------------------- 66 | class token_colorizer_t(abyss_filter_t): 67 | """filter that colorizes tokens""" 68 | 69 | def func_printed_ev(self, cfunc): 70 | pc = cfunc.get_pseudocode() 71 | for sl in pc: 72 | for color in CFG_TOKENS: 73 | for token in CFG_TOKENS[color]: 74 | sl.line = sl.line.replace(token, ida_lines.COLSTR(token, color)) 75 | return 0 76 | 77 | # ---------------------------------------------------------------------------- 78 | def FILTER_INIT(): 79 | read_cfg() 80 | return token_colorizer_t() -------------------------------------------------------------------------------- /abyss_filters/signed_ops.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_lines 3 | import ida_diskio 4 | import ida_hexrays as hr 5 | import os 6 | import configparser 7 | 8 | FMT = "%c%c%" + ("0%dX" % ida_lines.COLOR_ADDR_SIZE) 9 | CFG_DEFAULT_COLOR = "SCOLOR_REGCMT" 10 | CFG_COMMENT = "/*signed*/" 11 | CFG_COLOR = None 12 | 13 | SIGNED_OPS = [hr.cot_asgsshr, 14 | hr. cot_asgsdiv, 15 | hr.cot_asgsmod, 16 | hr.cot_sge, 17 | hr.cot_sle, 18 | hr.cot_sgt, 19 | hr.cot_slt, 20 | hr.cot_sshr, 21 | hr.cot_sdiv, 22 | hr.cot_smod] 23 | 24 | # ---------------------------------------------------------------------------- 25 | def get_self_filename(): 26 | mod, _ = os.path.splitext(os.path.basename(__file__)) 27 | return mod 28 | 29 | # ---------------------------------------------------------------------------- 30 | def get_cfg_filename(): 31 | """returns full path of config file name.""" 32 | return os.path.join( 33 | ida_diskio.get_user_idadir(), 34 | "cfg", 35 | "abyss_%s.cfg" % get_self_filename()) 36 | 37 | # ---------------------------------------------------------------------------- 38 | def create_cfg_file(): 39 | config = configparser.ConfigParser() 40 | config["init"] = { 41 | "comment":"%s" % CFG_COMMENT, 42 | "color":"%s" % CFG_DEFAULT_COLOR} 43 | with open(get_cfg_filename(), "w") as cfg_file: 44 | config.write(cfg_file) 45 | return 46 | 47 | # ---------------------------------------------------------------------------- 48 | def read_cfg(): 49 | global CFG_COLOR 50 | global CFG_COMMENT 51 | 52 | cfg_file = get_cfg_filename() 53 | if not os.path.isfile(cfg_file): 54 | create_cfg_file() 55 | read_cfg() 56 | return 57 | config = configparser.ConfigParser() 58 | config.read(cfg_file) 59 | 60 | CFG_COLOR = getattr( 61 | globals()["ida_lines"], 62 | config.get("init", "color", fallback=CFG_DEFAULT_COLOR)) 63 | CFG_COMMENT = config.get("init", "comment", fallback=CFG_COMMENT) 64 | return 65 | 66 | # ---------------------------------------------------------------------------- 67 | class signed_op_replacer_t(abyss_filter_t): 68 | """insert additional comments into the pseudo-c code. 69 | comments indicate the use of signed operations.""" 70 | 71 | def tag_signed_ops(self, cf, item_codes): 72 | ci = hr.ctree_item_t() 73 | ccode = cf.get_pseudocode() 74 | for line_idx in range(cf.hdrlines, len(ccode)): 75 | items = [] 76 | sl = ccode[line_idx] 77 | for char_idx in range(len(sl.line)): 78 | if cf.get_line_item(sl.line, char_idx, True, None, ci, None): 79 | if ci.it.is_expr() and ci.e.op in item_codes: 80 | #print("%s: signed op. line %d, pos %d" % (__file__, line_idx+1, char_idx)) 81 | items.append(ci.it.index) 82 | for item in list(dict.fromkeys(items)): 83 | tag = FMT % (ida_lines.COLOR_ON, ida_lines.COLOR_ADDR, item) 84 | sl.line = sl.line.replace(tag, tag+ida_lines.COLSTR(CFG_COMMENT, CFG_COLOR)) 85 | 86 | def func_printed_ev(self, cfunc): 87 | self.tag_signed_ops(cfunc, SIGNED_OPS) 88 | return 0 89 | 90 | # ---------------------------------------------------------------------------- 91 | def FILTER_INIT(): 92 | read_cfg() 93 | return signed_op_replacer_t() -------------------------------------------------------------------------------- /abyss_filters/experimental/lvars_alias.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_lines, ida_name, ida_hexrays as hr 3 | 4 | VAR_ASG_VAR_SUFFIX = "_" 5 | VAR_ASG_CALL_PREFIX = "res_" 6 | 7 | # mapping a type string representation to a deterministic name to use 8 | VAR_ASG_CALL_DETERMINISTIC = { 9 | "NTSTATUS": "status", 10 | } 11 | 12 | fDebug = False 13 | def debug_print(msg): 14 | if fDebug: 15 | print("%s" % msg) 16 | return 17 | 18 | def debug_lvars(lvars): 19 | if not fDebug: 20 | return 21 | print("%d variables" % len(lvars)) 22 | for one in lvars: 23 | print("%s %s; // %s" % (one.type(), one.name, one.cmt)) 24 | return 25 | 26 | def set_var_unique_name(var_x, var_y, lvars): 27 | old_name = var_x.name 28 | new_name = var_y.name + VAR_ASG_VAR_SUFFIX 29 | new_name = set_unique_name(var_x, new_name, lvars) 30 | debug_print("Renamed: %s (%s) = %s " % (old_name, new_name, var_y.name)) 31 | return 32 | 33 | def set_func_unique_name(var_x, func_name, lvars): 34 | old_name = var_x.name 35 | x_type = var_x.type() 36 | if str(x_type) in VAR_ASG_CALL_DETERMINISTIC.keys(): 37 | new_name = VAR_ASG_CALL_DETERMINISTIC[str(x_type)] 38 | else: 39 | new_name = VAR_ASG_CALL_PREFIX + func_name 40 | new_name = set_unique_name(var_x, new_name, lvars) 41 | debug_print("Renamed: %s (%s) = %s(...) " % (old_name, new_name, func_name)) 42 | return 43 | 44 | def set_unique_name(var_x, new_name, lvars): 45 | bExist = True 46 | while bExist: 47 | bExist = False 48 | for one in lvars: 49 | if new_name == one.name: 50 | new_name += VAR_ASG_VAR_SUFFIX 51 | bExist = True 52 | break 53 | var_x.name = new_name 54 | var_x.set_user_name() 55 | return new_name 56 | 57 | class asg_visitor_t(hr.ctree_visitor_t): 58 | 59 | def __init__(self, cfunc): 60 | hr.ctree_visitor_t.__init__(self, hr.CV_FAST) 61 | self.cfunc = cfunc 62 | self.lvars = cfunc.get_lvars() 63 | debug_lvars(self.lvars) 64 | return 65 | 66 | def visit_expr(self, e): 67 | if e.op == hr.cot_asg: 68 | # is x a var? 69 | if e.x.op == hr.cot_var: 70 | # handle "x = y" types of assignments 71 | if e.y.op == hr.cot_var: 72 | # get variable indexes 73 | x_idx = e.x.v.idx 74 | y_idx = e.y.v.idx 75 | # get lvar_t 76 | var_x = self.lvars[x_idx] 77 | var_y = self.lvars[y_idx] 78 | """ 79 | debug_print("Found: %s (user: %s, nice: %s) = %s (user: %s, nice: %s)" % ( 80 | var_x.name, 81 | var_x.has_user_name, 82 | var_x.has_nice_name, 83 | var_y.name, 84 | var_y.has_user_name, 85 | var_y.has_nice_name)) 86 | """ 87 | # if x has an autogenerated name and y has a custom name 88 | if not var_x.has_user_name and var_y.has_user_name: 89 | # rename x 90 | set_var_unique_name(var_x, var_y, self.lvars) 91 | else: 92 | debug_print("Skipped: %s = %s " % (var_x.name, var_y.name)) 93 | # handle "x = y()" types of assignments 94 | elif e.y.op in [hr.cot_call, hr.cot_cast]: 95 | if e.y.op == hr.cot_call: 96 | tmp_y = e.y 97 | elif e.y.op == hr.cot_cast and e.y.x.op == hr.cot_call: 98 | tmp_y = e.y.x 99 | else: # TBD 100 | tmp_y = None 101 | if tmp_y and tmp_y.x.op == hr.cot_obj: 102 | # get name of called function 103 | func_name = ida_name.get_ea_name(tmp_y.x.obj_ea, 104 | ida_name.GN_VISIBLE | ida_name.GN_LOCAL) 105 | # get var index and lvar_t 106 | x_idx = e.x.v.idx 107 | var_x = self.lvars[x_idx] 108 | if not var_x.has_user_name: 109 | # rename x 110 | set_func_unique_name(var_x, func_name, self.lvars) 111 | else: 112 | debug_print("Skipped: %s = %s(...) " % (var_x.name, func_name)) 113 | return 0 114 | 115 | 116 | class lvars_alias_t(abyss_filter_t): 117 | """example filter that shows how to rename/alias local variables 118 | used in assigment expressions. 119 | 120 | examples: 121 | 'v11 = buf' becomes 'buf_ = buf' 122 | 'v69 = strstr(a, b)' becomes 'result_strstr = strstr(a, b)'""" 123 | 124 | def __init__(self): 125 | abyss_filter_t.__init__(self) 126 | return 127 | 128 | def maturity_ev(self, cfunc, new_maturity): 129 | if new_maturity == hr.CMAT_FINAL: 130 | av = asg_visitor_t(cfunc) 131 | av.apply_to(cfunc.body, None) 132 | return 0 133 | 134 | def FILTER_INIT(): 135 | return lvars_alias_t() -------------------------------------------------------------------------------- /abyss_filters/experimental/item_sync.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | import ida_lines, ida_kernwin, ida_hexrays 3 | from idaapi import BADADDR 4 | 5 | COLOR = ida_kernwin.CK_EXTRA2 6 | 7 | class vu_sync_t(): 8 | def __init__(self, vu): 9 | self.vu = vu 10 | self.eadict = {} 11 | self._process() 12 | 13 | def __contains__(self, ea): 14 | return ea in self.eadict 15 | 16 | def get_items(self, ea): 17 | return self.eadict[ea] 18 | 19 | def _add_item(self, iea, ix, iy, ilen): 20 | if iea not in self.eadict: 21 | self.eadict[iea] = [] 22 | si = (ix, iy, ilen) 23 | if si not in self.eadict[iea]: 24 | self.eadict[iea].append((ix, iy, ilen)) 25 | return 26 | 27 | def _process(self): 28 | cf = self.vu.cfunc 29 | ci = ida_hexrays.ctree_item_t() 30 | ccode = cf.get_pseudocode() 31 | for ypos in range(cf.hdrlines, len(ccode)): 32 | tline = ccode.at(ypos).line 33 | # TODO: optimize the following loop 34 | idx = 0 35 | while idx < len(tline): 36 | citem_len = 0 37 | # get all items on a line 38 | if cf.get_line_item(tline, idx, True, None, ci, None): 39 | iea = ci.it.ea 40 | if iea != BADADDR: 41 | # generate color-tagged/addr-tagged text of current item 42 | citem = ci.it.print1(None) 43 | citem_len = len(ida_lines.tag_remove(citem)) 44 | # find (tagged) item text in current line 45 | pos = tline.find(citem) 46 | while pos != -1: 47 | # calculate x position of item text in line 48 | # by subtracting the number of color tag 49 | # characters up to position "pos" 50 | xpos = len(ida_lines.tag_remove(tline[:pos])) 51 | self._add_item(iea, xpos, ypos, citem_len) 52 | pos = tline.find(citem, pos + citem_len) 53 | idx += ida_lines.tag_advance(tline[idx], 1) 54 | return 55 | 56 | class item_sync_t(abyss_filter_t): 57 | """This plugin/filter highlights a decompiler view's 58 | citems that correspond to the current line ("screen ea") 59 | of a disassembly view. 60 | n.b.: experimental, untested and unoptimized code. 61 | 62 | Can be used alongside IDA's internal synchronization 63 | feature (the alpha channel of background overlay colors 64 | should be set to a value < 1.0). 65 | 66 | This plugin uses the "Extra line background overlay #2" 67 | color from IDA's color options.""" 68 | 69 | def __init__(self): 70 | super().__init__() 71 | self.ea = ida_kernwin.get_screen_ea() 72 | self.funcs = {} 73 | 74 | def set_activated(self, val): 75 | if not val: 76 | # cleanup if filter is about to be deactivated 77 | self.funcs = {} 78 | return super().set_activated(val) 79 | 80 | def refresh_pseudocode_ev(self, vu): 81 | entry_ea = vu.cfunc.entry_ea 82 | self.funcs[entry_ea] = vu_sync_t(vu) 83 | return 84 | 85 | def screen_ea_changed_ev(self, ea, prev_ea): 86 | # react to screen ea changes issued by PSEUDOCODE and DISASM views 87 | if (ida_kernwin.get_widget_type(ida_kernwin.get_current_widget()) in 88 | [ida_kernwin.BWN_PSEUDOCODE, ida_kernwin.BWN_DISASM]): 89 | self.ea = ea 90 | # why does refresh_idaview_anyway() work but request_refresh() doesn't? 91 | #ida_kernwin.clear_refresh_request(ida_kernwin.IWID_PSEUDOCODE) 92 | #ida_kernwin.request_refresh(ida_kernwin.IWID_PSEUDOCODE, True) 93 | ida_kernwin.refresh_idaview_anyway() 94 | return 95 | 96 | def get_lines_rendering_info_ev(self, out, widget, rin): 97 | wt = ida_kernwin.get_widget_type(widget) 98 | if wt == ida_kernwin.BWN_PSEUDOCODE: 99 | vu = ida_hexrays.get_widget_vdui(widget) 100 | if vu: 101 | cf = vu.cfunc 102 | if cf.entry_ea not in self.funcs: 103 | return 104 | 105 | vusync = self.funcs[cf.entry_ea] 106 | ea = self.ea 107 | if ea in vusync: 108 | slist = vusync.get_items(ea) 109 | for section_lines in rin.sections_lines: 110 | for line in section_lines: 111 | lnnum = ida_kernwin.place_t.as_simpleline_place_t(line.at).n 112 | for sync_info in slist: 113 | ix, iy, ilen = sync_info 114 | if lnnum == iy: 115 | e = ida_kernwin.line_rendering_output_entry_t(line) 116 | e.bg_color = COLOR 117 | e.cpx = ix 118 | e.nchars = ilen 119 | e.flags |= ida_kernwin.LROEF_CPS_RANGE 120 | out.entries.push_back(e) 121 | return 122 | 123 | def FILTER_INIT(): 124 | return item_sync_t() -------------------------------------------------------------------------------- /abyss_filters/hierarchy.py: -------------------------------------------------------------------------------- 1 | from abyss import abyss_filter_t 2 | from ida_idaapi import BADADDR 3 | import ida_kernwin 4 | import ida_hexrays 5 | import ida_funcs 6 | import idautils 7 | import ida_name 8 | import ida_bytes 9 | import ida_ua 10 | import ida_idp 11 | import ida_diskio 12 | import configparser 13 | import os 14 | 15 | CFG_MAX_DEPTH = 4 16 | CFG_MAX_FUNC = 30 17 | 18 | # ---------------------------------------------------------------------------- 19 | def get_self_filename(): 20 | mod, _ = os.path.splitext(os.path.basename(__file__)) 21 | return mod 22 | 23 | # ---------------------------------------------------------------------------- 24 | def get_cfg_filename(): 25 | """returns full path of config file name.""" 26 | return os.path.join( 27 | ida_diskio.get_user_idadir(), 28 | "cfg", 29 | "abyss_%s.cfg" % get_self_filename()) 30 | 31 | # ---------------------------------------------------------------------------- 32 | def create_cfg_file(): 33 | config = configparser.ConfigParser() 34 | config["init"] = { 35 | "max_recursion":"4", 36 | "max_functions":"30"} 37 | with open(get_cfg_filename(), "w") as cfg_file: 38 | config.write(cfg_file) 39 | return 40 | 41 | # ---------------------------------------------------------------------------- 42 | def read_cfg(): 43 | global CFG_MAX_DEPTH 44 | global CFG_MAX_FUNC 45 | 46 | cfg_file = get_cfg_filename() 47 | if not os.path.isfile(cfg_file): 48 | create_cfg_file() 49 | read_cfg() 50 | return 51 | config = configparser.ConfigParser() 52 | config.read(cfg_file) 53 | 54 | CFG_MAX_DEPTH = config.getint("init", "max_recursion", fallback=CFG_MAX_DEPTH) 55 | CFG_MAX_FUNC = config.getint("init", "max_functions", fallback=CFG_MAX_FUNC) 56 | return 57 | 58 | # ---------------------------------------------------------------------------- 59 | def Callees(ea): 60 | pfn = ida_funcs.get_func(ea) 61 | callees = [] 62 | if pfn: 63 | for item in pfn: 64 | F = ida_bytes.get_flags(item) 65 | if ida_bytes.is_code(F): 66 | insn = ida_ua.insn_t() 67 | if ida_ua.decode_insn(insn, item): 68 | if ida_idp.is_call_insn(insn): 69 | if insn.ops[0].type in [ida_ua.o_near, ida_ua.o_far]: 70 | callees.append(insn.ops[0].addr) 71 | return list(dict.fromkeys(callees)) 72 | 73 | # ---------------------------------------------------------------------------- 74 | class hierarchy_t(abyss_filter_t): 75 | def finish_populating_widget_popup_ev(self, widget, popup_handle): 76 | 77 | class EAHandler(ida_kernwin.action_handler_t): 78 | def __init__(self, ea): 79 | self.ea = ea 80 | ida_kernwin.action_handler_t.__init__(self) 81 | 82 | def activate(self, ctx): 83 | ida_kernwin.jumpto(self.ea) 84 | return 1 85 | 86 | def update(self, ctx): 87 | return ida_kernwin.AST_ENABLE_FOR_WIDGET 88 | 89 | class callees_t: 90 | def __init__(self, start_ea, max_recursion=CFG_MAX_DEPTH, CFG_MAX_FUNC=CFG_MAX_FUNC): 91 | self.ea = start_ea 92 | name = ida_name.get_short_name(self.ea) 93 | if not len(name): 94 | name = "unkn_%x" % self.ea 95 | self.base_path = "childs [%s]" % name 96 | self.mr = max_recursion 97 | self.mf = CFG_MAX_FUNC 98 | self.paths = {} 99 | self._recurse(self.ea, self.base_path, 0) 100 | 101 | # TODO: check processing of recursive functions 102 | def _recurse(self, ea, path, depth): 103 | if depth >= self.mr: 104 | self.paths[path] = [("[...]", BADADDR)] 105 | return 106 | 107 | # for all callees of ea... 108 | i = 0 109 | for cea in Callees(ea): 110 | if i+1 >= self.mf: 111 | self.paths[path].append(("...", BADADDR)) 112 | break 113 | loc_name = ida_name.get_short_name(cea) 114 | if not len(loc_name): 115 | loc_name = "unkn_%x" % cea 116 | elem = (loc_name, cea) 117 | # if path doesn't exist yet 118 | if path not in self.paths: 119 | self.paths[path] = [elem] 120 | # if callee doesn't exist yet 121 | if elem not in self.paths[path]: 122 | self.paths[path].append(elem) 123 | i += 1 124 | 125 | newpath = "%s/%s" % (path, loc_name) 126 | self._recurse(cea, newpath, depth+1) 127 | return 128 | 129 | class callers_t: 130 | def __init__(self, start_ea, max_recursion=CFG_MAX_DEPTH, CFG_MAX_FUNC=CFG_MAX_FUNC): 131 | self.ea = start_ea 132 | name = ida_name.get_short_name(self.ea) 133 | if not len(name): 134 | name = "unkn_%x" % self.ea 135 | self.base_path = "parents [%s]" % name 136 | self.mr = max_recursion 137 | self.mf = CFG_MAX_FUNC 138 | self.paths = {} 139 | self._recurse(self.ea, self.base_path , 0) 140 | 141 | # TODO: check processing of recursive functions 142 | def _recurse(self, ea, path, depth): 143 | if depth+1 >= self.mr: 144 | self.paths[path] = [("[...]", BADADDR)] 145 | return 146 | 147 | # for all callers of ea... 148 | i = 0 149 | for ref in idautils.CodeRefsTo(ea, False): 150 | if i+1 >= self.mf: 151 | self.paths[path].append(("...", BADADDR)) 152 | break 153 | cea = ref 154 | func = ida_funcs.get_func(cea) 155 | if func: 156 | cea = func.start_ea 157 | loc_name = ida_name.get_short_name(cea) 158 | if not len(loc_name): 159 | loc_name = "unkn_%x" % cea 160 | elem = (loc_name, cea) 161 | # if path doesn't exist yet 162 | if path not in self.paths: 163 | self.paths[path] = [elem] 164 | # if caller doesn't exist yet 165 | if elem not in self.paths[path]: 166 | self.paths[path].append(elem) 167 | i += 1 168 | 169 | newpath = "%s/%s" % (path, loc_name) 170 | self._recurse(cea, newpath, depth+1) 171 | return 172 | 173 | def build_menu(item_ea): 174 | callers = callers_t(item_ea) 175 | for path, info in callers.paths.items(): 176 | for loc_name, ea in info: 177 | desc = ida_kernwin.action_desc_t( 178 | "abyss:caller_%s_%s" % (path, loc_name), 179 | "%s" % (loc_name), 180 | EAHandler(ea), 181 | None, 182 | None, 183 | 41 if ea != BADADDR else -1) 184 | ida_kernwin.attach_dynamic_action_to_popup( 185 | widget, 186 | popup_handle, 187 | desc, 188 | "%s (%d)/" % (path, len(info)), 189 | ida_kernwin.SETMENU_APP) 190 | 191 | callees = callees_t(item_ea) 192 | for path, info in callees.paths.items(): 193 | for loc_name, ea in info: 194 | desc = ida_kernwin.action_desc_t( 195 | "abyss:callee_%s_%s" % (path, loc_name), 196 | "%s" % (loc_name), 197 | EAHandler(ea), 198 | None, 199 | None, 200 | 41 if ea != BADADDR else -1) 201 | ida_kernwin.attach_dynamic_action_to_popup( 202 | widget, 203 | popup_handle, 204 | desc, 205 | "%s (%d)/" % (path, len(info)), 206 | ida_kernwin.SETMENU_APP) 207 | 208 | ea = ida_kernwin.get_screen_ea() 209 | vu = ida_hexrays.get_widget_vdui(widget) 210 | if vu and vu.get_current_item(ida_hexrays.USE_KEYBOARD): 211 | if vu.item.it.is_expr() and vu.item.it.op is ida_hexrays.cot_obj: 212 | _ea = vu.item.e.cexpr.obj_ea 213 | if _ea != BADADDR: 214 | ea = _ea 215 | pfn = ida_funcs.get_func(ea) 216 | if pfn and pfn.start_ea == ea: 217 | build_menu(ea) 218 | 219 | # ---------------------------------------------------------------------------- 220 | def FILTER_INIT(): 221 | read_cfg() 222 | return hierarchy_t() -------------------------------------------------------------------------------- /abyss.py: -------------------------------------------------------------------------------- 1 | import ida_kernwin as kw 2 | import ida_hexrays as hr 3 | import ida_diskio, ida_idaapi 4 | import os, sys, configparser 5 | 6 | __author__ = "https://github.com/patois" 7 | 8 | DEBUG = False 9 | 10 | PLUGIN_NAME = "abyss" 11 | ACTION_NAME = "%s:" % PLUGIN_NAME 12 | POPUP_ENTRY = "%s/" % PLUGIN_NAME 13 | FILTER_DIR = "%s_filters" % PLUGIN_NAME 14 | CFG_FILENAME = "%s.cfg" % PLUGIN_NAME 15 | 16 | FILTERS = {} 17 | 18 | # ---------------------------------------------------------------------------- 19 | def dbg_print(s): 20 | if DEBUG: 21 | print("%s" % s) 22 | 23 | # ---------------------------------------------------------------------------- 24 | class abyss_filter_t: 25 | """new filters should inherit from this class and 26 | override respective handlers/methods""" 27 | 28 | def __init__(self): 29 | self.set_activated(False) 30 | return 31 | 32 | def finish_populating_widget_popup_ev(self, widget, popup_handle): 33 | return 34 | 35 | def refresh_pseudocode_ev(self, vu): 36 | return 0 37 | 38 | def print_func_ev(self, cfunc, printer): 39 | return 0 40 | 41 | def func_printed_ev(self, cfunc): 42 | return 0 43 | 44 | def curpos_ev(self, vu): 45 | return 0 46 | 47 | def maturity_ev(self, cfunc, new_maturity): 48 | return 0 49 | 50 | def create_hint_ev(self, vu): 51 | return 0 52 | 53 | def get_lines_rendering_info_ev(self, out, widget, info): 54 | return 55 | 56 | def screen_ea_changed_ev(self, ea, prev_ea): 57 | return 58 | 59 | def is_activated(self): 60 | return self.activated 61 | 62 | def set_activated(self, active): 63 | self.activated = active 64 | 65 | # ---------------------------------------------------------------------------- 66 | def get_cfg_filename(): 67 | """returns full path of abyss config file.""" 68 | return os.path.join( 69 | ida_diskio.get_user_idadir(), 70 | "cfg", 71 | "%s" % CFG_FILENAME) 72 | 73 | # ---------------------------------------------------------------------------- 74 | def apply_cfg(reload=False, filters={}): 75 | """loads abyss configuration.""" 76 | 77 | cfg_file = get_cfg_filename() 78 | kw.msg("%s: %sloading %s...\n" % (PLUGIN_NAME, 79 | "re" if reload else "", 80 | cfg_file)) 81 | if not os.path.isfile(cfg_file): 82 | kw.msg("%s: default configuration (%s) does not exist!\n" % (PLUGIN_NAME, cfg_file)) 83 | kw.msg("Creating default configuration\n") 84 | try: 85 | with open(cfg_file, "w") as f: 86 | f.write("[init]\n") 87 | for name, mod in filters.items(): 88 | f.write("%s=False\n" % name ) 89 | except: 90 | kw.msg("failed!\n") 91 | return False 92 | return apply_cfg(reload=True) 93 | 94 | config = configparser.ConfigParser() 95 | config.read(cfg_file) 96 | 97 | # read all sections 98 | for section in config.sections(): 99 | if section == "init": 100 | for name, value in config.items(section): 101 | try: 102 | filters[name].set_activated(config[section].getboolean(name)) 103 | dbg_print("%s -> %s" % (name, value)) 104 | except: 105 | pass 106 | kw.msg("done!\n") 107 | return True 108 | 109 | # ---------------------------------------------------------------------------- 110 | def create_cfg_folder(): 111 | cfg_file = get_cfg_filename() 112 | if not os.path.isfile(cfg_file): 113 | os.makedirs(os.path.dirname(cfg_file), exist_ok=True) 114 | return 115 | 116 | # ---------------------------------------------------------------------------- 117 | def load_filters(reload=False): 118 | global FILTERS 119 | 120 | create_cfg_folder() 121 | print("%s: %sloading filters..." % (PLUGIN_NAME, "re" if reload else "")) 122 | if reload: 123 | # TODO: properly clean-up and unload filters 124 | # implement FILTER_EXIT 125 | FILTERS = {} 126 | filterdir = os.path.join(os.path.dirname(__file__), FILTER_DIR) 127 | if os.path.exists(filterdir): 128 | for entry in os.listdir(filterdir): 129 | if entry.lower().endswith(".py") and entry.lower() != "__init__.py": 130 | mod, ext = os.path.splitext(entry) 131 | if mod not in FILTERS: 132 | try: 133 | ida_idaapi.require("%s.%s" % (FILTER_DIR, mod), FILTER_DIR) 134 | flt = sys.modules["%s.%s" % (FILTER_DIR, mod)].FILTER_INIT() 135 | if flt: 136 | print(" loaded: \"%s\"" % (mod)) 137 | FILTERS[mod] = flt 138 | except ModuleNotFoundError: 139 | print(" failed: \"%s\"" % (mod)) 140 | apply_cfg(reload, FILTERS) 141 | return 142 | 143 | # ---------------------------------------------------------------------------- 144 | class ui_event_t(kw.UI_Hooks): 145 | def finish_populating_widget_popup(self, widget, popup_handle): 146 | if kw.get_widget_type(widget) == kw.BWN_PSEUDOCODE: 147 | class FilterHandler(kw.action_handler_t): 148 | def __init__(self, name): 149 | self.name = name 150 | kw.action_handler_t.__init__(self) 151 | 152 | # on-click event handler for pop-up menu 153 | def activate(self, ctx): 154 | # toggle activated state for current filter 155 | obj = FILTERS[self.name] 156 | obj.set_activated(not obj.is_activated()) 157 | # refresh current decompiler widget 158 | vu = hr.get_widget_vdui(ctx.widget) 159 | if vu: 160 | vu.refresh_view(not obj.is_activated()) 161 | return 1 162 | 163 | def update(self, ctx): 164 | return kw.AST_ENABLE_FOR_WIDGET 165 | 166 | # dynamically create event handler for each filter 167 | # and attach to pop-up menu 168 | for name, obj in FILTERS.items(): 169 | action_desc = kw.action_desc_t( 170 | '%s%s' % (ACTION_NAME, name), 171 | name, 172 | FilterHandler(name), 173 | None, 174 | None, 175 | 34 if obj.is_activated() else -1) 176 | kw.attach_dynamic_action_to_popup(widget, popup_handle, action_desc, POPUP_ENTRY) 177 | 178 | # forward event finish_populating_widget_popup to activated filters 179 | for name, obj in FILTERS.items(): 180 | if obj.is_activated(): 181 | obj.finish_populating_widget_popup_ev(widget, popup_handle) 182 | return 183 | 184 | # forward event screen_ea_changed to activated filters 185 | def screen_ea_changed(self, ea, prev_ea): 186 | for name, obj in FILTERS.items(): 187 | if obj.is_activated(): 188 | obj.screen_ea_changed_ev(ea, prev_ea) 189 | return 190 | 191 | # forward event get_lines_rendering_info to activated filters 192 | def get_lines_rendering_info(self, out, widget, info): 193 | if kw.get_widget_type(widget) == kw.BWN_PSEUDOCODE: 194 | for name, obj in FILTERS.items(): 195 | if obj.is_activated(): 196 | obj.get_lines_rendering_info_ev(out, widget, info) 197 | return 198 | 199 | # ---------------------------------------------------------------------------- 200 | """ 201 | via hexrays.hpp: 202 | /// When the possible return value is not specified, your callback 203 | /// must return zero. 204 | """ 205 | class hx_event_t(hr.Hexrays_Hooks): 206 | def __init__(self): 207 | hr.Hexrays_Hooks.__init__(self) 208 | 209 | # forward event refresh_pseudocode to activated filters 210 | def refresh_pseudocode(self, vu): 211 | for name, obj in FILTERS.items(): 212 | if obj.is_activated(): 213 | # TBD 214 | obj.refresh_pseudocode_ev(vu) 215 | return 0 216 | 217 | # forward event print_func to activated filters 218 | def print_func(self, cfunc, vp): 219 | """via hexrays.hpp: 220 | ///< Returns: 1 if text has been generated by the plugin 221 | ///< It is forbidden to modify ctree at this event. 222 | """ 223 | custom_text = 0 224 | for name, obj in FILTERS.items(): 225 | if obj.is_activated(): 226 | custom_text |= obj.print_func_ev(cfunc, vp) 227 | return custom_text != 0 228 | 229 | # forward event func_printed to activated filters 230 | def func_printed(self, cfunc): 231 | ret = 0 232 | for name, obj in FILTERS.items(): 233 | if obj.is_activated(): 234 | # TBD 235 | ret |= obj.func_printed_ev(cfunc) 236 | return 0 237 | 238 | # forward event curpos to activated filters 239 | def curpos(self, vu): 240 | ret = 0 241 | for name, obj in FILTERS.items(): 242 | if obj.is_activated(): 243 | # TBD 244 | ret |= obj.curpos_ev(vu) 245 | return 0 246 | 247 | # forward event maturity to activated filters 248 | def maturity(self, cfunc, new_maturity): 249 | ret = 0 250 | for name, obj in FILTERS.items(): 251 | if obj.is_activated(): 252 | # TBD 253 | ret |= obj.maturity_ev(cfunc, new_maturity) 254 | return 0 255 | 256 | # forward event create_hint to activated filters 257 | def create_hint(self, vu): 258 | lines = "" 259 | count = 0 260 | for name, obj in FILTERS.items(): 261 | if obj.is_activated(): 262 | # TBD 263 | ret = obj.create_hint_ev(vu) 264 | if ret and isinstance(ret, tuple) and len(ret) == 3: 265 | rv, l, n = ret 266 | lines += l 267 | count += n 268 | if not count: 269 | return 0 270 | return (2, lines, count) 271 | 272 | # ---------------------------------------------------------------------------- 273 | class abyss_plugin_t(ida_idaapi.plugin_t): 274 | flags = 0 275 | comment = "Postprocess Hexrays Output" 276 | help = comment 277 | wanted_name = PLUGIN_NAME 278 | # pressing this hotkey will reload any filter scripts 279 | # without having to restart IDA (useful during development) 280 | wanted_hotkey = "Ctrl-Alt-R" 281 | 282 | def init(self): 283 | if hr.init_hexrays_plugin(): 284 | load_filters() 285 | self.ui_hooks = ui_event_t() 286 | self.ui_hooks.hook() 287 | self.hr_hooks = hx_event_t() 288 | self.hr_hooks.hook() 289 | return ida_idaapi.PLUGIN_KEEP 290 | return ida_idaapi.PLUGIN_SKIP 291 | 292 | def run(self, arg): 293 | load_filters(reload=True) 294 | return 295 | 296 | def term(self): 297 | try: 298 | self.ui_hooks.unhook() 299 | self.hr_hooks.unhook() 300 | except: 301 | pass 302 | return 303 | 304 | # ---------------------------------------------------------------------------- 305 | def PLUGIN_ENTRY(): 306 | return abyss_plugin_t() --------------------------------------------------------------------------------