├── .gitignore ├── HexRaysPyTools.py ├── HexRaysPyTools ├── __init__.py ├── api.py ├── callbacks │ ├── __init__.py │ ├── actions.py │ ├── callbacks.py │ ├── form_requests.py │ ├── function_signature_modifiers.py │ ├── guess_allocation.py │ ├── member_double_click.py │ ├── negative_offsets.py │ ├── new_field_creation.py │ ├── recasts.py │ ├── renames.py │ ├── scanners.py │ ├── struct_xref_collector.py │ ├── struct_xref_representation.py │ ├── structs_by_size.py │ ├── swap_if.py │ └── virtual_table_creation.py ├── core │ ├── __init__.py │ ├── cache.py │ ├── classes.py │ ├── common.py │ ├── const.py │ ├── helper.py │ ├── struct_xrefs.py │ ├── structure_graph.py │ ├── templated_types.py │ ├── temporary_structure.py │ ├── type_library.py │ └── variable_scanner.py ├── forms.py ├── settings.py └── types │ └── templated_types.toml ├── Img ├── bad.JPG ├── builder.png ├── classes.JPG ├── fields_xref.JPG ├── good.JPG ├── structure_builder.JPG ├── tmpl_types_after.png ├── tmpl_types_before.png ├── tmpl_types_view.png └── virtual_functions.JPG ├── readme.md └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Apple crap 5 | .DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # User-specific files 10 | *.suo 11 | *.user 12 | *.sln.docstates 13 | *.coati* 14 | *.pyc 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | .idea 22 | x64/ 23 | x86/ 24 | build/ 25 | bld/ 26 | [Bb]in/ 27 | [Oo]bj/ 28 | 29 | # Roslyn cache directories 30 | *.ide/ 31 | 32 | # MSTest test Results 33 | [Tt]est[Rr]esult*/ 34 | [Bb]uild[Ll]og.* 35 | 36 | #NUNIT 37 | *.VisualState.xml 38 | TestResult.xml 39 | 40 | # Build Results of an ATL Project 41 | [Dd]ebugPS/ 42 | [Rr]eleasePS/ 43 | dlldata.c 44 | 45 | *_i.c 46 | *_p.c 47 | *_i.h 48 | *.ilk 49 | *.meta 50 | *.obj 51 | *.pch 52 | *.pdb 53 | *.pgc 54 | *.pgd 55 | *.rsp 56 | *.sbr 57 | *.tlb 58 | *.tli 59 | *.tlh 60 | *.tmp 61 | *.tmp_proj 62 | *.log 63 | *.vspscc 64 | *.vssscc 65 | .builds 66 | *.pidb 67 | *.svclog 68 | *.scc 69 | 70 | # Chutzpah Test files 71 | _Chutzpah* 72 | 73 | # Visual C++ cache files 74 | ipch/ 75 | *.aps 76 | *.ncb 77 | *.opensdf 78 | *.sdf 79 | *.cachefile 80 | 81 | # Visual Studio profiler 82 | *.psess 83 | *.vsp 84 | *.vspx 85 | 86 | # TFS 2012 Local Workspace 87 | $tf/ 88 | 89 | # Guidance Automation Toolkit 90 | *.gpState 91 | 92 | # ReSharper is a .NET coding add-in 93 | _ReSharper*/ 94 | *.[Rr]e[Ss]harper 95 | *.DotSettings.user 96 | 97 | # JustCode is a .NET coding addin-in 98 | .JustCode 99 | 100 | # TeamCity is a build add-in 101 | _TeamCity* 102 | 103 | # DotCover is a Code Coverage Tool 104 | *.dotCover 105 | 106 | # NCrunch 107 | _NCrunch_* 108 | .*crunch*.local.xml 109 | 110 | # MightyMoose 111 | *.mm.* 112 | AutoTest.Net/ 113 | 114 | # Web workbench (sass) 115 | .sass-cache/ 116 | 117 | # Installshield output folder 118 | [Ee]xpress/ 119 | 120 | # DocProject is a documentation generator add-in 121 | DocProject/buildhelp/ 122 | DocProject/Help/*.HxT 123 | DocProject/Help/*.HxC 124 | DocProject/Help/*.hhc 125 | DocProject/Help/*.hhk 126 | DocProject/Help/*.hhp 127 | DocProject/Help/Html2 128 | DocProject/Help/html 129 | 130 | # Click-Once directory 131 | publish/ 132 | 133 | # Publish Web Output 134 | *.[Pp]ublish.xml 135 | *.azurePubxml 136 | # TODO: Comment the next line if you want to checkin your web deploy settings 137 | # but database connection strings (with potential passwords) will be unencrypted 138 | *.pubxml 139 | *.publishproj 140 | 141 | # NuGet Packages 142 | *.nupkg 143 | # The packages folder can be ignored because of Package Restore 144 | **/packages/* 145 | # except build/, which is used as an MSBuild target. 146 | !**/packages/build/ 147 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 148 | #!**/packages/repositories.config 149 | 150 | # Windows Azure Build Output 151 | csx/ 152 | *.build.csdef 153 | 154 | # Windows Store app package directory 155 | AppPackages/ 156 | 157 | # Others 158 | sql/ 159 | *.Cache 160 | ClientBin/ 161 | [Ss]tyle[Cc]op.* 162 | ~$* 163 | *~ 164 | *.dbmdl 165 | *.dbproj.schemaview 166 | *.pfx 167 | *.publishsettings 168 | node_modules/ 169 | 170 | # RIA/Silverlight projects 171 | Generated_Code/ 172 | 173 | # Backup & report files from converting an old project file 174 | # to a newer Visual Studio version. Backup files are not needed, 175 | # because we have git ;-) 176 | _UpgradeReport_Files/ 177 | Backup*/ 178 | UpgradeLog*.XML 179 | UpgradeLog*.htm 180 | 181 | # SQL Server files 182 | *.mdf 183 | *.ldf 184 | 185 | # Business Intelligence projects 186 | *.rdl.data 187 | *.bim.layout 188 | *.bim_*.settings 189 | 190 | # Microsoft Fakes 191 | FakesAssemblies/ 192 | .vscode/settings.json 193 | -------------------------------------------------------------------------------- /HexRaysPyTools.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import idaapi 4 | 5 | import HexRaysPyTools.core.cache as cache 6 | import HexRaysPyTools.core.const as const 7 | import HexRaysPyTools.settings as settings 8 | from HexRaysPyTools.callbacks import hx_callback_manager, action_manager 9 | from HexRaysPyTools.core.struct_xrefs import XrefStorage 10 | from HexRaysPyTools.core.temporary_structure import TemporaryStructureModel 11 | 12 | 13 | class MyPlugin(idaapi.plugin_t): 14 | flags = 0 15 | comment = "Plugin for automatic classes reconstruction" 16 | help = "See https://github.com/igogo-x86/HexRaysPyTools/blob/master/readme.md" 17 | wanted_name = "HexRaysPyTools" 18 | wanted_hotkey = "" 19 | 20 | @staticmethod 21 | def init(): 22 | if not idaapi.init_hexrays_plugin(): 23 | logging.error("Failed to initialize Hex-Rays SDK") 24 | return idaapi.PLUGIN_SKIP 25 | 26 | action_manager.initialize() 27 | hx_callback_manager.initialize() 28 | cache.temporary_structure = TemporaryStructureModel() 29 | const.init() 30 | XrefStorage().open() 31 | return idaapi.PLUGIN_KEEP 32 | 33 | @staticmethod 34 | def run(*args): 35 | pass 36 | 37 | @staticmethod 38 | def term(): 39 | action_manager.finalize() 40 | hx_callback_manager.finalize() 41 | XrefStorage().close() 42 | idaapi.term_hexrays_plugin() 43 | 44 | 45 | def PLUGIN_ENTRY(): 46 | settings.load_settings() 47 | logging.basicConfig(format='[%(levelname)s] %(message)s\t(%(module)s:%(funcName)s)') 48 | logging.root.setLevel(settings.DEBUG_MESSAGE_LEVEL) 49 | idaapi.notify_when(idaapi.NW_OPENIDB, cache.initialize_cache) 50 | return MyPlugin() 51 | -------------------------------------------------------------------------------- /HexRaysPyTools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/HexRaysPyTools/__init__.py -------------------------------------------------------------------------------- /HexRaysPyTools/api.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import idaapi 3 | import idc 4 | from .core.helper import to_hex 5 | from .core import helper 6 | 7 | logger = logging.getLogger(__name__) 8 | 9 | 10 | SETTING_START_FROM_CURRENT_EXPR = True 11 | 12 | 13 | class ScanObject(object): 14 | def __init__(self): 15 | self.ea = idaapi.BADADDR 16 | self.name = None 17 | self.tinfo = None 18 | self.id = 0 19 | 20 | @staticmethod 21 | def create(cfunc, arg): 22 | # Creates object suitable for scaning either from cexpr_t or ctree_item_t 23 | if isinstance(arg, idaapi.ctree_item_t): 24 | lvar = arg.get_lvar() 25 | if lvar: 26 | index = list(cfunc.get_lvars()).index(lvar) 27 | result = VariableObject(lvar, index) 28 | if arg.e: 29 | result.ea = ScanObject.get_expression_address(cfunc, arg.e) 30 | return result 31 | if arg.citype != idaapi.VDI_EXPR: 32 | return None 33 | cexpr = arg.e 34 | else: 35 | cexpr = arg 36 | 37 | if cexpr.op == idaapi.cot_var: 38 | lvar = cfunc.get_lvars()[cexpr.v.idx] 39 | result = VariableObject(lvar, cexpr.v.idx) 40 | result.ea = ScanObject.get_expression_address(cfunc, cexpr) 41 | return result 42 | elif cexpr.op == idaapi.cot_memptr: 43 | t = cexpr.x.type.get_pointed_object() 44 | result = StructPtrObject(t.dstr(), cexpr.m) 45 | result.name = helper.get_member_name(t, cexpr.m) 46 | elif cexpr.op == idaapi.cot_memref: 47 | t = cexpr.x.type 48 | result = StructRefObject(t.dstr(), cexpr.m) 49 | result.name = helper.get_member_name(t, cexpr.m) 50 | elif cexpr.op == idaapi.cot_obj: 51 | result = GlobalVariableObject(cexpr.obj_ea) 52 | result.name = idaapi.get_short_name(cexpr.obj_ea) 53 | else: 54 | return 55 | result.tinfo = cexpr.type 56 | result.ea = ScanObject.get_expression_address(cfunc, cexpr) 57 | return result 58 | 59 | @staticmethod 60 | def get_expression_address(cfunc, cexpr): 61 | expr = cexpr 62 | 63 | while expr and expr.ea == idaapi.BADADDR: 64 | expr = expr.to_specific_type 65 | expr = cfunc.body.find_parent_of(expr) 66 | 67 | assert expr is not None 68 | return expr.ea 69 | 70 | def __hash__(self): 71 | return hash((self.id, self.name)) 72 | 73 | def __eq__(self, other): 74 | return self.id == other.id and self.name == other.name 75 | 76 | def __repr__(self): 77 | return self.name 78 | 79 | 80 | SO_LOCAL_VARIABLE = 1 # cexpr.op == idaapi.cot_var 81 | SO_STRUCT_POINTER = 2 # cexpr.op == idaapi.cot_memptr 82 | SO_STRUCT_REFERENCE = 3 # cexpr.op == idaapi.cot_memref 83 | SO_GLOBAL_OBJECT = 4 # cexpr.op == idaapi.cot_obj 84 | SO_CALL_ARGUMENT = 5 # cexpr.op == idaapi.cot_call 85 | SO_MEMORY_ALLOCATOR = 6 86 | SO_RETURNED_OBJECT = 7 87 | 88 | 89 | class VariableObject(ScanObject): 90 | # Represents `var` expression 91 | def __init__(self, lvar, index): 92 | super(VariableObject, self).__init__() 93 | self.lvar = lvar 94 | self.tinfo = lvar.type() 95 | self.name = lvar.name 96 | self.index = index 97 | self.id = SO_LOCAL_VARIABLE 98 | 99 | def is_target(self, cexpr): 100 | return cexpr.op == idaapi.cot_var and cexpr.v.idx == self.index 101 | 102 | 103 | class StructPtrObject(ScanObject): 104 | # Represents `x->m` expression 105 | def __init__(self, struct_name, offset): 106 | super(StructPtrObject, self).__init__() 107 | self.struct_name = struct_name 108 | self.offset = offset 109 | self.id = SO_STRUCT_POINTER 110 | 111 | def is_target(self, cexpr): 112 | return cexpr.op == idaapi.cot_memptr and cexpr.m == self.offset and \ 113 | cexpr.x.type.get_pointed_object().dstr() == self.struct_name 114 | 115 | 116 | class StructRefObject(ScanObject): 117 | # Represents `x.m` expression 118 | def __init__(self, struct_name, offset): 119 | super(StructRefObject, self).__init__() 120 | self.struct_name = struct_name 121 | self.offset = offset 122 | self.id = SO_STRUCT_REFERENCE 123 | 124 | def is_target(self, cexpr): 125 | return cexpr.op == idaapi.cot_memref and cexpr.m == self.offset and cexpr.x.type.dstr() == self.struct_name 126 | 127 | 128 | class GlobalVariableObject(ScanObject): 129 | # Represents global object 130 | def __init__(self, object_address): 131 | super(GlobalVariableObject, self).__init__() 132 | self.obj_ea = object_address 133 | self.id = SO_GLOBAL_OBJECT 134 | 135 | def is_target(self, cexpr): 136 | return cexpr.op == idaapi.cot_obj and self.obj_ea == cexpr.obj_ea 137 | 138 | 139 | class CallArgObject(ScanObject): 140 | # Represents call of a function and argument index 141 | def __init__(self, func_address, arg_idx): 142 | super(CallArgObject, self).__init__() 143 | self.func_ea = func_address 144 | self.arg_idx = arg_idx 145 | self.id = SO_CALL_ARGUMENT 146 | 147 | def is_target(self, cexpr): 148 | return cexpr.op == idaapi.cot_call and cexpr.x.obj_ea == self.func_ea 149 | 150 | def create_scan_obj(self, cfunc, cexpr): 151 | e = cexpr.a[self.arg_idx] 152 | while e.op in (idaapi.cot_cast, idaapi.cot_ref, idaapi.cot_add, idaapi.cot_sub, idaapi.cot_idx): 153 | e = e.x 154 | return ScanObject.create(cfunc, e) 155 | 156 | @staticmethod 157 | def create(cfunc, arg_idx): 158 | result = CallArgObject(cfunc.entry_ea, arg_idx) 159 | result.name = cfunc.get_lvars()[arg_idx].name 160 | result.tinfo = cfunc.type 161 | return result 162 | 163 | def __repr__(self): 164 | return "{}" 165 | 166 | 167 | class ReturnedObject(ScanObject): 168 | # Represents value returned by function 169 | def __init__(self, func_address): 170 | super(ReturnedObject, self).__init__() 171 | self.__func_ea = func_address 172 | self.id = SO_RETURNED_OBJECT 173 | 174 | def is_target(self, cexpr): 175 | return cexpr.op == idaapi.cot_call and cexpr.x.obj_ea == self.__func_ea 176 | 177 | 178 | class MemoryAllocationObject(ScanObject): 179 | # Represents `operator new()' or `malloc' 180 | def __init__(self, name, size): 181 | super(MemoryAllocationObject, self).__init__() 182 | self.name = name 183 | self.size = size 184 | self.id = SO_MEMORY_ALLOCATOR 185 | 186 | @staticmethod 187 | def create(cfunc, cexpr): 188 | if cexpr.op == idaapi.cot_call: 189 | e = cexpr 190 | elif cexpr.op == idaapi.cot_cast and cexpr.x.op == idaapi.cot_call: 191 | e = cexpr.x 192 | else: 193 | return 194 | 195 | func_name = idaapi.get_short_name(e.x.obj_ea) 196 | if "malloc" in func_name or "operator new" in func_name: 197 | carg = e.a[0] 198 | if carg.op == idaapi.cot_num: 199 | size = carg.numval() 200 | else: 201 | size = 0 202 | result = MemoryAllocationObject(func_name, size) 203 | result.ea = ScanObject.get_expression_address(cfunc, e) 204 | return result 205 | 206 | 207 | ASSIGNMENT_RIGHT = 1 208 | ASSIGNMENT_LEFT = 2 209 | 210 | 211 | class ObjectVisitor(idaapi.ctree_parentee_t): 212 | def __init__(self, cfunc, obj, data, skip_until_object): 213 | super(ObjectVisitor, self).__init__() 214 | self._cfunc = cfunc 215 | self._objects = [obj] 216 | self._init_obj = obj 217 | self._data = data 218 | self._start_ea = obj.ea 219 | self._skip = skip_until_object if self._start_ea != idaapi.BADADDR else False 220 | self.crippled = False 221 | 222 | def process(self): 223 | self.apply_to(self._cfunc.body, None) 224 | 225 | def set_callbacks(self, manipulate=None): 226 | if manipulate: 227 | self.__manipulate = manipulate.__get__(self, ObjectDownwardsVisitor) 228 | 229 | def _get_line(self): 230 | for p in reversed(self.parents): 231 | if not p.is_expr(): 232 | return idaapi.tag_remove(p.print1(self._cfunc.__ref__())) 233 | AssertionError("Parent instruction is not found") 234 | 235 | def _manipulate(self, cexpr, obj): 236 | """ 237 | Method called for every object having assignment relationship with starter object. This method should be 238 | reimplemented in order to do something useful 239 | 240 | :param cexpr: idaapi_cexpr_t 241 | :param id: one of the SO_* constants 242 | :return: None 243 | """ 244 | self.__manipulate(cexpr, obj) 245 | 246 | def __manipulate(self, cexpr, obj): 247 | logger.debug("Expression {} at {} Id - {}".format( 248 | cexpr.opname, 249 | to_hex(helper.find_asm_address(cexpr, self.parents)), 250 | obj.id)) 251 | 252 | 253 | class ObjectDownwardsVisitor(ObjectVisitor): 254 | def __init__(self, cfunc, obj, data=None, skip_until_object=False): 255 | super(ObjectDownwardsVisitor, self).__init__(cfunc, obj, data, skip_until_object) 256 | self.cv_flags |= idaapi.CV_POST 257 | 258 | def visit_expr(self, cexpr): 259 | if self._skip: 260 | if self._is_initial_object(cexpr): 261 | self._skip = False 262 | else: 263 | return 0 264 | 265 | if cexpr.op != idaapi.cot_asg: 266 | return 0 267 | 268 | x_cexpr = cexpr.x 269 | if cexpr.y.op == idaapi.cot_cast: 270 | y_cexpr = cexpr.y.x 271 | else: 272 | y_cexpr = cexpr.y 273 | 274 | for obj in self._objects: 275 | if obj.is_target(x_cexpr): 276 | if self.__is_object_overwritten(x_cexpr, obj, y_cexpr): 277 | logger.info("Removed object {} from scanning at {}".format( 278 | obj, to_hex(helper.find_asm_address(x_cexpr, self.parents)))) 279 | self._objects.remove(obj) 280 | return 0 281 | elif obj.is_target(y_cexpr): 282 | new_obj = ScanObject.create(self._cfunc, x_cexpr) 283 | if new_obj: 284 | self._objects.append(new_obj) 285 | return 0 286 | return 0 287 | 288 | def leave_expr(self, cexpr): 289 | if self._skip: 290 | return 0 291 | 292 | for obj in self._objects: 293 | if obj.is_target(cexpr) and obj.id != SO_RETURNED_OBJECT: 294 | self._manipulate(cexpr, obj) 295 | return 0 296 | return 0 297 | 298 | def _is_initial_object(self, cexpr): 299 | if cexpr.op == idaapi.cot_asg: 300 | cexpr = cexpr.y 301 | if cexpr.op == idaapi.cot_cast: 302 | cexpr = cexpr.x 303 | return self._init_obj.is_target(cexpr) and helper.find_asm_address(cexpr, self.parents) == self._start_ea 304 | 305 | def __is_object_overwritten(self, x_cexpr, obj, y_cexpr): 306 | if len(self._objects) < 2: 307 | return False 308 | 309 | if y_cexpr.op == idaapi.cot_cast: 310 | e = y_cexpr.x 311 | else: 312 | e = y_cexpr 313 | 314 | if e.op != idaapi.cot_call or len(e.a) == 0: 315 | return True 316 | 317 | for obj in self._objects: 318 | if obj.is_target(e. a[0]): 319 | return False 320 | return True 321 | 322 | 323 | class ObjectUpwardsVisitor(ObjectVisitor): 324 | STAGE_PREPARE = 1 325 | STAGE_PARSING = 2 326 | 327 | def __init__(self, cfunc, obj, data=None, skip_after_object=False): 328 | super(ObjectUpwardsVisitor, self).__init__(cfunc, obj, data, skip_after_object) 329 | self._stage = self.STAGE_PREPARE 330 | self._tree = {} 331 | self._call_obj = obj if obj.id == SO_CALL_ARGUMENT else None 332 | 333 | def visit_expr(self, cexpr): 334 | if self._stage == self.STAGE_PARSING: 335 | return 0 336 | 337 | if self._call_obj and self._call_obj.is_target(cexpr): 338 | obj = self._call_obj.create_scan_obj(self._cfunc, cexpr) 339 | if obj: 340 | self._objects.append(obj) 341 | return 0 342 | 343 | if cexpr.op != idaapi.cot_asg: 344 | return 0 345 | 346 | x_cexpr = cexpr.x 347 | if cexpr.y.op == idaapi.cot_cast: 348 | y_cexpr = cexpr.y.x 349 | else: 350 | y_cexpr = cexpr.y 351 | 352 | obj_left = ScanObject.create(self._cfunc, x_cexpr) 353 | obj_right = ScanObject.create(self._cfunc, y_cexpr) 354 | if obj_left and obj_right: 355 | self.__add_object_assignment(obj_left, obj_right) 356 | 357 | if self._skip and self._is_initial_object(cexpr): 358 | return 1 359 | return 0 360 | 361 | def leave_expr(self, cexpr): 362 | if self._stage == self.STAGE_PREPARE: 363 | return 0 364 | 365 | if self._skip and self._is_initial_object(cexpr): 366 | self._manipulate(cexpr, self._init_obj) 367 | return 1 368 | 369 | for obj in self._objects: 370 | if obj.is_target(cexpr): 371 | self._manipulate(cexpr, obj) 372 | return 0 373 | return 0 374 | 375 | def process(self): 376 | self._stage = self.STAGE_PREPARE 377 | self.cv_flags &= ~idaapi.CV_POST 378 | super(ObjectUpwardsVisitor, self).process() 379 | self._stage = self.STAGE_PARSING 380 | self.cv_flags |= idaapi.CV_POST 381 | self.__prepare() 382 | super(ObjectUpwardsVisitor, self).process() 383 | 384 | def _is_initial_object(self, cexpr): 385 | return self._init_obj.is_target(cexpr) and helper.find_asm_address(cexpr, self.parents) == self._start_ea 386 | 387 | def __add_object_assignment(self, from_obj, to_obj): 388 | if from_obj in self._tree: 389 | self._tree[from_obj].add(to_obj) 390 | else: 391 | self._tree[from_obj] = {to_obj} 392 | 393 | def __prepare(self): 394 | result = set() 395 | todo = set(self._objects) 396 | while todo: 397 | obj = todo.pop() 398 | result.add(obj) 399 | if obj.id == SO_CALL_ARGUMENT or obj not in self._tree: 400 | continue 401 | o = self._tree[obj] 402 | todo |= o - result 403 | result |= o 404 | self._objects = list(result) 405 | self._tree.clear() 406 | 407 | 408 | class RecursiveObjectVisitor(ObjectVisitor): 409 | def __init__(self, cfunc, obj, data=None, skip_until_object=False, visited=None): 410 | super(RecursiveObjectVisitor, self).__init__(cfunc, obj, data, skip_until_object) 411 | self._visited = visited if visited else set() 412 | self._new_for_visit = set() 413 | self.crippled = False 414 | self._arg_idx = -1 415 | self._debug_scan_tree = {} 416 | self.__debug_scan_tree_root = idc.get_name(self._cfunc.entry_ea) 417 | self.__debug_message = [] 418 | 419 | def visit_expr(self, cexpr): 420 | return super(RecursiveObjectVisitor, self).visit_expr(cexpr) 421 | 422 | def set_callbacks(self, manipulate=None, start=None, start_iteration=None, finish=None, finish_iteration=None): 423 | super(RecursiveObjectVisitor, self).set_callbacks(manipulate) 424 | if start: 425 | self._start = start.__get__(self, RecursiveObjectDownwardsVisitor) 426 | if start_iteration: 427 | self._start_iteration = start_iteration.__get__(self, RecursiveObjectDownwardsVisitor) 428 | if finish: 429 | self._finish = finish.__get__(self, RecursiveObjectDownwardsVisitor) 430 | if finish_iteration: 431 | self._finish_iteration = finish_iteration.__get__(self, RecursiveObjectDownwardsVisitor) 432 | 433 | def prepare_new_scan(self, cfunc, arg_idx, obj, skip=False): 434 | self._cfunc = cfunc 435 | self._arg_idx = arg_idx 436 | self._objects = [obj] 437 | self._init_obj = obj 438 | self._skip = False 439 | self.crippled = self.__is_func_crippled() 440 | 441 | def process(self): 442 | self._start() 443 | self._recursive_process() 444 | self._finish() 445 | self.dump_scan_tree() 446 | 447 | def dump_scan_tree(self): 448 | self.__prepare_debug_message() 449 | logger.info("{}\n---------------".format("\n".join(self.__debug_message))) 450 | 451 | def __prepare_debug_message(self, key=None, level=1): 452 | if key is None: 453 | key = (self.__debug_scan_tree_root, -1) 454 | self.__debug_message.append("--- Scan Tree---\n{}".format(self.__debug_scan_tree_root)) 455 | if key in self._debug_scan_tree: 456 | for func_name, arg_idx in self._debug_scan_tree[key]: 457 | prefix = " | " * (level - 1) + " |_ " 458 | self.__debug_message.append("{}{} (idx: {})".format(prefix, func_name, arg_idx)) 459 | self.__prepare_debug_message((func_name, arg_idx), level + 1) 460 | 461 | def _recursive_process(self): 462 | self._start_iteration() 463 | super(RecursiveObjectVisitor, self).process() 464 | self._finish_iteration() 465 | 466 | def _manipulate(self, cexpr, obj): 467 | self._check_call(cexpr) 468 | super(RecursiveObjectVisitor, self)._manipulate(cexpr, obj) 469 | 470 | def _check_call(self, cexpr): 471 | raise NotImplemented 472 | 473 | def _add_visit(self, func_ea, arg_idx): 474 | if (func_ea, arg_idx) not in self._visited: 475 | self._visited.add((func_ea, arg_idx)) 476 | self._new_for_visit.add((func_ea, arg_idx)) 477 | return True 478 | return False 479 | 480 | def _add_scan_tree_info(self, func_ea, arg_idx): 481 | head_node = (idc.get_name(self._cfunc.entry_ea), self._arg_idx) 482 | tail_node = (idc.get_name(func_ea), arg_idx) 483 | if head_node in self._debug_scan_tree: 484 | self._debug_scan_tree[head_node].add(tail_node) 485 | else: 486 | self._debug_scan_tree[head_node] = {tail_node} 487 | 488 | def _start(self): 489 | """ Called at the beginning of visiting """ 490 | pass 491 | 492 | def _start_iteration(self): 493 | """ Called every time new function visiting started """ 494 | pass 495 | 496 | def _finish(self): 497 | """ Called after all visiting happened """ 498 | pass 499 | 500 | def _finish_iteration(self): 501 | """ Called every time new function visiting finished """ 502 | pass 503 | 504 | def __is_func_crippled(self): 505 | # Check if function is just call to another function 506 | b = self._cfunc.body.cblock 507 | if b.size() == 1: 508 | e = b.at(0) 509 | return e.op == idaapi.cit_return or (e.op == idaapi.cit_expr and e.cexpr.op == idaapi.cot_call) 510 | return False 511 | 512 | 513 | class RecursiveObjectDownwardsVisitor(RecursiveObjectVisitor, ObjectDownwardsVisitor): 514 | def __init__(self, cfunc, obj, data=None, skip_until_object=False, visited=None): 515 | super(RecursiveObjectDownwardsVisitor, self).__init__(cfunc, obj, data, skip_until_object, visited) 516 | 517 | def _check_call(self, cexpr): 518 | parent = self.parent_expr() 519 | grandparent = self.parents.at(self.parents.size() - 2) 520 | if parent.op == idaapi.cot_call: 521 | call_cexpr = parent 522 | arg_cexpr = cexpr 523 | elif parent.op == idaapi.cot_cast and grandparent.op == idaapi.cot_call: 524 | call_cexpr = grandparent.cexpr 525 | arg_cexpr = parent 526 | else: 527 | return 528 | idx, _ = helper.get_func_argument_info(call_cexpr, arg_cexpr) 529 | func_ea = call_cexpr.x.obj_ea 530 | if func_ea == idaapi.BADADDR: 531 | return 532 | if self._add_visit(func_ea, idx): 533 | self._add_scan_tree_info(func_ea, idx) 534 | 535 | def _recursive_process(self): 536 | super(RecursiveObjectDownwardsVisitor, self)._recursive_process() 537 | 538 | while self._new_for_visit: 539 | func_ea, arg_idx = self._new_for_visit.pop() 540 | if helper.is_imported_ea(func_ea): 541 | continue 542 | cfunc = helper.decompile_function(func_ea) 543 | if cfunc: 544 | assert arg_idx < len(cfunc.get_lvars()), "Wrong argument at func {}".format(to_hex(func_ea)) 545 | obj = VariableObject(cfunc.get_lvars()[arg_idx], arg_idx) 546 | self.prepare_new_scan(cfunc, arg_idx, obj) 547 | self._recursive_process() 548 | 549 | 550 | class RecursiveObjectUpwardsVisitor(RecursiveObjectVisitor, ObjectUpwardsVisitor): 551 | def __init__(self, cfunc, obj, data=None, skip_after_object=False, visited=None): 552 | super(RecursiveObjectUpwardsVisitor, self).__init__(cfunc, obj, data, skip_after_object, visited) 553 | 554 | def prepare_new_scan(self, cfunc, arg_idx, obj, skip=False): 555 | super(RecursiveObjectUpwardsVisitor, self).prepare_new_scan(cfunc, arg_idx, obj, skip) 556 | self._call_obj = obj if obj.id == SO_CALL_ARGUMENT else None 557 | 558 | def _check_call(self, cexpr): 559 | if cexpr.op == idaapi.cot_var and self._cfunc.get_lvars()[cexpr.v.idx].is_arg_var: 560 | func_ea = self._cfunc.entry_ea 561 | arg_idx = cexpr.v.idx 562 | if self._add_visit(func_ea, arg_idx): 563 | for callee_ea in helper.get_funcs_calling_address(func_ea): 564 | self._add_scan_tree_info(callee_ea, arg_idx) 565 | 566 | def _recursive_process(self): 567 | super(RecursiveObjectUpwardsVisitor, self)._recursive_process() 568 | 569 | while self._new_for_visit: 570 | new_visit = list(self._new_for_visit) 571 | self._new_for_visit.clear() 572 | for func_ea, arg_idx in new_visit: 573 | funcs = helper.get_funcs_calling_address(func_ea) 574 | obj = CallArgObject.create(idaapi.decompile(func_ea), arg_idx) 575 | for callee_ea in funcs: 576 | cfunc = helper.decompile_function(callee_ea) 577 | if cfunc: 578 | self.prepare_new_scan(cfunc, arg_idx, obj, False) 579 | super(RecursiveObjectUpwardsVisitor, self)._recursive_process() 580 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/__init__.py: -------------------------------------------------------------------------------- 1 | from .actions import * 2 | from .callbacks import * 3 | from . import form_requests 4 | from . import function_signature_modifiers 5 | from . import guess_allocation 6 | from . import member_double_click 7 | from . import negative_offsets 8 | from . import new_field_creation 9 | from . import recasts 10 | from . import renames 11 | from . import scanners 12 | from . import structs_by_size 13 | from . import struct_xref_collector 14 | from . import struct_xref_representation 15 | from . import swap_if 16 | from . import virtual_table_creation 17 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/actions.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | from .callbacks import hx_callback_manager, HexRaysEventHandler 4 | 5 | 6 | class ActionManager(object): 7 | def __init__(self): 8 | self.__actions = [] 9 | 10 | def register(self, action): 11 | self.__actions.append(action) 12 | idaapi.register_action( 13 | idaapi.action_desc_t(action.name, action.description, action, action.hotkey) 14 | ) 15 | if isinstance(action, HexRaysPopupAction): 16 | hx_callback_manager.register(idaapi.hxe_populating_popup, HexRaysPopupRequestHandler(action)) 17 | 18 | def initialize(self): 19 | pass 20 | 21 | def finalize(self): 22 | for action in self.__actions: 23 | idaapi.unregister_action(action.name) 24 | 25 | 26 | action_manager = ActionManager() 27 | 28 | 29 | class Action(idaapi.action_handler_t): 30 | """ 31 | Convenience wrapper with name property allowing to be registered in IDA using ActionManager 32 | """ 33 | description = None 34 | hotkey = None 35 | 36 | def __init__(self): 37 | super(Action, self).__init__() 38 | 39 | @property 40 | def name(self): 41 | return "HexRaysPyTools:" + type(self).__name__ 42 | 43 | def activate(self, ctx): 44 | # type: (idaapi.action_activation_ctx_t) -> None 45 | raise NotImplementedError 46 | 47 | def update(self, ctx): 48 | # type: (idaapi.action_activation_ctx_t) -> None 49 | raise NotImplementedError 50 | 51 | 52 | class HexRaysPopupAction(Action): 53 | """ 54 | Wrapper around Action. Represents Action which can be added to menu after right-clicking in Decompile window. 55 | Has `check` method that should tell whether Action should be added to popup menu when different items 56 | are right-clicked. 57 | Children of this class can also be fired by hot-key without right-clicking if one provided in `hotkey` 58 | static member. 59 | """ 60 | 61 | def __init__(self): 62 | super(HexRaysPopupAction, self).__init__() 63 | 64 | def activate(self, ctx): 65 | # type: (idaapi.action_activation_ctx_t) -> None 66 | raise NotImplementedError 67 | 68 | def check(self, hx_view): 69 | # type: (idaapi.vdui_t) -> bool 70 | raise NotImplementedError 71 | 72 | def update(self, ctx): 73 | if ctx.widget_type == idaapi.BWN_PSEUDOCODE: 74 | return idaapi.AST_ENABLE_FOR_WIDGET 75 | return idaapi.AST_DISABLE_FOR_WIDGET 76 | 77 | class HexRaysXrefAction(Action): 78 | """ 79 | Wrapper around Action. Represents Action which can be added to menu after right-clicking in Decompile window. 80 | Has `check` method that should tell whether Action should be added to popup menu when different items 81 | are right-clicked. 82 | Children of this class can also be fired by hot-key without right-clicking if one provided in `hotkey` 83 | static member. 84 | """ 85 | 86 | def __init__(self): 87 | super(HexRaysXrefAction, self).__init__() 88 | 89 | def activate(self, ctx): 90 | # type: (idaapi.action_activation_ctx_t) -> None 91 | raise NotImplementedError 92 | 93 | def check(self, hx_view): 94 | # type: (idaapi.vdui_t) -> bool 95 | raise NotImplementedError 96 | 97 | def update(self, ctx): 98 | if ctx.widget_type == idaapi.BWN_PSEUDOCODE or ctx.widget_type == idaapi.BWN_TILIST: 99 | return idaapi.AST_ENABLE_FOR_WIDGET 100 | return idaapi.AST_DISABLE_FOR_WIDGET 101 | 102 | 103 | class HexRaysPopupRequestHandler(HexRaysEventHandler): 104 | """ 105 | This is wrapper around HexRaysPopupAction which allows to dynamically decide whether to add Action to popup 106 | menu or not. 107 | Register this in CallbackManager. 108 | """ 109 | def __init__(self, action): 110 | super(HexRaysPopupRequestHandler, self).__init__() 111 | self.__action = action 112 | 113 | def handle(self, event, *args): 114 | form, popup, hx_view = args 115 | if self.__action.check(hx_view): 116 | idaapi.attach_action_to_popup(form, popup, self.__action.name, None) 117 | return 0 118 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/callbacks.py: -------------------------------------------------------------------------------- 1 | from collections import defaultdict 2 | import idaapi 3 | 4 | 5 | class HexRaysCallbackManager(object): 6 | def __init__(self): 7 | self.__hexrays_event_handlers = defaultdict(list) 8 | 9 | def initialize(self): 10 | idaapi.install_hexrays_callback(self.__handle) 11 | 12 | def finalize(self): 13 | idaapi.remove_hexrays_callback(self.__handle) 14 | 15 | def register(self, event, handler): 16 | self.__hexrays_event_handlers[event].append(handler) 17 | 18 | def __handle(self, event, *args): 19 | for handler in self.__hexrays_event_handlers[event]: 20 | handler.handle(event, *args) 21 | # IDA expects zero 22 | return 0 23 | 24 | 25 | hx_callback_manager = HexRaysCallbackManager() 26 | 27 | 28 | class HexRaysEventHandler(object): 29 | def __init__(self): 30 | super(HexRaysEventHandler, self).__init__() 31 | 32 | def handle(self, event, *args): 33 | raise NotImplementedError("This is an abstract class") 34 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/form_requests.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | from . import actions 4 | import HexRaysPyTools.core.cache as cache 5 | import HexRaysPyTools.core.classes as classes 6 | from HexRaysPyTools.core.structure_graph import StructureGraph 7 | from HexRaysPyTools.forms import StructureGraphViewer, ClassViewer, StructureBuilder 8 | 9 | 10 | class ShowGraph(actions.Action): 11 | description = "Show graph" 12 | hotkey = "G" 13 | 14 | def __init__(self): 15 | super(ShowGraph, self).__init__() 16 | self.graph = None 17 | self.graph_view = None 18 | 19 | def activate(self, ctx): 20 | widget = self.graph_view.GetWidget() if self.graph_view else None 21 | if widget: 22 | self.graph_view.change_selected([sel + 1 for sel in ctx.chooser_selection]) 23 | self.graph_view.Show() 24 | else: 25 | self.graph = StructureGraph([sel + 1 for sel in ctx.chooser_selection]) 26 | self.graph_view = StructureGraphViewer("Structure Graph", self.graph) 27 | self.graph_view.Show() 28 | 29 | def update(self, ctx): 30 | if ctx.widget_type == idaapi.BWN_LOCTYPS: 31 | idaapi.attach_action_to_popup(ctx.widget, None, self.name) 32 | return idaapi.AST_ENABLE_FOR_WIDGET 33 | return idaapi.AST_DISABLE_FOR_WIDGET 34 | 35 | 36 | actions.action_manager.register(ShowGraph()) 37 | 38 | 39 | class ShowClasses(actions.Action): 40 | description = "Classes" 41 | hotkey = "Alt+F1" 42 | 43 | def __init__(self): 44 | super(ShowClasses, self).__init__() 45 | 46 | def activate(self, ctx): 47 | tform = idaapi.find_widget('Classes') 48 | if not tform: 49 | class_viewer = ClassViewer(classes.ProxyModel(), classes.TreeModel()) 50 | class_viewer.Show() 51 | else: 52 | idaapi.activate_widget(tform, True) 53 | 54 | def update(self, ctx): 55 | return idaapi.AST_ENABLE_ALWAYS 56 | 57 | 58 | show_classes = ShowClasses() 59 | actions.action_manager.register(show_classes) 60 | idaapi.attach_action_to_menu('View/Open subviews/Local types', show_classes.name, idaapi.SETMENU_APP) 61 | 62 | 63 | class ShowStructureBuilder(actions.HexRaysPopupAction): 64 | description = "Show Structure Builder" 65 | hotkey = "Alt+F8" 66 | 67 | def __init__(self): 68 | super(ShowStructureBuilder, self).__init__() 69 | 70 | def check(self, hx_view): 71 | return True 72 | 73 | def activate(self, ctx): 74 | tform = idaapi.find_widget("Structure Builder") 75 | if tform: 76 | idaapi.activate_widget(tform, True) 77 | else: 78 | StructureBuilder(cache.temporary_structure).Show() 79 | 80 | def update(self, ctx): 81 | return idaapi.AST_ENABLE_ALWAYS 82 | 83 | 84 | actions.action_manager.register(ShowStructureBuilder()) 85 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/function_signature_modifiers.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | from . import actions 3 | import HexRaysPyTools.core.const as const 4 | 5 | 6 | class ConvertToUsercall(actions.HexRaysPopupAction): 7 | description = "Convert to __usercall" 8 | 9 | def __init__(self): 10 | super(ConvertToUsercall, self).__init__() 11 | 12 | def check(self, hx_view): 13 | return hx_view.item.citype == idaapi.VDI_FUNC 14 | 15 | def activate(self, ctx): 16 | vu = idaapi.get_widget_vdui(ctx.widget) 17 | function_tinfo = idaapi.tinfo_t() 18 | if not vu.cfunc.get_func_type(function_tinfo): 19 | return 20 | function_details = idaapi.func_type_data_t() 21 | function_tinfo.get_func_details(function_details) 22 | convention = idaapi.CM_CC_MASK & function_details.cc 23 | if convention == idaapi.CM_CC_CDECL: 24 | function_details.cc = idaapi.CM_CC_SPECIAL 25 | elif convention in (idaapi.CM_CC_STDCALL, idaapi.CM_CC_FASTCALL, idaapi.CM_CC_PASCAL, idaapi.CM_CC_THISCALL): 26 | function_details.cc = idaapi.CM_CC_SPECIALP 27 | elif convention == idaapi.CM_CC_ELLIPSIS: 28 | function_details.cc = idaapi.CM_CC_SPECIALE 29 | else: 30 | return 31 | function_tinfo.create_func(function_details) 32 | idaapi.apply_tinfo(vu.cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE) 33 | vu.refresh_view(True) 34 | 35 | 36 | class AddRemoveReturn(actions.HexRaysPopupAction): 37 | description = "Add/Remove Return" 38 | 39 | def __init__(self): 40 | super(AddRemoveReturn, self).__init__() 41 | 42 | def check(self, hx_view): 43 | return hx_view.item.citype == idaapi.VDI_FUNC 44 | 45 | def activate(self, ctx): 46 | vu = idaapi.get_widget_vdui(ctx.widget) 47 | function_tinfo = idaapi.tinfo_t() 48 | if not vu.cfunc.get_func_type(function_tinfo): 49 | return 50 | function_details = idaapi.func_type_data_t() 51 | function_tinfo.get_func_details(function_details) 52 | if function_details.rettype.equals_to(const.VOID_TINFO): 53 | function_details.rettype = idaapi.tinfo_t(const.PVOID_TINFO) 54 | else: 55 | function_details.rettype = idaapi.tinfo_t(idaapi.BT_VOID) 56 | function_tinfo.create_func(function_details) 57 | idaapi.apply_tinfo(vu.cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE) 58 | vu.refresh_view(True) 59 | 60 | 61 | class RemoveArgument(actions.HexRaysPopupAction): 62 | description = "Remove Argument" 63 | 64 | def __init__(self): 65 | super(RemoveArgument, self).__init__() 66 | 67 | def check(self, hx_view): 68 | if hx_view.item.citype != idaapi.VDI_LVAR: 69 | return False 70 | local_variable = hx_view.item.get_lvar() # type:idaapi.lvar_t 71 | return local_variable.is_arg_var 72 | 73 | def activate(self, ctx): 74 | vu = idaapi.get_widget_vdui(ctx.widget) 75 | function_tinfo = idaapi.tinfo_t() 76 | if not vu.cfunc.get_func_type(function_tinfo): 77 | return 78 | function_details = idaapi.func_type_data_t() 79 | function_tinfo.get_func_details(function_details) 80 | del_arg = vu.item.get_lvar() 81 | 82 | function_details.erase([x for x in function_details if x.name == del_arg.name][0]) 83 | 84 | function_tinfo.create_func(function_details) 85 | idaapi.apply_tinfo(vu.cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE) 86 | vu.refresh_view(True) 87 | 88 | 89 | actions.action_manager.register(ConvertToUsercall()) 90 | actions.action_manager.register(AddRemoveReturn()) 91 | actions.action_manager.register(RemoveArgument()) 92 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/guess_allocation.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | from . import actions 4 | import HexRaysPyTools.api as api 5 | import HexRaysPyTools.forms as forms 6 | import HexRaysPyTools.core.helper as helper 7 | 8 | 9 | class _StructAllocChoose(forms.MyChoose): 10 | def __init__(self, items): 11 | forms.MyChoose.__init__( 12 | self, items, "Possible structure allocations", 13 | [["Function", 30], ["Variable", 10], ["Line", 50], ["Type", 10]] 14 | ) 15 | 16 | def OnSelectLine(self, n): 17 | idaapi.jumpto(self.items[n][0]) 18 | 19 | def OnGetLine(self, n): 20 | func_ea, var, line, alloc_type = self.items[n] 21 | return [helper.to_nice_str(func_ea), var, line, alloc_type] 22 | 23 | 24 | class _GuessAllocationVisitor(api.RecursiveObjectUpwardsVisitor): 25 | def __init__(self, cfunc, obj): 26 | super(_GuessAllocationVisitor, self).__init__(cfunc, obj, skip_after_object=True) 27 | self._data = [] 28 | 29 | def _manipulate(self, cexpr, obj): 30 | if obj.id == api.SO_LOCAL_VARIABLE: 31 | parent = self.parent_expr() 32 | if parent.op == idaapi.cot_asg: 33 | alloc_obj = api.MemoryAllocationObject.create(self._cfunc, self.parent_expr().y) 34 | if alloc_obj: 35 | self._data.append([alloc_obj.ea, obj.name, self._get_line(), "HEAP"]) 36 | elif self.parent_expr().op == idaapi.cot_ref: 37 | self._data.append([helper.find_asm_address(cexpr, self.parents), obj.name, self._get_line(), "STACK"]) 38 | elif obj.id == api.SO_GLOBAL_OBJECT: 39 | self._data.append([helper.find_asm_address(cexpr, self.parents), obj.name, self._get_line(), "GLOBAL"]) 40 | 41 | def _finish(self): 42 | chooser = _StructAllocChoose(self._data) 43 | chooser.Show(False) 44 | 45 | 46 | class GuessAllocation(actions.HexRaysPopupAction): 47 | description = "Guess allocation" 48 | hotkey = None 49 | 50 | def __init__(self): 51 | super(GuessAllocation, self).__init__() 52 | 53 | def check(self, hx_view): 54 | if hx_view.item.citype != idaapi.VDI_EXPR: 55 | return False 56 | return api.ScanObject.create(hx_view.cfunc, hx_view.item) is not None 57 | 58 | def activate(self, ctx): 59 | hx_view = idaapi.get_widget_vdui(ctx.widget) 60 | obj = api.ScanObject.create(hx_view.cfunc, hx_view.item) 61 | if obj: 62 | visitor = _GuessAllocationVisitor(hx_view.cfunc, obj) 63 | visitor.process() 64 | 65 | actions.action_manager.register(GuessAllocation()) 66 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/member_double_click.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | from . import callbacks 4 | import HexRaysPyTools.core.helper as helper 5 | 6 | 7 | class MemberDoubleClick(callbacks.HexRaysEventHandler): 8 | def __init__(self): 9 | super(MemberDoubleClick, self).__init__() 10 | 11 | def handle(self, event, *args): 12 | hx_view = args[0] 13 | item = hx_view.item 14 | if item.citype == idaapi.VDI_EXPR and item.e.op in (idaapi.cot_memptr, idaapi.cot_memref): 15 | # Look if we double clicked on expression that is member pointer. Then get tinfo_t of the structure. 16 | # After that remove pointer and get member name with the same offset 17 | if item.e.x.op == idaapi.cot_memref and item.e.x.x.op == idaapi.cot_memptr: 18 | vtable_tinfo = item.e.x.type.get_pointed_object() 19 | method_offset = item.e.m 20 | class_tinfo = item.e.x.x.x.type.get_pointed_object() 21 | vtable_offset = item.e.x.x.m 22 | elif item.e.x.op == idaapi.cot_memptr: 23 | vtable_tinfo = item.e.x.type 24 | if vtable_tinfo.is_ptr(): 25 | vtable_tinfo = vtable_tinfo.get_pointed_object() 26 | method_offset = item.e.m 27 | class_tinfo = item.e.x.x.type.get_pointed_object() 28 | vtable_offset = item.e.x.m 29 | else: 30 | func_offset = item.e.m 31 | struct_tinfo = item.e.x.type.get_pointed_object() 32 | func_ea = helper.choose_virtual_func_address(helper.get_member_name(struct_tinfo, func_offset)) 33 | if func_ea: 34 | idaapi.jumpto(func_ea) 35 | return 0 36 | 37 | func_name = helper.get_member_name(vtable_tinfo, method_offset) 38 | func_ea = helper.choose_virtual_func_address(func_name, class_tinfo, vtable_offset) 39 | if func_ea: 40 | idaapi.jumpto(func_ea) 41 | return 1 42 | 43 | callbacks.hx_callback_manager.register(idaapi.hxe_double_click, MemberDoubleClick()) 44 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/negative_offsets.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | import idaapi 4 | 5 | from . import actions 6 | from . import callbacks 7 | import HexRaysPyTools.core.helper as helper 8 | import HexRaysPyTools.core.type_library as type_library 9 | import HexRaysPyTools.forms as forms 10 | 11 | logger = logging.getLogger(__name__) 12 | potential_negatives = {} 13 | 14 | 15 | def _has_magic_comment(lvar): 16 | # type: (idaapi.lvar_t) -> bool 17 | # FIXME: Use internal IDA storage for CONTAINING_RECORD macro 18 | return bool(re.search("```.*```", lvar.cmt)) 19 | 20 | 21 | def _parse_magic_comment(lvar): 22 | if lvar.type().is_ptr(): 23 | m = re.search('```(.+)```', lvar.cmt) 24 | if m: 25 | structure_name, offset = m.group(1).split('+') 26 | offset = int(offset) 27 | parent_tinfo = idaapi.tinfo_t() 28 | if parent_tinfo.get_named_type(idaapi.get_idati(), structure_name) and parent_tinfo.get_size() > offset: 29 | member_name = dict(find_deep_members(parent_tinfo, lvar.type().get_pointed_object())).get(offset, None) 30 | if member_name: 31 | return NegativeLocalInfo(lvar.type().get_pointed_object(), parent_tinfo, offset, member_name) 32 | return None 33 | 34 | 35 | def find_deep_members(parent_tinfo, target_tinfo): 36 | udt_data = idaapi.udt_type_data_t() 37 | parent_tinfo.get_udt_details(udt_data) 38 | result = [] 39 | for udt_member in udt_data: 40 | if udt_member.type.equals_to(target_tinfo): 41 | result.append((udt_member.offset // 8, udt_member.name)) 42 | elif udt_member.type.is_udt(): 43 | for offset, name in find_deep_members(udt_member.type, target_tinfo): 44 | final_name = udt_member.name + '.' + name if udt_member.name else name 45 | result.append((udt_member.offset // 8 + offset, final_name)) 46 | return result 47 | 48 | 49 | class NegativeLocalInfo: 50 | def __init__(self, tinfo, parent_tinfo, offset, member_name): 51 | self.tinfo = tinfo 52 | self.size = tinfo.get_size() if tinfo.is_udt else 0 53 | self.parent_tinfo = parent_tinfo 54 | self.offset = offset 55 | self.member_name = member_name 56 | 57 | def __repr__(self): 58 | return "Type - {0}, parent type - {1}, offset - {2}, member_name - {3}".format( 59 | self.tinfo.dstr(), 60 | self.parent_tinfo.dstr(), 61 | self.offset, 62 | self.member_name 63 | ) 64 | 65 | 66 | class NegativeLocalCandidate: 67 | def __init__(self, tinfo, offset): 68 | """ 69 | Tinfo - type of the structure tha local variable points to. So it's stripped from pointer. Offset - is first 70 | found offset that points outside of the structure. 71 | :param tinfo: idaapi.tinfo_t 72 | :param offset: int 73 | """ 74 | self.tinfo = tinfo 75 | self.offsets = [offset] 76 | 77 | def __repr__(self): 78 | return self.tinfo.dstr() + ' ' + str(self.offsets) 79 | 80 | def is_structure_offset(self, tinfo, offset): 81 | # Checks if structure tinfo contains a member at given offset 82 | # TODO:array checking 83 | udt_member = idaapi.udt_member_t() 84 | udt_member.offset = offset * 8 85 | if offset >= 0 and tinfo.find_udt_member(udt_member, idaapi.STRMEM_OFFSET) != -1: 86 | if udt_member.type.is_udt(): 87 | return self.is_structure_offset(udt_member.type, offset - udt_member.offset // 8) 88 | return udt_member.offset == offset * 8 89 | return False 90 | 91 | def find_containing_structures(self, type_library): 92 | """ 93 | Given the type library creates a list of structures from this library, that contains this structure and 94 | satisfy offset conditions. 95 | :param type_library: idaapi.til_t 96 | :returns: ordinal, offset, member_name, containing structure name 97 | """ 98 | 99 | min_offset = min(self.offsets) 100 | min_offset = min_offset if min_offset < 0 else 0 101 | max_offset = max(self.offsets) 102 | max_offset = max_offset if max_offset > 0 else self.tinfo.get_size() 103 | # TODO: Check if all offsets are legal 104 | 105 | # Least acceptable size of the containing structure 106 | min_struct_size = max_offset - min_offset 107 | result = [] 108 | parent_tinfo = idaapi.tinfo_t() 109 | target_tinfo = idaapi.tinfo_t() 110 | if not target_tinfo.get_named_type(type_library, self.tinfo.dstr()): 111 | print("[Warning] Such type doesn't exist in '{0}' library".format(type_library.name)) 112 | return result 113 | for ordinal in range(1, idaapi.get_ordinal_count(type_library)): 114 | parent_tinfo.create_typedef(type_library, ordinal) 115 | if parent_tinfo.get_size() >= min_struct_size: 116 | for offset, name in find_deep_members(parent_tinfo, target_tinfo): 117 | # print "[DEBUG] Found {0} at {1} in {2}".format(name, offset, parent_tinfo.dstr()) 118 | if offset + min_offset >= 0 and offset + max_offset <= parent_tinfo.get_size(): 119 | result.append((ordinal, offset, name, parent_tinfo.dstr())) 120 | return result 121 | 122 | 123 | class ReplaceVisitor(idaapi.ctree_parentee_t): 124 | 125 | def __init__(self, negative_lvars): 126 | super(ReplaceVisitor, self).__init__() 127 | self.negative_lvars = negative_lvars 128 | self.pvoid_tinfo = idaapi.tinfo_t(idaapi.BT_VOID) 129 | self.pvoid_tinfo.create_ptr(self.pvoid_tinfo) 130 | 131 | def visit_expr(self, expression): 132 | if expression.op == idaapi.cot_add and expression.x.op == idaapi.cot_var and expression.y.op == idaapi.cot_num: 133 | index = expression.x.v.idx 134 | if index in self.negative_lvars: 135 | offset = expression.y.numval() 136 | if offset >= self.negative_lvars[index].size: 137 | self.create_containing_record(expression, index, offset) 138 | elif expression.op == idaapi.cot_sub and expression.x.op == idaapi.cot_var and expression.y.op == idaapi.cot_num: 139 | index = expression.x.v.idx 140 | if index in self.negative_lvars: 141 | offset = -expression.y.n.value(idaapi.tinfo_t(idaapi.BT_INT)) 142 | self.create_containing_record(expression, index, offset) 143 | return 0 144 | 145 | def create_containing_record(self, expression, index, offset): 146 | negative_lvar = self.negative_lvars[index] 147 | logger.debug("Creating CONTAINING_RECORD macro, offset: {}, negative offset: {}, TYPE: {}".format( 148 | negative_lvar.offset, 149 | offset, 150 | negative_lvar.parent_tinfo.dstr() 151 | )) 152 | 153 | arg_address = idaapi.carg_t() 154 | if expression.op == idaapi.cot_var: 155 | arg_address.assign(expression) 156 | else: 157 | arg_address.assign(expression.x) 158 | 159 | arg_type = idaapi.carg_t() 160 | cexpr_helper = idaapi.create_helper(True, self.pvoid_tinfo, negative_lvar.parent_tinfo.dstr()) 161 | arg_type.assign(cexpr_helper) 162 | 163 | arg_field = idaapi.carg_t() 164 | cexpr_helper = idaapi.create_helper( 165 | True, 166 | self.pvoid_tinfo, 167 | negative_lvar.member_name 168 | ) 169 | arg_field.assign(cexpr_helper) 170 | return_tinfo = idaapi.tinfo_t(negative_lvar.parent_tinfo) 171 | return_tinfo.create_ptr(return_tinfo) 172 | new_cexpr_call = idaapi.call_helper(return_tinfo, None, "CONTAINING_RECORD") 173 | new_cexpr_call.a.push_back(arg_address) 174 | new_cexpr_call.a.push_back(arg_type) 175 | new_cexpr_call.a.push_back(arg_field) 176 | new_cexpr_call.thisown = False 177 | 178 | parent = next(reversed(self.parents)).cexpr 179 | 180 | diff = negative_lvar.offset + offset 181 | if diff: 182 | number = idaapi.make_num(diff) 183 | number.thisown = False 184 | new_cexpr_add = helper.my_cexpr_t(idaapi.cot_add, x=new_cexpr_call, y=number) 185 | new_cexpr_add.type = return_tinfo 186 | 187 | if parent.op == idaapi.cot_ptr: 188 | tmp_tinfo = idaapi.tinfo_t() 189 | tmp_tinfo.create_ptr(parent.type) 190 | new_cexpr_cast = helper.my_cexpr_t(idaapi.cot_cast, x=new_cexpr_add) 191 | new_cexpr_cast.thisown = False 192 | new_cexpr_cast.type = tmp_tinfo 193 | expression.assign(new_cexpr_cast) 194 | else: 195 | expression.assign(new_cexpr_add) 196 | else: 197 | if parent.op == idaapi.cot_ptr: 198 | tmp_tinfo = idaapi.tinfo_t() 199 | tmp_tinfo.create_ptr(parent.type) 200 | new_cexpr_cast = helper.my_cexpr_t(idaapi.cot_cast, x=new_cexpr_call) 201 | new_cexpr_cast.thisown = False 202 | new_cexpr_cast.type = tmp_tinfo 203 | expression.assign(new_cexpr_cast) 204 | else: 205 | expression.assign(new_cexpr_call) 206 | 207 | 208 | class SearchVisitor(idaapi.ctree_parentee_t): 209 | def __init__(self, cfunc): 210 | super(SearchVisitor, self).__init__() 211 | self.cfunc = cfunc 212 | self.result = {} 213 | 214 | def visit_expr(self, expression): 215 | if expression.op == idaapi.cot_call and expression.x.op == idaapi.cot_helper and len(expression.a) == 3: 216 | if expression.x.helper == "CONTAINING_RECORD": 217 | if expression.a[0].op == idaapi.cot_var: 218 | idx = expression.a[0].v.idx 219 | if expression.a[1].op == idaapi.cot_helper and expression.a[2].op == idaapi.cot_helper: 220 | parent_name = expression.a[1].helper 221 | member_name = expression.a[2].helper 222 | parent_tinfo = idaapi.tinfo_t() 223 | if not parent_tinfo.get_named_type(idaapi.get_idati(), parent_name): 224 | return 0 225 | udt_data = idaapi.udt_type_data_t() 226 | parent_tinfo.get_udt_details(udt_data) 227 | udt_member = [x for x in udt_data if x.name == member_name] 228 | if udt_member: 229 | tinfo = udt_member[0].type 230 | self.result[idx] = NegativeLocalInfo( 231 | tinfo, 232 | parent_tinfo, 233 | udt_member[0].offset // 8, 234 | member_name 235 | ) 236 | return 1 237 | return 0 238 | 239 | 240 | class AnalyseVisitor(idaapi.ctree_parentee_t): 241 | def __init__(self, candidates): 242 | global potential_negatives 243 | super(AnalyseVisitor, self).__init__() 244 | self.candidates = candidates 245 | self.potential_negatives = potential_negatives 246 | self.potential_negatives.clear() 247 | 248 | def visit_expr(self, expression): 249 | if expression.op == idaapi.cot_add and expression.y.op == idaapi.cot_num: 250 | if expression.x.op == idaapi.cot_var and expression.x.v.idx in self.candidates: 251 | idx = expression.x.v.idx 252 | number = expression.y.numval() 253 | if self.candidates[idx].get_size() <= number: 254 | if idx in self.potential_negatives: 255 | self.potential_negatives[idx].offsets.append(number) 256 | else: 257 | self.potential_negatives[idx] = NegativeLocalCandidate(self.candidates[idx], number) 258 | elif expression.op == idaapi.cot_sub and expression.y.op == idaapi.cot_num: 259 | if expression.x.op == idaapi.cot_var and expression.x.v.idx in self.candidates: 260 | idx = expression.x.v.idx 261 | number = -expression.y.numval() 262 | if idx in self.potential_negatives: 263 | self.potential_negatives[idx].offsets.append(number) 264 | else: 265 | self.potential_negatives[idx] = NegativeLocalCandidate(self.candidates[idx], number) 266 | 267 | return 0 268 | 269 | 270 | class PotentialNegativeCollector(callbacks.HexRaysEventHandler): 271 | def __init__(self): 272 | super(PotentialNegativeCollector, self).__init__() 273 | 274 | def handle(self, event, *args): 275 | global potential_negatives 276 | 277 | cfunc, level_of_maturity = args 278 | if level_of_maturity == idaapi.CMAT_BUILT: 279 | # First search for CONTAINING_RECORD made by Ida 280 | visitor = SearchVisitor(cfunc) 281 | visitor.apply_to(cfunc.body, None) 282 | negative_lvars = visitor.result 283 | 284 | # Second get saved information from comments 285 | lvars = cfunc.get_lvars() 286 | for idx in range(len(lvars)): 287 | result = _parse_magic_comment(lvars[idx]) 288 | if result and result.tinfo.equals_to(lvars[idx].type().get_pointed_object()): 289 | negative_lvars[idx] = result 290 | 291 | # Third analyze local variables that are a structure pointers and have references going beyond 292 | # structure boundaries. This variables will be considered as potential pointers to substructure 293 | # and will get a special menu on right click 294 | 295 | # First collect all structure pointers 296 | structure_pointer_variables = {} 297 | for idx in set(range(len(lvars))) - set(negative_lvars.keys()): 298 | if lvars[idx].type().is_ptr(): 299 | pointed_tinfo = lvars[idx].type().get_pointed_object() 300 | if pointed_tinfo.is_udt(): 301 | structure_pointer_variables[idx] = pointed_tinfo 302 | 303 | # Then use them in order to find all potential negative offset situations 304 | if structure_pointer_variables: 305 | visitor = AnalyseVisitor(structure_pointer_variables) 306 | visitor.apply_to(cfunc.body, None) 307 | 308 | # If negative offsets were found, then we replace them with CONTAINING_RECORD macro 309 | if negative_lvars: 310 | visitor = ReplaceVisitor(negative_lvars) 311 | visitor.apply_to(cfunc.body, None) 312 | 313 | 314 | callbacks.hx_callback_manager.register(idaapi.hxe_maturity, PotentialNegativeCollector()) 315 | 316 | 317 | class ResetContainingStructure(actions.HexRaysPopupAction): 318 | description = "Reset Containing Structure" 319 | 320 | def __init__(self): 321 | super(ResetContainingStructure, self).__init__() 322 | 323 | def check(self, hx_view): 324 | ctree_item = hx_view.item 325 | if ctree_item.citype != idaapi.VDI_EXPR or ctree_item.e.op != idaapi.cot_var: 326 | return False 327 | return _has_magic_comment(hx_view.cfunc.get_lvars()[ctree_item.e.v.idx]) 328 | 329 | def activate(self, ctx): 330 | hx_view = idaapi.get_widget_vdui(ctx.widget) 331 | lvar = hx_view.cfunc.get_lvars()[hx_view.item.e.v.idx] 332 | hx_view.set_lvar_cmt(lvar, re.sub("```.*```", '', lvar.cmt)) 333 | hx_view.refresh_view(True) 334 | 335 | 336 | actions.action_manager.register(ResetContainingStructure()) 337 | 338 | 339 | class SelectContainingStructure(actions.HexRaysPopupAction): 340 | description = "Select Containing Structure" 341 | 342 | def __init__(self): 343 | super(SelectContainingStructure, self).__init__() 344 | 345 | def check(self, hx_view): 346 | ctree_item = hx_view.item 347 | if ctree_item.citype != idaapi.VDI_EXPR or ctree_item.e.op != idaapi.cot_var: 348 | return False 349 | return ctree_item.e.v.idx in potential_negatives 350 | 351 | def activate(self, ctx): 352 | global potential_negatives 353 | 354 | hx_view = idaapi.get_widget_vdui(ctx.widget) 355 | result = type_library.choose_til() 356 | if not result: 357 | return 358 | 359 | selected_library, max_ordinal, is_local_types = result 360 | lvar_idx = hx_view.item.e.v.idx 361 | candidate = potential_negatives[lvar_idx] 362 | structures = candidate.find_containing_structures(selected_library) 363 | items = [[str(x[0]), "0x{0:08X}".format(x[1]), x[2], x[3]] for x in structures] 364 | structure_chooser = forms.MyChoose( 365 | items, 366 | "Select Containing Structure", 367 | [["Ordinal", 5], ["Offset", 10], ["Member_name", 20], ["Structure Name", 20]], 368 | 165 369 | ) 370 | selected_idx = structure_chooser.Show(modal=True) 371 | if selected_idx != -1: 372 | if not is_local_types: 373 | type_library.import_type(selected_library, items[selected_idx][3]) 374 | lvar = hx_view.cfunc.get_lvars()[lvar_idx] 375 | lvar_cmt = re.sub("```.*```", '', lvar.cmt) 376 | hx_view.set_lvar_cmt( 377 | lvar, 378 | lvar_cmt + "```{0}+{1}```".format( 379 | structures[selected_idx][3], 380 | structures[selected_idx][1]) 381 | ) 382 | hx_view.refresh_view(True) 383 | 384 | 385 | actions.action_manager.register(SelectContainingStructure()) 386 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/new_field_creation.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import re 3 | 4 | import idaapi 5 | import idc 6 | 7 | from . import actions 8 | import HexRaysPyTools.core.helper as helper 9 | import HexRaysPyTools.core.const as const 10 | 11 | logger = logging.getLogger(__name__) 12 | 13 | 14 | def _is_gap_field(cexpr): 15 | if cexpr.op not in (idaapi.cot_memptr, idaapi.cot_memref): 16 | return False 17 | struct_type = cexpr.x.type 18 | struct_type.remove_ptr_or_array() 19 | return helper.get_member_name(struct_type, cexpr.m)[0:3] == "gap" 20 | 21 | 22 | class CreateNewField(actions.HexRaysPopupAction): 23 | description = "Create New Field" 24 | hotkey = "Ctrl+F" 25 | 26 | def __init__(self): 27 | super(CreateNewField, self).__init__() 28 | 29 | def check(self, hx_view): 30 | if hx_view.item.citype != idaapi.VDI_EXPR: 31 | return False 32 | 33 | return _is_gap_field(hx_view.item.it.to_specific_type) 34 | 35 | def activate(self, ctx): 36 | hx_view = idaapi.get_widget_vdui(ctx.widget) 37 | if not self.check(hx_view): 38 | return 39 | 40 | item = hx_view.item.it.to_specific_type 41 | parent = hx_view.cfunc.body.find_parent_of(item).to_specific_type 42 | if parent.op != idaapi.cot_idx or parent.y.op != idaapi.cot_num: 43 | idx = 0 44 | else: 45 | idx = parent.y.numval() 46 | 47 | struct_tinfo = item.x.type 48 | struct_tinfo.remove_ptr_or_array() 49 | 50 | offset = item.m 51 | ordinal = struct_tinfo.get_ordinal() 52 | struct_name = struct_tinfo.dstr() 53 | 54 | if (offset + idx) % 2: 55 | default_field_type = "_BYTE" 56 | elif (offset + idx) % 4: 57 | default_field_type = "_WORD" 58 | elif (offset + idx) % 8: 59 | default_field_type = "_DWORD" 60 | else: 61 | default_field_type = "_QWORD" if const.EA64 else "_DWORD" 62 | 63 | declaration = idaapi.ask_text( 64 | 0x10000, "{0} field_{1:X}".format(default_field_type, offset + idx), "Enter new structure member:" 65 | ) 66 | if declaration is None: 67 | return 68 | 69 | result = self.parse_declaration(declaration) 70 | if result is None: 71 | logger.warn("Bad member declaration") 72 | return 73 | 74 | field_tinfo, field_name = result 75 | field_size = field_tinfo.get_size() 76 | udt_data = idaapi.udt_type_data_t() 77 | udt_member = idaapi.udt_member_t() 78 | 79 | struct_tinfo.get_udt_details(udt_data) 80 | udt_member.offset = offset * 8 81 | struct_tinfo.find_udt_member(udt_member, idaapi.STRMEM_OFFSET) 82 | gap_size = udt_member.size // 8 83 | 84 | gap_leftover = gap_size - idx - field_size 85 | 86 | if gap_leftover < 0: 87 | logger.error("Too big size for the field. Type with maximum {0} bytes can be used".format(gap_size - idx)) 88 | return 89 | 90 | iterator = udt_data.find(udt_member) 91 | iterator = udt_data.erase(iterator) 92 | 93 | if gap_leftover > 0: 94 | udt_data.insert(iterator, helper.create_padding_udt_member(offset + idx + field_size, gap_leftover)) 95 | 96 | udt_member = idaapi.udt_member_t() 97 | udt_member.offset = offset * 8 + idx 98 | udt_member.name = field_name 99 | udt_member.type = field_tinfo 100 | udt_member.size = field_size 101 | 102 | iterator = udt_data.insert(iterator, udt_member) 103 | 104 | if idx > 0: 105 | udt_data.insert(iterator, helper.create_padding_udt_member(offset, idx)) 106 | 107 | struct_tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) 108 | struct_tinfo.set_numbered_type(idaapi.get_idati(), ordinal, idaapi.BTF_STRUCT, struct_name) 109 | hx_view.refresh_view(True) 110 | 111 | @staticmethod 112 | def parse_declaration(declaration): 113 | m = re.search(r"^(\w+[ *]+)(\w+)(\[(\d+)\])?$", declaration) 114 | if m is None: 115 | logger.error("Member declaration should be like `TYPE_NAME NAME[SIZE]` (Array is optional)") 116 | return 117 | 118 | type_name, field_name, _, arr_size = m.groups() 119 | if field_name[0].isdigit(): 120 | logger.error("Bad field name") 121 | return 122 | 123 | result = idc.parse_decl(type_name, 0) 124 | if result is None: 125 | logger.error("Failed to parse member type. It should be like `TYPE_NAME NAME[SIZE]` (Array is optional)") 126 | return 127 | 128 | _, tp, fld = result 129 | tinfo = idaapi.tinfo_t() 130 | tinfo.deserialize(idaapi.get_idati(), tp, fld, None) 131 | if arr_size: 132 | assert tinfo.create_array(tinfo, int(arr_size)) 133 | return tinfo, field_name 134 | 135 | actions.action_manager.register(CreateNewField()) 136 | 137 | # TODO: All this stuff can be done automatically when we either use ctrl+F5 or regular F5 138 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/recasts.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple 2 | import idaapi 3 | from . import actions 4 | import HexRaysPyTools.core.helper as helper 5 | 6 | 7 | RecastLocalVariable = namedtuple('RecastLocalVariable', ['recast_tinfo', 'local_variable']) 8 | RecastGlobalVariable = namedtuple('RecastGlobalVariable', ['recast_tinfo', 'global_variable_ea']) 9 | RecastArgument = namedtuple('RecastArgument', ['recast_tinfo', 'arg_idx', 'func_ea', 'func_tinfo']) 10 | RecastReturn = namedtuple('RecastReturn', ['recast_tinfo', 'func_ea']) 11 | RecastStructure = namedtuple('RecastStructure', ['recast_tinfo', 'structure_name', 'field_offset']) 12 | 13 | 14 | class RecastItemLeft(actions.HexRaysPopupAction): 15 | 16 | description = "Recast Item" 17 | hotkey = "Shift+L" 18 | 19 | def __init__(self): 20 | super(RecastItemLeft, self).__init__() 21 | 22 | def extract_recast_info(self, cfunc, ctree_item): 23 | # type: (idaapi.cfunc_t, idaapi.ctree_item_t) -> namedtuple 24 | # Returns one of the Recast... namedtuple or None if nothing was found 25 | 26 | if ctree_item.citype != idaapi.VDI_EXPR: 27 | return 28 | 29 | expression = ctree_item.it.to_specific_type 30 | child = None 31 | 32 | # Look through parents until we found Return, Assignment or Call 33 | while expression and expression.op not in (idaapi.cot_asg, idaapi.cit_return, idaapi.cot_call): 34 | child = expression.to_specific_type 35 | expression = cfunc.body.find_parent_of(expression) 36 | if not expression: 37 | return 38 | 39 | expression = expression.to_specific_type 40 | if expression.op == idaapi.cot_asg: 41 | 42 | if expression.x.opname not in ('var', 'obj', 'memptr', 'memref'): 43 | return 44 | 45 | right_expr = expression.y 46 | right_tinfo = right_expr.x.type if right_expr.op == idaapi.cot_cast else right_expr.type 47 | 48 | # Check if both left and right parts of expression are of the same types. 49 | # If not then we can recast then. 50 | if right_tinfo.dstr() == expression.x.type.dstr(): 51 | return 52 | 53 | if expression.x.op == idaapi.cot_var: 54 | # var = (TYPE ) ...; 55 | variable = cfunc.get_lvars()[expression.x.v.idx] 56 | return RecastLocalVariable(right_tinfo, variable) 57 | 58 | elif expression.x.op == idaapi.cot_obj: 59 | # g_var = (TYPE ) ...; 60 | return RecastGlobalVariable(right_tinfo, expression.x.obj_ea) 61 | 62 | elif expression.x.op == idaapi.cot_memptr: 63 | # struct->member = (TYPE ) ...; 64 | struct_name = expression.x.x.type.get_pointed_object().dstr() 65 | struct_offset = expression.x.m 66 | return RecastStructure(right_tinfo, struct_name, struct_offset) 67 | 68 | elif expression.x.op == idaapi.cot_memref: 69 | # struct.member = (TYPE ) ...; 70 | struct_name = expression.x.x.type.dstr() 71 | struct_offset = expression.x.m 72 | return RecastStructure(right_tinfo, struct_name, struct_offset) 73 | 74 | elif expression.op == idaapi.cit_return: 75 | child = child or expression.creturn.expr 76 | if child.op == idaapi.cot_cast: 77 | # return (TYPE) ...; 78 | return RecastReturn(child.x.type, cfunc.entry_ea) 79 | 80 | func_tinfo = idaapi.tinfo_t() 81 | cfunc.get_func_type(func_tinfo) 82 | rettype = func_tinfo.get_rettype() 83 | if rettype.dstr() != child.type.dstr(): 84 | # return ...; 85 | # This's possible when returned type and value are both pointers to different types 86 | return RecastReturn(child.type, cfunc.entry_ea) 87 | 88 | elif expression.op == idaapi.cot_call: 89 | if expression.x == child: 90 | return 91 | func_ea = expression.x.obj_ea 92 | arg_index, param_tinfo = helper.get_func_argument_info(expression, child) 93 | if expression.x.op == idaapi.cot_memptr: 94 | if child.op == idaapi.cot_cast: 95 | # struct_ptr->func(..., (TYPE) var, ...); 96 | arg_tinfo = child.x.type 97 | else: 98 | # struct_ptr->func(..., var, ...); When `var` and `arg` are different pointers 99 | if param_tinfo.equals_to(child.type): 100 | return 101 | arg_tinfo = child.type 102 | 103 | struct_tinfo = expression.x.x.type.get_pointed_object() 104 | funcptr_tinfo = expression.x.type 105 | helper.set_funcptr_argument(funcptr_tinfo, arg_index, arg_tinfo) 106 | return RecastStructure(funcptr_tinfo, struct_tinfo.dstr(), expression.x.m) 107 | 108 | if child.op == idaapi.cot_ref: 109 | if child.x.op == idaapi.cot_memref and child.x.m == 0: 110 | # func(..., &struct.field_0, ...) 111 | arg_tinfo = idaapi.tinfo_t() 112 | arg_tinfo.create_ptr(child.x.x.type) 113 | elif child.x.op == idaapi.cot_memptr and child.x.m == 0: 114 | # func(..., &struct->field_0, ...) 115 | arg_tinfo = child.x.x.type 116 | else: 117 | # func(..., &var, ...) 118 | arg_tinfo = child.type 119 | elif child.op == idaapi.cot_cast: 120 | arg_tinfo = child.x.type 121 | else: 122 | arg_tinfo = child.type 123 | 124 | func_tinfo = expression.x.type.get_pointed_object() 125 | return RecastArgument(arg_tinfo, arg_index, func_ea, func_tinfo) 126 | 127 | def set_label(self, label): 128 | idaapi.update_action_label(self.name, label) 129 | 130 | def check(self, hx_view): 131 | cfunc, ctree_item = hx_view.cfunc, hx_view.item 132 | 133 | ri = self.extract_recast_info(cfunc, ctree_item) 134 | if not ri: 135 | return False 136 | 137 | if isinstance(ri, RecastLocalVariable): 138 | self.set_label('Recast Variable "{0}" to {1}'.format(ri.local_variable.name, ri.recast_tinfo.dstr())) 139 | elif isinstance(ri, RecastGlobalVariable): 140 | gvar_name = idaapi.get_name(ri.global_variable_ea) 141 | self.set_label('Recast Global Variable "{0}" to {1}'.format(gvar_name, ri.recast_tinfo.dstr())) 142 | elif isinstance(ri, RecastArgument): 143 | self.set_label("Recast Argument") 144 | elif isinstance(ri, RecastStructure): 145 | self.set_label("Recast Field of {0} structure".format(ri.structure_name)) 146 | elif isinstance(ri, RecastReturn): 147 | self.set_label("Recast Return to ".format(ri.recast_tinfo.dstr())) 148 | else: 149 | raise NotImplementedError 150 | return True 151 | 152 | def activate(self, ctx): 153 | hx_view = idaapi.get_widget_vdui(ctx.widget) 154 | ri = self.extract_recast_info(hx_view.cfunc, hx_view.item) 155 | if not ri: 156 | return 0 157 | 158 | if isinstance(ri, RecastLocalVariable): 159 | hx_view.set_lvar_type(ri.local_variable, ri.recast_tinfo) 160 | 161 | elif isinstance(ri, RecastGlobalVariable): 162 | idaapi.apply_tinfo(ri.global_variable_ea, ri.recast_tinfo, idaapi.TINFO_DEFINITE) 163 | 164 | elif isinstance(ri, RecastArgument): 165 | if ri.recast_tinfo.is_array(): 166 | ri.recast_tinfo.convert_array_to_ptr() 167 | helper.set_func_argument(ri.func_tinfo, ri.arg_idx, ri.recast_tinfo) 168 | idaapi.apply_tinfo(ri.func_ea, ri.func_tinfo, idaapi.TINFO_DEFINITE) 169 | 170 | elif isinstance(ri, RecastReturn): 171 | cfunc = helper.decompile_function(ri.func_ea) 172 | if not cfunc: 173 | return 0 174 | 175 | func_tinfo = idaapi.tinfo_t() 176 | cfunc.get_func_type(func_tinfo) 177 | helper.set_func_return(func_tinfo, ri.recast_tinfo) 178 | idaapi.apply_tinfo(cfunc.entry_ea, func_tinfo, idaapi.TINFO_DEFINITE) 179 | 180 | elif isinstance(ri, RecastStructure): 181 | tinfo = idaapi.tinfo_t() 182 | tinfo.get_named_type(idaapi.get_idati(), ri.structure_name) 183 | ordinal = idaapi.get_type_ordinal(idaapi.get_idati(), ri.structure_name) 184 | if ordinal == 0: 185 | return 0 186 | 187 | udt_member = idaapi.udt_member_t() 188 | udt_member.offset = ri.field_offset * 8 189 | idx = tinfo.find_udt_member(udt_member, idaapi.STRMEM_OFFSET) 190 | if udt_member.offset != ri.field_offset * 8: 191 | print("[Info] Can't handle with arrays yet") 192 | elif udt_member.type.get_size() != ri.recast_tinfo.get_size(): 193 | print("[Info] Can't recast different sizes yet") 194 | else: 195 | udt_data = idaapi.udt_type_data_t() 196 | tinfo.get_udt_details(udt_data) 197 | udt_data[idx].type = ri.recast_tinfo 198 | tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) 199 | tinfo.set_numbered_type(idaapi.get_idati(), ordinal, idaapi.NTF_REPLACE, ri.structure_name) 200 | else: 201 | raise NotImplementedError 202 | 203 | hx_view.refresh_view(True) 204 | return 0 205 | 206 | 207 | class RecastItemRight(RecastItemLeft): 208 | 209 | name = "my:RecastItemRight" 210 | description = "Recast Item" 211 | hotkey = "Shift+R" 212 | 213 | def __init__(self): 214 | super(RecastItemRight, self).__init__() 215 | 216 | def extract_recast_info(self, cfunc, ctree_item): 217 | if ctree_item.citype != idaapi.VDI_EXPR: 218 | return 219 | 220 | expression = ctree_item.it 221 | result = RecastItemRight._check_potential_array(cfunc, expression) 222 | if result: 223 | return result 224 | 225 | # Look through parents until we found Cast 226 | while expression and expression.op != idaapi.cot_cast: 227 | expression = expression.to_specific_type 228 | expression = cfunc.body.find_parent_of(expression) 229 | if not expression: 230 | return 231 | 232 | expression = expression.to_specific_type 233 | 234 | # Find `(TYPE) something;` or `(TYPE *) &something;` and calculate appropriate type for recast 235 | if expression.x.op == idaapi.cot_ref: 236 | tinfo = expression.type.get_pointed_object() 237 | expression = expression.x 238 | else: 239 | tinfo = expression.type 240 | 241 | if expression.x.op == idaapi.cot_var: 242 | # (TYPE) var; 243 | variable = cfunc.get_lvars()[expression.x.v.idx] 244 | return RecastLocalVariable(tinfo, variable) 245 | 246 | elif expression.x.op == idaapi.cot_obj: 247 | # (TYPE) g_var; 248 | if helper.is_code_ea(expression.x.obj_ea) and tinfo.is_funcptr(): 249 | # (TYPE) sub_XXXXXX; 250 | tinfo = tinfo.get_pointed_object() 251 | gvar_ea = expression.x.obj_ea 252 | return RecastGlobalVariable(tinfo, gvar_ea) 253 | 254 | elif expression.x.op == idaapi.cot_call: 255 | # (TYPE) call(); 256 | idaapi.update_action_label(RecastItemRight.name, "Recast Return") 257 | func_ea = expression.x.x.obj_ea 258 | return RecastReturn(tinfo, func_ea) 259 | 260 | elif expression.x.op == idaapi.cot_memptr: 261 | # (TYPE) var->member; 262 | idaapi.update_action_label(RecastItemRight.name, "Recast Field") 263 | struct_name = expression.x.x.type.get_pointed_object().dstr() 264 | struct_offset = expression.x.m 265 | return RecastStructure(tinfo, struct_name, struct_offset) 266 | 267 | @staticmethod 268 | def _check_potential_array(cfunc, expr): 269 | """ Checks `call(..., &buffer, ..., number)` and returns information for recasting """ 270 | if expr.op != idaapi.cot_var: 271 | return 272 | 273 | var_expr = expr.to_specific_type 274 | parent = cfunc.body.find_parent_of(expr) 275 | if parent.op != idaapi.cot_ref: 276 | return 277 | 278 | parent = cfunc.body.find_parent_of(parent) 279 | if parent.op != idaapi.cot_call: 280 | return 281 | 282 | call_expr = parent.to_specific_type 283 | for arg_expr in call_expr.a: 284 | if arg_expr.op == idaapi.cot_num: 285 | number = arg_expr.numval() 286 | if number: 287 | variable = cfunc.lvars[var_expr.v.idx] 288 | char_array_tinfo = idaapi.tinfo_t() 289 | char_array_tinfo.create_array(idaapi.tinfo_t(idaapi.BTF_CHAR), number) 290 | idaapi.update_action_label(RecastItemRight.name, 'Recast Variable "{}" to "{}"'.format( 291 | variable.name, char_array_tinfo.dstr() 292 | )) 293 | return RecastLocalVariable(char_array_tinfo, variable) 294 | 295 | 296 | actions.action_manager.register(RecastItemLeft()) 297 | actions.action_manager.register(RecastItemRight()) 298 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/renames.py: -------------------------------------------------------------------------------- 1 | import re 2 | import logging 3 | 4 | import idaapi 5 | import idc 6 | 7 | from . import actions 8 | import HexRaysPyTools.api as api 9 | import HexRaysPyTools.core.helper as helper 10 | import HexRaysPyTools.settings as settings 11 | 12 | 13 | logger = logging.getLogger(__name__) 14 | 15 | 16 | def _should_be_renamed(old_name, new_name): 17 | # type: (str, str) -> bool 18 | """ Checks if there's a point to rename a variable or argument """ 19 | 20 | # There's no point to rename into default name 21 | if _is_default_name(new_name): 22 | return False 23 | 24 | # Strip prefixes and check if names are the same 25 | return old_name.lstrip('_') != new_name.lstrip('_') 26 | 27 | 28 | def _is_default_name(string): 29 | return re.match(r"[av]\d+$", string) is not None or \ 30 | re.match(r"[qd]?word|field_|off_", string) is not None 31 | 32 | 33 | class RenameOther(actions.HexRaysPopupAction): 34 | description = "Take other name" 35 | hotkey = "Ctrl+N" 36 | 37 | def __init__(self): 38 | super(RenameOther, self).__init__() 39 | 40 | def check(self, hx_view): 41 | return self.__extract_rename_info(hx_view.cfunc, hx_view.item) is not None 42 | 43 | def activate(self, ctx): 44 | hx_view = idaapi.get_widget_vdui(ctx.widget) 45 | result = self.__extract_rename_info(hx_view.cfunc, hx_view.item) 46 | 47 | if result: 48 | lvar, name = result 49 | while not hx_view.rename_lvar(lvar, name, True): 50 | name = '_' + name 51 | 52 | @staticmethod 53 | def __extract_rename_info(cfunc, ctree_item): 54 | # type: (idaapi.cfunc_t, idaapi.ctree_item_t) -> (idaapi.lvar_t, str) 55 | 56 | if ctree_item.citype != idaapi.VDI_EXPR: 57 | return 58 | 59 | expression = ctree_item.it.to_specific_type 60 | if expression.op != idaapi.cot_var: 61 | return 62 | 63 | parent = cfunc.body.find_parent_of(expression).to_specific_type 64 | if parent.op != idaapi.cot_asg: 65 | return 66 | 67 | other = parent.theother(expression) 68 | if other.op != idaapi.cot_var: 69 | return 70 | 71 | this_lvar = ctree_item.get_lvar() 72 | other_lvar = cfunc.get_lvars()[other.v.idx] 73 | 74 | if _should_be_renamed(this_lvar.name, other_lvar.name): 75 | return this_lvar, other_lvar.name.lstrip('_') 76 | 77 | 78 | class RenameInside(actions.HexRaysPopupAction): 79 | description = "Rename inside argument" 80 | hotkey = "Shift+N" 81 | 82 | def __init__(self): 83 | super(RenameInside, self).__init__() 84 | 85 | def check(self, hx_view): 86 | return self.__extract_rename_info(hx_view.cfunc, hx_view.item) is not None 87 | 88 | def activate(self, ctx): 89 | hx_view = idaapi.get_widget_vdui(ctx.widget) 90 | result = self.__extract_rename_info(hx_view.cfunc, hx_view.item) 91 | 92 | if result: 93 | func_tinfo, address, arg_index, name = result 94 | helper.set_func_arg_name(func_tinfo, arg_index, name) 95 | idaapi.apply_tinfo(address, func_tinfo, idaapi.TINFO_DEFINITE) 96 | hx_view.refresh_view(True) 97 | 98 | @staticmethod 99 | def __extract_rename_info(cfunc, ctree_item): 100 | # type: (idaapi.cfunc_t, idaapi.ctree_item_t) -> (idaapi.tinfo_t, long, int, str) 101 | 102 | if ctree_item.citype != idaapi.VDI_EXPR: 103 | return False 104 | 105 | expression = ctree_item.it.to_specific_type 106 | if expression.op != idaapi.cot_var: 107 | return 108 | 109 | parent = cfunc.body.find_parent_of(expression).to_specific_type 110 | if parent.op != idaapi.cot_call or parent.x.obj_ea == idaapi.BADADDR: 111 | return 112 | 113 | lvar = ctree_item.get_lvar() 114 | arg_index, _ = helper.get_func_argument_info(parent, expression) 115 | func_tinfo = parent.x.type.get_pointed_object() 116 | arg_name = helper.get_func_arg_name(func_tinfo, arg_index) 117 | if _should_be_renamed(arg_name, lvar.name): 118 | return func_tinfo, parent.x.obj_ea, arg_index, lvar.name.lstrip('_') 119 | 120 | 121 | class RenameOutside(actions.HexRaysPopupAction): 122 | description = "Take argument name" 123 | hotkey = "Ctrl+Shift+N" 124 | 125 | def __init__(self): 126 | super(RenameOutside, self).__init__() 127 | 128 | def check(self, hx_view): 129 | return self.__extract_rename_info(hx_view.cfunc, hx_view.item) is not None 130 | 131 | def activate(self, ctx): 132 | hx_view = idaapi.get_widget_vdui(ctx.widget) 133 | result = self.__extract_rename_info(hx_view.cfunc, hx_view.item) 134 | 135 | if result: 136 | lvar, name = result 137 | while not hx_view.rename_lvar(lvar, name, True): 138 | name = '_' + name 139 | 140 | @staticmethod 141 | def __extract_rename_info(cfunc, ctree_item): 142 | # type: (idaapi.cfunc_t, idaapi.ctree_item_t) -> (idaapi.lvar_t, str) 143 | 144 | if ctree_item.citype != idaapi.VDI_EXPR: 145 | return 146 | 147 | expression = ctree_item.it.to_specific_type 148 | if expression.op != idaapi.cot_var: 149 | return 150 | 151 | parent = cfunc.body.find_parent_of(expression).to_specific_type 152 | if parent.op != idaapi.cot_call or parent.x.obj_ea == idaapi.BADADDR: 153 | return 154 | 155 | lvar = ctree_item.get_lvar() 156 | arg_index, _ = helper.get_func_argument_info(parent, expression) 157 | func_tinfo = parent.x.type.get_pointed_object() 158 | arg_name = helper.get_func_arg_name(func_tinfo, arg_index) 159 | if arg_name and _should_be_renamed(lvar.name, arg_name): 160 | return lvar, arg_name.lstrip("_") 161 | 162 | class RenameMemberFromFunctionName(actions.HexRaysPopupAction): 163 | description = "Take name from function" 164 | hotkey = "Ctrl+N" 165 | 166 | def __init__(self): 167 | super(RenameMemberFromFunctionName, self).__init__() 168 | 169 | def check(self, hx_view): 170 | return self.__extract_rename_info(hx_view.cfunc, hx_view.item) is not None 171 | 172 | def activate(self, ctx): 173 | hx_view = idaapi.get_widget_vdui(ctx.widget) 174 | info = self.__extract_rename_info(hx_view.cfunc, hx_view.item) 175 | 176 | if info: 177 | mname = info.name; 178 | sname = re.sub('struct ', '', info.struct_name); 179 | 180 | if not re.search("_vtbl$", sname): 181 | mname = re.sub('^(get|set)*', 'm', mname, flags=re.IGNORECASE) 182 | 183 | if not helper.change_member_name(sname, info.offset, mname): 184 | mname = mname + '_' + hex(info.offset)[2:] 185 | helper.change_member_name(sname, info.offset, mname) 186 | 187 | hx_view.refresh_view(True) 188 | 189 | @staticmethod 190 | def __extract_rename_info(cfunc, ctree_item): 191 | # type: (idaapi.cfunc_t, idaapi.ctree_item_t) -> api.StructRefObject 192 | 193 | if ctree_item.citype != idaapi.VDI_EXPR: 194 | return 195 | 196 | expr = ctree_item.it.to_specific_type 197 | if expr.op == idaapi.cot_memptr: 198 | t = expr.x.type.get_pointed_object() 199 | elif expr.op == idaapi.cot_memref: 200 | t = expr.x.type 201 | else: 202 | return 203 | 204 | # Get name string 205 | result = api.StructRefObject(t.dstr(), expr.m) 206 | result.name = idaapi.get_name(cfunc.entry_ea) 207 | if idaapi.is_valid_typename(result.name): 208 | return result 209 | 210 | result.name = idc.demangle_name(result.name, idc.get_inf_attr(3)) # Get only main name 211 | if result.name is None: 212 | return 213 | 214 | result.name = re.sub('^.*:', '', result.name) 215 | if result.name is None: 216 | return 217 | 218 | return result 219 | 220 | class _RenameUsingAssertVisitor(idaapi.ctree_parentee_t): 221 | 222 | def __init__(self, cfunc, func_addr, arg_idx): 223 | idaapi.ctree_parentee_t.__init__(self) 224 | self.__cfunc = cfunc 225 | self.__func_addr = func_addr 226 | self.__arg_idx = arg_idx 227 | self.__possible_names = set() 228 | 229 | def visit_expr(self, expr): 230 | if expr.op == idaapi.cot_call and expr.x.op == idaapi.cot_obj and expr.x.obj_ea == self.__func_addr: 231 | arg_expr = expr.a[self.__arg_idx] 232 | if arg_expr.op != idaapi.cot_obj: 233 | cexpr_ea = helper.find_asm_address(expr, self.parents) 234 | logger.error("Argument is a not string at {}".format(helper.to_hex(cexpr_ea))) 235 | return 1 236 | self.__add_func_name(arg_expr) 237 | return 0 238 | 239 | def process(self): 240 | self.apply_to(self.__cfunc.body, None) 241 | if len(self.__possible_names) == 1: 242 | # Only one potential name was found, rename function using it 243 | new_name = self.__possible_names.pop() 244 | logging.info("Renaming function at {} to `{}`".format(helper.to_hex(self.__cfunc.entry_ea), new_name)) 245 | idc.set_name(self.__cfunc.entry_ea, new_name) 246 | elif len(self.__possible_names) > 1: 247 | logger.error("Function at {} has more than one candidate for renaming: {}".format( 248 | helper.to_hex(self.__cfunc.entry_ea), ", ".join(self.__possible_names))) 249 | 250 | def __add_func_name(self, arg_expr): 251 | new_name = idc.get_strlit_contents(arg_expr.obj_ea) 252 | if type(new_name) is not str: 253 | # convert bytes to str (python 3) 254 | new_name = new_name.decode('ascii') 255 | if not idaapi.is_valid_typename(new_name): 256 | logger.warn("Argument has a weird name `{}` at {}".format( 257 | new_name, helper.to_hex(helper.find_asm_address(arg_expr, self.parents)))) 258 | return 259 | 260 | self.__possible_names.add(new_name) 261 | 262 | 263 | class RenameUsingAssert(actions.HexRaysPopupAction): 264 | description = "Rename as assert argument" 265 | hotkey = None 266 | 267 | def __init__(self): 268 | super(RenameUsingAssert, self).__init__() 269 | 270 | @staticmethod 271 | def __can_be_part_of_assert(cfunc, ctree_item): 272 | # type: (idaapi.cfunc_t, idaapi.ctree_item_t) -> bool 273 | """ 274 | Returns true if expression we clicked is an argument passed to a function 275 | and this argument is a string that can be a valid function name 276 | """ 277 | 278 | if ctree_item.citype != idaapi.VDI_EXPR: 279 | return False 280 | 281 | expression = ctree_item.it.to_specific_type 282 | if expression.op != idaapi.cot_obj: 283 | return False 284 | 285 | parent = cfunc.body.find_parent_of(expression).to_specific_type 286 | if parent.op != idaapi.cot_call or parent.x.op != idaapi.cot_obj: 287 | return False 288 | 289 | obj_ea = expression.obj_ea 290 | if not helper.is_code_ea(obj_ea) and idc.get_str_type(obj_ea) == idc.STRTYPE_C: 291 | str_potential_name = idc.get_strlit_contents(obj_ea) 292 | if type(str_potential_name) is not str: 293 | # convert bytes to str (python 3) 294 | str_potential_name = str_potential_name.decode('ascii') 295 | return idaapi.is_valid_typename(str_potential_name) 296 | return False 297 | 298 | def check(self, hx_view): 299 | return self.__can_be_part_of_assert(hx_view.cfunc, hx_view.item) 300 | 301 | def activate(self, ctx): 302 | hx_view = idaapi.get_widget_vdui(ctx.widget) 303 | if not self.__can_be_part_of_assert(hx_view.cfunc, hx_view.item): 304 | return 305 | 306 | # So we clicked on function an func argument that is a string. Now we extract 307 | # argument index and address of assert function 308 | expr_arg = hx_view.item.it.to_specific_type 309 | expr_call = hx_view.cfunc.body.find_parent_of(expr_arg).to_specific_type 310 | arg_idx, _ = helper.get_func_argument_info(expr_call, expr_arg) 311 | assert_func_ea = expr_call.x.obj_ea 312 | 313 | # Iterate through all places where assert function and rename using helper class 314 | all_callers = helper.get_funcs_calling_address(assert_func_ea) 315 | for caller_ea in all_callers: 316 | cfunc = helper.decompile_function(caller_ea) 317 | if cfunc: 318 | _RenameUsingAssertVisitor(cfunc, assert_func_ea, arg_idx).process() 319 | 320 | hx_view.refresh_view(True) 321 | 322 | 323 | class _NamePropagator(api.RecursiveObjectDownwardsVisitor): 324 | def __init__(self, hx_view, cfunc, obj): 325 | super(_NamePropagator, self).__init__(cfunc, obj, skip_until_object=True) 326 | self.__hx_view = hx_view 327 | self.__propagated_name = obj.name 328 | 329 | def _start_iteration(self): 330 | self.__hx_view.switch_to(self._cfunc, False) 331 | 332 | def _manipulate(self, cexpr, obj): 333 | if self.crippled: 334 | logger.debug("Skipping crippled function at {}".format(helper.to_hex(self._cfunc.entry_ea))) 335 | return 336 | 337 | if obj.id == api.SO_GLOBAL_OBJECT: 338 | old_name = idaapi.get_short_name(cexpr.obj_ea) 339 | if settings.PROPAGATE_THROUGH_ALL_NAMES or _is_default_name(old_name): 340 | new_name = self.__rename_with_prefix( 341 | lambda x: idaapi.set_name(cexpr.obj_ea, x), 342 | self.__propagated_name) 343 | logger.debug("Renamed global variable from {} to {}".format(old_name, new_name)) 344 | elif obj.id == api.SO_LOCAL_VARIABLE: 345 | lvar = self._cfunc.get_lvars()[cexpr.v.idx] 346 | old_name = lvar.name 347 | if settings.PROPAGATE_THROUGH_ALL_NAMES or _is_default_name(old_name): 348 | new_name = self.__rename_with_prefix( 349 | lambda x: self.__hx_view.rename_lvar(lvar, x, True), 350 | self.__propagated_name) 351 | logger.debug("Renamed local variable from {} to {}".format(old_name, new_name)) 352 | elif obj.id in (api.SO_STRUCT_POINTER, api.SO_STRUCT_REFERENCE): 353 | struct_tinfo = cexpr.x.type 354 | offset = cexpr.m 355 | struct_tinfo.remove_ptr_or_array() 356 | old_name = helper.get_member_name(struct_tinfo, offset) 357 | if settings.PROPAGATE_THROUGH_ALL_NAMES or _is_default_name(old_name): 358 | new_name = self.__rename_with_prefix( 359 | lambda x: helper.change_member_name(struct_tinfo.dstr(), offset, x), 360 | self.__propagated_name) 361 | logger.debug("Renamed struct member from {} to {}".format(old_name, new_name)) 362 | 363 | def _finish(self): 364 | self.__hx_view.switch_to(self._cfunc, True) 365 | 366 | @staticmethod 367 | def __rename_with_prefix(rename_func, name): 368 | while not rename_func(name): 369 | name = "_" + name 370 | return name 371 | 372 | 373 | class PropagateName(actions.HexRaysPopupAction): 374 | description = "Propagate name" 375 | hotkey = "P" 376 | 377 | def __init__(self): 378 | super(PropagateName, self).__init__() 379 | 380 | @staticmethod 381 | def __extract_propagate_info(cfunc, ctree_item): 382 | if ctree_item.citype != idaapi.VDI_EXPR: 383 | return 384 | 385 | obj = api.ScanObject.create(cfunc, ctree_item) 386 | if obj and not _is_default_name(obj.name): 387 | return obj 388 | 389 | def check(self, hx_view): 390 | return self.__extract_propagate_info(hx_view.cfunc, hx_view.item) is not None 391 | 392 | def activate(self, ctx): 393 | hx_view = idaapi.get_widget_vdui(ctx.widget) 394 | obj = self.__extract_propagate_info(hx_view.cfunc, hx_view.item) 395 | if obj: 396 | cfunc = hx_view.cfunc 397 | visitor = _NamePropagator(hx_view, cfunc, obj) 398 | visitor.process() 399 | hx_view.refresh_view(True) 400 | 401 | 402 | actions.action_manager.register(RenameOther()) 403 | actions.action_manager.register(RenameInside()) 404 | actions.action_manager.register(RenameOutside()) 405 | actions.action_manager.register(RenameMemberFromFunctionName()) 406 | actions.action_manager.register(RenameUsingAssert()) 407 | actions.action_manager.register(PropagateName()) 408 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/scanners.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | from . import actions 3 | import HexRaysPyTools.api as api 4 | import HexRaysPyTools.core.cache as cache 5 | import HexRaysPyTools.core.helper as helper 6 | from ..core.variable_scanner import NewShallowSearchVisitor, NewDeepSearchVisitor, DeepReturnVisitor 7 | from ..core.temporary_structure import TemporaryStructureModel 8 | 9 | 10 | class Scanner(actions.HexRaysPopupAction): 11 | """ 12 | Abstract class containing common check of whether object can be scanned or not. 13 | Concrete class implement actual scan process in activate method 14 | """ 15 | def __init__(self): 16 | super(Scanner, self).__init__() 17 | 18 | def _can_be_scanned(self, cfunc, ctree_item): 19 | obj = api.ScanObject.create(cfunc, ctree_item) 20 | return obj and helper.is_legal_type(obj.tinfo) 21 | 22 | def check(self, hx_view): 23 | cfunc, ctree_item = hx_view.cfunc, hx_view.item 24 | return self._can_be_scanned(cfunc, ctree_item) 25 | 26 | 27 | class ShallowScanVariable(Scanner): 28 | description = "Scan Variable" 29 | hotkey = "F" 30 | 31 | def __init__(self): 32 | super(ShallowScanVariable, self).__init__() 33 | 34 | def activate(self, ctx): 35 | hx_view = idaapi.get_widget_vdui(ctx.widget) 36 | cfunc = hx_view.cfunc 37 | origin = cache.temporary_structure.main_offset 38 | 39 | if self._can_be_scanned(cfunc, hx_view.item): 40 | obj = api.ScanObject.create(cfunc, hx_view.item) 41 | visitor = NewShallowSearchVisitor(cfunc, origin, obj, cache.temporary_structure) 42 | visitor.process() 43 | 44 | 45 | class DeepScanVariable(Scanner): 46 | description = "Deep Scan Variable" 47 | hotkey = "Shift+Alt+F" 48 | 49 | def __init__(self): 50 | super(DeepScanVariable, self).__init__() 51 | 52 | def activate(self, ctx): 53 | hx_view = idaapi.get_widget_vdui(ctx.widget) 54 | cfunc = hx_view.cfunc 55 | origin = cache.temporary_structure.main_offset 56 | 57 | if self._can_be_scanned(cfunc, hx_view.item): 58 | obj = api.ScanObject.create(cfunc, hx_view.item) 59 | if helper.FunctionTouchVisitor(cfunc).process(): 60 | hx_view.refresh_view(True) 61 | visitor = NewDeepSearchVisitor(hx_view.cfunc, origin, obj, cache.temporary_structure) 62 | visitor.process() 63 | 64 | 65 | class RecognizeShape(Scanner): 66 | description = "Recognize Shape" 67 | 68 | def __init__(self): 69 | super(RecognizeShape, self).__init__() 70 | 71 | def activate(self, ctx): 72 | hx_view = idaapi.get_widget_vdui(ctx.widget) 73 | cfunc = hx_view.cfunc 74 | 75 | if not self._can_be_scanned(cfunc, hx_view.item): 76 | return 77 | 78 | obj = api.ScanObject.create(cfunc, hx_view.item) 79 | tmp_struct = TemporaryStructureModel() 80 | visitor = NewShallowSearchVisitor(cfunc, 0, obj, tmp_struct) 81 | visitor.process() 82 | tinfo = tmp_struct.get_recognized_shape() 83 | if tinfo: 84 | tinfo.create_ptr(tinfo) 85 | if obj.id == api.SO_LOCAL_VARIABLE: 86 | hx_view.set_lvar_type(obj.lvar, tinfo) 87 | elif obj.id == api.SO_GLOBAL_OBJECT: 88 | idaapi.apply_tinfo(obj.obj_ea, tinfo, idaapi.TINFO_DEFINITE) 89 | hx_view.refresh_view(True) 90 | 91 | 92 | class DeepScanReturn(Scanner): 93 | description = "Deep Scan Returned Variables" 94 | 95 | def __init__(self): 96 | super(DeepScanReturn, self).__init__() 97 | 98 | def check(self, hx_view): 99 | cfunc, ctree_item = hx_view.cfunc, hx_view.item 100 | if ctree_item.citype != idaapi.VDI_FUNC: 101 | return False 102 | tinfo = idaapi.tinfo_t() 103 | hx_view.cfunc.get_func_type(tinfo) 104 | return helper.is_legal_type(tinfo.get_rettype()) 105 | 106 | def activate(self, ctx): 107 | hx_view = idaapi.get_widget_vdui(ctx.widget) 108 | func_ea = hx_view.cfunc.entry_ea 109 | obj = api.ReturnedObject(func_ea) 110 | origin = cache.temporary_structure.main_offset 111 | visitor = DeepReturnVisitor(hx_view.cfunc, origin, obj, cache.temporary_structure) 112 | visitor.process() 113 | 114 | 115 | class DeepScanFunctions(actions.Action): 116 | description = "Scan First Argument" 117 | 118 | def __init__(self): 119 | super(DeepScanFunctions, self).__init__() 120 | 121 | def activate(self, ctx): 122 | for idx in ctx.chooser_selection: 123 | func_ea = idaapi.getn_func(idx - 1).start_ea 124 | cfunc = helper.decompile_function(func_ea) 125 | obj = api.VariableObject(cfunc.get_lvars()[0], 0) 126 | if cfunc: 127 | NewDeepSearchVisitor(cfunc, 0, obj, cache.temporary_structure).process() 128 | 129 | def update(self, ctx): 130 | if ctx.widget_type == idaapi.BWN_FUNCS: 131 | idaapi.attach_action_to_popup(ctx.widget, None, self.name) 132 | return idaapi.AST_ENABLE_FOR_WIDGET 133 | return idaapi.AST_DISABLE_FOR_WIDGET 134 | 135 | 136 | actions.action_manager.register(ShallowScanVariable()) 137 | actions.action_manager.register(DeepScanVariable()) 138 | actions.action_manager.register(RecognizeShape()) 139 | actions.action_manager.register(DeepScanReturn()) 140 | actions.action_manager.register(DeepScanFunctions()) 141 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/struct_xref_collector.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | 4 | import idaapi 5 | 6 | from . import callbacks 7 | import HexRaysPyTools.core.struct_xrefs as struct_xrefs 8 | import HexRaysPyTools.core.helper as helper 9 | 10 | logger = logging.getLogger(__name__) 11 | 12 | 13 | class StructXrefCollectorVisitor(idaapi.ctree_parentee_t): 14 | def __init__(self, cfunc, storage): 15 | super(StructXrefCollectorVisitor, self).__init__() 16 | self.__cfunc = cfunc 17 | self.__function_address = cfunc.entry_ea 18 | self.__result = {} 19 | self.__storage = storage 20 | 21 | def visit_expr(self, expression): 22 | # Checks if expression is reference by pointer or by value 23 | if expression.op == idaapi.cot_memptr: 24 | struct_type = expression.x.type.get_pointed_object() 25 | elif expression.op == idaapi.cot_memref: 26 | struct_type = expression.x.type 27 | else: 28 | return 0 29 | 30 | # Getting information about structure, field offset, address and one line corresponding to code 31 | ordinal = helper.get_ordinal(struct_type) 32 | field_offset = expression.m 33 | ea = self.__find_ref_address(expression) 34 | usage_type = self.__get_type(expression) 35 | 36 | if ea == idaapi.BADADDR or not ordinal: 37 | logger.warning("Failed to parse at address {0}, ordinal - {1}, type - {2}".format( 38 | helper.to_hex(ea), ordinal, struct_type.dstr() 39 | )) 40 | 41 | one_line = self.__get_line() 42 | 43 | occurrence_offset = ea - self.__function_address 44 | xref_info = (occurrence_offset, one_line, usage_type) 45 | 46 | # Saving results 47 | if ordinal not in self.__result: 48 | self.__result[ordinal] = {field_offset: [xref_info]} 49 | elif field_offset not in self.__result[ordinal]: 50 | self.__result[ordinal][field_offset] = [xref_info] 51 | else: 52 | self.__result[ordinal][field_offset].append(xref_info) 53 | return 0 54 | 55 | def process(self): 56 | t = time.time() 57 | self.apply_to(self.__cfunc.body, None) 58 | self.__storage.update(self.__function_address - idaapi.get_imagebase(), self.__result) 59 | 60 | storage_mb_size = len(self.__storage) * 1.0 // 1024 ** 2 61 | logger.debug("Xref processing: %f seconds passed, storage size - %.2f MB ", (time.time() - t), storage_mb_size) 62 | 63 | def __find_ref_address(self, cexpr): 64 | """ Returns most close virtual address corresponding to cexpr """ 65 | 66 | ea = cexpr.ea 67 | if ea != idaapi.BADADDR: 68 | return ea 69 | 70 | for p in reversed(self.parents): 71 | if p.ea != idaapi.BADADDR: 72 | return p.ea 73 | 74 | def __get_type(self, cexpr): 75 | """ Returns one of the following types: 'R' - read value, 'W' - write value, 'A' - function argument""" 76 | child = cexpr 77 | for p in reversed(self.parents): 78 | assert p, "Failed to get type at " + helper.to_hex(self.__function_address) 79 | 80 | if p.cexpr.op == idaapi.cot_call: 81 | return 'Arg' 82 | if not p.is_expr(): 83 | return 'R' 84 | if p.cexpr.op == idaapi.cot_asg: 85 | if p.cexpr.x == child: 86 | return 'W' 87 | return 'R' 88 | child = p.cexpr 89 | 90 | def __get_line(self): 91 | for p in reversed(self.parents): 92 | if not p.is_expr(): 93 | return idaapi.tag_remove(p.print1(self.__cfunc)) 94 | AssertionError("Parent instruction is not found") 95 | 96 | 97 | class StructXrefCollector(callbacks.HexRaysEventHandler): 98 | def __init__(self): 99 | super(StructXrefCollector, self).__init__() 100 | 101 | def handle(self, event, *args): 102 | cfunc, level_of_maturity = args 103 | if level_of_maturity == idaapi.CMAT_FINAL: 104 | StructXrefCollectorVisitor(cfunc, struct_xrefs.XrefStorage()).process() 105 | 106 | 107 | callbacks.hx_callback_manager.register(idaapi.hxe_maturity, StructXrefCollector()) 108 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/struct_xref_representation.py: -------------------------------------------------------------------------------- 1 | # encoding:utf-8 2 | import idaapi 3 | import idc 4 | 5 | from . import actions 6 | import HexRaysPyTools.core.helper as helper 7 | import HexRaysPyTools.core.struct_xrefs as struct_xrefs 8 | import HexRaysPyTools.forms as forms 9 | 10 | 11 | class FindFieldXrefs(actions.HexRaysXrefAction): 12 | description = "Field Xrefs" 13 | hotkey = "Ctrl+X" 14 | 15 | def __init__(self): 16 | super(FindFieldXrefs, self).__init__() 17 | 18 | #def check(self, hx_view):#old 19 | # return hx_view.item.citype == idaapi.VDI_EXPR and \ 20 | # hx_view.item.it.to_specific_type.op in (idaapi.cot_memptr, idaapi.cot_memref) 21 | 22 | def check(self,ctree_item): 23 | return ctree_item.citype == idaapi.VDI_EXPR and \ 24 | ctree_item.it.to_specific_type.op in (idaapi.cot_memptr, idaapi.cot_memref) 25 | 26 | def activate(self, ctx): 27 | ordinal = 0 28 | offset = 0 29 | data = [] 30 | struct_name='' 31 | field_name='' 32 | 33 | 34 | #print('activate widget_type: ',ctx.widget_type) 35 | 36 | if ctx.widget_type == idaapi.BWN_PSEUDOCODE:# pseudocode window 37 | hx_view = idaapi.get_widget_vdui(ctx.widget)#vdui_t 38 | item = hx_view.item 39 | if not self.check(item): 40 | return 41 | offset = item.e.m 42 | #print (item.e.x.type 43 | #print (dir(item.e.x.type); 44 | struct_type = idaapi.remove_pointer(item.e.x.type) 45 | #print (struct_type 46 | #print (dir(struct_type); 47 | ordinal = helper.get_ordinal(struct_type)#ordinal Id 48 | struct_name=struct_type.dstr() 49 | field_name=helper.get_member_name(struct_type, offset) 50 | 51 | if ctx.widget_type == idaapi.BWN_TILIST:#struct window ctrl+x 52 | #print (dir(ctx)); 53 | #print (dir(ctx.chooser_selection)); 54 | #print (dir(ctx.cur_struc));#struc_t * 55 | #print (type(ctx.cur_struc)) 56 | #print (dir(ctx.cur_strmem));#member_t * the current structure member 57 | ordinal= ctx.cur_struc.ordinal 58 | offset= ctx.cur_strmem.soff 59 | struct_name = idc.get_struc_name(ctx.cur_struc.id) 60 | field_name = idc.get_member_name(ctx.cur_strmem.id) 61 | 62 | 63 | result = struct_xrefs.XrefStorage().get_structure_info(ordinal, offset) 64 | for xref_info in result: 65 | data.append([ 66 | idaapi.get_short_name(xref_info.func_ea) + "+" + hex(int(xref_info.offset)), 67 | xref_info.type, 68 | xref_info.line 69 | ]) 70 | 71 | chooser = forms.MyChoose( 72 | data, 73 | "Cross-references to {0}::{1}".format(struct_name, field_name), 74 | [["Function", 20 | idaapi.Choose.CHCOL_PLAIN], 75 | ["Type", 2 | idaapi.Choose.CHCOL_PLAIN], 76 | ["Line", 40 | idaapi.Choose.CHCOL_PLAIN]] 77 | ) 78 | idx = chooser.Show(True) 79 | if idx == -1: 80 | return 81 | 82 | xref = result[idx] 83 | idaapi.open_pseudocode(xref.func_ea + xref.offset, False) 84 | 85 | actions.action_manager.register(FindFieldXrefs()) 86 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/structs_by_size.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | from . import actions 4 | import HexRaysPyTools.forms as forms 5 | import HexRaysPyTools.core.type_library as type_library 6 | 7 | 8 | def _choose_structure_by_size(size): 9 | result = type_library.choose_til() 10 | if result: 11 | selected_library, max_ordinal, is_local_type = result 12 | matched_types = [] 13 | tinfo = idaapi.tinfo_t() 14 | for ordinal in range(1, max_ordinal): 15 | tinfo.create_typedef(selected_library, ordinal) 16 | if tinfo.get_size() == size: 17 | name = tinfo.dstr() 18 | description = idaapi.print_tinfo(None, 0, 0, idaapi.PRTYPE_DEF, tinfo, None, None) 19 | matched_types.append([str(ordinal), name, description]) 20 | 21 | type_chooser = forms.MyChoose( 22 | matched_types, 23 | "Select Type", 24 | [["Ordinal", 5 | idaapi.Choose.CHCOL_HEX], ["Type Name", 25], ["Declaration", 50]], 25 | 165 26 | ) 27 | selected_type = type_chooser.Show(True) 28 | if selected_type != -1: 29 | if is_local_type: 30 | return int(matched_types[selected_type][0]) 31 | return type_library.import_type(selected_library, matched_types[selected_type][1]) 32 | return None 33 | 34 | 35 | class GetStructureBySize(actions.HexRaysPopupAction): 36 | # TODO: apply type automatically if expression like `var = new(size)` 37 | description = "Structures with this size" 38 | 39 | def __init__(self): 40 | super(GetStructureBySize, self).__init__() 41 | 42 | def check(self, hx_view): 43 | return hx_view.item.citype == idaapi.VDI_EXPR and hx_view.item.e.op == idaapi.cot_num 44 | 45 | def activate(self, ctx): 46 | hx_view = idaapi.get_widget_vdui(ctx.widget) 47 | if not self.check(hx_view): 48 | return 49 | ea = ctx.cur_ea 50 | c_number = hx_view.item.e 51 | number_value = c_number.numval() 52 | ordinal = _choose_structure_by_size(number_value) 53 | if ordinal: 54 | number_format_old = c_number.n.nf 55 | number_format_new = idaapi.number_format_t() 56 | number_format_new.flags = idaapi.FF_1STRO | idaapi.FF_0STRO 57 | operand_number = number_format_old.opnum 58 | number_format_new.opnum = operand_number 59 | number_format_new.props = number_format_old.props 60 | number_format_new.type_name = idaapi.get_numbered_type_name(idaapi.get_idati(), ordinal) 61 | 62 | c_function = hx_view.cfunc 63 | number_formats = c_function.numforms # type: idaapi.user_numforms_t 64 | # print "(number) flags: {0:#010X}, type_name: {1}, opnum: {2}".format( 65 | # number_format.flags, 66 | # number_format.type_name, 67 | # number_format.opnum 68 | # ) 69 | operand_locator = idaapi.operand_locator_t(ea, ord(operand_number) if operand_number else 0) 70 | if operand_locator in number_formats: 71 | del number_formats[operand_locator] 72 | 73 | number_formats[operand_locator] = number_format_new 74 | c_function.save_user_numforms() 75 | hx_view.refresh_view(True) 76 | 77 | actions.action_manager.register(GetStructureBySize()) 78 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/swap_if.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import idc 3 | 4 | from . import actions 5 | from . import callbacks 6 | 7 | 8 | def inverse_if_condition(cif): 9 | # cexpr_t has become broken but fortunately still exist `assing` method which copies one expr into another 10 | cit_if_condition = cif.expr 11 | tmp_cexpr = idaapi.cexpr_t() 12 | tmp_cexpr.assign(cit_if_condition) 13 | new_if_condition = idaapi.lnot(tmp_cexpr) 14 | cif.expr.swap(new_if_condition) 15 | del cit_if_condition 16 | 17 | 18 | def inverse_if(cif): 19 | inverse_if_condition(cif) 20 | idaapi.qswap(cif.ithen, cif.ielse) 21 | 22 | 23 | _ARRAY_STORAGE_PREFIX = "$HexRaysPyTools:IfThenElse:" 24 | 25 | 26 | def has_inverted(func_ea): 27 | # Find if function has any swapped THEN-ELSE branches 28 | internal_name = _ARRAY_STORAGE_PREFIX + hex(int(func_ea - idaapi.get_imagebase())) 29 | internal_id = idc.get_array_id(internal_name) 30 | return internal_id != -1 31 | 32 | 33 | def get_inverted(func_ea): 34 | # Returns set of relative virtual addresses which are tied to IF and swapped 35 | internal_name = _ARRAY_STORAGE_PREFIX + hex(int(func_ea - idaapi.get_imagebase())) 36 | internal_id = idc.get_array_id(internal_name) 37 | array = idc.get_array_element(idc.AR_STR, internal_id, 0) 38 | return set(map(int, array.split())) 39 | 40 | 41 | def invert(func_ea, if_ea): 42 | # Store information about swaps (affected through actions) 43 | iv_rva = if_ea - idaapi.get_imagebase() 44 | func_rva = func_ea - idaapi.get_imagebase() 45 | internal_name = _ARRAY_STORAGE_PREFIX + hex(int(func_rva)) 46 | internal_id = idc.get_array_id(internal_name) 47 | if internal_id == -1: 48 | internal_id = idc.create_array(internal_name) 49 | idc.set_array_string(internal_id, 0, str(iv_rva)) 50 | else: 51 | inverted = get_inverted(func_ea) 52 | try: 53 | inverted.remove(iv_rva) 54 | if not inverted: 55 | idc.delete_array(internal_id) 56 | 57 | except KeyError: 58 | inverted.add(iv_rva) 59 | 60 | idc.set_array_string(internal_id, 0, " ".join(map(str, inverted))) 61 | 62 | 63 | class SwapThenElse(actions.HexRaysPopupAction): 64 | description = "Swap then/else" 65 | hotkey = "Shift+Alt+S" # Shift+S is now "Split Variable" 66 | 67 | def __init__(self): 68 | super(SwapThenElse, self).__init__() 69 | 70 | def check(self, hx_view): 71 | # Checks if we clicked on IF and this if has both THEN and ELSE branches 72 | if hx_view.item.citype != idaapi.VDI_EXPR: 73 | return False 74 | insn = hx_view.item.it.to_specific_type 75 | if insn.op != idaapi.cit_if or insn.cif.ielse is None: 76 | return False 77 | return insn.op == idaapi.cit_if and insn.cif.ielse 78 | 79 | def activate(self, ctx): 80 | hx_view = idaapi.get_widget_vdui(ctx.widget) 81 | if self.check(hx_view): 82 | insn = hx_view.item.it.to_specific_type 83 | inverse_if(insn.cif) 84 | hx_view.refresh_ctext() 85 | 86 | invert(hx_view.cfunc.entry_ea, insn.ea) 87 | 88 | def update(self, ctx): 89 | if ctx.widget_type == idaapi.BWN_PSEUDOCODE: 90 | return idaapi.AST_ENABLE_FOR_WIDGET 91 | return idaapi.AST_DISABLE_FOR_WIDGET 92 | 93 | 94 | actions.action_manager.register(SwapThenElse()) 95 | 96 | 97 | class SwapThenElseVisitor(idaapi.ctree_parentee_t): 98 | def __init__(self, inverted): 99 | super(SwapThenElseVisitor, self).__init__() 100 | self.__inverted = inverted 101 | 102 | def visit_insn(self, insn): 103 | if insn.op != idaapi.cit_if or insn.cif.ielse is None: 104 | return 0 105 | 106 | if insn.ea in self.__inverted: 107 | inverse_if(insn.cif) 108 | 109 | return 0 110 | 111 | def apply_to(self, *args): 112 | if self.__inverted: 113 | super(SwapThenElseVisitor, self).apply_to(*args) 114 | 115 | 116 | class SpaghettiVisitor(idaapi.ctree_parentee_t): 117 | def __init__(self): 118 | super(SpaghettiVisitor, self).__init__() 119 | 120 | def visit_insn(self, instruction): 121 | if instruction.op != idaapi.cit_block: 122 | return 0 123 | 124 | while True: 125 | cblock = instruction.cblock 126 | size = cblock.size() 127 | # Find block that has "If" and "return" as last 2 statements 128 | if size < 2: 129 | break 130 | 131 | if cblock.at(size - 2).op != idaapi.cit_if: 132 | break 133 | 134 | cif = cblock.at(size - 2).cif 135 | if cblock.back().op != idaapi.cit_return or cif.ielse: 136 | break 137 | 138 | cit_then = cif.ithen 139 | 140 | # Skip if only one (not "if") statement in "then" branch 141 | if cit_then.cblock.size() == 1 and cit_then.cblock.front().op != idaapi.cit_if: 142 | return 0 143 | 144 | inverse_if_condition(cif) 145 | 146 | # Take return from list of statements and later put it back 147 | cit_return = idaapi.cinsn_t() 148 | cit_return.assign(instruction.cblock.back()) 149 | cit_return.thisown = False 150 | instruction.cblock.pop_back() 151 | 152 | # Fill main block with statements from "Then" branch 153 | while cit_then.cblock: 154 | instruction.cblock.push_back(cit_then.cblock.front()) 155 | cit_then.cblock.pop_front() 156 | 157 | # Put back main return if there's no another return or "GOTO" already 158 | if instruction.cblock.back().op not in (idaapi.cit_return, idaapi.cit_goto): 159 | new_return = idaapi.cinsn_t() 160 | new_return.thisown = False 161 | new_return.assign(cit_return) 162 | instruction.cblock.push_back(new_return) 163 | 164 | # Put return into "Then" branch 165 | cit_then.cblock.push_back(cit_return) 166 | return 0 167 | 168 | 169 | class SilentIfSwapper(callbacks.HexRaysEventHandler): 170 | 171 | def __init__(self): 172 | super(SilentIfSwapper, self).__init__() 173 | 174 | def handle(self, event, *args): 175 | cfunc, level_of_maturity = args 176 | if level_of_maturity == idaapi.CMAT_TRANS1 and has_inverted(cfunc.entry_ea): 177 | # Make RVA from VA of IF instructions that should be inverted 178 | inverted = [n + idaapi.get_imagebase() for n in get_inverted(cfunc.entry_ea)] 179 | visitor = SwapThenElseVisitor(inverted) 180 | visitor.apply_to(cfunc.body, None) 181 | elif level_of_maturity == idaapi.CMAT_TRANS2: 182 | visitor = SpaghettiVisitor() 183 | visitor.apply_to(cfunc.body, None) 184 | 185 | 186 | callbacks.hx_callback_manager.register(idaapi.hxe_maturity, SilentIfSwapper()) 187 | -------------------------------------------------------------------------------- /HexRaysPyTools/callbacks/virtual_table_creation.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | from . import actions 4 | from HexRaysPyTools.core.temporary_structure import VirtualTable 5 | 6 | 7 | class CreateVtable(actions.Action): 8 | description = "Create Virtual Table" 9 | hotkey = "V" 10 | 11 | def __init__(self): 12 | super(CreateVtable, self).__init__() 13 | 14 | @staticmethod 15 | def check(ea): 16 | return ea != idaapi.BADADDR and VirtualTable.check_address(ea) 17 | 18 | def activate(self, ctx): 19 | ea = ctx.cur_ea 20 | if self.check(ea): 21 | vtable = VirtualTable(0, ea) 22 | vtable.import_to_structures(True) 23 | 24 | def update(self, ctx): 25 | if ctx.widget_type == idaapi.BWN_DISASM: 26 | if self.check(ctx.cur_ea): 27 | idaapi.attach_action_to_popup(ctx.widget, None, self.name) 28 | return idaapi.AST_ENABLE 29 | idaapi.detach_action_from_popup(ctx.widget, self.name) 30 | return idaapi.AST_DISABLE 31 | return idaapi.AST_DISABLE_FOR_WIDGET 32 | 33 | 34 | actions.action_manager.register(CreateVtable()) 35 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/HexRaysPyTools/core/__init__.py -------------------------------------------------------------------------------- /HexRaysPyTools/core/cache.py: -------------------------------------------------------------------------------- 1 | import collections 2 | 3 | import idaapi 4 | import idautils 5 | import idc 6 | 7 | from . import common 8 | 9 | # All virtual addresses where imported by module function pointers are stored 10 | imported_ea = set() 11 | 12 | # Map from demangled and simplified to C-language compatible names of functions to their addresses 13 | demangled_names = collections.defaultdict(set) 14 | 15 | # Functions that went through "touching" decompilation. This is done before Deep Scanning and 16 | # enhance arguments parsing for subroutines called by scanned functions. 17 | touched_functions = set() 18 | 19 | # This is where all information about structure being reconstructed stored 20 | # TODO: Make some way to store several structures and switch between them. See issue #22 (3) 21 | temporary_structure = None # type: temporary_structure.TemporaryStructureModel 22 | 23 | 24 | def _init_imported_ea(): 25 | 26 | def imp_cb(ea, name, ord): 27 | imported_ea.add(ea - idaapi.get_imagebase()) 28 | # True -> Continue enumeration 29 | # False -> Stop enumeration 30 | return True 31 | 32 | print("[Info] Collecting information about imports") 33 | imported_ea.clear() 34 | nimps = idaapi.get_import_module_qty() 35 | 36 | for i in range(0, nimps): 37 | name = idaapi.get_import_module_name(i) 38 | if not name: 39 | print("[Warning] Failed to get import module name for #%d" % i) 40 | continue 41 | 42 | # print "Walking-> %s" % name 43 | idaapi.enum_import_names(i, imp_cb) 44 | print("[Info] Done...") 45 | 46 | 47 | def _init_demangled_names(): 48 | """ 49 | Creates dictionary of demangled names => set of address, that will be used further when user makes double click 50 | on methods in Decompiler output. 51 | """ 52 | demangled_names.clear() 53 | for address, name in idautils.Names(): 54 | short_name = idc.demangle_name(name, idc.INF_SHORT_DN) 55 | if short_name: 56 | short_name = common.demangled_name_to_c_str(short_name) 57 | demangled_names[short_name].add(address - idaapi.get_imagebase()) 58 | print("[DEBUG] Demangled names have been initialized") 59 | 60 | 61 | def _reset_touched_functions(*args): 62 | global touched_functions 63 | 64 | touched_functions = set() 65 | 66 | 67 | def initialize_cache(*args): 68 | global temporary_structure 69 | 70 | _init_demangled_names() 71 | _init_imported_ea() 72 | _reset_touched_functions() 73 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/common.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | 4 | BAD_C_NAME_PATTERN = re.compile(":::+|(?=:(?=[^:]))(?=(?<=[^:]):):|^:[^:]|[^:]:$|^:$|[^a-zA-Z_0-9:]") 5 | 6 | 7 | def demangled_name_to_c_str(name): 8 | """ 9 | Removes or replaces characters from demangled symbol so that it was possible to create legal C structure from it 10 | """ 11 | if not BAD_C_NAME_PATTERN.findall(name): 12 | return name 13 | 14 | # filter `vtable and `typeinfo 15 | if name.startswith('`'): 16 | return name 17 | 18 | # FIXME: This is very ugly way to find and replace illegal characters 19 | idx = name.find("::operator") 20 | if idx >= 0: 21 | idx += len("::operator") 22 | if idx == len(name) or name[idx].isalpha(): 23 | # `operator` is part of name of some name and not a keyword 24 | pass 25 | elif name[idx:idx + 2] == "==": 26 | name = name.replace("operator==", "operator_EQ_") 27 | elif name[idx:idx + 2] == "!=": 28 | name = name.replace("operator!=", "operator_NEQ_") 29 | elif name[idx] == "=": 30 | name = name.replace("operator=", "operator_ASSIGN_") 31 | elif name[idx:idx + 2] == "+=": 32 | name = name.replace("operator+=", "operator_PLUS_ASSIGN_") 33 | elif name[idx:idx + 2] == "-=": 34 | name = name.replace("operator-=", "operator_MINUS_ASSIGN_") 35 | elif name[idx:idx + 2] == "*=": 36 | name = name.replace("operator*=", "operator_MUL_ASSIGN_") 37 | elif name[idx:idx + 2] == "/=": 38 | name = name.replace("operator/=", "operator_DIV_ASSIGN_") 39 | elif name[idx:idx + 2] == "%=": 40 | name = name.replace("operator%=", "operator_MODULO_DIV_ASSIGN_") 41 | elif name[idx:idx + 2] == "|=": 42 | name = name.replace("operator|=", "operator_OR_ASSIGN_") 43 | elif name[idx:idx + 2] == "&=": 44 | name = name.replace("operator&=", "operator_AND_ASSIGN_") 45 | elif name[idx:idx + 2] == "^=": 46 | name = name.replace("operator^=", "operator_XOR_ASSIGN_") 47 | elif name[idx:idx + 3] == "<<=": 48 | name = name.replace("operator<<=", "operator_LEFT_SHIFT_ASSIGN_") 49 | elif name[idx:idx + 3] == ">>=": 50 | name = name.replace("operator>>=", "operator_RIGHT_SHIFT_ASSIGN_") 51 | elif name[idx:idx + 2] == "++": 52 | name = name.replace("operator++", "operator_INC_") 53 | elif name[idx:idx + 2] == "--": 54 | name = name.replace("operator--", "operator_PTR_") 55 | elif name[idx:idx + 2] == "->": 56 | name = name.replace("operator->", "operator_REF_") 57 | elif name[idx:idx + 2] == "[]": 58 | name = name.replace("operator[]", "operator_IDX_") 59 | elif name[idx] == "*": 60 | name = name.replace("operator*", "operator_STAR_") 61 | elif name[idx:idx + 2] == "&&": 62 | name = name.replace("operator&&", "operator_LAND_") 63 | elif name[idx:idx + 2] == "||": 64 | name = name.replace("operator||", "operator_LOR_") 65 | elif name[idx] == "!": 66 | name = name.replace("operator!", "operator_LNOT_") 67 | elif name[idx] == "&": 68 | name = name.replace("operator&", "operator_AND_") 69 | elif name[idx] == "|": 70 | name = name.replace("operator|", "operator_OR_") 71 | elif name[idx] == "^": 72 | name = name.replace("operator^", "operator_XOR_") 73 | elif name[idx:idx + 2] == "<<": 74 | name = name.replace("operator<<", "operator_LEFT_SHIFT_") 75 | elif name[idx:idx + 2] == ">>": 76 | name = name.replace("operator>", "operator_GREATER_") 77 | elif name[idx:idx + 2] == "<=": 78 | name = name.replace("operator<=", "operator_LESS_EQUAL_") 79 | elif name[idx:idx + 2] == ">=": 80 | name = name.replace("operator>>", "operator_RIGHT_SHIFT_") 81 | elif name[idx] == "<": 82 | name = name.replace("operator<", "operator_LESS_") 83 | elif name[idx] == ">": 84 | name = name.replace("operator>=", "operator_GREATER_EQUAL_") 85 | elif name[idx] == "+": 86 | name = name.replace("operator+", "operator_ADD_") 87 | elif name[idx] == "-": 88 | name = name.replace("operator-", "operator_SUB_") 89 | elif name[idx] == "/": 90 | name = name.replace("operator/", "operator_DIV_") 91 | elif name[idx] == "%": 92 | name = name.replace("operator%", "operator_MODULO_DIV_") 93 | elif name[idx:idx + 2] == "()": 94 | name = name.replace("operator()", "operator_CALL_") 95 | elif name[idx: idx + 6] == " new[]": 96 | name = name.replace("operator new[]", "operator_NEW_ARRAY_") 97 | elif name[idx: idx + 9] == " delete[]": 98 | name = name.replace("operator delete[]", "operator_DELETE_ARRAY_") 99 | elif name[idx: idx + 4] == " new": 100 | name = name.replace("operator new", "operator_NEW_") 101 | elif name[idx: idx + 7] == " delete": 102 | name = name.replace("operator delete", "operator_DELETE_") 103 | elif name[idx:idx + 2] == "\"\" ": 104 | name = name.replace("operator\"\" ", "operator_LITERAL_") 105 | elif name[idx] == "~": 106 | name = name.replace("operator~", "operator_NOT_") 107 | elif name[idx] == ' ': 108 | pass 109 | else: 110 | raise AssertionError("Replacement of demangled string by c-string for keyword `operatorXXX` is not yet" 111 | "implemented ({}). You can do it by yourself or create an issue".format(name)) 112 | 113 | name = name.replace("public:", "") 114 | name = name.replace("protected:", "") 115 | name = name.replace("private:", "") 116 | name = name.replace("~", "DESTRUCTOR_") 117 | name = name.replace("*", "_PTR") 118 | name = name.replace("<", "_t_") 119 | name = name.replace(">", "_t_") 120 | name = "_".join(filter(len, BAD_C_NAME_PATTERN.split(name))) 121 | return name 122 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/const.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | 3 | EA64 = None 4 | EA_SIZE = None 5 | 6 | COT_ARITHMETIC = (idaapi.cot_num, idaapi.cot_fnum, idaapi.cot_add, idaapi.cot_fadd, idaapi.cot_sub, idaapi.cot_fsub, 7 | idaapi.cot_mul, idaapi.cot_fmul, idaapi.cot_fdiv) 8 | 9 | VOID_TINFO = None 10 | PVOID_TINFO = idaapi.tinfo_t() 11 | CONST_VOID_TINFO = None 12 | CONST_PVOID_TINFO = idaapi.tinfo_t() 13 | CHAR_TINFO = None 14 | PCHAR_TINFO = idaapi.tinfo_t() 15 | CONST_PCHAR_TINFO = idaapi.tinfo_t() 16 | BYTE_TINFO = None 17 | PBYTE_TINFO = None 18 | 19 | WORD_TINFO = None 20 | PWORD_TINFO = idaapi.tinfo_t() 21 | 22 | X_WORD_TINFO = None # DWORD for x32 and QWORD for x64 23 | PX_WORD_TINFO = None 24 | 25 | DUMMY_FUNC = None 26 | 27 | LEGAL_TYPES = [] 28 | 29 | 30 | def init(): 31 | """ All tinfo should be reinitialized between session. Otherwise they could have wrong type """ 32 | global VOID_TINFO, PVOID_TINFO, CONST_PVOID_TINFO, BYTE_TINFO, PBYTE_TINFO, LEGAL_TYPES, X_WORD_TINFO, \ 33 | PX_WORD_TINFO, DUMMY_FUNC, CONST_PCHAR_TINFO, CHAR_TINFO, PCHAR_TINFO, CONST_VOID_TINFO, \ 34 | WORD_TINFO, PWORD_TINFO, EA64, EA_SIZE 35 | 36 | EA64 = idaapi.inf_is_64bit() 37 | EA_SIZE = 8 if EA64 else 4 38 | 39 | VOID_TINFO = idaapi.tinfo_t(idaapi.BT_VOID) 40 | PVOID_TINFO.create_ptr(VOID_TINFO) 41 | CONST_VOID_TINFO = idaapi.tinfo_t(idaapi.BT_VOID | idaapi.BTM_CONST) 42 | CONST_PVOID_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BT_VOID | idaapi.BTM_CONST)) 43 | CONST_PCHAR_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BTF_CHAR | idaapi.BTM_CONST)) 44 | CHAR_TINFO = idaapi.tinfo_t(idaapi.BTF_CHAR) 45 | PCHAR_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BTF_CHAR)) 46 | BYTE_TINFO = idaapi.tinfo_t(idaapi.BTF_BYTE) 47 | PBYTE_TINFO = idaapi.dummy_ptrtype(1, False) 48 | X_WORD_TINFO = idaapi.get_unk_type(EA_SIZE) 49 | PX_WORD_TINFO = idaapi.dummy_ptrtype(EA_SIZE, False) 50 | 51 | WORD_TINFO = idaapi.tinfo_t(idaapi.BT_UNK_WORD) 52 | PWORD_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BT_UNK_WORD)) 53 | 54 | func_data = idaapi.func_type_data_t() 55 | func_data.rettype = PVOID_TINFO 56 | func_data.cc = idaapi.CM_CC_UNKNOWN 57 | DUMMY_FUNC = idaapi.tinfo_t() 58 | DUMMY_FUNC.create_func(func_data, idaapi.BT_FUNC) 59 | 60 | LEGAL_TYPES = [PVOID_TINFO, PX_WORD_TINFO, PWORD_TINFO, PBYTE_TINFO, X_WORD_TINFO] 61 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/helper.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import logging 3 | 4 | import idaapi 5 | import idc 6 | import ida_ida 7 | 8 | import HexRaysPyTools.core.cache as cache 9 | import HexRaysPyTools.core.const as const 10 | import HexRaysPyTools.settings as settings 11 | import HexRaysPyTools.forms as forms 12 | 13 | 14 | logger = logging.getLogger(__name__) 15 | 16 | 17 | def is_imported_ea(ea): 18 | if idc.get_segm_name(ea) == ".plt": 19 | return True 20 | return ea + idaapi.get_imagebase() in cache.imported_ea 21 | 22 | 23 | def is_code_ea(ea): 24 | if ida_ida.inf_get_procname() == "ARM": 25 | # In case of ARM code in THUMB mode we sometimes get pointers with thumb bit set 26 | flags = idaapi.get_full_flags(ea & -2) # flags_t 27 | else: 28 | flags = idaapi.get_full_flags(ea) 29 | return idaapi.is_code(flags) 30 | 31 | 32 | def is_rw_ea(ea): 33 | seg = idaapi.getseg(ea) 34 | return seg.perm & idaapi.SEGPERM_WRITE and seg.perm & idaapi.SEGPERM_READ 35 | 36 | 37 | def get_ptr(ea): 38 | """ Reads ptr at specified address. """ 39 | if const.EA64: 40 | return idaapi.get_64bit(ea) 41 | ptr = idaapi.get_32bit(ea) 42 | if ida_ida.inf_get_procname() == "ARM": 43 | ptr &= -2 # Clear thumb bit 44 | return ptr 45 | 46 | 47 | def get_ordinal(tinfo): 48 | """ Returns non-zero ordinal of tinfo if it exist in database """ 49 | ordinal = tinfo.get_ordinal() 50 | if ordinal == 0: 51 | t = idaapi.tinfo_t() 52 | struct_name = tinfo.dstr().split()[-1] # Get rid of `struct` prefix or something else 53 | t.get_named_type(idaapi.get_idati(), struct_name) 54 | ordinal = t.get_ordinal() 55 | return ordinal 56 | 57 | 58 | def get_virtual_func_addresses(name, tinfo=None, offset=None): 59 | """ 60 | Returns set of possible addresses of virtual function by its name. 61 | If there're symbols in binary and name is the name of an overloaded function, then returns list of all address of 62 | this overloaded function. 63 | TODO: After implementing inheritance return set of methods of all child classes 64 | 65 | :param name: method name, can be mangled 66 | :param tinfo: class tinfo to which this method belong 67 | :param offset: virtual table offset 68 | :return: list of possible addresses 69 | """ 70 | 71 | address = idc.get_name_ea_simple(name) 72 | 73 | if address != idaapi.BADADDR: 74 | return [address] 75 | 76 | raw_addresses = cache.demangled_names.get(name) 77 | if raw_addresses: 78 | addresses = [ea + idaapi.get_imagebase() for ea in raw_addresses] 79 | return addresses 80 | 81 | if tinfo is None or offset is None: 82 | return [] 83 | 84 | offset *= 8 85 | udt_member = idaapi.udt_member_t() 86 | while tinfo.is_struct(): 87 | address = cache.demangled_names.get(tinfo.dstr() + '::' + name, idaapi.BADADDR) 88 | if address != idaapi.BADADDR: 89 | return [address + idaapi.get_imagebase()] 90 | udt_member.offset = offset 91 | tinfo.find_udt_member(udt_member, idaapi.STRMEM_OFFSET) 92 | tinfo = udt_member.type 93 | offset = offset - udt_member.offset 94 | 95 | 96 | def choose_virtual_func_address(name, tinfo=None, offset=None): 97 | addresses = get_virtual_func_addresses(name, tinfo, offset) 98 | if not addresses: 99 | return 100 | 101 | if len(addresses) == 1: 102 | return addresses[0] 103 | 104 | chooser = forms.MyChoose( 105 | [[to_hex(ea), idc.demangle_name(idc.get_name(ea), idc.INF_LONG_DN)] for ea in addresses], 106 | "Select Function", 107 | [["Address", 10], ["Full name", 50]] 108 | ) 109 | idx = chooser.Show(modal=True) 110 | if idx != -1: 111 | return addresses[idx] 112 | 113 | 114 | def get_func_argument_info(function, expression): 115 | """ 116 | Function is cexpr with opname == 'cot_call', expression is any son. Returns index of argument and it's type 117 | 118 | :param function: idaapi.cexpr_t 119 | :param expression: idaapi.cexpr_t 120 | :return: (int, idaapi.tinfo_t) 121 | """ 122 | for idx, argument in enumerate(function.a): 123 | if expression == argument.cexpr: 124 | func_tinfo = function.x.type 125 | if idx < func_tinfo.get_nargs(): 126 | return idx, func_tinfo.get_nth_arg(idx) 127 | return idx, None 128 | print("[ERROR] Wrong usage of 'Helper.get_func_argument_info()'") 129 | 130 | 131 | def set_func_argument(func_tinfo, index, arg_tinfo): 132 | func_data = idaapi.func_type_data_t() 133 | func_tinfo.get_func_details(func_data) 134 | func_data[index].type = arg_tinfo 135 | func_tinfo.create_func(func_data) 136 | 137 | 138 | def get_func_arg_name(func_tinfo, arg_idx): 139 | # type: (idaapi.tinfo_t, int) -> str 140 | 141 | func_data = idaapi.func_type_data_t() 142 | func_tinfo.get_func_details(func_data) 143 | if arg_idx < func_tinfo.get_nargs(): 144 | return func_data[arg_idx].name 145 | 146 | 147 | def set_func_arg_name(func_tinfo, arg_idx, name): 148 | # type: (idaapi.tinfo_t, int, str) -> None 149 | 150 | func_data = idaapi.func_type_data_t() 151 | func_tinfo.get_func_details(func_data) 152 | func_data[arg_idx].name = name 153 | func_tinfo.create_func(func_data) 154 | 155 | 156 | def set_funcptr_argument(funcptr_tinfo, index, arg_tinfo): 157 | func_tinfo = funcptr_tinfo.get_pointed_object() 158 | set_func_argument(func_tinfo, index, arg_tinfo) 159 | funcptr_tinfo.create_ptr(func_tinfo) 160 | 161 | 162 | def set_func_return(func_tinfo, return_tinfo): 163 | func_data = idaapi.func_type_data_t() 164 | func_tinfo.get_func_details(func_data) 165 | func_data.rettype = return_tinfo 166 | func_tinfo.create_func(func_data) 167 | 168 | 169 | def get_nice_pointed_object(tinfo): 170 | """ 171 | Returns nice pointer name (if exist) or None. 172 | For example if tinfo is PKSPIN_LOCK which is typedef of unsigned int *, then if in local types exist KSPIN_LOCK with 173 | type unsigned int, this function returns KSPIN_LOCK 174 | """ 175 | try: 176 | name = tinfo.dstr() 177 | if name[0] == 'P': 178 | pointed_tinfo = idaapi.tinfo_t() 179 | if pointed_tinfo.get_named_type(idaapi.get_idati(), name[1:]): 180 | if tinfo.get_pointed_object().equals_to(pointed_tinfo): 181 | return pointed_tinfo 182 | except TypeError: 183 | pass 184 | 185 | 186 | def get_fields_at_offset(tinfo, offset): 187 | """ 188 | Given tinfo and offset of the structure or union, returns list of all tinfo at that offset. 189 | This function helps to find appropriate structures by type of the offset 190 | """ 191 | result = [] 192 | if offset == 0: 193 | result.append(tinfo) 194 | udt_data = idaapi.udt_type_data_t() 195 | tinfo.get_udt_details(udt_data) 196 | udt_member = idaapi.udt_member_t() 197 | udt_member.offset = offset * 8 198 | idx = tinfo.find_udt_member(udt_member, idaapi.STRMEM_OFFSET) 199 | if idx != -1: 200 | while idx < tinfo.get_udt_nmembers() and udt_data[idx].offset <= offset * 8: 201 | udt_member = udt_data[idx] 202 | if udt_member.offset == offset * 8: 203 | if udt_member.type.is_ptr(): 204 | result.append(idaapi.get_unk_type(const.EA_SIZE)) 205 | result.append(udt_member.type) 206 | result.append(idaapi.dummy_ptrtype(const.EA_SIZE, False)) 207 | elif not udt_member.type.is_udt(): 208 | result.append(udt_member.type) 209 | if udt_member.type.is_array(): 210 | if (offset - udt_member.offset // 8) % udt_member.type.get_array_element().get_size() == 0: 211 | result.append(udt_member.type.get_array_element()) 212 | elif udt_member.type.is_udt(): 213 | result.extend(get_fields_at_offset(udt_member.type, offset - udt_member.offset // 8)) 214 | idx += 1 215 | return result 216 | 217 | def is_legal_type(tinfo: idaapi.tinfo_t): 218 | tinfo.clr_const() 219 | if tinfo.is_ptr() and tinfo.get_pointed_object().is_forward_decl(): 220 | is_bad_size = tinfo.get_pointed_object().get_size() == idaapi.BADSIZE 221 | print(f"[DEBUG] Type {tinfo.dstr()} is forward declaration: {is_bad_size}") 222 | return is_bad_size 223 | if tinfo.is_unknown(): 224 | print(f"[DEBUG] Type {tinfo.dstr()} is unknown") 225 | return False 226 | return True 227 | 228 | # This function after 9.0 nearly always returns False 229 | # def is_legal_type(tinfo: idaapi.tinfo_t): 230 | # tinfo.clr_const() 231 | 232 | # if tinfo.is_ptr() and tinfo.get_pointed_object().is_forward_decl(): 233 | # is_bad_size = tinfo.get_pointed_object().get_size() == idaapi.BADSIZE 234 | # print(f"[DEBUG] Type {tinfo.dstr()} is forward declaration: {is_bad_size}") 235 | # return is_bad_size 236 | # legal_type = settings.SCAN_ANY_TYPE or bool([x for x in const.LEGAL_TYPES if x.equals_to(tinfo)]) 237 | # legal_type = tinfo.is_ptr() 238 | # print(f"[DEBUG] Type {tinfo.dstr()} is legal: {legal_type}") 239 | # return legal_type 240 | 241 | 242 | def search_duplicate_fields(udt_data): 243 | # Returns list of lists with duplicate fields 244 | 245 | default_dict = collections.defaultdict(list) 246 | for idx, udt_member in enumerate(udt_data): 247 | default_dict[udt_member.name].append(idx) 248 | return [indices for indices in list(default_dict.values()) if len(indices) > 1] 249 | 250 | 251 | def get_member_name(tinfo, offset): 252 | udt_member = idaapi.udt_member_t() 253 | udt_member.offset = offset * 8 254 | tinfo.find_udt_member(udt_member, idaapi.STRMEM_OFFSET) 255 | return udt_member.name 256 | 257 | 258 | def change_member_name(struct_name, offset, name): 259 | return idc.set_member_name(idc.get_struc_id(struct_name), offset, name) 260 | 261 | 262 | def import_structure(name, tinfo): 263 | cdecl_typedef = idaapi.print_tinfo(None, 4, 5, idaapi.PRTYPE_MULTI | idaapi.PRTYPE_TYPE | idaapi.PRTYPE_SEMI, 264 | tinfo, name, None) 265 | if idc.parse_decl(cdecl_typedef, idaapi.PT_TYP) is None: 266 | return 0 267 | 268 | previous_ordinal = idaapi.get_type_ordinal(idaapi.get_idati(), name) 269 | if previous_ordinal: 270 | idaapi.del_numbered_type(idaapi.get_idati(), previous_ordinal) 271 | ordinal = idaapi.idc_set_local_type(previous_ordinal, cdecl_typedef, idaapi.PT_TYP) 272 | else: 273 | ordinal = idaapi.idc_set_local_type(-1, cdecl_typedef, idaapi.PT_TYP) 274 | return ordinal 275 | 276 | 277 | def get_funcs_calling_address(ea): 278 | """ Returns all addresses of functions which make call to a function at `ea`""" 279 | xref_ea = idaapi.get_first_cref_to(ea) 280 | xrefs = set() 281 | while xref_ea != idaapi.BADADDR: 282 | xref_func_ea = idc.get_func_attr(xref_ea, idc.FUNCATTR_START) 283 | if xref_func_ea != idaapi.BADADDR: 284 | xrefs.add(xref_func_ea) 285 | else: 286 | print("[Warning] Function not found at 0x{0:08X}".format(xref_ea)) 287 | xref_ea = idaapi.get_next_cref_to(ea, xref_ea) 288 | return xrefs 289 | 290 | 291 | class FunctionTouchVisitor(idaapi.ctree_parentee_t): 292 | def __init__(self, cfunc): 293 | super(FunctionTouchVisitor, self).__init__() 294 | self.functions = set() 295 | self.cfunc = cfunc 296 | 297 | def visit_expr(self, expression): 298 | if expression.op == idaapi.cot_call: 299 | self.functions.add(expression.x.obj_ea) 300 | return 0 301 | 302 | def touch_all(self): 303 | diff = self.functions.difference(cache.touched_functions) 304 | for address in diff: 305 | if is_imported_ea(address): 306 | continue 307 | try: 308 | cfunc = idaapi.decompile(address) 309 | if cfunc: 310 | FunctionTouchVisitor(cfunc).process() 311 | except idaapi.DecompilationFailure: 312 | logger.warn("IDA failed to decompile function at {}".format(to_hex(address))) 313 | cache.touched_functions.add(address) 314 | idaapi.decompile(self.cfunc.entry_ea) 315 | 316 | def process(self): 317 | if self.cfunc.entry_ea not in cache.touched_functions: 318 | cache.touched_functions.add(self.cfunc.entry_ea) 319 | self.apply_to(self.cfunc.body, None) 320 | self.touch_all() 321 | return True 322 | return False 323 | 324 | 325 | def to_hex(ea): 326 | """ Formats address so it could be double clicked at console """ 327 | if const.EA64: 328 | return "0x{:016X}".format(ea) 329 | return "0x{:08X}".format(ea) 330 | 331 | 332 | def to_nice_str(ea): 333 | """ Shows address as function name + offset """ 334 | func_start_ea = idc.get_func_attr(ea, idc.FUNCATTR_START) 335 | func_name = idc.get_name(func_start_ea) 336 | offset = ea - func_start_ea 337 | return "{}+0x{:X}".format(func_name, offset) 338 | 339 | 340 | def save_long_str_to_idb(array_name, value): 341 | """ Overwrites old array completely in process """ 342 | id = idc.get_array_id(array_name) 343 | if id != -1: 344 | idc.delete_array(id) 345 | id = idc.create_array(array_name) 346 | r = [] 347 | for idx in range(len(value) // 1024 + 1): 348 | s = value[idx * 1024: (idx + 1) * 1024] 349 | r.append(s) 350 | idc.set_array_string(id, idx, s) 351 | 352 | 353 | def load_long_str_from_idb(array_name): 354 | id = idc.get_array_id(array_name) 355 | if id == -1: 356 | return None 357 | max_idx = idc.get_last_index(idc.AR_STR, id) 358 | result = [] 359 | for idx in range(max_idx + 1): 360 | e = idc.get_array_element(idc.AR_STR, id, idx) 361 | if type(e) == int: 362 | e = e.to_bytes((e.bit_length() + 7) // 8, 'little') 363 | result.append(e) 364 | return b"".join(result).decode("utf-8") 365 | 366 | def create_padding_udt_member(offset, size): 367 | # type: (long, long) -> idaapi.udt_member_t 368 | """ Creates internal IDA structure with name gap_XXX and appropriate size and offset """ 369 | 370 | udt_member = idaapi.udt_member_t() 371 | udt_member.name = "gap_{0:X}".format(offset) 372 | udt_member.offset = offset 373 | udt_member.size = size 374 | 375 | if size == 1: 376 | udt_member.type = const.BYTE_TINFO 377 | else: 378 | if size < 1 or size > 0xffffffff: 379 | print("HexRaysPyTools::core::helper::create_padding_udt_member: size is out of uint32 range (offset:{} size:{})".format(offset, size)) 380 | array_data = idaapi.array_type_data_t() 381 | array_data.base = 0 382 | array_data.elem_type = const.BYTE_TINFO 383 | array_data.nelems = size 384 | tmp_tinfo = idaapi.tinfo_t() 385 | tmp_tinfo.create_array(array_data) 386 | udt_member.type = tmp_tinfo 387 | return udt_member 388 | 389 | 390 | def decompile_function(address): 391 | try: 392 | cfunc = idaapi.decompile(address) 393 | if cfunc: 394 | return cfunc 395 | except idaapi.DecompilationFailure: 396 | pass 397 | logger.warn("IDA failed to decompile function at 0x{address:08X}".format(address=address)) 398 | 399 | 400 | def find_asm_address(cexpr, parents): 401 | """ Returns most close virtual address corresponding to cexpr """ 402 | 403 | ea = cexpr.ea 404 | if ea != idaapi.BADADDR: 405 | return ea 406 | 407 | for p in reversed(parents): 408 | if p.ea != idaapi.BADADDR: 409 | return p.ea 410 | 411 | 412 | def my_cexpr_t(*args, **kwargs): 413 | """ Replacement of bugged cexpr_t() function """ 414 | 415 | if len(args) == 0: 416 | return idaapi.cexpr_t() 417 | 418 | if len(args) != 1: 419 | raise NotImplementedError 420 | 421 | cexpr = idaapi.cexpr_t() 422 | cexpr.thisown = False 423 | if type(args[0]) == idaapi.cexpr_t: 424 | cexpr.assign(args[0]) 425 | else: 426 | op = args[0] 427 | cexpr._set_op(op) 428 | 429 | if 'x' in kwargs: 430 | cexpr._set_x(kwargs['x']) 431 | if 'y' in kwargs: 432 | cexpr._set_y(kwargs['y']) 433 | if 'z' in kwargs: 434 | cexpr._set_z(kwargs['z']) 435 | return cexpr 436 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/struct_xrefs.py: -------------------------------------------------------------------------------- 1 | from collections import namedtuple, defaultdict 2 | import json 3 | import logging 4 | 5 | import idaapi 6 | from . import helper 7 | import HexRaysPyTools.settings as settings 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | XrefInfo = namedtuple('XrefInfo', ['func_ea', 'offset', 'line', 'type']) 12 | 13 | 14 | def singleton(cls): 15 | instances = {} 16 | 17 | def get_instance(): 18 | if cls not in instances: 19 | instances[cls] = cls() 20 | return instances[cls] 21 | return get_instance 22 | 23 | 24 | @singleton 25 | class XrefStorage(object): 26 | ARRAY_NAME = "$HexRaysPyTools:XrefStorage" 27 | 28 | def __init__(self): 29 | """ 30 | storage - {ordinal: {func_offset: (code_offset, line, usage_type)}} 31 | __delete_items_helper - {func_offset: set(ordinals)} 32 | """ 33 | self.storage = None 34 | self.__delete_items_helper = defaultdict(set) 35 | 36 | def open(self): 37 | if not settings.STORE_XREFS: 38 | self.storage = {} 39 | return 40 | 41 | result = helper.load_long_str_from_idb(self.ARRAY_NAME) 42 | if result: 43 | try: 44 | self.storage = json.loads(result, object_hook=self.json_keys_to_str) 45 | self.__init_delete_helper() 46 | return 47 | except ValueError: 48 | logger.error("Failed to read previous info about Xrefs. Try Ctrl+F5 to cache data") 49 | self.storage = {} 50 | 51 | def close(self): 52 | self.save() 53 | self.storage = None 54 | self.__delete_items_helper = defaultdict(set) 55 | 56 | def save(self): 57 | if not settings.STORE_XREFS: 58 | return 59 | 60 | if self.storage: 61 | helper.save_long_str_to_idb(self.ARRAY_NAME, json.dumps(self.storage)) 62 | 63 | def update(self, function_offset, data): 64 | """ data - {ordinal : (code_offset, line, usage_type)} """ 65 | for ordinal, info in list(data.items()): 66 | self.__update_ordinal_info(ordinal, function_offset, info) 67 | 68 | deleted_ordinals = self.__delete_items_helper[function_offset].difference(list(data.keys())) 69 | for ordinal in deleted_ordinals: 70 | self.__remove_ordinal_info(ordinal, function_offset) 71 | 72 | def get_structure_info(self, ordinal, struct_offset): 73 | """ By given ordinal and offset within a structure returns dictionary {func_address -> list(offsets)} """ 74 | result = [] 75 | 76 | if ordinal not in self.storage: 77 | return result 78 | 79 | for func_offset, info in list(self.storage[ordinal].items()): 80 | if struct_offset in info: 81 | func_ea = func_offset + idaapi.get_imagebase() 82 | for xref_info in info[struct_offset]: 83 | offset, line, usage_type = xref_info 84 | result.append(XrefInfo(func_ea, offset, line, usage_type)) 85 | return result 86 | 87 | @staticmethod 88 | def json_keys_to_str(x): 89 | if isinstance(x, dict): 90 | return {int(k): v for k, v in list(x.items())} 91 | return x 92 | 93 | def __len__(self): 94 | return len(str(self.storage)) 95 | 96 | def __init_delete_helper(self): 97 | for ordinal, data in list(self.storage.items()): 98 | for func_offset in data: 99 | self.__delete_items_helper[func_offset].add(ordinal) 100 | 101 | def __remove_ordinal_info(self, ordinal, function_offset): 102 | del self.storage[ordinal][function_offset] 103 | self.__delete_items_helper[function_offset].remove(ordinal) 104 | 105 | def __update_ordinal_info(self, ordinal, function_offset, info): 106 | if ordinal not in self.storage: 107 | self.storage[ordinal] = {} 108 | self.storage[ordinal][function_offset] = info 109 | self.__delete_items_helper[function_offset].add(ordinal) 110 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/structure_graph.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import idaapi 4 | import idc 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | 9 | class LocalType: 10 | def __init__(self, name, members_ordinals, hint, is_selected=False, is_typedef=False, is_enum=False, is_union=False): 11 | self.name = name 12 | self.members_ordinals = members_ordinals 13 | self.hint = hint 14 | self.is_selected = is_selected 15 | self.is_typedef = is_typedef 16 | self.is_enum = is_enum 17 | self.is_union = is_union 18 | 19 | def __call__(self): 20 | return self.name, self.members_ordinals 21 | 22 | def __str__(self): 23 | return "<{0}, {1}>".format(self.name, self.members_ordinals) 24 | 25 | def __repr__(self): 26 | return self.__str__() 27 | 28 | @property 29 | def name_and_color(self): 30 | if self.is_selected: 31 | return self.name, 0x0000FF 32 | elif self.is_typedef: 33 | return self.name, 0x99FFFF 34 | elif self.is_enum: 35 | return self.name, 0x33FF33 36 | elif self.is_union: 37 | return self.name, 0xCCCC00 38 | return self.name, 0xffdd99 39 | 40 | 41 | class StructureGraph: 42 | # TODO:Enum types display 43 | def __init__(self, ordinal_list=None): 44 | self.ordinal_list = ordinal_list if ordinal_list else range(1, idaapi.get_ordinal_count()) 45 | self.local_types = {} 46 | self.edges = [] 47 | self.final_edges = [] 48 | self.visited_downward = [] 49 | self.visited_upward = [] 50 | self.downward_edges = {} 51 | self.upward_edges = {} 52 | self.initialize_nodes() 53 | self.calculate_edges() 54 | 55 | def change_selected(self, selected): 56 | self.visited_downward = [] 57 | self.visited_upward = [] 58 | self.final_edges = [] 59 | for ordinal in self.ordinal_list: 60 | self.local_types[ordinal].is_selected = False 61 | self.ordinal_list = set(self.local_types).intersection(selected) 62 | for ordinal in self.ordinal_list: 63 | self.local_types[ordinal].is_selected = True 64 | 65 | @staticmethod 66 | def get_ordinal(tinfo): 67 | while tinfo.is_ptr() or tinfo.is_array(): 68 | tinfo.remove_ptr_or_array() 69 | if tinfo.is_udt(): 70 | return tinfo.get_ordinal() 71 | elif tinfo.is_enum(): 72 | return tinfo.get_ordinal() 73 | elif tinfo.is_typeref(): 74 | typeref_ordinal = tinfo.get_ordinal() 75 | if typeref_ordinal: 76 | typeref_tinfo = StructureGraph.get_tinfo_by_ordinal(typeref_ordinal) 77 | if typeref_tinfo is None: 78 | logger.warn("You have dependencies of deleted %s type", tinfo.dstr()) 79 | return 0 80 | 81 | if typeref_tinfo.is_typeref() or typeref_tinfo.is_udt() or typeref_tinfo.is_ptr(): 82 | return typeref_ordinal 83 | return 0 84 | 85 | @staticmethod 86 | def get_members_ordinals(tinfo): 87 | ordinals = [] 88 | if tinfo.is_udt(): 89 | udt_data = idaapi.udt_type_data_t() 90 | tinfo.get_udt_details(udt_data) 91 | for udt_member in udt_data: 92 | ordinal = StructureGraph.get_ordinal(udt_member.type) 93 | if ordinal: 94 | ordinals.append(ordinal) 95 | return ordinals 96 | 97 | @staticmethod 98 | def get_tinfo_by_ordinal(ordinal): 99 | local_typestring = idc.get_local_tinfo(ordinal) 100 | if local_typestring: 101 | p_type, fields = local_typestring 102 | local_tinfo = idaapi.tinfo_t() 103 | local_tinfo.deserialize(idaapi.get_idati(), p_type, fields) 104 | return local_tinfo 105 | return None 106 | 107 | def initialize_nodes(self): 108 | for ordinal in range(1, idaapi.get_ordinal_count()): 109 | # if ordinal == 15: 110 | # import pydevd 111 | # pydevd.settrace("localhost", port=12345, stdoutToServer=True, stderrToServer=True) 112 | 113 | local_tinfo = StructureGraph.get_tinfo_by_ordinal(ordinal) 114 | if not local_tinfo: 115 | continue 116 | name = idc.get_numbered_type_name(ordinal) 117 | 118 | if local_tinfo.is_typeref(): 119 | typeref_ordinal = local_tinfo.get_ordinal() 120 | members_ordinals = [] 121 | if typeref_ordinal: 122 | typeref_tinfo = StructureGraph.get_tinfo_by_ordinal(typeref_ordinal) 123 | if typeref_tinfo.is_typeref() or typeref_tinfo.is_udt() or typeref_tinfo.is_ptr(): 124 | members_ordinals = [typeref_ordinal] 125 | cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x3, local_tinfo, None, None) 126 | self.local_types[ordinal] = LocalType(name, members_ordinals, cdecl_typedef, is_typedef=True) 127 | elif local_tinfo.is_udt(): 128 | # udt_data = idaapi.udt_type_data_t() 129 | # local_tinfo.get_udt_details(udt_data) 130 | members_ordinals = StructureGraph.get_members_ordinals(local_tinfo) 131 | cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x1, local_tinfo, None, None) 132 | self.local_types[ordinal] = LocalType(name, members_ordinals, cdecl_typedef, is_union=local_tinfo.is_union()) 133 | elif local_tinfo.is_ptr(): 134 | typeref_ordinal = StructureGraph.get_ordinal(local_tinfo) 135 | members_ordinals = [typeref_ordinal] if typeref_ordinal else [] 136 | cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x2, local_tinfo, None, None) 137 | self.local_types[ordinal] = LocalType( 138 | name, 139 | members_ordinals, 140 | cdecl_typedef + ' *', 141 | is_typedef=True 142 | ) 143 | elif local_tinfo.is_enum(): 144 | cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x21, local_tinfo, None, None) 145 | self.local_types[ordinal] = LocalType(name, [], cdecl_typedef, is_enum=True) 146 | 147 | self.ordinal_list = set(self.ordinal_list).intersection(self.local_types) 148 | for ordinal in self.ordinal_list: 149 | self.local_types[ordinal].is_selected = True 150 | 151 | def calculate_edges(self): 152 | for first in list(self.local_types.keys()): 153 | for second in self.local_types[first].members_ordinals: 154 | self.edges.append((first, second)) 155 | 156 | self.downward_edges = {key: [] for key in list(self.local_types.keys())} 157 | self.upward_edges = {key: [] for key in list(self.local_types.keys())} 158 | 159 | for key, value in self.edges: 160 | self.downward_edges[key].append(value) 161 | self.upward_edges[value].append(key) 162 | 163 | def generate_final_edges_down(self, node): 164 | if node not in self.visited_downward: 165 | self.visited_downward.append(node) 166 | else: 167 | return 168 | for next_node in self.downward_edges[node]: 169 | self.final_edges.append((node, next_node)) 170 | for next_node in self.downward_edges[node]: 171 | self.generate_final_edges_down(next_node) 172 | 173 | def generate_final_edges_up(self, node): 174 | if node not in self.visited_upward: 175 | self.visited_upward.append(node) 176 | else: 177 | return 178 | for next_node in self.upward_edges[node]: 179 | self.final_edges.append((next_node, node)) 180 | for next_node in self.upward_edges[node]: 181 | self.generate_final_edges_up(next_node) 182 | 183 | def get_nodes(self): 184 | for ordinal in self.ordinal_list: 185 | if ordinal in self.local_types: 186 | self.generate_final_edges_down(ordinal) 187 | self.generate_final_edges_up(ordinal) 188 | return set([node for nodes in self.final_edges for node in nodes]) 189 | 190 | def get_edges(self): 191 | return self.final_edges 192 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/templated_types.py: -------------------------------------------------------------------------------- 1 | # Based on Rolf Rolles TemplatedTypes script 2 | # https://www.msreverseengineering.com/blog/2021/9/21/automation-in-reverse-engineering-c-template-code 3 | 4 | import os 5 | import toml 6 | import HexRaysPyTools.settings as settings 7 | 8 | class TemplatedTypes: 9 | def __init__(self): 10 | self.toml_path = "" 11 | self._types_dict = {} 12 | self.keys = [] 13 | self.set_file_path(settings.TEMPLATED_TYPES_FILE) 14 | 15 | def get_decl_str(self, key: str, args): 16 | # ensure type is in our dictionary 17 | if key in self._types_dict: 18 | type_count = len(self._types_dict[key]["types"]) 19 | # ensure that the number of types is what we expect for format string 20 | if type_count * 2 == len(args): 21 | type_struct = self._types_dict[key]["struct"] 22 | type_name = self._types_dict[key]["base_name"] 23 | # apply formatting to struct string 24 | type_struct = type_struct.format(*args) 25 | type_name = type_name.format(*args) 26 | # return tuple 27 | return type_name, type_struct 28 | else: 29 | print("[ERROR] arg count does not match type") 30 | return None 31 | else: 32 | print(f"[ERROR] type is not in type dictionary: {key}") 33 | return None 34 | 35 | def get_types(self, key): 36 | if key in self._types_dict: 37 | return self._types_dict[key]["types"] 38 | else: 39 | print("[ERROR] type is not in type dictionary") 40 | return None 41 | 42 | def get_struct(self, key): 43 | if key in self._types_dict: 44 | return self._types_dict[key]["struct"] 45 | else: 46 | print("[ERROR] struct is not in type dictionary") 47 | return None 48 | 49 | def get_base_name(self, key): 50 | if key in self._types_dict: 51 | return self._types_dict[key]["base_name"] 52 | else: 53 | print("[ERROR] struct is not in type dictionary") 54 | return None 55 | 56 | def set_file_path(self, path): 57 | self.toml_path = path 58 | self.reload_types() 59 | 60 | def reload_types(self): 61 | with open(self.toml_path, "r") as f: 62 | types_dict = toml.loads(f.read()) 63 | 64 | self._types_dict = types_dict 65 | self.keys = list(types_dict.keys()) 66 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/type_library.py: -------------------------------------------------------------------------------- 1 | import ctypes 2 | import sys 3 | 4 | import idaapi 5 | import idc 6 | 7 | from . import const 8 | from idaapi import til_t 9 | import HexRaysPyTools.forms as forms 10 | 11 | 12 | def _enable_library_ordinals(library_num): 13 | idaname = "ida64" if const.EA64 else "ida" 14 | if sys.platform == "win32": 15 | dll = ctypes.windll[idaname + ".dll"] 16 | elif sys.platform == "linux2": 17 | dll = ctypes.cdll["lib" + idaname + ".so"] 18 | elif sys.platform == "darwin": 19 | dll = ctypes.cdll["lib" + idaname + ".dylib"] 20 | else: 21 | print("[ERROR] Failed to enable ordinals") 22 | return 23 | 24 | print("HexRaysPyTools DLL: {}".format(dll)) 25 | 26 | dll.get_idati.restype = ctypes.POINTER(til_t) 27 | idati = dll.get_idati() 28 | dll.enable_numbered_types(idati.contents.base[library_num], True) 29 | 30 | 31 | def choose_til(): 32 | # type: () -> (idaapi.til_t, int, bool) 33 | """ Creates a list of loaded libraries, asks user to take one of them and returns it with 34 | information about max ordinal and whether it's local or imported library """ 35 | idati = idaapi.get_idati() 36 | list_type_library = [(idati, idati.name, idati.desc)] 37 | for idx in range(idati.nbases): 38 | type_library = idati.base(idx) # type: idaapi.til_t 39 | list_type_library.append((type_library, type_library.name, type_library.desc)) 40 | 41 | library_chooser = forms.MyChoose( 42 | list([[x[1], x[2]] for x in list_type_library]), 43 | "Select Library", 44 | [["Library", 10 | idaapi.Choose.CHCOL_PLAIN], ["Description", 30 | idaapi.Choose.CHCOL_PLAIN]], 45 | 69 46 | ) 47 | library_num = library_chooser.Show(True) 48 | if library_num != -1: 49 | selected_library = list_type_library[library_num][0] # type: idaapi.til_t 50 | max_ordinal = idaapi.get_ordinal_count(selected_library) 51 | if max_ordinal == idaapi.BADORD: 52 | _enable_library_ordinals(library_num - 1) 53 | max_ordinal = idaapi.get_ordinal_count(selected_library) 54 | print("[DEBUG] Maximal ordinal of lib {0} = {1}".format(selected_library.name, max_ordinal)) 55 | return selected_library, max_ordinal, library_num == 0 56 | 57 | def create_type(name: str, declaration: str) -> bool: 58 | tif = idaapi.tinfo_t() 59 | if tif.get_named_type(None, name): 60 | print("[ERROR] Type with name '{}' already exists".format(name)) 61 | return False 62 | idaapi.idc_parse_types(declaration, 0) 63 | if not tif.get_named_type(None, name): 64 | print("[ERROR] Failed to create type '{}'".format(name)) 65 | return False 66 | return True 67 | 68 | def import_type(library, name): 69 | last_ordinal = idaapi.get_ordinal_count(idaapi.get_idati()) 70 | type_id = idc.import_type(library, -1, name) # tid_t 71 | if type_id != idaapi.BADORD: 72 | return last_ordinal 73 | -------------------------------------------------------------------------------- /HexRaysPyTools/core/variable_scanner.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import idaapi 3 | import idc 4 | from . import const 5 | from . import helper 6 | from . import temporary_structure 7 | import HexRaysPyTools.api as api 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | # If disabled then recursion will be triggered only for variable passed as first argument to function 12 | SETTING_SCAN_ALL_ARGUMENTS = True 13 | 14 | # Global set which is populated when deep scanning and cleared after completion 15 | scanned_functions = set() 16 | debug_scan_tree = [] 17 | 18 | 19 | class ScannedObject(object): 20 | def __init__(self, name, expression_address, origin, applicable=True): 21 | """ 22 | :param name: Object name 23 | :param expression_address: ea_t 24 | :param origin: which offset had structure at scan moment 25 | :param applicable: whether to apply type after creating structure 26 | """ 27 | self.name = name 28 | self.expression_address = expression_address 29 | self.func_ea = idc.get_func_attr(self.expression_address, idc.FUNCATTR_START) 30 | self.origin = origin 31 | self._applicable = applicable 32 | 33 | @property 34 | def function_name(self): 35 | return idaapi.get_short_name(self.func_ea) 36 | 37 | def apply_type(self, tinfo): 38 | """ Finally apply Class'es tinfo to this variable """ 39 | raise NotImplementedError 40 | 41 | @staticmethod 42 | def create(obj, expression_address, origin, applicable): 43 | """ Creates suitable instance of ScannedObject depending on obj """ 44 | if obj.id == api.SO_GLOBAL_OBJECT: 45 | return ScannedGlobalObject(obj.ea, obj.name, expression_address, origin, applicable) 46 | elif obj.id == api.SO_LOCAL_VARIABLE: 47 | return ScannedVariableObject(obj.lvar, obj.name, expression_address, origin, applicable) 48 | elif obj.id in (api.SO_STRUCT_REFERENCE, api.SO_STRUCT_POINTER): 49 | return ScannedStructureMemberObject(obj.struct_name, obj.offset, expression_address, origin, applicable) 50 | else: 51 | raise AssertionError 52 | 53 | def to_list(self): 54 | """ Creates list that is acceptable to MyChoose2 viewer """ 55 | return [ 56 | "0x{0:04X}".format(self.origin), 57 | self.function_name, 58 | self.name, 59 | helper.to_hex(self.expression_address) 60 | ] 61 | 62 | def __eq__(self, other): 63 | return self.func_ea == other.func_ea and self.name == other.name and \ 64 | self.expression_address == other.expression_address 65 | 66 | def __hash__(self): 67 | return hash((self.func_ea, self.name, self.expression_address)) 68 | 69 | def __repr__(self): 70 | return "{} : {}".format(self.name, helper.to_hex(self.expression_address)) 71 | 72 | 73 | class ScannedGlobalObject(ScannedObject): 74 | def __init__(self, obj_ea, name, expression_address, origin, applicable=True): 75 | super(ScannedGlobalObject, self).__init__(name, expression_address, origin, applicable) 76 | self.__obj_ea = obj_ea 77 | 78 | def apply_type(self, tinfo): 79 | if self._applicable: 80 | idaapi.set_tinfo(self.__obj_ea, tinfo) 81 | 82 | 83 | class ScannedVariableObject(ScannedObject): 84 | def __init__(self, lvar, name, expression_address, origin, applicable=True): 85 | super(ScannedVariableObject, self).__init__(name, expression_address, origin, applicable) 86 | self.__lvar = idaapi.lvar_locator_t(lvar.location, lvar.defea) 87 | 88 | def apply_type(self, tinfo): 89 | if not self._applicable: 90 | return 91 | 92 | hx_view = idaapi.open_pseudocode(self.func_ea, -1) 93 | if hx_view: 94 | logger.debug("Applying tinfo to variable {0} in function {1}".format(self.name, self.function_name)) 95 | # Finding lvar of new window that have the same name that saved one and applying tinfo_t 96 | lvar = [x for x in hx_view.cfunc.get_lvars() if x == self.__lvar] 97 | if lvar: 98 | logger.debug("Successful") 99 | hx_view.set_lvar_type(lvar[0], tinfo) 100 | else: 101 | logger.warn("Failed to find previously scanned local variable {} from {}".format( 102 | self.name, helper.to_hex(self.expression_address))) 103 | 104 | 105 | class ScannedStructureMemberObject(ScannedObject): 106 | def __init__(self, struct_name, struct_offset, name, expression_address, origin, applicable=True): 107 | super(ScannedStructureMemberObject, self).__init__(name, expression_address, origin, applicable) 108 | self.__struct_name = struct_name 109 | self.__struct_offset = struct_offset 110 | 111 | def apply_type(self, tinfo): 112 | if self._applicable: 113 | logger.warn("Changing type of structure field is not yet implemented. Address - {}".format( 114 | helper.to_hex(self.expression_address))) 115 | 116 | 117 | class SearchVisitor(api.ObjectVisitor): 118 | def __init__(self, cfunc, origin, obj, temporary_structure): 119 | super(SearchVisitor, self).__init__(cfunc, obj, None, True) 120 | self.__origin = origin 121 | self.__temporary_structure = temporary_structure 122 | 123 | def _manipulate(self, cexpr, obj): 124 | super(SearchVisitor, self)._manipulate(cexpr, obj) 125 | 126 | if obj.tinfo and not helper.is_legal_type(obj.tinfo): 127 | cexpr_ea = helper.find_asm_address(cexpr, self.parents) 128 | logger.warn("Variable obj.name has weird type at {}".format(helper.to_hex(cexpr_ea))) 129 | return 130 | if cexpr.type.is_ptr(): 131 | member = self.__extract_member_from_pointer(cexpr, obj) 132 | else: 133 | member = self.__extract_member_from_xword(cexpr, obj) 134 | if member: 135 | logger.debug("\tCreating member with type {}, {}, offset - {}".format( 136 | member.type_name, member.scanned_variables, member.offset)) 137 | self.__temporary_structure.add_row(member) 138 | 139 | def _get_member(self, offset, cexpr, obj, tinfo=None, obj_ea=None): 140 | cexpr_ea = helper.find_asm_address(cexpr, self.parents) 141 | if offset < 0: 142 | logger.error("Considered to be impossible: offset - {}, obj - {}".format( 143 | offset, helper.to_hex(cexpr_ea))) 144 | raise AssertionError 145 | 146 | applicable = not self.crippled 147 | scan_obj = ScannedObject.create(obj, cexpr_ea, self.__origin, applicable) 148 | if obj_ea: 149 | if temporary_structure.VirtualTable.check_address(obj_ea): 150 | return temporary_structure.VirtualTable(offset, obj_ea, scan_obj, self.__origin) 151 | if helper.is_code_ea(obj_ea): 152 | cfunc = helper.decompile_function(obj_ea) 153 | if cfunc: 154 | tinfo = cfunc.type 155 | tinfo.create_ptr(tinfo) 156 | else: 157 | tinfo = const.DUMMY_FUNC 158 | return temporary_structure.Member(offset, tinfo, scan_obj, self.__origin) 159 | # logger.warn("Want to see this ea - {},".format(Helper.to_hex(cexpr_ea))) 160 | 161 | if not tinfo or tinfo.equals_to(const.VOID_TINFO) or tinfo.equals_to(const.CONST_VOID_TINFO): 162 | return temporary_structure.VoidMember(offset, scan_obj, self.__origin) 163 | 164 | if tinfo.equals_to(const.CONST_PCHAR_TINFO): 165 | tinfo = const.PCHAR_TINFO 166 | elif tinfo.equals_to(const.CONST_PVOID_TINFO): 167 | tinfo = const.PVOID_TINFO 168 | else: 169 | tinfo.clr_const() 170 | return temporary_structure.Member(offset, tinfo, scan_obj, self.__origin) 171 | 172 | def _parse_call(self, call_cexpr, arg_cexpr, offset): 173 | _, tinfo = helper.get_func_argument_info(call_cexpr, arg_cexpr) 174 | if tinfo: 175 | return self.__deref_tinfo(tinfo) 176 | # TODO: Find example with UTF-16 strings 177 | return const.CHAR_TINFO 178 | 179 | def _parse_left_assignee(self, cexpr, offset): 180 | pass 181 | 182 | def __extract_member_from_pointer(self, cexpr, obj): 183 | parents_type = [idaapi.get_ctype_name(x.cexpr.op) for x in list(self.parents)[:0:-1]] 184 | parents = [x.cexpr for x in list(self.parents)[:0:-1]] 185 | 186 | logger.debug("Parsing expression {}. Parents - {}".format(obj.name, parents_type)) 187 | 188 | # Extracting offset and removing expression parents making this offset 189 | if parents_type[0] in ('idx', 'add'): 190 | # `obj[idx]' or `(TYPE *) + x' 191 | if parents[0].y.op != idaapi.cot_num: 192 | # There's no way to handle with dynamic offset 193 | return 194 | offset = parents[0].y.numval() * cexpr.type.get_ptrarr_objsize() 195 | cexpr = self.parent_expr() 196 | if parents_type[0] == 'add': 197 | del parents_type[0] 198 | del parents[0] 199 | elif parents_type[0:2] == ['cast', 'add']: 200 | # (TYPE *)obj + offset or (TYPE)obj + offset 201 | if parents[1].y.op != idaapi.cot_num: 202 | return 203 | if parents[0].type.is_ptr(): 204 | size = parents[0].type.get_ptrarr_objsize() 205 | else: 206 | size = 1 207 | offset = parents[1].theother(parents[0]).numval() * size 208 | cexpr = parents[1] 209 | del parents_type[0:2] 210 | del parents[0:2] 211 | else: 212 | offset = 0 213 | 214 | return self.__extract_member(cexpr, obj, offset, parents, parents_type) 215 | 216 | def __extract_member_from_xword(self, cexpr, obj): 217 | parents_type = [idaapi.get_ctype_name(x.cexpr.op) for x in list(self.parents)[:0:-1]] 218 | parents = [x.cexpr for x in list(self.parents)[:0:-1]] 219 | 220 | logger.debug("Parsing expression {}. Parents - {}".format(obj.name, parents_type)) 221 | 222 | if parents_type[0] == 'add': 223 | if parents[0].theother(cexpr).op != idaapi.cot_num: 224 | return 225 | offset = parents[0].theother(cexpr).numval() 226 | cexpr = self.parent_expr() 227 | del parents_type[0] 228 | del parents[0] 229 | else: 230 | offset = 0 231 | 232 | return self.__extract_member(cexpr, obj, offset, parents, parents_type) 233 | 234 | def __extract_member(self, cexpr, obj, offset, parents, parents_type): 235 | if parents_type[0] == 'cast': 236 | default_tinfo = parents[0].type 237 | cexpr = parents[0] 238 | del parents_type[0] 239 | del parents[0] 240 | else: 241 | default_tinfo = const.PX_WORD_TINFO 242 | 243 | if parents_type[0] in ('idx', 'ptr'): 244 | if parents_type[1] == 'cast': 245 | default_tinfo = parents[1].type 246 | cexpr = parents[0] 247 | del parents_type[0] 248 | del parents[0] 249 | else: 250 | default_tinfo = self.__deref_tinfo(default_tinfo) 251 | 252 | if parents_type[1] == 'asg': 253 | if parents[1].x == parents[0]: 254 | # *(TYPE *)(var + x) = ??? 255 | obj_ea = self.__extract_obj_ea(parents[1].y) 256 | return self._get_member(offset, cexpr, obj, parents[1].y.type, obj_ea) 257 | return self._get_member(offset, cexpr, obj, parents[1].x.type) 258 | elif parents_type[1] == 'call': 259 | if parents[1].x == parents[0]: 260 | # ((type (__some_call *)(..., ..., ...)var[idx])(..., ..., ...) 261 | # ((type (__some_call *)(..., ..., ...)*(TYPE *)(var + x))(..., ..., ...) 262 | return self._get_member(offset, cexpr, obj, parents[0].type) 263 | _, tinfo = helper.get_func_argument_info(parents[1], parents[0]) 264 | if tinfo is None: 265 | tinfo = const.PCHAR_TINFO 266 | return self._get_member(offset, cexpr, obj, tinfo) 267 | return self._get_member(offset, cexpr, obj, default_tinfo) 268 | 269 | elif parents_type[0] == 'call': 270 | # call(..., (TYPE)(var + x), ...) 271 | tinfo = self._parse_call(parents[0], cexpr, offset) 272 | return self._get_member(offset, cexpr, obj, tinfo) 273 | 274 | elif parents_type[0] == 'asg': 275 | if parents[0].y == cexpr: 276 | # other_obj = (TYPE) (var + offset) 277 | self._parse_left_assignee(parents[1].x, offset) 278 | return self._get_member(offset, cexpr, obj, self.__deref_tinfo(default_tinfo)) 279 | 280 | @staticmethod 281 | def __extract_obj_ea(cexpr): 282 | if cexpr.op == idaapi.cot_ref: 283 | cexpr = cexpr.x 284 | if cexpr.op == idaapi.cot_obj: 285 | if cexpr.obj_ea != idaapi.BADADDR: 286 | return cexpr.obj_ea 287 | 288 | @staticmethod 289 | def __deref_tinfo(tinfo): 290 | if tinfo.is_ptr(): 291 | if tinfo.get_ptrarr_objsize() == 1: 292 | if tinfo.equals_to(const.PCHAR_TINFO) or tinfo.equals_to(const.CONST_PCHAR_TINFO): 293 | return const.CHAR_TINFO 294 | return None # Turns into VoidMember 295 | return tinfo.get_pointed_object() 296 | return tinfo 297 | 298 | 299 | class NewShallowSearchVisitor(SearchVisitor, api.ObjectDownwardsVisitor): 300 | def __init__(self, cfunc, origin, obj, temporary_structure): 301 | super(NewShallowSearchVisitor, self).__init__(cfunc, origin, obj, temporary_structure) 302 | 303 | 304 | class NewDeepSearchVisitor(SearchVisitor, api.RecursiveObjectDownwardsVisitor): 305 | def __init__(self, cfunc, origin, obj, temporary_structure): 306 | super(NewDeepSearchVisitor, self).__init__(cfunc, origin, obj, temporary_structure) 307 | 308 | 309 | class DeepReturnVisitor(NewDeepSearchVisitor): 310 | def __init__(self, cfunc, origin, obj, temporary_structure): 311 | super(DeepReturnVisitor, self).__init__(cfunc, origin, obj, temporary_structure) 312 | self.__callers_ea = helper.get_funcs_calling_address(cfunc.entry_ea) 313 | self.__call_obj = obj 314 | 315 | def _start(self): 316 | for ea in self.__callers_ea: 317 | self._add_scan_tree_info(ea, -1) 318 | assert self.__prepare_scanner() 319 | 320 | def _finish(self): 321 | if self.__prepare_scanner(): 322 | self._recursive_process() 323 | 324 | def __prepare_scanner(self): 325 | try: 326 | cfunc = next(self.__iter_callers()) 327 | except StopIteration: 328 | return False 329 | 330 | self.prepare_new_scan(cfunc, -1, self.__call_obj) 331 | return True 332 | 333 | def __iter_callers(self): 334 | for ea in self.__callers_ea: 335 | cfunc = helper.decompile_function(ea) 336 | if cfunc: 337 | yield cfunc 338 | -------------------------------------------------------------------------------- /HexRaysPyTools/forms.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from PyQt5 import QtCore, QtWidgets, QtGui 4 | 5 | import idaapi 6 | import ida_kernwin 7 | import idc 8 | import re 9 | 10 | 11 | class MyChoose(idaapi.Choose): 12 | def __init__(self, items, title, cols, icon=-1): 13 | idaapi.Choose.__init__(self, title, cols, flags=idaapi.Choose.CH_MODAL, icon=icon) 14 | self.items = items 15 | 16 | def OnClose(self): 17 | pass 18 | 19 | def OnGetLine(self, n): 20 | return self.items[n] 21 | 22 | def OnGetSize(self): 23 | return len(self.items) 24 | 25 | 26 | class StructureBuilder(idaapi.PluginForm): 27 | def __init__(self, structure_model): 28 | super(StructureBuilder, self).__init__() 29 | self.structure_model = structure_model 30 | self.parent = None 31 | 32 | def OnCreate(self, form): 33 | self.parent = idaapi.PluginForm.FormToPyQtWidget(form) 34 | self.init_ui() 35 | 36 | def init_ui(self): 37 | self.parent.setStyleSheet( 38 | "QTableView {background-color: transparent; selection-background-color: #87bdd8;}" 39 | "QHeaderView::section {background-color: transparent; border: 0.5px solid;}" 40 | "QPushButton {width: 50px; height: 20px;}" 41 | # "QPushButton::pressed {background-color: #ccccff}" 42 | ) 43 | self.parent.resize(400, 600) 44 | self.parent.setWindowTitle('Structure Builder') 45 | 46 | btn_finalize = QtWidgets.QPushButton("&Finalize") 47 | btn_disable = QtWidgets.QPushButton("&Disable") 48 | btn_enable = QtWidgets.QPushButton("&Enable") 49 | btn_origin = QtWidgets.QPushButton("&Origin") 50 | btn_array = QtWidgets.QPushButton("&Array") 51 | btn_pack = QtWidgets.QPushButton("&Pack") 52 | btn_unpack = QtWidgets.QPushButton("&Unpack") 53 | btn_remove = QtWidgets.QPushButton("&Remove") 54 | btn_resolve = QtWidgets.QPushButton("Resolve") 55 | btn_load = QtWidgets.QPushButton("&Load") 56 | btn_clear = QtWidgets.QPushButton("Clear") # Clear button doesn't have shortcut because it can fuck up all work 57 | btn_recognize = QtWidgets.QPushButton("Recognize Shape") 58 | btn_recognize.setStyleSheet("QPushButton {width: 100px; height: 20px;}") 59 | btn_stl = QtWidgets.QPushButton("Templated Types View") 60 | btn_stl.setStyleSheet("QPushButton {width: 150px; height: 20px;}") 61 | btn_struct = QtWidgets.QPushButton("Structure View") 62 | btn_struct.setStyleSheet("QPushButton {width: 150px; height: 20px;}") 63 | 64 | btn_finalize.setShortcut("f") 65 | btn_disable.setShortcut("d") 66 | btn_enable.setShortcut("e") 67 | btn_origin.setShortcut("o") 68 | btn_array.setShortcut("a") 69 | btn_pack.setShortcut("p") 70 | btn_unpack.setShortcut("u") 71 | btn_remove.setShortcut("r") 72 | btn_load.setShortcut("l") 73 | 74 | struct_view = QtWidgets.QTableView() 75 | struct_view.setModel(self.structure_model) 76 | # struct_view.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection) 77 | 78 | struct_view.verticalHeader().setVisible(False) 79 | struct_view.verticalHeader().setDefaultSectionSize(24) 80 | struct_view.horizontalHeader().setStretchLastSection(True) 81 | struct_view.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) 82 | 83 | self.stl_list = QtWidgets.QListWidget() 84 | # stl_list_item_0 = QtWidgets.QListWidgetItem("std::string") 85 | for item in self.structure_model.tmpl_types.keys: 86 | self.stl_list.addItem(item) 87 | self.stl_list.setFixedWidth(300) 88 | self.stl_list.setCurrentRow(0) 89 | 90 | self.stl_title_fields = QtWidgets.QLabel("Selected Type: ") 91 | self.stl_title_struct = QtWidgets.QLabel("Creating Type: ") 92 | self.stl_struct_view = QtWidgets.QTextEdit() 93 | self.stl_struct_view.setReadOnly(True) 94 | font = QtGui.QFont("Courier", 11) 95 | self.stl_struct_view.setFont(font) 96 | # self.stl_struct_view.setAlignment(QtCore.Qt.AlignTop) 97 | # self.stl_struct_view.setStyleSheet("border: 1px solid black;") 98 | 99 | btn_reload_stl_list = QtWidgets.QPushButton("Reload Templated Types TOML") 100 | btn_reload_stl_list.setFixedWidth(300) 101 | 102 | btn_open_stl_toml = QtWidgets.QPushButton("Open Templated Types TOML") 103 | btn_reload_stl_list.setFixedWidth(300) 104 | 105 | self.stl_widget = QtWidgets.QWidget() 106 | self.stl_form_layout = QtWidgets.QFormLayout() 107 | 108 | self.update_stl_form() 109 | 110 | self.stl_layout = QtWidgets.QGridLayout() 111 | self.stl_layout.addWidget(QtWidgets.QLabel("Type List"), 0, 0) 112 | self.stl_layout.addWidget(self.stl_title_fields, 0, 1) 113 | self.stl_layout.addWidget(self.stl_title_struct, 0, 2) 114 | self.stl_layout.addWidget(self.stl_struct_view, 1, 2) 115 | self.stl_layout.addWidget(self.stl_list, 1, 0) 116 | self.stl_layout.addWidget(self.stl_widget, 1, 1) 117 | self.stl_layout.addWidget(btn_reload_stl_list, 2, 0) 118 | self.stl_layout.addWidget(btn_open_stl_toml, 3, 0) 119 | 120 | self.stl_layout.setColumnStretch(1, 1) 121 | self.stl_layout.setColumnStretch(2, 1) 122 | 123 | self.stl_view = QtWidgets.QWidget() 124 | self.stl_view.setLayout(self.stl_layout) 125 | 126 | grid_box = QtWidgets.QGridLayout() 127 | grid_box.setSpacing(0) 128 | grid_box.addWidget(btn_finalize, 0, 0) 129 | grid_box.addWidget(btn_enable, 0, 1) 130 | grid_box.addWidget(btn_disable, 0, 2) 131 | grid_box.addWidget(btn_origin, 0, 3) 132 | grid_box.addItem(QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Expanding), 0, 6) 133 | grid_box.addWidget(btn_array, 1, 0) 134 | grid_box.addWidget(btn_pack, 1, 1) 135 | grid_box.addWidget(btn_unpack, 1, 2) 136 | grid_box.addWidget(btn_remove, 1, 3) 137 | grid_box.addWidget(btn_resolve, 0, 4) 138 | grid_box.addWidget(btn_load, 1, 4) 139 | grid_box.addWidget(btn_stl, 0, 5) 140 | grid_box.addWidget(btn_struct, 1, 5) 141 | grid_box.addItem(QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Expanding), 1, 6) 142 | grid_box.addWidget(btn_recognize, 0, 7) 143 | grid_box.addWidget(btn_clear, 1, 7) 144 | 145 | stack = QtWidgets.QStackedWidget() 146 | stack.addWidget(struct_view) 147 | stack.addWidget(self.stl_view) 148 | 149 | vertical_box = QtWidgets.QVBoxLayout() 150 | vertical_box.addWidget(stack) 151 | vertical_box.addLayout(grid_box) 152 | self.parent.setLayout(vertical_box) 153 | 154 | btn_finalize.clicked.connect(lambda: self.structure_model.finalize()) 155 | btn_disable.clicked.connect(lambda: self.structure_model.disable_rows(struct_view.selectedIndexes())) 156 | btn_enable.clicked.connect(lambda: self.structure_model.enable_rows(struct_view.selectedIndexes())) 157 | btn_origin.clicked.connect(lambda: self.structure_model.set_origin(struct_view.selectedIndexes())) 158 | btn_array.clicked.connect(lambda: self.structure_model.make_array(struct_view.selectedIndexes())) 159 | btn_pack.clicked.connect(lambda: self.structure_model.pack_substructure(struct_view.selectedIndexes())) 160 | btn_unpack.clicked.connect(lambda: self.structure_model.unpack_substructure(struct_view.selectedIndexes())) 161 | btn_remove.clicked.connect(lambda: self.structure_model.remove_items(struct_view.selectedIndexes())) 162 | btn_resolve.clicked.connect(lambda: self.structure_model.resolve_types()) 163 | btn_stl.clicked.connect(lambda: stack.setCurrentIndex(1)) 164 | btn_struct.clicked.connect(lambda: stack.setCurrentIndex(0)) 165 | btn_load.clicked.connect(lambda: self.structure_model.load_struct()) 166 | btn_clear.clicked.connect(lambda: self.structure_model.clear()) 167 | btn_recognize.clicked.connect(lambda: self.structure_model.recognize_shape(struct_view.selectedIndexes())) 168 | struct_view.activated[QtCore.QModelIndex].connect(self.structure_model.activated) 169 | self.structure_model.dataChanged.connect(struct_view.clearSelection) 170 | 171 | self.stl_list.currentRowChanged.connect(self.update_stl_form) 172 | btn_reload_stl_list.clicked.connect(self.reload_stl_list) 173 | btn_open_stl_toml.clicked.connect(self.open_dialog_stl_file) 174 | 175 | def update_stl_form(self): 176 | # wrapped in a try/except, as exception is thrown when TOML is refreshed 177 | try: 178 | # get key and update title 179 | key = self.stl_list.currentItem().text() 180 | self.stl_title_fields.setText("Selected Type: {}".format(key)) 181 | types = self.structure_model.tmpl_types.get_types(key) 182 | 183 | # remove previous widgets from layout... QT needs to do this 184 | for i in reversed(range(self.stl_form_layout.count())): 185 | self.stl_form_layout.itemAt(i).widget().setParent(None) 186 | 187 | # for each template type we add a type & name field 188 | for t in types: 189 | e1 = QtWidgets.QLineEdit() 190 | e2 = QtWidgets.QLineEdit() 191 | self.stl_form_layout.addRow(QtWidgets.QLabel("{0} Type".format(t)), e1) 192 | self.stl_form_layout.addRow(QtWidgets.QLabel("{0} Name".format(t)), e2) 193 | e1.textChanged.connect(lambda: self.reload_stl_struct(key)) 194 | e2.textChanged.connect(lambda: self.reload_stl_struct(key)) 195 | 196 | # add the button and apply layout to widget 197 | btn_set_type = QtWidgets.QPushButton("Set Type") 198 | self.stl_form_layout.addRow(btn_set_type) 199 | self.stl_widget.setLayout(self.stl_form_layout) 200 | 201 | self.reload_stl_struct(key) 202 | 203 | # connect a callback to the button 204 | btn_set_type.clicked.connect(lambda: self.call_set_stl_type(key)) 205 | except: 206 | pass 207 | 208 | def reload_stl_list(self): 209 | self.stl_list.clear() 210 | self.structure_model.tmpl_types.reload_types() 211 | for item in self.structure_model.tmpl_types.keys: 212 | self.stl_list.addItem(item) 213 | 214 | def reload_stl_struct(self, key): 215 | try: 216 | struct = self.structure_model.tmpl_types.get_struct(key) 217 | base_name = "Creating Type: " + self.structure_model.tmpl_types.get_base_name(key) 218 | args = self.get_stl_args(key) 219 | self.stl_struct_view.setPlainText(struct.format(*args)) 220 | self.stl_title_struct.setText(base_name.format(*args)) 221 | except: 222 | pass 223 | 224 | def get_stl_args(self, key): 225 | args = () 226 | # collect text in the text boxes push into tuple 227 | for w in self.stl_widget.findChildren(QtWidgets.QLineEdit): 228 | arg = w.text() 229 | if arg == "": 230 | arg = "$void$" 231 | args = args + (arg,) 232 | return args 233 | 234 | def call_set_stl_type(self, key): 235 | args = self.get_stl_args(key) 236 | 237 | for i in range(len(args)): 238 | # type line edit 239 | if i % 2 == 0: 240 | if not re.match(r"^[a-zA-Z_]([\w_](::){0,2})+(? 1: 399 | if [x for x in indexes if len(x.internalPointer().children) > 0]: 400 | self.action_set_arg.setEnabled(False) 401 | self.menu.exec_(self.class_tree.mapToGlobal(point)) 402 | -------------------------------------------------------------------------------- /HexRaysPyTools/settings.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | import ida_diskio 4 | 5 | try: 6 | import configparser 7 | except ImportError: 8 | # for python 2 9 | import ConfigParser as configparser 10 | 11 | CONFIG_FILE_PATH = os.path.join(ida_diskio.get_user_idadir(), 'cfg', 'HexRaysPyTools.cfg') 12 | CONFIG_DIRECTORY = os.path.join(ida_diskio.get_user_idadir(), 'cfg') 13 | 14 | DEBUG_MESSAGE_LEVEL = logging.INFO 15 | # Whether propagate names (Propagate name feature) through all names or only defaults like v11, a3, this, field_4 16 | PROPAGATE_THROUGH_ALL_NAMES = False 17 | # Store Xref information in database. I don't know how much size it consumes yet 18 | STORE_XREFS = True 19 | # There're some types that can be pointers to structures like int, PVOID etc and by default plugin scans only them 20 | # Full list can be found in `Const.LEGAL_TYPES`. 21 | # But if set this option to True than variable of every type could be possible to scan 22 | SCAN_ANY_TYPE = False 23 | 24 | TEMPLATED_TYPES_FILE = os.path.join( 25 | ida_diskio.get_user_idadir(), 'plugins', 'HexRaysPyTools', 'types', 'templated_types.toml') 26 | 27 | 28 | def add_default_settings(config): 29 | updated = False 30 | if not config.has_option("DEFAULT", "DEBUG_MESSAGE_LEVEL"): 31 | config.set(None, 'DEBUG_MESSAGE_LEVEL', str(DEBUG_MESSAGE_LEVEL)) 32 | updated = True 33 | if not config.has_option("DEFAULT", "PROPAGATE_THROUGH_ALL_NAMES"): 34 | config.set(None, 'PROPAGATE_THROUGH_ALL_NAMES', str(PROPAGATE_THROUGH_ALL_NAMES)) 35 | updated = True 36 | if not config.has_option("DEFAULT", "STORE_XREFS"): 37 | config.set(None, 'STORE_XREFS', str(STORE_XREFS)) 38 | updated = True 39 | if not config.has_option("DEFAULT", "SCAN_ANY_TYPE"): 40 | config.set(None, 'SCAN_ANY_TYPE', str(SCAN_ANY_TYPE)) 41 | updated = True 42 | if not config.has_option("DEFAULT", "TEMPLATED_TYPES_FILE"): 43 | config.set(None, 'TEMPLATED_TYPES_FILE', str(TEMPLATED_TYPES_FILE)) 44 | updated = True 45 | 46 | if updated: 47 | try: 48 | if not os.path.exists(CONFIG_DIRECTORY): 49 | os.makedirs(CONFIG_DIRECTORY) 50 | with open(CONFIG_FILE_PATH, "w") as f: 51 | config.write(f) 52 | except IOError: 53 | print("[ERROR] Failed to write or update config file at {}. Default settings will be used instead.\n" \ 54 | "Consider running IDA Pro under administrator once".format(CONFIG_FILE_PATH)) 55 | 56 | 57 | def load_settings(): 58 | global \ 59 | DEBUG_MESSAGE_LEVEL, \ 60 | PROPAGATE_THROUGH_ALL_NAMES, \ 61 | STORE_XREFS, \ 62 | SCAN_ANY_TYPE, \ 63 | TEMPLATED_TYPES_FILE 64 | 65 | config = configparser.ConfigParser() 66 | if os.path.isfile(CONFIG_FILE_PATH): 67 | config.read(CONFIG_FILE_PATH) 68 | 69 | add_default_settings(config) 70 | 71 | DEBUG_MESSAGE_LEVEL = config.getint("DEFAULT", 'DEBUG_MESSAGE_LEVEL') 72 | PROPAGATE_THROUGH_ALL_NAMES = config.getboolean("DEFAULT", 'PROPAGATE_THROUGH_ALL_NAMES') 73 | STORE_XREFS = config.getboolean("DEFAULT", 'STORE_XREFS') 74 | SCAN_ANY_TYPE = config.getboolean("DEFAULT", 'SCAN_ANY_TYPE') 75 | TEMPLATED_TYPES_FILE = config.get("DEFAULT", 'TEMPLATED_TYPES_FILE') 76 | -------------------------------------------------------------------------------- /HexRaysPyTools/types/templated_types.toml: -------------------------------------------------------------------------------- 1 | # ===================================================================================================================== 2 | # Layout of each class 3 | # ===================================================================================================================== 4 | # ["std::vector"] - pretty type name, this is used as dictionary key and in the types list 5 | # base_name = "std_vector_{1}" - name used for IDA's structs as we cannot use `::`, format identifiers for 6 | # type name, these are always odd numbers 7 | # (NOTE: this *must* match the base structure name within `struct` string) 8 | # types = ["T"] - define the types here as a list of strings, each type will have 2 format tokens 9 | # struct = """ - this is where you define the struct, format specifers work as the following: 10 | # struct std_vector_{1} (same format applies to the base name) 11 | # {{ (NOTE: brackets need to be doubled up `{{` & `}}` due to `str.format` 12 | # {0} *_Myfirst; - 1st type - {0} (actual type) 13 | # {0} *_Mylast; - {1} (pretty print type, int* -> pInt) 14 | # {0} *_Myend; - 2nd type - {2} (actual type) 15 | # }}; - {3} (pretty print type, std::string* -> pStdString) 16 | # """ - etc 17 | 18 | ["std::string"] 19 | base_name = "std_string" 20 | types = [] 21 | struct = """ 22 | #pragma pack(push, 1) 23 | union std_string_union 24 | {{ 25 | char _Buf[16]; 26 | char *_Ptr; 27 | }}; 28 | 29 | struct std_string 30 | {{ 31 | union std_string_union u; 32 | size_t _Mysize; 33 | size_t _Myres; 34 | }}; 35 | #pragma pack(pop) 36 | """ 37 | 38 | ["std::wstring"] 39 | base_name = "std_wstring" 40 | types = [] 41 | struct = """ 42 | #pragma pack(push, 1) 43 | union std_wstring_union 44 | {{ 45 | wchar_t _Buf[8]; 46 | wchar_t *_Ptr; 47 | }}; 48 | 49 | struct std_wstring 50 | {{ 51 | union std_wstring_union u; 52 | size_t _Mysize; 53 | size_t _Myres; 54 | }}; 55 | #pragma pack(pop) 56 | """ 57 | 58 | ["std::vector"] 59 | base_name = "std_vector_{1}" 60 | types = ["T"] 61 | struct = """ 62 | struct std_vector_{1} 63 | {{ 64 | {0} *_Myfirst; 65 | {0} *_Mylast; 66 | {0} *_Myend; 67 | }}; 68 | """ 69 | 70 | ["std::list"] 71 | base_name = "std_list_{1}" 72 | types = ["T"] 73 | struct = """ 74 | struct std_list_node_{1}; 75 | struct std_list_node_{1} 76 | {{ 77 | std_list_node_{1} *_Next; 78 | std_list_node_{1} *_Prev; 79 | {0} _Myval; 80 | }}; 81 | 82 | struct std_list_{1} 83 | {{ 84 | std_list_node_{1} *_Myhead; 85 | size_t _Mysize; 86 | }}; 87 | """ 88 | 89 | ["std::deque"] 90 | base_name = "std_deque_{1}" 91 | types = ["T"] 92 | struct = """ 93 | struct std_deque_{1} 94 | {{ 95 | void *_Myproxy; 96 | {0} **_Map; 97 | size_t _Mapsize; 98 | _QWORD _Myoff; 99 | _QWORD _Mysize; 100 | }}; 101 | """ 102 | 103 | ["std::set"] 104 | base_name = "std_set_{1}" 105 | types = ["T"] 106 | struct = """ 107 | struct _Tree_node_{1}; 108 | struct _Tree_node_{1} 109 | {{ 110 | _Tree_node_{1} *_Left; 111 | _Tree_node_{1} *_Parent; 112 | _Tree_node_{1} *_Right; 113 | bool _Color; 114 | bool _IsNil; 115 | {0} _Key; 116 | }}; 117 | 118 | struct __cppobj std_set_{1} 119 | {{ 120 | _Tree_node_{1} *_Myhead; 121 | size_t _Mysize; 122 | }}; 123 | 124 | struct __cppobj std_set_{1}_iterator_pairib 125 | {{ 126 | _Tree_node_{1} *_Myhead; 127 | bool _Second; 128 | }}; 129 | """ 130 | 131 | ["std::map"] 132 | base_name = "std_map_{1}_{3}" 133 | types = ["K", "V"] 134 | struct = """ 135 | struct {1}_{3}_keyvalue_t 136 | {{ 137 | {0} key; 138 | {2} value; 139 | }}; 140 | 141 | struct _Tree_node_{1}_{3}; 142 | struct _Tree_node_{1}_{3} 143 | {{ 144 | _Tree_node_{1}_{3} *_Left; 145 | _Tree_node_{1}_{3} *_Parent; 146 | _Tree_node_{1}_{3} *_Right; 147 | bool _Color; 148 | bool _IsNil; 149 | {1}_{3}_keyvalue_t KeyValue; 150 | }}; 151 | 152 | struct __cppobj std_map_{1}_{3} 153 | {{ 154 | _Tree_node_{1}_{3} *_Myhead; 155 | unsigned __int64 _Mysize; 156 | }}; 157 | 158 | struct __cppobj std_map_{1}_{3}_iterator_pairib 159 | {{ 160 | _Tree_node_{1}_{3} *_Myhead; 161 | bool _Second; 162 | }}; 163 | """ 164 | 165 | ["alloc::raw_vec::RawVec"] 166 | base_name = "alloc_raw_vec_{1}" 167 | types = ["T"] 168 | struct = """ 169 | struct alloc_raw_vec_{1} 170 | {{ 171 | {0} *pointer; 172 | __int64 cap; 173 | }}; 174 | """ 175 | 176 | ["alloc::vec::Vec"] 177 | base_name = "alloc_vec_{1}" 178 | types = ["T"] 179 | struct = """ 180 | struct alloc_vec_{1} 181 | {{ 182 | alloc_raw_vec_{1} buf; 183 | unsigned __int64 len; 184 | }}; 185 | """ 186 | 187 | ["alloc::string::String"] 188 | base_name = "alloc_string" 189 | types = [] 190 | struct = """ 191 | struct alloc_raw_vec_u8 192 | {{ 193 | u8 *pointer; 194 | __int64 cap; 195 | }}; 196 | 197 | struct alloc_vec_u8 198 | {{ 199 | alloc_raw_vec_u8 buf; 200 | unsigned __int64 len; 201 | }}; 202 | 203 | struct alloc_string 204 | {{ 205 | alloc_vec_u8 vec; 206 | }}; 207 | """ 208 | -------------------------------------------------------------------------------- /Img/bad.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/bad.JPG -------------------------------------------------------------------------------- /Img/builder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/builder.png -------------------------------------------------------------------------------- /Img/classes.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/classes.JPG -------------------------------------------------------------------------------- /Img/fields_xref.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/fields_xref.JPG -------------------------------------------------------------------------------- /Img/good.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/good.JPG -------------------------------------------------------------------------------- /Img/structure_builder.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/structure_builder.JPG -------------------------------------------------------------------------------- /Img/tmpl_types_after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/tmpl_types_after.png -------------------------------------------------------------------------------- /Img/tmpl_types_before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/tmpl_types_before.png -------------------------------------------------------------------------------- /Img/tmpl_types_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/tmpl_types_view.png -------------------------------------------------------------------------------- /Img/virtual_functions.JPG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oopsmishap/HexRaysPyTools/4742ce4ac7db72ad0cfb862d34cb15065b1b136e/Img/virtual_functions.JPG -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ### Pulled in useful PR's & forks to keep upto date as main repo has gone stale 2 | 3 | #### Added new [Templated Type view](#templated-types-view) to Structure Builder 4 | 5 | --- 6 | 7 | Plugin for IDA Pro 8 | 9 | **Table of Contents** 10 | 11 | * [About](#about) 12 | * [Installation](#installation) 13 | * [Configuration](#configuration) 14 | * [Features](#features) 15 | * [Structure reconstruction](#structure-reconstruction) 16 | * [Structure View](#structure-view) 17 | * [Templated Types View](#templated-types-view) 18 | * [Disassembler code manipulations](#disassembler-code-manipulations) 19 | * [Containing structures](#containing-structures) 20 | * [Function signature manipulation](#function-signature-manipulation) 21 | * [Recasting & Renaming](#recasting-shiftr-shiftl-renaming-shiftn-ctrlshiftn) 22 | * [Name Propagation](#name-propagation-p) 23 | * [Untangling 'if' statements](#untangling-if-statements) 24 | * [Classes](#classes) 25 | * [Structure Graph](#structure-graph) 26 | * [API](#api) 27 | * [Presentations](#presentations) 28 | 29 | # About 30 | 31 | The plugin assists in the creation of classes/structures and detection of virtual tables. It also facilitates transforming decompiler output faster and allows to do some stuff which is otherwise impossible. 32 | 33 | **Note**: The plugin supports IDA Pro 7.x with Python 2/3. 34 | 35 | # Installation 36 | 37 | - Copy `HexRaysPyTools.py` file and `HexRaysPyTools` directory to [`$IDAUSR/plugins`](https://hex-rays.com/blog/igors-tip-of-the-week-33-idas-user-directory-idausr/) directory. 38 | 39 | - Install the plugin requirements found in the `requirements.txt` file. Be sure to install these for the Python environment used by your IDA installation. 40 | 41 | - `pip install -r requirements.txt` 42 | 43 | ## Configuration 44 | 45 | Can be found at [`$IDAUSR\cfg\HexRaysPyTools.cfg`](https://hex-rays.com/blog/igors-tip-of-the-week-33-idas-user-directory-idausr/) 46 | 47 | * `debug_message_level`. Set 10 if you have a bug and want to show the log along with the information about how it was encountered in the issue. 48 | * `propagate_through_all_names`. Set `True` if you want to rename not only the default variables for the [Propagate Name](#Propagate) feature. 49 | * `store_xrefs`. Specifies whether to store the cross-references collected during the decompilation phase inside the database. (Default - True) 50 | * `scan_any_type`. Set `True` if you want to apply scanning to any variable type. By default, it is possible to scan only basic types like `DWORD`, `QWORD`, `void *` e t.c. and pointers to non-defined structure declarations. 51 | * `templated_types_file`. Set to default TOML file path for templated types view. (Default - Empty, will auto load `%IDA_DIR%/Plugins/HexRaysPyTools/types/templated_types.toml`) 52 | 53 | # Features 54 | 55 | **[Recently added][feature_history]** 56 | 57 | --- 58 | ## Structure reconstruction 59 | 60 | The reconstruction process usually comprises the following steps: 61 | 62 | 1) Open structure builder. 63 | 2) Find a local variable that points to the structure you would like to reconstruct. 64 | 3) Apply "Scan variable". It will collect the information about the fields that were accessed in the boundaries of one function. As an option, you can apply "Deep Scan variable", which will do the same thing but will also recursively visit other functions that has the same variable as its argument. 65 | 4) After applying steps 2 and 3 enough times, resolve conflicts in the structure builder and finalize structure creation. All the scanned variables will get a new type. Also, cross-references will be remembered and usable anytime. 66 | 67 | Now, a few more details. 68 | 69 | ### Structure Builder (Alt + F8) 70 | 71 | The place where all the collected information about the scanned variables can be viewed and modified. Ways of collecting information: 72 | * Right Click on a variable -> Scan Variable. Recognizes fields usage within the current function. 73 | * Right Click on a variable -> Deep Scan Variable. First, recursively touches functions to make Ida recognize proper arguments (it happens only once for each function during a session). Then, it recursively applies the scanner to variables and functions, which get the structure pointer as their argument. 74 | * Right Click on a function -> Deep Scan Returned Value. If you have the singleton pattern or the constructor is called in many places, it is possible to scan all the places, where a pointer to an object was recieved or an object was created. 75 | * API [TODO] 76 | 77 | ### Structure View 78 | 79 | ![img][builder] 80 | 81 | Structure builder stores collected information and enables interaction: 82 | 83 | * Types with the __BOLD__ font are virtual tables. A double click opens the list with all virtual functions, which helps to visit them. The visited functions are marked with a cross and color: 84 | 85 | ![img][virtual_functions] 86 | 87 | * Types with the _ITALIC_ font have been found as passed argument. It can help in finding substructures. [TODO] 88 | * Double click on field `Name` or `Type` to edit. 89 | * Double click on `Offset` opens a window with all the places, where this field has been extracted. Click the "Ok" button to open a selected place in the pseudocode window: 90 | 91 | ![img][scanned_variables] 92 | 93 | Buttons serve the following purpose: 94 | 95 | __Finalize__ - opens a window with an editable C-like declaration and assigns new types to all scanned variables. 96 | 97 | __Disable__, __Enable__ - are used for collision resolution. 98 | 99 | __Origin__ - switches the base offset which is used to produce new fields to structure (this value will be added to every offset of a newly-scanned variable, default = 0). 100 | 101 | __Array__ - renders a selected field as an array the size of which is automatically calculated. 102 | 103 | __Pack__ - creates and substitutes a substructure for selected items (collisions for these items should be resolved). 104 | 105 | __Unpack__ - dismembers a selected structure and adds all its fields to the builder. 106 | 107 | __Load__ - loads a predefined type that is loaded into IDA into the structure builder. 108 | 109 | __Remove__ - removes the information about selected fields. 110 | 111 | __Clear__ - clears all. 112 | 113 | __Recognize Shape__ - looks for appropriates structure for selected fields. 114 | 115 | __Resolve Conflicts (new)__ - attempts to disable less meaningful fields in favor of more useful ones. (`char` > `_BYTE`, `SOCKET` > `_DWORD` etc). Doesn't help to find arrays. 116 | 117 | __Templated Types View__ - switches the templated types view 118 | 119 | __Structure View__ - switches to the structure builder view 120 | 121 | ### Structure Cross-references (Ctrl + X) 122 | 123 | With HexRaysPyTools, every time the F5 button is pressed and code is decompiled, the information about addressing to fields is stored inside cache. It can be retrieved with the "Field Xrefs" menu. So, it is better to apply reconstructed types to as many locations as possible to have more information about the way structures are used. 124 | 125 | Note: IDA 7.4 has now an official implementation of this feature, available through Shift-X hotkey. 126 | 127 | ### Guessing Allocation 128 | 129 | **Warning!! Very raw feature.** The idea is to help find where a variable came from so as to run Deep Scan Process at the very top level and not to skip large amounts of code. 130 | 131 | ### Structures with given size 132 | 133 | Usage: 134 | 135 | 1. In Pseudocode viewer, right click on a number -> "Structures with this size". (hotkey "W") 136 | 2. Select a library to be looked for structures. 137 | 3. Select a structure. The Number will become `sizeof(Structure Name)`, and type will be imported to Local Types. 138 | 139 | ### Recognition of structures by shapes 140 | 141 | Helps find a suitable structure by the information gleaned from pseudocode after variable scanning. 142 | 143 | Usage: 144 | 145 | * _Method 1_ 146 | 1. Right click on a variable with -> Select "Recognize Shape". 147 | 2. Select Type Library. 148 | 3. Select structure. 149 | 4. Type of the variable will be changed automatically. 150 | * _Method 2_ 151 | 1. Clear Structure Builder if it's currently used. 152 | 2. Right click on the variables that are supposed to be the same -> "Scan Variable". 153 | 3. Edit types (will be implemented later), disable or remove uninteresting fields, and click the "Recognize Shape" button. 154 | 4. You can select several fields and try to recognize their shapes. If found and selected, they will be replaced with a new structure. 155 | 5. After final structure selection, types of all scanned variables will be changed automatically. 156 | 157 | ### Templated Types View 158 | 159 | ![img.png](Img/tmpl_types_view.png) 160 | 161 | The templated types view allows you to easily define and propagate templated types such as the data types you find within 162 | STL. 163 | 164 | __Type List__ - list of the loaded types, click on the type you want to set to populate the middle form. 165 | 166 | __Selected Type__ - each templated typename has two fields, `Type` & `Name`. The `Type` field is the actual type that 167 | will be defined, this has to be a real type, as it will throw an error if it is not. The `Name` field is used to create 168 | an unique typename for the templated type. 169 | 170 | __Creating Type__ - a live output of the types you will set. 171 | 172 | __Reload Templated Types TOML__ - if you have edited the current TOML file you will have to reload the list. 173 | 174 | __Open Templated Types TOML__ - open your own custom templated types TOML following the templated types structure 175 | 176 | **Before:** 177 | 178 | ![img.png](Img/tmpl_types_before.png) 179 | 180 | **After** 181 | 182 | ![img.png](Img/tmpl_types_after.png) 183 | 184 | ### TOML Format 185 | 186 | ```toml 187 | ["std::vector"] 188 | base_name = "std_vector_{1}" 189 | types = ["T"] 190 | struct = """ 191 | struct std_vector_{1} 192 | {{ 193 | {0} *_Myfirst; 194 | {0} *_Mylast; 195 | {0} *_Myend; 196 | }}; 197 | """ 198 | ``` 199 | 200 | __Class Name__ - pretty type name, this is used as dictionary key and in the types list (`["std::vector"]`) 201 | 202 | __Base Name__ - - name used for IDA's structs as we cannot use `::`, format identifiers for type name, these are always odd numbers (Note: this *must* match the base structure name within `struct` string) 203 | 204 | __Types__ - define the types here as a list of strings, each type will have 2 format specifier tokens 205 | 206 | __Struct__ - this is where you define the struct, format specifiers work as the following: 207 | * 1st type: 208 | - `{0}` (actual type) 209 | - `{1}` (pretty print type, `int*` -> `pInt`) 210 | * 2nd type: 211 | - `{2}` (actual type) 212 | - `{3}` (pretty print type, `std::string*` -> `pStdString`) 213 | * (Note: brackets need to be doubled up `{{` & `}}` due to `str.format` 214 | 215 | --- 216 | ## Disassembler code manipulations 217 | 218 | ### Containing structures 219 | 220 | Helps find containing structure and makes code prettier by replacing pointers with [CONTAINING_RECORD][1] macro 221 | 222 | __Before:__ 223 | 224 | ![img][bad_structures] 225 | 226 | __After:__ 227 | 228 | ![img][good_structures] 229 | 230 | Usage: 231 | 232 | If a variable is a structure pointer and there's an access to outside of the boundaries of that structure, then: 233 | 234 | 1. Right click -> Select Containing Structure. 235 | 2. Select Type Library. 236 | 3. Select appropriate Structure and Offset. 237 | 4. If the result does not satisfy the requirements, then Right Click -> Reset Containing Structure and go back to step 1. 238 | 239 | ### Function signature manipulation 240 | 241 | 1. Right click first line -> "Remove Return" converts return type to void (or from void to _DWORD). 242 | 2. Right click on argument -> "Remove Argument" disposes of this argument. 243 | 3. Right click on convention -> "Convert to __usercall" switches to __usercall or __userpurge (same as __usercall but the callee cleans the stack). 244 | 245 | ### Recasting (Shift+R, Shift+L), Renaming (Shift+N, Ctrl+Shift+N) 246 | 247 | Every time you have two sides in an expression, where each side may be a local or global variable, argument or return value of the function signature, it is possible to right-click or press the hotkey to give both sides of the expression similar types. Below, there is the table of possible conversions: 248 | 249 | | Original | Shift+L | Shift+R 250 | | --- | --- | --- | 251 | | var = (TYPE) expr | var type -> TYPE | | 252 | | exp = (TYPE) var | | var type -> TYPE | 253 | | function(..., (TYPE) var, ...) | functions' argument -> TYPE | var type -> TYPE | 254 | | (TYPE) function(...) | | functions' return type -> TYPE | 255 | | return (TYPE) var | functions' return type -> TYPE | var type -> TYPE | 256 | | struct.field = (TYPE) var | type(field) -> TYPE | | 257 | | pstruct->field = (TYPE) var | type(field) -> TYPE | | 258 | 259 | When you have an expression like `function(..., some_good_name, ...)`, you can rename function parameter. 260 | 261 | When you have an expression like `function(..., v12, ...)`, and function has an appropriate parameter name, you can quickly apply this name to the variable. 262 | 263 | Also possible to rename `vXX = v_named` into `_v_named = v_named` and vice versa. 264 | 265 | And there's a feature for massive renaming functions using assert statements. If you find a function that looks like an assert, right-click the string argument with the function name and select "Rename as assert argument". All the functions where a call to assert statement has happened will be renamed (provided that there is no conflicts, either way, you'll see the warning in the output window) 266 | 267 | ### Name Propagation (P) 268 | 269 | This feature does the same recursive traversal over functions as the Deep Scan Variable does. But this time, all elements that have a connection with the selected one receive its name. It’s possible to rename it or use names of both local and global variables, as well as structure members. By default, the plugin propagates names only over default names like `v1`, `a2`. See **Configuration** in order to change that. 270 | 271 | ### Untangling 'if' statements 272 | 273 | * Clicking `if` manually allows to switch `then` and `else` branches 274 | * Automatically applies the following transformations: 275 | 276 | Before: 277 | 278 | ```c 279 | ... 280 | if (condition) { 281 | statement_1; 282 | statement_2; 283 | ... 284 | return another_value; 285 | } 286 | return value; 287 | ``` 288 | 289 | After: 290 | ```c 291 | ... 292 | if (opposite_condition) { 293 | return value; 294 | } 295 | statement_1; 296 | statement_2; 297 | ... 298 | return another_value; // if 'then' branch has no return, than `return value;` 299 | ``` 300 | 301 | Classes 302 | ------- 303 | 304 | Also, it can be found at _View->Open Subview->Classes_. Helps to manage classes (structures with virtual tables). 305 | 306 | ![img][classes] 307 | 308 | ##### !! Better to rename all functions before debugging, because Ida can mess up default names, and the information in virtual tables will be inconsistent. 309 | 310 | Class, virtual tables, and functions names are editable. Also a function's declaration can be edited. After editting, the altered items change font to _italic_. Right click opens the following menu options: 311 | 312 | * Expand All / Collapse All. 313 | * Refresh - clear all and rescan local types for information again. 314 | * Rollback - undo changes. 315 | * Commit - apply changes. Functions will be renamed and recasted both in virtual tables in Local Types and disassembly code. 316 | * Set First Argument type - allows selecting the first argument for a function among all classes. If right click was used on class name, then its type will be automatically applied to the virtual table at offset 0. 317 | 318 | You can also filter classes using Regexp either by class_name or by existence of specific functions. Simply input an expression in line edit for filtering by class_name or prepend it with "!" to filter by function name. 319 | 320 | --- 321 | ## Structure Graph 322 | 323 | Shows relationship between structures: 324 | 325 | ![img][structure_graph] 326 | 327 | Also: dark green node is union, light green - enum. 328 | 329 | Usage: 330 | 331 | 1. Open Local Types. 332 | 2. Select structures and right click -> "Show Graph" (Hotkey "G"). 333 | 3. Plugin creates a graph of all structures that have relationship with selected items. 334 | 4. Double clicking on a node recalculates the graph for it. 335 | 5. Every node has a hint message that shows C-like typedef. 336 | 337 | --- 338 | ## API 339 | 340 | **Under construction** 341 | 342 | --- 343 | ## Presentations 344 | 345 | * [ZeroNights 2016](https://2016.zeronights.ru/wp-content/uploads/2016/12/zeronights_2016_Kirillov.pptx) 346 | * [Insomni'hack 2018](https://www.youtube.com/watch?v=pnPuwBtW2_4) 347 | * [Infosec in the City 2018](https://www.infosec-city.com/sg18-1-hex-rays) ([Slides](https://1drv.ms/p/s!AocQazyOQ8prgxNCpajrkwURQnPd)) 348 | 349 | [0]: https://sourceforge.net/projects/classinformer/ 350 | [1]: https://msdn.microsoft.com/en-us/library/windows/hardware/ff542043%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 351 | [structure_graph]: Img/structure_builder.JPG 352 | [bad_structures]: Img/bad.JPG 353 | [good_structures]: Img/good.JPG 354 | [builder]: Img/builder.png 355 | [virtual_functions]: Img/virtual_functions.JPG 356 | [scanned_variables]: Img/fields_xref.JPG 357 | [classes]: Img/classes.JPG 358 | [feature_history]: https://github.com/igogo-x86/HexRaysPyTools/wiki/History 359 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | toml --------------------------------------------------------------------------------