├── img ├── inlays.png └── settings.png ├── HexInlay ├── ida-plugin.json └── hexrays_inlay.py ├── README.md └── LICENSE /img/inlays.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milankovo/hexinlay/HEAD/img/inlays.png -------------------------------------------------------------------------------- /img/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/milankovo/hexinlay/HEAD/img/settings.png -------------------------------------------------------------------------------- /HexInlay/ida-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDAMetadataDescriptorVersion": 1, 3 | "plugin": { 4 | "name": "HexInlay", 5 | "entryPoint": "hexrays_inlay.py" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HexInlay 2 | Inlay hints for hex-rays decompiler 3 | 4 | ![inlays](img/inlays.png) 5 | 6 | # Settings 7 | You can enable / disable inlay hints in the `Edit` -> `Plugins` -> `HexInlay` menu. You can also choose whether to hide the redundant hints. 8 | 9 | ![settings](img/settings.png) 10 | 11 | ## Installation 12 | ### Ida 9+ 13 | Put the folder [HexInlay](HexInlay) into your ida plugins folder. 14 | ### Ida 8.4 15 | Put the file [hexrays_inlay.py](HexInlay/hexrays_inlay.py) into your ida plugins folder. 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 milankovo 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 | -------------------------------------------------------------------------------- /HexInlay/hexrays_inlay.py: -------------------------------------------------------------------------------- 1 | import re 2 | import idaapi 3 | import enum 4 | 5 | 6 | class plugin_state(enum.IntEnum): 7 | disabled = 0 8 | show_all = 1 9 | hide_some = 2 10 | hide_more = 3 11 | 12 | 13 | plugin_state_examples = { 14 | plugin_state.disabled: "memmove(this->dst, src, 10)", 15 | plugin_state.show_all: "memmove(dst: this->dst, src: src, len: 10)", 16 | plugin_state.hide_some: "memmove(dst: this->dst, src, len: 10)", 17 | plugin_state.hide_more: "memmove(this->dst, src, len: 10)", 18 | } 19 | 20 | class config_form_t(idaapi.Form): 21 | def __init__(self, state: plugin_state): 22 | F = idaapi.Form 23 | 24 | F.__init__( 25 | self, 26 | r"""STARTITEM 0 27 | BUTTON YES* OK 28 | BUTTON CANCEL Cancel 29 | HexInlay settings 30 | {FormChangeCb} 31 | <##inlay hints##~d~isabled:{rDisabled}> 32 | <#Show function argument names in the decompiled code as inlay hints#~s~how all:{rShowAll}> 33 | <#Hide the inlay hint if the argument name is equal to the function's argument name#~h~ide some:{rHideSome}> 34 | <#Hide the inlay hint if the argument name is contained in the function's argument name or vice versa#hide ~m~ore:{rHideAll}>{cGroup1}> 35 | example:{example} 36 | """, 37 | { 38 | "FormChangeCb": F.FormChangeCb(self.OnFormChange), 39 | "cGroup1": F.RadGroupControl( 40 | ["rDisabled", "rShowAll", "rHideSome", "rHideAll"], state.value 41 | ), 42 | "example": F.StringLabel(max(plugin_state_examples.values(), key=len)), 43 | }, 44 | ) 45 | self.state = state 46 | self.changed = False 47 | 48 | def read_state(self): 49 | return plugin_state(self.GetControlValue(self.cGroup1)) 50 | 51 | def OnFormChange(self, fid): 52 | match fid: 53 | case self.cGroup1.id: 54 | state = self.read_state() 55 | self.SetControlValue(self.example, plugin_state_examples[state]) 56 | 57 | case idaapi.CB_YES: 58 | state = self.read_state() 59 | self.changed = self.state != state 60 | self.state = state 61 | return 1 62 | 63 | @staticmethod 64 | def test(execute=True): 65 | cfg = config_t() 66 | cfg.load() 67 | f = config_form_t(cfg.state) 68 | f, args = f.Compile() 69 | print(f"{args=}") 70 | 71 | if execute: 72 | ok = f.Execute() 73 | else: 74 | print(args[0]) 75 | print(args[1:]) 76 | ok = 0 77 | if ok == 1: 78 | print(f"OK {f.state=} {f.changed=}") 79 | cfg.state = f.state 80 | cfg.save() 81 | f.Free() 82 | 83 | 84 | class config_t: 85 | def __init__(self): 86 | self.state = plugin_state.show_all 87 | 88 | def load(self): 89 | self.state = plugin_state( 90 | idaapi.reg_read_int("state", plugin_state.show_all.value, "HexInlay") 91 | ) 92 | 93 | def save(self): 94 | idaapi.reg_write_int("state", self.state.value, "HexInlay") 95 | 96 | def ask_user(self): 97 | form = config_form_t(self.state) 98 | form, args = form.Compile() 99 | ok = form.Execute() 100 | if ok == 1 and form.changed: 101 | self.state = form.state 102 | self.save() 103 | return True 104 | return False 105 | 106 | 107 | def modifytext(cfunc: idaapi.cfunc_t, index_to_name_map: dict): 108 | rg = re.compile("\1\\(([A-F0-9]{8,})") 109 | ps = cfunc.get_pseudocode() 110 | 111 | used = set() 112 | 113 | # current_line = "" 114 | 115 | def callback(m): 116 | res = m.group(0) 117 | num = int(m.group(1), 16) 118 | # print(f"Matched {repr(m.group(0))} {it} {m.group(1)}") 119 | 120 | name = index_to_name_map.get(num, None) 121 | if name is None: 122 | return res 123 | 124 | if num in used: 125 | # print(f"Already used {num} - {repr(res)} in {repr(current_line)}") 126 | return res 127 | used.add(num) 128 | 129 | # print(f"Replacing {num} with {name} in {repr(res)}") 130 | 131 | # SCOLOR_REGCMT, SCOLOR_AUTOCMT, SCOLOR_RPTCMT 132 | res += idaapi.COLSTR(name + ": ", idaapi.SCOLOR_AUTOCMT) 133 | 134 | return res 135 | 136 | for l in ps: 137 | # print(repr(l.line)) 138 | # current_line = l.line 139 | used = set() 140 | l.line = rg.sub(callback, l.line) 141 | 142 | 143 | def type_to_argnames(t: idaapi.tinfo_t) -> dict: 144 | t.remove_ptr_or_array() 145 | funcdata = idaapi.func_type_data_t() 146 | got_data = t.get_func_details(funcdata) 147 | if not got_data: 148 | # print(f"Failed to get function details for {t.dstr()}") 149 | return 150 | 151 | argnames = {} 152 | for arg_idx, arg in enumerate(funcdata): 153 | # print(f"arg {arg_idx} {arg.name} {arg.type.dstr()}") 154 | argnames[arg_idx] = arg.name 155 | 156 | return argnames 157 | 158 | 159 | class hexinlay_hooks_t(idaapi.Hexrays_Hooks): 160 | def __init__(self, config: config_t = None): 161 | self.config = config 162 | super().__init__() 163 | 164 | def is_the_same_argument(self, argument_name: str, arg: idaapi.carg_t) -> bool: 165 | match self.config.state: 166 | case plugin_state.disabled: 167 | return False 168 | case plugin_state.show_all: 169 | return False 170 | case plugin_state.hide_some: 171 | if arg.op not in [idaapi.cot_obj, idaapi.cot_var]: 172 | return False 173 | function_argument_name = arg.dstr() 174 | 175 | if argument_name == function_argument_name: 176 | return True 177 | 178 | if self.config.state == plugin_state.hide_some: 179 | return False 180 | 181 | assert self.config.state == plugin_state.hide_more 182 | # based on https://github.com/JetBrains/intellij-community/blob/6ddf70b998a05ce01b3d58f04553548ba5ff767f/java/java-impl/src/com/intellij/codeInsight/hints/JavaHintUtils.kt#L325 183 | 184 | if len(argument_name) < 3: 185 | return False 186 | 187 | if len(function_argument_name) < 3: 188 | return False 189 | 190 | if argument_name in function_argument_name: 191 | return True 192 | 193 | if function_argument_name in argument_name: 194 | return True 195 | return False 196 | 197 | def func_printed(self, cfunc: "idaapi.cfunc_t") -> "int": 198 | # print(f"Function {cfunc.entry_ea:x} printed") 199 | 200 | # should never happen as we are hooked only when the hints are enabled 201 | if self.config.state == plugin_state.disabled: 202 | return 0 203 | 204 | call_item: idaapi.citem_t 205 | 206 | obj_id_pos_map = {} 207 | obj_id_name_map = {} 208 | 209 | for i, call_item in enumerate(cfunc.treeitems): 210 | obj_id_pos_map[call_item.obj_id] = i 211 | if call_item.op == idaapi.cot_call: 212 | call_expr: idaapi.cexpr_t = call_item.cexpr 213 | # 1. collect argument names from the function type 214 | # print(f"Function {call_expr.type.dstr()}") 215 | t: idaapi.tinfo_t = call_expr.x.type 216 | argnames = type_to_argnames(t) 217 | if argnames is None: 218 | print( 219 | f"Failed to get function details for {t.dstr()} at {call_expr.ea:x}" 220 | ) 221 | continue 222 | elif len(argnames) == 0: 223 | continue 224 | # 2. collect argument objects from the call expression 225 | arglist: idaapi.carglist_t = call_expr.a 226 | arg: idaapi.carg_t 227 | 228 | for arg_idx, arg in enumerate(arglist): 229 | argname = argnames.get(arg_idx, None) 230 | if not argname: 231 | continue 232 | 233 | # if the argument name is the same as the function argument name, skip it 234 | if self.is_the_same_argument(argname, arg): 235 | # print( f"Skipping {arg_idx} {argname} {arg.dstr()} {idaapi.get_ctype_name(arg.op)=} " ) 236 | continue 237 | 238 | # skip to the leftmost object 239 | # otherwise we get strings like " x a1: + y" instead of "a1: x + y" 240 | while 1: 241 | if idaapi.is_binary(arg.op): 242 | arg = arg.x 243 | continue 244 | if arg.op in [ 245 | idaapi.cot_call, 246 | idaapi.cot_memptr, 247 | idaapi.cot_memref, 248 | ]: 249 | arg = arg.x 250 | continue 251 | break 252 | 253 | # print(f"arg {arg_idx} {arg.obj_id} {repr(arg.dstr())} should be named {argnames[arg_idx]}") 254 | obj_id_name_map[arg.obj_id] = argname 255 | 256 | index_to_name_map = {} 257 | for obj_id, name in obj_id_name_map.items(): 258 | index = obj_id_pos_map[obj_id] 259 | index_to_name_map[index] = name 260 | # print(f"Mapping {index} to {repr(name)}") 261 | 262 | modifytext(cfunc, index_to_name_map) 263 | return 0 264 | 265 | 266 | if __name__ == "__main__": 267 | idaapi.msg_clear() 268 | if "hex_cb_info" in globals(): 269 | print(f"Unhooking {hex_cb_info}") 270 | hex_cb_info.unhook() 271 | 272 | hex_cb_info = hexinlay_hooks_t() 273 | hex_cb_info.hook() 274 | config_form_t.test() 275 | 276 | print("Hooked") 277 | 278 | 279 | class HexInlayPlugin_t(idaapi.plugin_t): 280 | flags = 0 281 | comment = "Show function argument names in decompiled code as inlay hints" 282 | help = "" 283 | wanted_name = "HexInlay" 284 | wanted_hotkey = "" 285 | 286 | def init(self): 287 | self.hooked = False 288 | if not idaapi.init_hexrays_plugin(): 289 | return idaapi.PLUGIN_SKIP 290 | 291 | self.config = config_t() 292 | self.config.load() 293 | 294 | self.hook = hexinlay_hooks_t(self.config) 295 | self.enable(self.config.state) 296 | 297 | addon = idaapi.addon_info_t() 298 | addon.id = "milan.bohacek.hexinlay" 299 | addon.name = "HexInlay" 300 | addon.producer = "Milan Bohacek" 301 | addon.url = "https://github.com/milankovo/hexinlay" 302 | addon.version = "9.0" 303 | idaapi.register_addon(addon) 304 | 305 | return idaapi.PLUGIN_KEEP 306 | 307 | def enable(self, state: plugin_state): 308 | match state > plugin_state.disabled, self.hooked: 309 | case True, False: 310 | self.hook.hook() 311 | self.hooked = True 312 | return True 313 | case False, True: 314 | self.hook.unhook() 315 | self.hooked = False 316 | return True 317 | case _: 318 | return False 319 | 320 | def run(self, arg=0): 321 | if not self.config.ask_user(): 322 | return 323 | self.enable(self.config.state) 324 | self.refresh_pseudocode_widgets() 325 | 326 | def refresh_pseudocode_widgets(self): 327 | for name in "ABCDEFGHIJKLMNOPQRSTUVWXY": 328 | widget = idaapi.find_widget(f"Pseudocode-{name}") 329 | if not widget: 330 | continue 331 | vdui: idaapi.vdui_t = idaapi.get_widget_vdui(widget) 332 | if not vdui: 333 | continue 334 | vdui.refresh_ctext(False) 335 | 336 | def term(self): 337 | if self.hooked: 338 | self.hook.unhook() 339 | self.hooked = False 340 | 341 | 342 | def PLUGIN_ENTRY(): 343 | return HexInlayPlugin_t() 344 | --------------------------------------------------------------------------------