diff --git a/plugins/HexRaysCodeXplorer.dll b/plugins/HexRaysCodeXplorer.dll deleted file mode 100644 index 264810e..0000000 Binary files a/plugins/HexRaysCodeXplorer.dll and /dev/null differ diff --git a/plugins/HexRaysCodeXplorer64.dll b/plugins/HexRaysCodeXplorer64.dll deleted file mode 100644 index 8e708be..0000000 Binary files a/plugins/HexRaysCodeXplorer64.dll and /dev/null differ diff --git a/plugins/HexRaysPyTools.py b/plugins/HexRaysPyTools.py new file mode 100644 index 0000000..30e954a --- /dev/null +++ b/plugins/HexRaysPyTools.py @@ -0,0 +1,279 @@ +import logging +import HexRaysPyTools.Actions as Actions +import HexRaysPyTools.Core.Cache +from HexRaysPyTools.Core.TemporaryStructure import * +import HexRaysPyTools.Forms as Forms +import idaapi +import HexRaysPyTools.Core.NegativeOffsets as NegativeOffsets +import HexRaysPyTools.Core.Helper as Helper +import HexRaysPyTools.Core.Cache as Cache +import HexRaysPyTools.Core.Const as Const +from HexRaysPyTools.Core.SpaghettiCode import SpaghettiVisitor, SwapThenElseVisitor +from HexRaysPyTools.Core.StructXrefs import * + +potential_negatives = {} + + +def hexrays_events_callback(*args): + global potential_negatives + + hexrays_event = args[0] + + if hexrays_event == idaapi.hxe_populating_popup: + form, popup, hx_view = args[1:] + item = hx_view.item # current ctree_item_t + + if Actions.GuessAllocation.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.GuessAllocation.name, None) + + if Actions.RecastItemRight.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.RecastItemRight.name, None) + + if Actions.RecastItemLeft.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.RecastItemLeft.name, None) + + if Actions.RenameOther.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.RenameOther.name, None) + + if Actions.RenameInside.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.RenameInside.name, None) + + if Actions.RenameOutside.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.RenameOutside.name, None) + + if Actions.RenameUsingAssert.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.RenameUsingAssert.name, None) + + if Actions.SwapThenElse.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.SwapThenElse.name, None) + + if Actions.ShallowScanVariable.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.ShallowScanVariable.name, None) + idaapi.attach_action_to_popup(form, popup, Actions.DeepScanVariable.name, None) + idaapi.attach_action_to_popup(form, popup, Actions.RecognizeShape.name, None) + + if Actions.CreateNewField.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.CreateNewField.name, None) + + if Actions.FindFieldXrefs.check(item): + idaapi.attach_action_to_popup(form, popup, Actions.FindFieldXrefs.name, None) + + if Actions.PropagateName.check(hx_view.cfunc, item): + idaapi.attach_action_to_popup(form, popup, Actions.PropagateName.name, None) + + if item.citype == idaapi.VDI_FUNC: + # If we clicked on function + if not hx_view.cfunc.entry_ea == idaapi.BADADDR: # Probably never happen + idaapi.attach_action_to_popup(form, popup, Actions.AddRemoveReturn.name, None) + idaapi.attach_action_to_popup(form, popup, Actions.ConvertToUsercall.name, None) + if Actions.DeepScanReturn.check(hx_view): + idaapi.attach_action_to_popup(form, popup, Actions.DeepScanReturn.name, None) + + elif item.citype == idaapi.VDI_LVAR: + # If we clicked on argument + local_variable = hx_view.item.get_lvar() # idaapi.lvar_t + if local_variable.is_arg_var: + idaapi.attach_action_to_popup(form, popup, Actions.RemoveArgument.name, None) + + elif item.citype == idaapi.VDI_EXPR: + if item.e.op == idaapi.cot_num: + # number_format = item.e.n.nf # idaapi.number_format_t + # print "(number) flags: {0:#010X}, type_name: {1}, opnum: {2}".format( + # number_format.flags, + # number_format.type_name, + # number_format.opnum + # ) + idaapi.attach_action_to_popup(form, popup, Actions.GetStructureBySize.name, None) + elif item.e.op == idaapi.cot_var: + # Check if we clicked on variable that is a pointer to a structure that is potentially part of + # containing structure + if item.e.v.idx in potential_negatives: + idaapi.attach_action_to_popup(form, popup, Actions.SelectContainingStructure.name, None) + if Actions.ResetContainingStructure.check(hx_view.cfunc.get_lvars()[item.e.v.idx]): + idaapi.attach_action_to_popup(form, popup, Actions.ResetContainingStructure.name, None) + + elif hexrays_event == idaapi.hxe_double_click: + hx_view = args[1] + item = hx_view.item + if item.citype == idaapi.VDI_EXPR and item.e.op == idaapi.cot_memptr: + # Look if we double clicked on expression that is member pointer. Then get tinfo_t of the structure. + # After that remove pointer and get member name with the same offset + if item.e.x.op == idaapi.cot_memref and item.e.x.x.op == idaapi.cot_memptr: + vtable_tinfo = item.e.x.type.get_pointed_object() + method_offset = item.e.m + class_tinfo = item.e.x.x.x.type.get_pointed_object() + vtable_offset = item.e.x.x.m + elif item.e.x.op == idaapi.cot_memptr: + vtable_tinfo = item.e.x.type.get_pointed_object() + method_offset = item.e.m + class_tinfo = item.e.x.x.type.get_pointed_object() + vtable_offset = item.e.x.m + else: + func_offset = item.e.m + struct_tinfo = item.e.x.type.get_pointed_object() + func_ea = Helper.get_virtual_func_address(Helper.get_member_name(struct_tinfo, func_offset)) + if func_ea: + idaapi.jumpto(func_ea) + return 0 + + func_name = Helper.get_member_name(vtable_tinfo, method_offset) + func_ea = Helper.get_virtual_func_address(func_name, class_tinfo, vtable_offset) + if func_ea: + idaapi.open_pseudocode(func_ea, 0) + return 1 + + elif hexrays_event == idaapi.hxe_maturity: + cfunc, level_of_maturity = args[1:] + + if level_of_maturity == idaapi.CMAT_BUILT: + # print '=' * 40 + # print '=' * 15, "LEVEL", level_of_maturity, '=' * 16 + # print '=' * 40 + # print cfunc + + # First search for CONTAINING_RECORD made by Ida + visitor = NegativeOffsets.SearchVisitor(cfunc) + visitor.apply_to(cfunc.body, None) + negative_lvars = visitor.result + + # Second get saved information from comments + lvars = cfunc.get_lvars() + for idx in xrange(len(lvars)): + result = NegativeOffsets.parse_lvar_comment(lvars[idx]) + if result and result.tinfo.equals_to(lvars[idx].type().get_pointed_object()): + negative_lvars[idx] = result + + # Third make an analysis of local variables that a structure pointers and have reference that pass + # through structure boundaries. This variables will be considered as potential pointers to substructure + # and will get a menu on right click that helps to select Containing Structure from different libraries + + structure_pointer_variables = {} + for idx in set(range(len(lvars))) - set(negative_lvars.keys()): + if lvars[idx].type().is_ptr(): + pointed_tinfo = lvars[idx].type().get_pointed_object() + if pointed_tinfo.is_udt(): + structure_pointer_variables[idx] = pointed_tinfo + + if structure_pointer_variables: + visitor = NegativeOffsets.AnalyseVisitor(structure_pointer_variables, potential_negatives) + visitor.apply_to(cfunc.body, None) + + if negative_lvars: + visitor = NegativeOffsets.ReplaceVisitor(negative_lvars) + visitor.apply_to(cfunc.body, None) + + elif level_of_maturity == idaapi.CMAT_TRANS1: + + visitor = SwapThenElseVisitor(cfunc.entry_ea) + visitor.apply_to(cfunc.body, None) + + elif level_of_maturity == idaapi.CMAT_TRANS2: + # print '=' * 40 + # print '=' * 15, "LEVEL", level_of_maturity, '=' * 16 + # print '=' * 40 + # print cfunc + visitor = SpaghettiVisitor() + visitor.apply_to(cfunc.body, None) + + elif level_of_maturity == idaapi.CMAT_FINAL: + StructXrefVisitor(cfunc).process() + + return 0 + + +class MyPlugin(idaapi.plugin_t): + # flags = idaapi.PLUGIN_HIDE + flags = 0 + comment = "Plugin for automatic classes reconstruction" + help = "This is help" + wanted_name = "HexRaysPyTools" + wanted_hotkey = "Alt-F8" + + @staticmethod + def init(): + if not idaapi.init_hexrays_plugin(): + print "[ERROR] Failed to initialize Hex-Rays SDK" + return idaapi.PLUGIN_SKIP + + Cache.temporary_structure = TemporaryStructureModel() + # Actions.register(Actions.CreateVtable) + Actions.register(Actions.ShowGraph) + Actions.register(Actions.ShowClasses) + Actions.register(Actions.GetStructureBySize) + Actions.register(Actions.RemoveArgument) + Actions.register(Actions.AddRemoveReturn) + Actions.register(Actions.ConvertToUsercall) + Actions.register(Actions.ShallowScanVariable, Cache.temporary_structure) + Actions.register(Actions.DeepScanVariable, Cache.temporary_structure) + Actions.register(Actions.DeepScanReturn, Cache.temporary_structure) + Actions.register(Actions.DeepScanFunctions, Cache.temporary_structure) + Actions.register(Actions.RecognizeShape) + Actions.register(Actions.CreateNewField) + Actions.register(Actions.SelectContainingStructure, potential_negatives) + Actions.register(Actions.ResetContainingStructure) + Actions.register(Actions.RecastItemRight) + Actions.register(Actions.RecastItemLeft) + Actions.register(Actions.RenameOther) + Actions.register(Actions.RenameInside) + Actions.register(Actions.RenameOutside) + Actions.register(Actions.RenameUsingAssert) + Actions.register(Actions.SwapThenElse) + Actions.register(Actions.FindFieldXrefs) + Actions.register(Actions.PropagateName) + Actions.register(Actions.GuessAllocation) + + idaapi.attach_action_to_menu('View/Open subviews/Local types', Actions.ShowClasses.name, idaapi.SETMENU_APP) + idaapi.install_hexrays_callback(hexrays_events_callback) + + Const.init() + XrefStorage().open() + + return idaapi.PLUGIN_KEEP + + @staticmethod + def run(arg): + tform = idaapi.find_tform("Structure Builder") + if tform: + idaapi.switchto_tform(tform, True) + else: + Forms.StructureBuilder(Cache.temporary_structure).Show() + + @staticmethod + def term(): + if Cache.temporary_structure: + Cache.temporary_structure.clear() + # Actions.unregister(Actions.CreateVtable) + Actions.unregister(Actions.ShowGraph) + Actions.unregister(Actions.ShowClasses) + Actions.unregister(Actions.GetStructureBySize) + Actions.unregister(Actions.RemoveArgument) + Actions.unregister(Actions.AddRemoveReturn) + Actions.unregister(Actions.ConvertToUsercall) + Actions.unregister(Actions.ShallowScanVariable) + Actions.unregister(Actions.DeepScanVariable) + Actions.unregister(Actions.DeepScanReturn) + Actions.unregister(Actions.DeepScanFunctions) + Actions.unregister(Actions.RecognizeShape) + Actions.unregister(Actions.CreateNewField) + Actions.unregister(Actions.SelectContainingStructure) + Actions.unregister(Actions.ResetContainingStructure) + Actions.unregister(Actions.RecastItemRight) + Actions.unregister(Actions.RecastItemLeft) + Actions.unregister(Actions.RenameOther) + Actions.unregister(Actions.RenameInside) + Actions.unregister(Actions.RenameOutside) + Actions.unregister(Actions.RenameUsingAssert) + Actions.unregister(Actions.SwapThenElse) + Actions.unregister(Actions.FindFieldXrefs) + Actions.unregister(Actions.PropagateName) + Actions.unregister(Actions.GuessAllocation) + idaapi.term_hexrays_plugin() + XrefStorage().close() + + +def PLUGIN_ENTRY(): + idaapi.notify_when(idaapi.NW_OPENIDB, Cache.init_demangled_names) + idaapi.notify_when(idaapi.NW_OPENIDB, Cache.init_imported_ea) + idaapi.notify_when(idaapi.NW_OPENIDB, Cache.reset_touched_functions) + Helper.extend_ida() + return MyPlugin() diff --git a/plugins/HexRaysPyTools/Actions.py b/plugins/HexRaysPyTools/Actions.py new file mode 100644 index 0000000..503faaa --- /dev/null +++ b/plugins/HexRaysPyTools/Actions.py @@ -0,0 +1,1440 @@ +import ctypes +import sys +import re +import logging + +import idaapi +import idc + +import HexRaysPyTools.Forms as Forms +import HexRaysPyTools.Core.Const as Const +import HexRaysPyTools.Core.Helper as Helper +import HexRaysPyTools.Api as Api +import Settings +from HexRaysPyTools.Core.StructureGraph import StructureGraph +from HexRaysPyTools.Core.TemporaryStructure import VirtualTable, TemporaryStructureModel +from HexRaysPyTools.Core.VariableScanner import NewShallowSearchVisitor, NewDeepSearchVisitor, DeepReturnVisitor +from HexRaysPyTools.Core.Helper import FunctionTouchVisitor +from HexRaysPyTools.Core.SpaghettiCode import * +from HexRaysPyTools.Core.StructXrefs import XrefStorage + +RECAST_LOCAL_VARIABLE = 0 +RECAST_GLOBAL_VARIABLE = 1 +RECAST_ARGUMENT = 2 +RECAST_RETURN = 3 +RECAST_STRUCTURE = 4 + +logger = logging.getLogger(__name__) + + +def register(action, *args): + idaapi.register_action( + idaapi.action_desc_t( + action.name, + action.description, + action(*args), + action.hotkey + ) + ) + + +def unregister(action): + idaapi.unregister_action(action.name) + + +class TypeLibrary: + + class til_t(ctypes.Structure): + pass + + til_t._fields_ = [ + ("name", ctypes.c_char_p), + ("desc", ctypes.c_char_p), + ("nbases", ctypes.c_int), + ("base", ctypes.POINTER(ctypes.POINTER(til_t))) + ] + + def __init__(self): + pass + + @staticmethod + def enable_library_ordinals(library_num): + idaname = "ida64" if Const.EA64 else "ida" + if sys.platform == "win32": + dll = ctypes.windll[idaname + ".wll"] + elif sys.platform == "linux2": + dll = ctypes.cdll["lib" + idaname + ".so"] + elif sys.platform == "darwin": + dll = ctypes.cdll["lib" + idaname + ".dylib"] + else: + print "[ERROR] Failed to enable ordinals" + return + + idati = ctypes.POINTER(TypeLibrary.til_t).in_dll(dll, "idati") + dll.enable_numbered_types(idati.contents.base[library_num], True) + + @staticmethod + def choose_til(): + idati = idaapi.cvar.idati + list_type_library = [(idati, idati.name, idati.desc)] + for idx in xrange(idaapi.cvar.idati.nbases): + type_library = idaapi.cvar.idati.base(idx) # idaapi.til_t type + list_type_library.append((type_library, type_library.name, type_library.desc)) + + library_chooser = Forms.MyChoose( + list(map(lambda x: [x[1], x[2]], list_type_library)), + "Select Library", + [["Library", 10 | idaapi.Choose2.CHCOL_PLAIN], ["Description", 30 | idaapi.Choose2.CHCOL_PLAIN]], + 69 + ) + library_num = library_chooser.Show(True) + if library_num != -1: + selected_library = list_type_library[library_num][0] + max_ordinal = idaapi.get_ordinal_qty(selected_library) + if max_ordinal == idaapi.BADORD: + TypeLibrary.enable_library_ordinals(library_num - 1) + max_ordinal = idaapi.get_ordinal_qty(selected_library) + print "[DEBUG] Maximal ordinal of lib {0} = {1}".format(selected_library.name, max_ordinal) + return selected_library, max_ordinal, library_num == 0 + return None + + @staticmethod + def import_type(library, name): + if library.name != idaapi.cvar.idati.name: + last_ordinal = idaapi.get_ordinal_qty(idaapi.cvar.idati) + type_id = idaapi.import_type(library, -1, name) # tid_t + if type_id != idaapi.BADORD: + return last_ordinal + return None + + +class RemoveArgument(idaapi.action_handler_t): + + name = "my:RemoveArgument" + description = "Remove Argument" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + vu = idaapi.get_widget_vdui(ctx.widget) + function_tinfo = idaapi.tinfo_t() + if not vu.cfunc.get_func_type(function_tinfo): + return + function_details = idaapi.func_type_data_t() + function_tinfo.get_func_details(function_details) + del_arg = vu.item.get_lvar() # lvar_t + + function_details.erase(filter(lambda x: x.name == del_arg.name, function_details)[0]) + + function_tinfo.create_func(function_details) + idaapi.apply_tinfo2(vu.cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE) + vu.refresh_view(True) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + +class AddRemoveReturn(idaapi.action_handler_t): + + name = "my:RemoveReturn" + description = "Add/Remove Return" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + # ctx - action_activation_ctx_t + vu = idaapi.get_widget_vdui(ctx.widget) + function_tinfo = idaapi.tinfo_t() + if not vu.cfunc.get_func_type(function_tinfo): + return + function_details = idaapi.func_type_data_t() + function_tinfo.get_func_details(function_details) + if function_details.rettype.equals_to(Const.VOID_TINFO): + function_details.rettype = idaapi.tinfo_t(Const.PVOID_TINFO) + else: + function_details.rettype = idaapi.tinfo_t(idaapi.BT_VOID) + function_tinfo.create_func(function_details) + idaapi.apply_tinfo2(vu.cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE) + vu.refresh_view(True) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + +class ConvertToUsercall(idaapi.action_handler_t): + + name = "my:ConvertToUsercall" + description = "Convert to __usercall" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + # ctx - action_activation_ctx_t + vu = idaapi.get_widget_vdui(ctx.widget) + function_tinfo = idaapi.tinfo_t() + if not vu.cfunc.get_func_type(function_tinfo): + return + function_details = idaapi.func_type_data_t() + function_tinfo.get_func_details(function_details) + convention = idaapi.CM_CC_MASK & function_details.cc + if convention == idaapi.CM_CC_CDECL: + function_details.cc = idaapi.CM_CC_SPECIAL + elif convention in (idaapi.CM_CC_STDCALL, idaapi.CM_CC_FASTCALL, idaapi.CM_CC_PASCAL, idaapi.CM_CC_THISCALL): + function_details.cc = idaapi.CM_CC_SPECIALP + elif convention == idaapi.CM_CC_ELLIPSIS: + function_details.cc = idaapi.CM_CC_SPECIALE + else: + return + function_tinfo.create_func(function_details) + idaapi.apply_tinfo2(vu.cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE) + vu.refresh_view(True) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + +class GetStructureBySize(idaapi.action_handler_t): + # TODO: apply type automatically if expression like `var = new(size)` + + name = "my:WhichStructHaveThisSize" + description = "Structures with this size" + hotkey = "W" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def select_structure_by_size(size): + result = TypeLibrary.choose_til() + if result: + selected_library, max_ordinal, is_local_type = result + matched_types = [] + tinfo = idaapi.tinfo_t() + for ordinal in xrange(1, max_ordinal): + tinfo.create_typedef(selected_library, ordinal) + if tinfo.get_size() == size: + name = tinfo.dstr() + description = idaapi.print_tinfo(None, 0, 0, idaapi.PRTYPE_DEF, tinfo, None, None) + matched_types.append([str(ordinal), name, description]) + + type_chooser = Forms.MyChoose( + matched_types, + "Select Type", + [["Ordinal", 5 | idaapi.Choose2.CHCOL_HEX], ["Type Name", 25], ["Declaration", 50]], + 165 + ) + selected_type = type_chooser.Show(True) + if selected_type != -1: + if is_local_type: + return int(matched_types[selected_type][0]) + return TypeLibrary.import_type(selected_library, matched_types[selected_type][1]) + return None + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + if hx_view.item.citype != idaapi.VDI_EXPR or hx_view.item.e.op != idaapi.cot_num: + return + ea = ctx.cur_ea + c_number = hx_view.item.e.n + number_value = c_number._value + ordinal = GetStructureBySize.select_structure_by_size(number_value) + if ordinal: + number_format_old = c_number.nf + number_format_new = idaapi.number_format_t() + number_format_new.flags = idaapi.FF_1STRO | idaapi.FF_0STRO + operand_number = number_format_old.opnum + number_format_new.opnum = operand_number + number_format_new.props = number_format_old.props + number_format_new.type_name = idaapi.create_numbered_type_name(ordinal) + + c_function = hx_view.cfunc + number_formats = c_function.numforms # idaapi.user_numforms_t + operand_locator = idaapi.operand_locator_t(ea, ord(operand_number) if operand_number else 0) + if operand_locator in number_formats: + del number_formats[operand_locator] + + number_formats[operand_locator] = number_format_new + c_function.save_user_numforms() + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class ShallowScanVariable(idaapi.action_handler_t): + + name = "my:ShallowScanVariable" + description = "Scan Variable" + hotkey = "F" + + def __init__(self, temporary_structure): + self.temporary_structure = temporary_structure + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + lvar = ctree_item.get_lvar() + if lvar is not None: + return Helper.is_legal_type(lvar.type()) + + if ctree_item.citype != idaapi.VDI_EXPR: + return False + + obj = Api.ScanObject.create(cfunc, ctree_item.e) + return obj and Helper.is_legal_type(obj.tinfo) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + cfunc = hx_view.cfunc + origin = self.temporary_structure.main_offset + + if self.check(cfunc, hx_view.item): + obj = Api.ScanObject.create(cfunc, hx_view.item) + visitor = NewShallowSearchVisitor(cfunc, origin, obj, self.temporary_structure) + visitor.process() + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class DeepScanVariable(idaapi.action_handler_t): + + name = "my:DeepScanVariable" + description = "Deep Scan Variable" + hotkey = "shift+F" + + def __init__(self, temporary_structure): + self.temporary_structure = temporary_structure + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + cfunc = hx_view.cfunc + origin = self.temporary_structure.main_offset + + if ShallowScanVariable.check(cfunc, hx_view.item): + obj = Api.ScanObject.create(cfunc, hx_view.item) + if FunctionTouchVisitor(cfunc).process(): + hx_view.refresh_view(True) + visitor = NewDeepSearchVisitor(hx_view.cfunc, origin, obj, self.temporary_structure) + visitor.process() + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class DeepScanReturn(idaapi.action_handler_t): + name = "my:DeepScanReturn" + description = "Deep Scan Returned Variables" + hotkey = None + + def __init__(self, temporary_structure): + self.temp_struct = temporary_structure + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(hx_view): + tinfo = idaapi.tinfo_t() + hx_view.cfunc.get_func_type(tinfo) + return not tinfo.get_rettype().equals_to(Const.VOID_TINFO) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + func_ea = hx_view.cfunc.entry_ea + + obj = Api.ReturnedObject(func_ea) + visitor = DeepReturnVisitor(hx_view.cfunc, self.temp_struct.main_offset, obj, self.temp_struct) + visitor.process() + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class DeepScanFunctions(idaapi.action_handler_t): + + name = "my:DeepScanFunctions" + description = "Scan First Argument" + hotkey = None + + def __init__(self, temporary_structure): + self.temporary_structure = temporary_structure + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + for idx in ctx.chooser_selection: + func_ea = idaapi.getn_func(idx - 1).startEA + cfunc = Api.decompile_function(func_ea) + obj = Api.VariableObject(cfunc.get_lvars()[0], 0) + if cfunc: + NewDeepSearchVisitor(cfunc, 0, obj, self.temporary_structure).process() + + def update(self, ctx): + if ctx.form_type == idaapi.BWN_FUNCS: + idaapi.attach_action_to_popup(ctx.widget, None, self.name) + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class RecognizeShape(idaapi.action_handler_t): + + name = "my:RecognizeShape" + description = "Recognize Shape" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + cfunc = hx_view.cfunc + + if not ShallowScanVariable.check(cfunc, hx_view.item): + return + + obj = Api.ScanObject.create(cfunc, hx_view.item) + temp_struct = TemporaryStructureModel() + visitor = NewShallowSearchVisitor(cfunc, 0, obj, temp_struct) + visitor.process() + tinfo = temp_struct.get_recognized_shape() + if tinfo: + tinfo.create_ptr(tinfo) + if obj.id == Api.SO_LOCAL_VARIABLE: + hx_view.set_lvar_type(obj.lvar, tinfo) + elif obj.id == Api.SO_GLOBAL_OBJECT: + idaapi.apply_tinfo2(obj.obj_ea, tinfo, idaapi.TINFO_DEFINITE) + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class CreateNewField(idaapi.action_handler_t): + name = "my:CreateNewField" + description = "Create New Field" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return + + item = ctree_item.it.to_specific_type + if item.op != idaapi.cot_memptr: + return + + parent = cfunc.body.find_parent_of(ctree_item.it).to_specific_type + if parent.op != idaapi.cot_idx or parent.y.op != idaapi.cot_num: + idx = 0 + else: + idx = parent.y.numval() + + struct_type = item.x.type.get_pointed_object() + udt_member = idaapi.udt_member_t() + udt_member.offset = item.m * 8 + struct_type.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) + if udt_member.name[0:3] != "gap": + return + + return struct_type, udt_member.offset // 8, idx + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + result = self.check(hx_view.cfunc, hx_view.item) + if result is None: + return + + struct_tinfo, offset, idx = result + ordinal = struct_tinfo.get_ordinal() + struct_name = struct_tinfo.dstr() + + if (offset + idx) % 2: + default_field_type = "_BYTE" + elif (offset + idx) % 4: + default_field_type = "_WORD" + else: + default_field_type = "_DWORD" + + declaration = idaapi.asktext( + 0x10000, "{0} field_{1:X}".format(default_field_type, offset + idx), "Enter new structure member:" + ) + if declaration is None: + return + + result = self.parse_declaration(declaration) + if result is None: + logger.warn("Bad member declaration") + return + + field_tinfo, field_name = result + field_size = field_tinfo.get_size() + udt_data = idaapi.udt_type_data_t() + udt_member = idaapi.udt_member_t() + + struct_tinfo.get_udt_details(udt_data) + udt_member.offset = offset * 8 + struct_tinfo.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) + gap_size = udt_member.size // 8 + + gap_leftover = gap_size - idx - field_size + + if gap_leftover < 0: + print "[ERROR] Too big size for the field. Type with maximum {0} bytes can be used".format(gap_size - idx) + return + + iterator = udt_data.find(udt_member) + iterator = udt_data.erase(iterator) + + if gap_leftover > 0: + udt_data.insert(iterator, TemporaryStructureModel.get_padding_member(offset + idx + field_size, gap_leftover)) + + udt_member = idaapi.udt_member_t() + udt_member.offset = offset * 8 + idx + udt_member.name = field_name + udt_member.type = field_tinfo + udt_member.size = field_size + + iterator = udt_data.insert(iterator, udt_member) + + if idx > 0: + udt_data.insert(iterator, TemporaryStructureModel.get_padding_member(offset, idx)) + + struct_tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) + struct_tinfo.set_numbered_type(idaapi.cvar.idati, ordinal, idaapi.BTF_STRUCT, struct_name) + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + @staticmethod + def parse_declaration(declaration): + m = re.search(r"^(\w+[ *]+)(\w+)(\[(\d+)\])?$", declaration) + if m is None: + return + + type_name, field_name, _, arr_size = m.groups() + if field_name[0].isdigit(): + print "[ERROR] Bad field name" + return + + result = idc.ParseType(type_name, 0) + if result is None: + return + + _, tp, fld = result + tinfo = idaapi.tinfo_t() + tinfo.deserialize(idaapi.cvar.idati, tp, fld, None) + if arr_size: + assert tinfo.create_array(tinfo, int(arr_size)) + return tinfo, field_name + + +class ShowGraph(idaapi.action_handler_t): + name = "my:ShowGraph" + description = "Show graph" + hotkey = "G" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + self.graph = None + self.graph_view = None + + def activate(self, ctx): + """ + :param ctx: idaapi.action_activation_ctx_t + :return: None + """ + form = self.graph_view.GetTForm() if self.graph_view else None + if form: + self.graph_view.change_selected([sel + 1 for sel in ctx.chooser_selection]) + self.graph_view.Show() + else: + self.graph = StructureGraph([sel + 1 for sel in ctx.chooser_selection]) + self.graph_view = Forms.StructureGraphViewer("Structure Graph", self.graph) + self.graph_view.Show() + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_LOCTYPS: + idaapi.attach_action_to_popup(ctx.widget, None, self.name) + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class ShowClasses(idaapi.action_handler_t): + + name = "my:ShowClasses" + description = "Classes" + hotkey = "Alt+F1" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + """ + :param ctx: idaapi.action_activation_ctx_t + :return: None + """ + tform = idaapi.find_tform('Classes') + if not tform: + class_viewer = Forms.ClassViewer() + class_viewer.Show() + else: + idaapi.switchto_tform(tform, True) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + +class CreateVtable(idaapi.action_handler_t): + + name = "my:CreateVtable" + description = "Create Virtual Table" + hotkey = "V" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + def activate(self, ctx): + ea = ctx.cur_ea + if ea != idaapi.BADADDR and VirtualTable.check_address(ea): + vtable = VirtualTable(0, ea) + vtable.import_to_structures(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_DISASM: + idaapi.attach_action_to_popup(ctx.widget, None, self.name) + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class SelectContainingStructure(idaapi.action_handler_t): + + name = "my:SelectContainingStructure" + description = "Select Containing Structure" + hotkey = None + + def __init__(self, potential_negatives): + idaapi.action_handler_t.__init__(self) + self.potential_negative = potential_negatives + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + result = TypeLibrary.choose_til() + if result: + selected_library, max_ordinal, is_local_types = result + lvar_idx = hx_view.item.e.v.idx + candidate = self.potential_negative[lvar_idx] + structures = candidate.find_containing_structures(selected_library) + items = map(lambda x: [str(x[0]), "0x{0:08X}".format(x[1]), x[2], x[3]], structures) + structure_chooser = Forms.MyChoose( + items, + "Select Containing Structure", + [["Ordinal", 5], ["Offset", 10], ["Member_name", 20], ["Structure Name", 20]], + 165 + ) + selected_idx = structure_chooser.Show(modal=True) + if selected_idx != -1: + if not is_local_types: + TypeLibrary.import_type(selected_library, items[selected_idx][3]) + lvar = hx_view.cfunc.get_lvars()[lvar_idx] + lvar_cmt = re.sub("```.*```", '', lvar.cmt) + hx_view.set_lvar_cmt( + lvar, + lvar_cmt + "```{0}+{1}```".format( + structures[selected_idx][3], + structures[selected_idx][1]) + ) + hx_view.refresh_view(True) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + +class ResetContainingStructure(idaapi.action_handler_t): + + name = "my:ResetContainingStructure" + description = "Reset Containing Structure" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(lvar): + return True if re.search("```.*```", lvar.cmt) else False + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + lvar = hx_view.cfunc.get_lvars()[hx_view.item.e.v.idx] + hx_view.set_lvar_cmt(lvar, re.sub("```.*```", '', lvar.cmt)) + hx_view.refresh_view(True) + + def update(self, ctx): + return idaapi.AST_ENABLE_ALWAYS + + +class RecastItemLeft(idaapi.action_handler_t): + + name = "my:RecastItemLeft" + description = "Recast Item" + hotkey = "Shift+L" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return + + expression = ctree_item.it.to_specific_type + child = None + + # Look through parents until we found Return, Assignment or Call + while expression and expression.op not in (idaapi.cot_asg, idaapi.cit_return, idaapi.cot_call): + child = expression.to_specific_type + expression = cfunc.body.find_parent_of(expression) + if not expression: + return + + expression = expression.to_specific_type + if expression.opname == 'asg': + + if expression.x.opname not in ('var', 'obj', 'memptr', 'memref'): + return + + right_expr = expression.y + right_tinfo = right_expr.x.type if right_expr.op == idaapi.cot_cast else right_expr.type + + # Check if both left and right parts of expression are of the same types. + # If not then we can recast then. + if right_tinfo.dstr() == expression.x.type.dstr(): + return + + if expression.x.op == idaapi.cot_var: + # var = (TYPE ) ...; + variable = cfunc.get_lvars()[expression.x.v.idx] + idaapi.update_action_label(RecastItemLeft.name, 'Recast Variable "{0}"'.format(variable.name)) + return RECAST_LOCAL_VARIABLE, right_tinfo, variable + elif expression.x.op == idaapi.cot_obj: + # g_var = (TYPE ) ...; + idaapi.update_action_label(RecastItemLeft.name, 'Recast Global') + return RECAST_GLOBAL_VARIABLE, right_tinfo, expression.x.obj_ea + elif expression.x.op == idaapi.cot_memptr: + # struct->member = (TYPE ) ...; + idaapi.update_action_label(RecastItemLeft.name, 'Recast Field') + return RECAST_STRUCTURE, expression.x.x.type.get_pointed_object().dstr(), expression.x.m, right_tinfo + elif expression.x.op == idaapi.cot_memref: + # struct.member = (TYPE ) ...; + idaapi.update_action_label(RecastItemLeft.name, 'Recast Field') + return RECAST_STRUCTURE, expression.x.x.type.dstr(), expression.x.m, right_tinfo + + elif expression.op == idaapi.cit_return: + + idaapi.update_action_label(RecastItemLeft.name, "Recast Return") + child = child or expression.creturn.expr + + if child.op == idaapi.cot_cast: + # return (TYPE) ...; + return RECAST_RETURN, child.x.type, None + + func_tinfo = idaapi.tinfo_t() + cfunc.get_func_type(func_tinfo) + rettype = func_tinfo.get_rettype() + + if func_tinfo.get_rettype().dstr() != child.type.dstr(): + # return ...; + # This's possible when returned type and value are both pointers to different types + return RECAST_RETURN, child.type, None + + elif expression.op == idaapi.cot_call: + if expression.x.op == idaapi.cot_memptr: + if expression.x == child: + return + + arg_index, arg_tinfo = Helper.get_func_argument_info(expression, child) + if child.op == idaapi.cot_cast: + # struct_ptr->func(..., (TYPE) var, ...); + new_arg_tinfo = child.x.type + else: + # struct_ptr->func(..., var, ...); When `var` and `arg` are different pointers + if arg_tinfo.equals_to(child.type): + return + new_arg_tinfo = child.type + + struct_type = expression.x.x.type.get_pointed_object() + funcptr_tinfo = expression.x.type + Helper.set_funcptr_argument(funcptr_tinfo, arg_index, new_arg_tinfo) + return RECAST_STRUCTURE, struct_type.dstr(), expression.x.m, funcptr_tinfo + + if child and child.op == idaapi.cot_cast: + if child.cexpr.x.op == idaapi.cot_memptr and expression.ea == idaapi.BADADDR: + idaapi.update_action_label(RecastItemLeft.name, 'Recast Virtual Function') + return RECAST_STRUCTURE, child.cexpr.x.x.type.get_pointed_object().dstr(), child.cexpr.x.m, child.type + + if expression.x == child.cexpr: + return + arg_index, _ = Helper.get_func_argument_info(expression, child.cexpr) + idaapi.update_action_label(RecastItemLeft.name, "Recast Argument") + return ( + RECAST_ARGUMENT, + arg_index, + expression.x.type.get_pointed_object(), + child.x.type, + expression.x.obj_ea + ) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + result = self.check(hx_view.cfunc, hx_view.item) + + if not result: + return + + if result[0] == RECAST_LOCAL_VARIABLE: + logger.debug("Recasting local variable. Type - %s", result[1].dstr()) + tinfo, lvar = result[1:] + if hx_view.set_lvar_type(lvar, tinfo): + hx_view.refresh_view(True) + + elif result[0] == RECAST_GLOBAL_VARIABLE: + logger.debug("Recasting global. Type - %s. Address - %s", result[1].dstr(), Helper.to_hex(result[2])) + tinfo, address = result[1:] + if idaapi.apply_tinfo2(address, tinfo, idaapi.TINFO_DEFINITE): + hx_view.refresh_view(True) + + elif result[0] == RECAST_ARGUMENT: + arg_index, func_tinfo, arg_tinfo, address = result[1:] + if arg_tinfo.is_array(): + arg_tinfo.convert_array_to_ptr() + + func_data = idaapi.func_type_data_t() + func_tinfo.get_func_details(func_data) + func_data[arg_index].type = arg_tinfo + new_func_tinfo = idaapi.tinfo_t() + new_func_tinfo.create_func(func_data) + if idaapi.apply_tinfo2(address, new_func_tinfo, idaapi.TINFO_DEFINITE): + hx_view.refresh_view(True) + + elif result[0] == RECAST_RETURN: + return_type, func_address = result[1:] + try: + cfunc = idaapi.decompile(func_address) if func_address else hx_view.cfunc + except idaapi.DecompilationFailure: + print "[ERROR] Ida failed to decompile function at 0x{0:08X}".format(func_address) + return + + function_tinfo = idaapi.tinfo_t() + cfunc.get_func_type(function_tinfo) + func_data = idaapi.func_type_data_t() + function_tinfo.get_func_details(func_data) + func_data.rettype = return_type + function_tinfo.create_func(func_data) + if idaapi.apply_tinfo2(cfunc.entry_ea, function_tinfo, idaapi.TINFO_DEFINITE): + hx_view.refresh_view(True) + + elif result[0] == RECAST_STRUCTURE: + structure_name, field_offset, new_type = result[1:] + tinfo = idaapi.tinfo_t() + tinfo.get_named_type(idaapi.cvar.idati, structure_name) + + ordinal = idaapi.get_type_ordinal(idaapi.cvar.idati, structure_name) + + if ordinal: + udt_member = idaapi.udt_member_t() + udt_member.offset = field_offset * 8 + idx = tinfo.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) + if udt_member.offset != field_offset * 8: + print "[Info] Can't handle with arrays yet" + elif udt_member.type.get_size() != new_type.get_size(): + print "[Info] Can't recast different sizes yet" + else: + udt_data = idaapi.udt_type_data_t() + tinfo.get_udt_details(udt_data) + udt_data[idx].type = new_type + tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) + tinfo.set_numbered_type(idaapi.cvar.idati, ordinal, idaapi.NTF_REPLACE, structure_name) + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class RecastItemRight(RecastItemLeft): + + name = "my:RecastItemRight" + description = "Recast Item" + hotkey = "Shift+R" + + def __init__(self): + RecastItemLeft.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return + + expression = ctree_item.it + + result = RecastItemRight._check_potential_array(cfunc, expression) + if result: + return result + + # Look through parents until we found Cast + while expression and expression.op != idaapi.cot_cast: + expression = expression.to_specific_type + expression = cfunc.body.find_parent_of(expression) + if not expression: + return + + expression = expression.to_specific_type + + # Find `(TYPE) something;` or `(TYPE *) &something;` and calculate appropriate type for recast + if expression.x.op == idaapi.cot_ref: + new_type = expression.type.get_pointed_object() + expression = expression.x + else: + new_type = expression.type + + if expression.x.op == idaapi.cot_var: + # (TYPE) var; + variable = cfunc.get_lvars()[expression.x.v.idx] + idaapi.update_action_label(RecastItemRight.name, 'Recast Variable "{0}"'.format(variable.name)) + return RECAST_LOCAL_VARIABLE, new_type, variable + + elif expression.x.op == idaapi.cot_obj: + # (TYPE) g_var; + if Helper.is_code_ea(expression.x.obj_ea) and new_type.is_funcptr(): + # (TYPE) sub_XXXXXX; + new_type = new_type.get_pointed_object() + + idaapi.update_action_label(RecastItemRight.name, 'Recast Global') + return RECAST_GLOBAL_VARIABLE, new_type, expression.x.obj_ea + + elif expression.x.op == idaapi.cot_call: + # (TYPE) call(); + idaapi.update_action_label(RecastItemRight.name, "Recast Return") + return RECAST_RETURN, new_type, expression.x.x.obj_ea + + elif expression.x.op == idaapi.cot_memptr: + # (TYPE) var->member; + idaapi.update_action_label(RecastItemRight.name, "Recast Field") + return RECAST_STRUCTURE, expression.x.x.type.get_pointed_object().dstr(), expression.x.m, new_type + + @staticmethod + def _check_potential_array(cfunc, expr): + """ Checks `call(..., &buffer, ..., number)` and returns information for recasting """ + if expr.op != idaapi.cot_var: + return + + var_expr = expr.to_specific_type + parent = cfunc.body.find_parent_of(expr) + if parent.op != idaapi.cot_ref: + return + + parent = cfunc.body.find_parent_of(parent) + if parent.op != idaapi.cot_call: + return + + call_expr = parent.to_specific_type + for arg_expr in call_expr.a: + if arg_expr.op == idaapi.cot_num: + number = arg_expr.numval() + if number: + variable = cfunc.lvars[var_expr.v.idx] + char_array_tinfo = idaapi.tinfo_t() + char_array_tinfo.create_array(idaapi.tinfo_t(idaapi.BTF_CHAR), number) + idaapi.update_action_label(RecastItemRight.name, 'Recast Variable "{}" to "{}"'.format( + variable.name, char_array_tinfo.dstr() + )) + return RECAST_LOCAL_VARIABLE, char_array_tinfo, variable + + +class RenameOther(idaapi.action_handler_t): + name = "my:RenameOther" + description = "Take other name" + hotkey = "Ctrl+N" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return + + expression = ctree_item.it.to_specific_type + if expression.op != idaapi.cot_var: + return + + parent = cfunc.body.find_parent_of(expression).to_specific_type + if parent.op != idaapi.cot_asg: + return + + other = parent.theother(expression) + if other.op != idaapi.cot_var: + return + + this_lvar = ctree_item.get_lvar() + other_lvar = cfunc.get_lvars()[other.v.idx] + if (other_lvar.has_user_name or other_lvar.is_arg_var and re.search("a\d*$", other_lvar.name) is None) \ + and this_lvar.name.lstrip('_') != other_lvar.name.lstrip('_'): + return '_' + other_lvar.name, this_lvar + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + result = self.check(hx_view.cfunc, hx_view.item) + + if result: + name, lvar = result + hx_view.rename_lvar(lvar, name, True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class RenameInside(idaapi.action_handler_t): + name = "my:RenameInto" + description = "Rename inside argument" + hotkey = "Shift+N" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return False + + expression = ctree_item.it.to_specific_type + if expression.op == idaapi.cot_var: + lvar = ctree_item.get_lvar() + # Check if it's either variable with user name or argument with not standard `aX` name + if lvar.has_user_name or lvar.is_arg_var and re.search("a\d*$", lvar.name) is None: + parent = cfunc.body.find_parent_of(expression).to_specific_type + if parent.op == idaapi.cot_call: + arg_index, _ = Helper.get_func_argument_info(parent, expression) + func_tinfo = parent.x.type.get_pointed_object() + func_data = idaapi.func_type_data_t() + func_tinfo.get_func_details(func_data) + if arg_index < func_tinfo.get_nargs() and lvar.name.lstrip('_') != func_data[arg_index].name: + return func_tinfo, parent.x.obj_ea, arg_index, lvar.name.lstrip('_') + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + result = self.check(hx_view.cfunc, hx_view.item) + + if result: + func_tinfo, address, arg_index, name = result + + func_data = idaapi.func_type_data_t() + func_tinfo.get_func_details(func_data) + func_data[arg_index].name = name + new_func_tinfo = idaapi.tinfo_t() + new_func_tinfo.create_func(func_data) + idaapi.apply_tinfo2(address, new_func_tinfo, idaapi.TINFO_DEFINITE) + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class RenameOutside(idaapi.action_handler_t): + name = "my:RenameOutside" + description = "Take argument name" + hotkey = "Ctrl+Shift+N" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return False + + expression = ctree_item.it.to_specific_type + if expression.op == idaapi.cot_var: + lvar = ctree_item.get_lvar() + parent = cfunc.body.find_parent_of(expression).to_specific_type + + if parent.op == idaapi.cot_call: + arg_index, _ = Helper.get_func_argument_info(parent, expression) + func_tinfo = parent.x.type.get_pointed_object() + if func_tinfo.get_nargs() < arg_index: + return + func_data = idaapi.func_type_data_t() + func_tinfo.get_func_details(func_data) + name = func_data[arg_index].name + if name and re.search("a\d*$", name) is None and name != 'this' and name != lvar.name: + return name, lvar + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + result = self.check(hx_view.cfunc, hx_view.item) + + if result: + name, lvar = result + hx_view.rename_lvar(lvar, name, True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class RenameUsingAssertVisitor(idaapi.ctree_parentee_t): + + def __init__(self, cfunc, func_addr, arg_idx): + idaapi.ctree_parentee_t.__init__(self) + self.__cfunc = cfunc + self.__func_addr = func_addr + self.__arg_idx = arg_idx + self.__possible_names = set() + + def visit_expr(self, expr): + if expr.op == idaapi.cot_call and expr.x.op == idaapi.cot_obj and expr.x.obj_ea == self.__func_addr: + arg_expr = expr.a[self.__arg_idx] + if arg_expr.op != idaapi.cot_obj: + logger.error("Argument is not string at {}".format(Helper.to_hex(self._find_asm_address(expr)))) + return 1 + self.__add_func_name(arg_expr) + return 0 + + def process(self): + self.apply_to(self.__cfunc.body, None) + if len(self.__possible_names) == 1: + self.__rename_func() + else: + logger.error("Function at {} has more than one candidate for renaming: {}".format( + Helper.to_hex(self.__cfunc.entry_ea), ", ".join(self.__possible_names))) + + def __add_func_name(self, arg_expr): + new_name = idc.get_strlit_contents(arg_expr.obj_ea) + if not idaapi.is_valid_typename(new_name): + logger.warn("Argument has weird name `{}` at {}".format( + new_name, Helper.to_hex(self._find_asm_address(arg_expr)))) + return + + self.__possible_names.add(new_name) + + def __rename_func(self): + idc.set_name(self.__cfunc.entry_ea, self.__possible_names.pop()) + + +class RenameUsingAssert(idaapi.action_handler_t): + + name = "my:RenameUsingAssert" + description = "Rename as assert argument" + hotkey = None + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return False + + expression = ctree_item.it.to_specific_type + if expression.op != idaapi.cot_obj: + return False + + parent = cfunc.body.find_parent_of(expression).to_specific_type + if parent.op != idaapi.cot_call or parent.x.op != idaapi.cot_obj: + return False + + obj_ea = expression.obj_ea + if not Helper.is_code_ea(obj_ea) and idc.get_str_type(obj_ea) == idc.STRTYPE_C: + str_potential_name = idc.get_strlit_contents(obj_ea) + return idaapi.is_valid_typename(str_potential_name) + return False + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + cfunc = hx_view.cfunc + ctree_item = hx_view.item + if not self.check(cfunc, ctree_item): + return + + expr_arg = ctree_item.it.to_specific_type + expr_call = cfunc.body.find_parent_of(expr_arg).to_specific_type + + arg_idx, _ = Helper.get_func_argument_info(expr_call, expr_arg) + + assert_ea = expr_call.x.obj_ea + all_callers = Helper.get_funcs_calling_address(assert_ea) + + for caller_ea in all_callers: + try: + cfunc = idaapi.decompile(caller_ea) + if not cfunc: + raise idaapi.DecompilationFailure + + RenameUsingAssertVisitor(cfunc, assert_ea, arg_idx).process() + + except idaapi.DecompilationFailure: + logger.warn("IDA failed to decompile at {}".format(Helper.to_hex(caller_ea))) + + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class PropagateName(idaapi.action_handler_t): + name = "my:PropagateName" + description = "Propagate name" + hotkey = "P" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def callback_start(self): + hx_view, _ = self._data + hx_view.switch_to(self._cfunc, False) + + @staticmethod + def callback_manipulate(self, cexpr, obj): + if self.crippled: + logger.debug("Skipping crippled function at {}".format(Helper.to_hex(self._cfunc.entry_ea))) + return + + if obj.id == Api.SO_GLOBAL_OBJECT: + old_name = idaapi.get_short_name(cexpr.obj_ea) + if Settings.PROPAGATE_THROUGH_ALL_NAMES or PropagateName._is_default_name(old_name): + _, name = self._data + new_name = PropagateName.rename(lambda x: idaapi.set_name(cexpr.obj_ea, x), name) + logger.debug("Renamed global variable from {} to {}".format(old_name, new_name)) + elif obj.id == Api.SO_LOCAL_VARIABLE: + lvar = self._cfunc.get_lvars()[cexpr.v.idx] + old_name = lvar.name + if Settings.PROPAGATE_THROUGH_ALL_NAMES or PropagateName._is_default_name(old_name): + hx_view, name = self._data + new_name = PropagateName.rename(lambda x: hx_view.rename_lvar(lvar, x, True), name) + logger.debug("Renamed local variable from {} to {}".format(old_name, new_name)) + elif obj.id in (Api.SO_STRUCT_POINTER, Api.SO_STRUCT_REFERENCE): + struct_tinfo = cexpr.x.type + offset = cexpr.m + struct_tinfo.remove_ptr_or_array() + old_name = Helper.get_member_name(struct_tinfo, offset) + if Settings.PROPAGATE_THROUGH_ALL_NAMES or PropagateName._is_default_name(old_name): + _, name = self._data + new_name = PropagateName.rename(lambda x: Helper.change_member_name(struct_tinfo.dstr(), offset, x), name) + logger.debug("Renamed struct member from {} to {}".format(old_name, new_name)) + + @staticmethod + def rename(rename_func, name): + while not rename_func(name): + name = "_" + name + return name + + @staticmethod + def _is_default_name(string): + return re.match(r"[av]\d+$", string) is not None or \ + re.match(r"this|[qd]?word|field_|off_", string) is not None + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return + + obj = Api.ScanObject.create(cfunc, ctree_item) + if obj and not PropagateName._is_default_name(obj.name): + return obj + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + obj = self.check(hx_view.cfunc, hx_view.item) + if obj: + cfunc = hx_view.cfunc + visitor = Api.RecursiveObjectDownwardsVisitor(cfunc, obj, (hx_view, obj.name), True) + visitor.set_callbacks( + manipulate=PropagateName.callback_manipulate, + start_iteration=PropagateName.callback_start, + finish=lambda x: hx_view.switch_to(cfunc, True) + ) + visitor.process() + hx_view.refresh_view(True) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class GuessAllocation(idaapi.action_handler_t): + name = "my:ActionApi" + description = "Guess allocation" + hotkey = None + + class StructAllocChoose(Forms.MyChoose): + def __init__(self, items): + Forms.MyChoose.__init__( + self, items, "Possible structure allocations", + [["Function", 30], ["Variable", 10], ["Line", 50], ["Type", 10]] + ) + + def OnSelectLine(self, n): + idaapi.jumpto(self.items[n][0]) + + def OnGetLine(self, n): + func_ea, var, line, alloc_type = self.items[n] + return [Helper.to_nice_str(func_ea), var, line, alloc_type] + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return + return Api.ScanObject.create(cfunc, ctree_item) + + @staticmethod + def callback_manipulate(self, cexpr, obj): + if obj.id == Api.SO_LOCAL_VARIABLE: + parent = self.parent_expr() + if parent.op == idaapi.cot_asg: + alloc_obj = Api.MemoryAllocationObject.create(self._cfunc, self.parent_expr().y) + if alloc_obj: + self._data.append([alloc_obj.ea, obj.name, self._get_line(), "HEAP"]) + elif self.parent_expr().op == idaapi.cot_ref: + self._data.append([self._find_asm_address(cexpr), obj.name, self._get_line(), "STACK"]) + elif obj.id == Api.SO_GLOBAL_OBJECT: + self._data.append([self._find_asm_address(cexpr), obj.name, self._get_line(), "GLOBAL"]) + + @staticmethod + def callback_finish(self): + chooser = GuessAllocation.StructAllocChoose(self._data) + chooser.Show(False) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + item = hx_view.item + obj = GuessAllocation.check(hx_view.cfunc, item) + if obj: + visitor = Api.RecursiveObjectUpwardsVisitor(hx_view.cfunc, obj, data=[], skip_after_object=True) + visitor.set_callbacks( + manipulate=self.callback_manipulate, + finish=self.callback_finish + ) + visitor.process() + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class SwapThenElse(idaapi.action_handler_t): + name = "my:SwapIfElse" + description = "Swap then/else" + hotkey = "Shift+S" + + def __init__(self): + idaapi.action_handler_t.__init__(self) + + @staticmethod + def check(cfunc, ctree_item): + if ctree_item.citype != idaapi.VDI_EXPR: + return False + + insn = ctree_item.it.to_specific_type + + if insn.op != idaapi.cit_if or insn.cif.ielse is None: + return False + + return insn.op == idaapi.cit_if and insn.cif.ielse + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + if self.check(hx_view.cfunc, hx_view.item): + insn = hx_view.item.it.to_specific_type + inverse_if(insn.cif) + hx_view.refresh_ctext() + + InversionInfo(hx_view.cfunc.entry_ea).switch_inverted(insn.ea) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM + + +class FindFieldXrefs(idaapi.action_handler_t): + name = "my:FindFieldXrefs" + description = "Field Xrefs" + hotkey = "Ctrl+X" + + @staticmethod + def check(ctree_item): + return ctree_item.citype == idaapi.VDI_EXPR and \ + ctree_item.it.to_specific_type.op in (idaapi.cot_memptr, idaapi.cot_memref) + + def activate(self, ctx): + hx_view = idaapi.get_widget_vdui(ctx.widget) + item = hx_view.item + + if not self.check(item): + return + + data = [] + offset = item.e.m + struct_type = idaapi.remove_pointer(item.e.x.type) + ordinal = struct_type.get_ordinal() + result = XrefStorage().get_structure_info(ordinal, offset) + for xref_info in result: + data.append([ + idaapi.get_short_name(xref_info.func_ea) + "+" + hex(int(xref_info.offset)), + xref_info.type, + xref_info.line + ]) + + field_name = Helper.get_member_name(struct_type, offset) + chooser = Forms.MyChoose( + data, + "Cross-references to {0}::{1}".format(struct_type.dstr(), field_name), + [["Function", 20 | idaapi.Choose2.CHCOL_PLAIN], + ["Type", 2 | idaapi.Choose2.CHCOL_PLAIN], + ["Line", 40 | idaapi.Choose2.CHCOL_PLAIN]] + ) + idx = chooser.Show(True) + if idx == -1: + return + + xref = result[idx] + idaapi.open_pseudocode(xref.func_ea + xref.offset, False) + + def update(self, ctx): + if ctx.widget_type == idaapi.BWN_PSEUDOCODE: + return idaapi.AST_ENABLE_FOR_FORM + return idaapi.AST_DISABLE_FOR_FORM diff --git a/plugins/HexRaysPyTools/Api.py b/plugins/HexRaysPyTools/Api.py new file mode 100644 index 0000000..0e1bbe7 --- /dev/null +++ b/plugins/HexRaysPyTools/Api.py @@ -0,0 +1,583 @@ +import logging +import idaapi +import idc +from Core.Helper import to_hex, get_member_name, get_func_argument_info, get_funcs_calling_address, is_imported_ea + +logger = logging.getLogger(__name__) + + +SETTING_START_FROM_CURRENT_EXPR = True + + +def decompile_function(address): + try: + cfunc = idaapi.decompile(address) + if cfunc: + return cfunc + except idaapi.DecompilationFailure: + pass + logger.warn("IDA failed to decompile function at 0x{address:08X}".format(address=address)) + + +class ScanObject(object): + def __init__(self): + self.ea = idaapi.BADADDR + self.name = None + self.tinfo = None + self.id = 0 + + @staticmethod + def create(cfunc, arg): + # Creates object suitable for scaning either from cexpr_t or ctree_item_t + if isinstance(arg, idaapi.ctree_item_t): + lvar = arg.get_lvar() + if lvar: + index = list(cfunc.get_lvars()).index(lvar) + result = VariableObject(lvar, index) + if arg.e: + result.ea = ScanObject.get_expression_address(cfunc, arg.e) + return result + cexpr = arg.e + else: + cexpr = arg + + if cexpr.op == idaapi.cot_var: + lvar = cfunc.get_lvars()[cexpr.v.idx] + result = VariableObject(lvar, cexpr.v.idx) + result.ea = ScanObject.get_expression_address(cfunc, cexpr) + return result + elif cexpr.op == idaapi.cot_memptr: + t = cexpr.x.type.get_pointed_object() + result = StructPtrObject(t.dstr(), cexpr.m) + result.name = get_member_name(t, cexpr.m) + elif cexpr.op == idaapi.cot_memref: + t = cexpr.x.type + result = StructRefObject(t.dstr(), cexpr.m) + result.name = get_member_name(t, cexpr.m) + elif cexpr.op == idaapi.cot_obj: + result = GlobalVariableObject(cexpr.obj_ea) + result.name = idaapi.get_short_name(cexpr.obj_ea) + else: + return + result.tinfo = cexpr.type + result.ea = ScanObject.get_expression_address(cfunc, cexpr) + return result + + @staticmethod + def get_expression_address(cfunc, cexpr): + expr = cexpr + + while expr and expr.ea == idaapi.BADADDR: + expr = expr.to_specific_type + expr = cfunc.body.find_parent_of(expr) + + assert expr is not None + return expr.ea + + def __hash__(self): + return hash((self.id, self.name)) + + def __eq__(self, other): + return self.id == other.id and self.name == other.name + + def __repr__(self): + return self.name + + +SO_LOCAL_VARIABLE = 1 # cexpr.op == idaapi.cot_var +SO_STRUCT_POINTER = 2 # cexpr.op == idaapi.cot_memptr +SO_STRUCT_REFERENCE = 3 # cexpr.op == idaapi.cot_memref +SO_GLOBAL_OBJECT = 4 # cexpr.op == idaapi.cot_obj +SO_CALL_ARGUMENT = 5 # cexpr.op == idaapi.cot_call +SO_MEMORY_ALLOCATOR = 6 +SO_RETURNED_OBJECT = 7 + + +class VariableObject(ScanObject): + # Represents `var` expression + def __init__(self, lvar, index): + super(VariableObject, self).__init__() + self.lvar = lvar + self.tinfo = lvar.type() + self.name = lvar.name + self.index = index + self.id = SO_LOCAL_VARIABLE + + def is_target(self, cexpr): + return cexpr.op == idaapi.cot_var and cexpr.v.idx == self.index + + +class StructPtrObject(ScanObject): + # Represents `x->m` expression + def __init__(self, struct_name, offset): + super(StructPtrObject, self).__init__() + self.struct_name = struct_name + self.offset = offset + self.id = SO_STRUCT_POINTER + + def is_target(self, cexpr): + return cexpr.op == idaapi.cot_memptr and cexpr.m == self.offset and \ + cexpr.x.type.get_pointed_object().dstr() == self.struct_name + + +class StructRefObject(ScanObject): + # Represents `x.m` expression + def __init__(self, struct_name, offset): + super(StructRefObject, self).__init__() + self.__struct_name = struct_name + self.__offset = offset + self.id = SO_STRUCT_REFERENCE + + def is_target(self, cexpr): + return cexpr.op == idaapi.cot_memref and cexpr.m == self.__offset and cexpr.x.type.dstr() == self.__struct_name + + +class GlobalVariableObject(ScanObject): + # Represents global object + def __init__(self, object_address): + super(GlobalVariableObject, self).__init__() + self.obj_ea = object_address + self.id = SO_GLOBAL_OBJECT + + def is_target(self, cexpr): + return cexpr.op == idaapi.cot_obj and self.obj_ea == cexpr.obj_ea + + +class CallArgObject(ScanObject): + # Represents call of function and specified argument + def __init__(self, func_address, arg_idx): + super(CallArgObject, self).__init__() + self.__func_ea = func_address + self.__arg_idx = arg_idx + self.id = SO_CALL_ARGUMENT + + def is_target(self, cexpr): + return cexpr.op == idaapi.cot_call and cexpr.x.obj_ea == self.__func_ea + + def create_scan_obj(self, cfunc, cexpr): + e = cexpr.a[self.__arg_idx] + while e.op in (idaapi.cot_cast, idaapi.cot_ref, idaapi.cot_add, idaapi.cot_sub, idaapi.cot_idx): + e = e.x + return ScanObject.create(cfunc, e) + + @staticmethod + def create(cfunc, arg_idx): + result = CallArgObject(cfunc.entry_ea, arg_idx) + result.name = cfunc.get_lvars()[arg_idx].name + result.tinfo = cfunc.type + return result + + def __repr__(self): + return "{}" + + +class ReturnedObject(ScanObject): + # Represents value returned by function + def __init__(self, func_address): + super(ReturnedObject, self).__init__() + self.__func_ea = func_address + self.id = SO_RETURNED_OBJECT + + def is_target(self, cexpr): + return cexpr.op == idaapi.cot_call and cexpr.x.obj_ea == self.__func_ea + + +class MemoryAllocationObject(ScanObject): + # Represents `operator new()' or `malloc' + def __init__(self, name, size): + super(MemoryAllocationObject, self).__init__() + self.name = name + self.size = size + self.id = SO_MEMORY_ALLOCATOR + + @staticmethod + def create(cfunc, cexpr): + if cexpr.op == idaapi.cot_call: + e = cexpr + elif cexpr.op == idaapi.cot_cast and cexpr.x.op == idaapi.cot_call: + e = cexpr.x + else: + return + + func_name = idaapi.get_short_name(e.x.obj_ea) + if "malloc" in func_name or "operator new" in func_name: + carg = e.a[0] + if carg.op == idaapi.cot_num: + size = carg.numval() + else: + size = 0 + result = MemoryAllocationObject(func_name, size) + result.ea = ScanObject.get_expression_address(cfunc, e) + return result + + +ASSIGNMENT_RIGHT = 1 +ASSIGNMENT_LEFT = 2 + + +class ObjectVisitor(idaapi.ctree_parentee_t): + def __init__(self, cfunc, obj, data, skip_until_object): + super(ObjectVisitor, self).__init__() + self._cfunc = cfunc + self._objects = [obj] + self._init_obj = obj + self._data = data + self._start_ea = obj.ea + self._skip = skip_until_object if self._start_ea != idaapi.BADADDR else False + self.crippled = False + + def process(self): + self.apply_to(self._cfunc.body, None) + + def set_callbacks(self, manipulate=None): + if manipulate: + self.__manipulate = manipulate.__get__(self, ObjectDownwardsVisitor) + + def _get_line(self): + for p in reversed(self.parents): + if not p.is_expr(): + return idaapi.tag_remove(p.print1(self._cfunc.__ref__())) + AssertionError("Parent instruction is not found") + + def _manipulate(self, cexpr, obj): + """ + Method called for every object having assignment relationship with starter object. This method should be + reimplemented in order to do something useful + + :param cexpr: idaapi_cexpr_t + :param id: one of the SO_* constants + :return: None + """ + self.__manipulate(cexpr, obj) + + def __manipulate(self, cexpr, obj): + logger.debug("Expression {} at {} Id - {}".format(cexpr.opname, to_hex(self._find_asm_address(cexpr)), obj.id)) + + +class ObjectDownwardsVisitor(ObjectVisitor): + def __init__(self, cfunc, obj, data=None, skip_until_object=False): + super(ObjectDownwardsVisitor, self).__init__(cfunc, obj, data, skip_until_object) + self.cv_flags |= idaapi.CV_POST + + def visit_expr(self, cexpr): + if self._skip: + if self._is_initial_object(cexpr): + self._skip = False + else: + return 0 + + if cexpr.op != idaapi.cot_asg: + return 0 + + x_cexpr = cexpr.x + if cexpr.y.op == idaapi.cot_cast: + y_cexpr = cexpr.y.x + else: + y_cexpr = cexpr.y + + for obj in self._objects: + if obj.is_target(x_cexpr): + if self.__is_object_overwritten(x_cexpr, obj, y_cexpr): + logger.info("Removed object {} from scanning at {}".format( + obj, to_hex(self._find_asm_address(x_cexpr)))) + self._objects.remove(obj) + return 0 + elif obj.is_target(y_cexpr): + new_obj = ScanObject.create(self._cfunc, x_cexpr) + if new_obj: + self._objects.append(new_obj) + return 0 + return 0 + + def leave_expr(self, cexpr): + if self._skip: + return 0 + + for obj in self._objects: + if obj.is_target(cexpr) and obj.id != SO_RETURNED_OBJECT: + self._manipulate(cexpr, obj) + return 0 + return 0 + + def _is_initial_object(self, cexpr): + if cexpr.op == idaapi.cot_asg: + cexpr = cexpr.y + if cexpr.op == idaapi.cot_cast: + cexpr = cexpr.x + return self._init_obj.is_target(cexpr) and self._find_asm_address(cexpr) == self._start_ea + + def __is_object_overwritten(self, x_cexpr, obj, y_cexpr): + if len(self._objects) < 2: + return False + + if y_cexpr.op == idaapi.cot_cast: + e = y_cexpr.x + else: + e = y_cexpr + + if e.op != idaapi.cot_call or len(e.a) == 0: + return True + + for obj in self._objects: + if obj.is_target(e. a[0]): + return False + return True + + +class ObjectUpwardsVisitor(ObjectVisitor): + STAGE_PREPARE = 1 + STAGE_PARSING = 2 + + def __init__(self, cfunc, obj, data=None, skip_after_object=False): + super(ObjectUpwardsVisitor, self).__init__(cfunc, obj, data, skip_after_object) + self._stage = self.STAGE_PREPARE + self._tree = {} + self._call_obj = obj if obj.id == SO_CALL_ARGUMENT else None + + def visit_expr(self, cexpr): + if self._stage == self.STAGE_PARSING: + return 0 + + if self._call_obj and self._call_obj.is_target(cexpr): + obj = self._call_obj.create_scan_obj(self._cfunc, cexpr) + if obj: + self._objects.append(obj) + return 0 + + if cexpr.op != idaapi.cot_asg: + return 0 + + x_cexpr = cexpr.x + if cexpr.y.op == idaapi.cot_cast: + y_cexpr = cexpr.y.x + else: + y_cexpr = cexpr.y + + obj_left = ScanObject.create(self._cfunc, x_cexpr) + obj_right = ScanObject.create(self._cfunc, y_cexpr) + if obj_left and obj_right: + self.__add_object_assignment(obj_left, obj_right) + + if self._skip and self._is_initial_object(cexpr): + return 1 + return 0 + + def leave_expr(self, cexpr): + if self._stage == self.STAGE_PREPARE: + return 0 + + if self._skip and self._is_initial_object(cexpr): + self._manipulate(cexpr, self._init_obj) + return 1 + + for obj in self._objects: + if obj.is_target(cexpr): + self._manipulate(cexpr, obj) + return 0 + return 0 + + def process(self): + self._stage = self.STAGE_PREPARE + self.cv_flags &= ~idaapi.CV_POST + super(ObjectUpwardsVisitor, self).process() + self._stage = self.STAGE_PARSING + self.cv_flags |= idaapi.CV_POST + self.__prepare() + super(ObjectUpwardsVisitor, self).process() + + def _is_initial_object(self, cexpr): + return self._init_obj.is_target(cexpr) and self._find_asm_address(cexpr) == self._start_ea + + def __add_object_assignment(self, from_obj, to_obj): + if from_obj in self._tree: + self._tree[from_obj].add(to_obj) + else: + self._tree[from_obj] = {to_obj} + + def __prepare(self): + result = set() + todo = set(self._objects) + while todo: + obj = todo.pop() + result.add(obj) + if obj.id == SO_CALL_ARGUMENT or obj not in self._tree: + continue + o = self._tree[obj] + todo |= o - result + result |= o + self._objects = list(result) + self._tree.clear() + + +class RecursiveObjectVisitor(ObjectVisitor): + def __init__(self, cfunc, obj, data=None, skip_until_object=False, visited=None): + super(RecursiveObjectVisitor, self).__init__(cfunc, obj, data, skip_until_object) + self._visited = visited if visited else set() + self._new_for_visit = set() + self.crippled = False + self._arg_idx = -1 + self._debug_scan_tree = {} + self.__debug_scan_tree_root = idc.Name(self._cfunc.entry_ea) + self.__debug_message = [] + + def visit_expr(self, cexpr): + return super(RecursiveObjectVisitor, self).visit_expr(cexpr) + + def set_callbacks(self, manipulate=None, start=None, start_iteration=None, finish=None, finish_iteration=None): + super(RecursiveObjectVisitor, self).set_callbacks(manipulate) + if start: + self._start = start.__get__(self, RecursiveObjectDownwardsVisitor) + if start_iteration: + self._start_iteration = start_iteration.__get__(self, RecursiveObjectDownwardsVisitor) + if finish: + self._finish = finish.__get__(self, RecursiveObjectDownwardsVisitor) + if finish_iteration: + self._finish_iteration = finish_iteration.__get__(self, RecursiveObjectDownwardsVisitor) + + def prepare_new_scan(self, cfunc, arg_idx, obj, skip=False): + self._cfunc = cfunc + self._arg_idx = arg_idx + self._objects = [obj] + self._init_obj = obj + self._skip = False + self.crippled = self.__is_func_crippled() + + def process(self): + self._start() + self._recursive_process() + self._finish() + self.dump_scan_tree() + + def dump_scan_tree(self): + self.__prepare_debug_message() + logger.info("{}\n---------------".format("\n".join(self.__debug_message))) + + def __prepare_debug_message(self, key=None, level=1): + if key is None: + key = (self.__debug_scan_tree_root, -1) + self.__debug_message.append("--- Scan Tree---\n{}".format(self.__debug_scan_tree_root)) + if key in self._debug_scan_tree: + for func_name, arg_idx in self._debug_scan_tree[key]: + prefix = " | " * (level - 1) + " |_ " + self.__debug_message.append("{}{} (idx: {})".format(prefix, func_name, arg_idx)) + self.__prepare_debug_message((func_name, arg_idx), level + 1) + + def _recursive_process(self): + self._start_iteration() + super(RecursiveObjectVisitor, self).process() + self._finish_iteration() + + def _manipulate(self, cexpr, obj): + self._check_call(cexpr) + super(RecursiveObjectVisitor, self)._manipulate(cexpr, obj) + + def _check_call(self, cexpr): + raise NotImplemented + + def _add_visit(self, func_ea, arg_idx): + if (func_ea, arg_idx) not in self._visited: + self._visited.add((func_ea, arg_idx)) + self._new_for_visit.add((func_ea, arg_idx)) + return True + return False + + def _add_scan_tree_info(self, func_ea, arg_idx): + head_node = (idc.Name(self._cfunc.entry_ea), self._arg_idx) + tail_node = (idc.Name(func_ea), arg_idx) + if head_node in self._debug_scan_tree: + self._debug_scan_tree[head_node].add(tail_node) + else: + self._debug_scan_tree[head_node] = {tail_node} + + def _start(self): + """ Called at the beginning of visiting """ + pass + + def _start_iteration(self): + """ Called every time new function visiting started """ + pass + + def _finish(self): + """ Called after all visiting happened """ + pass + + def _finish_iteration(self): + """ Called every time new function visiting finished """ + pass + + def __is_func_crippled(self): + # Check if function is just call to another function + b = self._cfunc.body.cblock + if b.size() == 1: + e = b.at(0) + return e.op == idaapi.cit_return or (e.op == idaapi.cit_expr and e.cexpr.op == idaapi.cot_call) + return False + + +class RecursiveObjectDownwardsVisitor(RecursiveObjectVisitor, ObjectDownwardsVisitor): + def __init__(self, cfunc, obj, data=None, skip_until_object=False, visited=None): + super(RecursiveObjectDownwardsVisitor, self).__init__(cfunc, obj, data, skip_until_object, visited) + + def _check_call(self, cexpr): + parent = self.parent_expr() + grandparent = self.parents.at(self.parents.size() - 2) + if parent.op == idaapi.cot_call: + call_cexpr = parent + arg_cexpr = cexpr + elif parent.op == idaapi.cot_cast and grandparent.op == idaapi.cot_call: + call_cexpr = grandparent.cexpr + arg_cexpr = parent + else: + return + idx, _ = get_func_argument_info(call_cexpr, arg_cexpr) + func_ea = call_cexpr.x.obj_ea + if func_ea == idaapi.BADADDR: + return + if self._add_visit(func_ea, idx): + self._add_scan_tree_info(func_ea, idx) + + def _recursive_process(self): + super(RecursiveObjectDownwardsVisitor, self)._recursive_process() + + while self._new_for_visit: + func_ea, arg_idx = self._new_for_visit.pop() + if is_imported_ea(func_ea): + continue + cfunc = decompile_function(func_ea) + if cfunc: + assert arg_idx < len(cfunc.get_lvars()), "Wrong argument at func {}".format(to_hex(func_ea)) + obj = VariableObject(cfunc.get_lvars()[arg_idx], arg_idx) + self.prepare_new_scan(cfunc, arg_idx, obj) + self._recursive_process() + + +class RecursiveObjectUpwardsVisitor(RecursiveObjectVisitor, ObjectUpwardsVisitor): + def __init__(self, cfunc, obj, data=None, skip_after_object=False, visited=None): + super(RecursiveObjectUpwardsVisitor, self).__init__(cfunc, obj, data, skip_after_object, visited) + + def prepare_new_scan(self, cfunc, arg_idx, obj, skip=False): + super(RecursiveObjectUpwardsVisitor, self).prepare_new_scan(cfunc, arg_idx, obj, skip) + self._call_obj = obj if obj.id == SO_CALL_ARGUMENT else None + + def _check_call(self, cexpr): + if cexpr.op == idaapi.cot_var and self._cfunc.get_lvars()[cexpr.v.idx].is_arg_var: + func_ea = self._cfunc.entry_ea + arg_idx = cexpr.v.idx + if self._add_visit(func_ea, arg_idx): + for callee_ea in get_funcs_calling_address(func_ea): + self._add_scan_tree_info(callee_ea, arg_idx) + + def _recursive_process(self): + super(RecursiveObjectUpwardsVisitor, self)._recursive_process() + + while self._new_for_visit: + new_visit = list(self._new_for_visit) + self._new_for_visit.clear() + for func_ea, arg_idx in new_visit: + funcs = get_funcs_calling_address(func_ea) + obj = CallArgObject.create(idaapi.decompile(func_ea), arg_idx) + for callee_ea in funcs: + cfunc = decompile_function(callee_ea) + if cfunc: + self.prepare_new_scan(cfunc, arg_idx, obj, False) + super(RecursiveObjectUpwardsVisitor, self)._recursive_process() diff --git a/plugins/HexRaysPyTools/Core/Cache.py b/plugins/HexRaysPyTools/Core/Cache.py new file mode 100644 index 0000000..479fbcf --- /dev/null +++ b/plugins/HexRaysPyTools/Core/Cache.py @@ -0,0 +1,62 @@ +# Information about explored functions +import re + +import idaapi +import idautils +import idc + + +imported_ea = set() +demangled_names = {} +touched_functions = set() +temporary_structure = None + + +def init_imported_ea(*args): + + def imp_cb(ea, name, ord): + imported_ea.add(ea) + # True -> Continue enumeration + # False -> Stop enumeration + return True + + print "[Info] Collecting information about imports" + imported_ea.clear() + nimps = idaapi.get_import_module_qty() + + for i in xrange(0, nimps): + name = idaapi.get_import_module_name(i) + if not name: + print "[Warning] Failed to get import module name for #%d" % i + continue + + # print "Walking-> %s" % name + idaapi.enum_import_names(i, imp_cb) + print "[Info] Done..." + + +def init_demangled_names(*args): + """ + Creates dictionary of demangled names => address, that will be used further at double click on methods got from + symbols. + """ + demangled_names.clear() + for address, name in idautils.Names(): + short_name = idc.Demangle(name, idc.GetLongPrm(idc.INF_SHORT_DN)) + if short_name: + demangled_names[short_name.split('(')[0]] = address - idaapi.get_imagebase() + + # Names can have templates and should be transformed before creating local type + name = re.sub(r'[<>]', '_t_', name) + + # Thunk functions with name like "[thunk]:CWarmupHostProvider::Release`adjustor{8}'" + result = re.search(r"(\[thunk\]:)?([^`]*)(.*\{(\d+)}.*)?", short_name) + name, adjustor = result.group(2), result.group(4) + if adjustor: + demangled_names[name + "_adj_" + adjustor] = address - idaapi.get_imagebase() + + print "[DEBUG] Demangled names have been initialized" + + +def reset_touched_functions(*args): + touched_functions = set() diff --git a/plugins/HexRaysPyTools/Core/Classes.py b/plugins/HexRaysPyTools/Core/Classes.py new file mode 100644 index 0000000..b5a6a4e --- /dev/null +++ b/plugins/HexRaysPyTools/Core/Classes.py @@ -0,0 +1,536 @@ +import idc +import idaapi +import HexRaysPyTools.Forms +import Helper + +# from PySide import QtGui, QtCore +from HexRaysPyTools.Cute import * + +all_virtual_functions = {} # name -> VirtualMethod +all_virtual_tables = {} # ordinal -> VirtualTable + + +class VirtualMethod(object): + def __init__(self, tinfo, name, parent): + self.tinfo = tinfo + self.tinfo_modified = False + self.name = name + self.class_name = None + self.name_modified = False + self.parents = [parent] + self.base_address = Helper.get_virtual_func_address(name) + if self.base_address: + self.base_address -= idaapi.get_imagebase() + + self.rowcount = 0 + self.children = [] + + @staticmethod + def create(tinfo, name, parent): + result = all_virtual_functions.get(name) + if result: + result.parents.append(parent) + return result + result = VirtualMethod(tinfo, name, parent) + all_virtual_functions[name] = result + return result + + def update(self, name, tinfo): + self.name = name + self.tinfo = tinfo + self.name_modified = False + self.tinfo_modified = False + + self.base_address = idc.LocByName(self.name) + if self.base_address != idaapi.BADADDR: + self.base_address -= idaapi.get_imagebase() + + def data(self, column): + if column == 0: + return self.name + elif column == 1: + return self.tinfo.get_pointed_object().dstr() + elif column == 2: + return "0x{0:08X}".format(self.address) if self.address else None + + def setData(self, column, value): + if column == 0: + if idaapi.isident(value) and self.name != value: + self.name = value + self.name_modified = True + for parent in self.parents: + parent.modified = True + return True + elif column == 1: + tinfo = idaapi.tinfo_t() + split = value.split('(') + if len(split) == 2: + value = split[0] + ' ' + self.name + '(' + split[1] + ';' + if idaapi.parse_decl2(idaapi.cvar.idati, value, '', tinfo, idaapi.PT_TYP): + if tinfo.is_func(): + tinfo.create_ptr(tinfo) + if tinfo.dstr() != self.tinfo.dstr(): + self.tinfo = tinfo + self.tinfo_modified = True + for parent in self.parents: + parent.modified = True + return True + return False + + def font(self, column): + if column == 0 and self.name_modified: + return QtGui.QFont("Consolas", 10, italic=True) + elif column == 1 and self.tinfo_modified: + return QtGui.QFont("Consolas", 10, italic=True) + return QtGui.QFont("Consolas", 10, 0) + + def flags(self, column): + if column != 2: + if self.address: + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsEditable + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + + + @property + def color(self): + # return QtGui.QBrush(QtGui.QColor("#ffffb3")) + return QtGui.QColor("#fefbd8") + + @property + def tooltip(self): + return None + + @property + def address(self): + return self.base_address + idaapi.get_imagebase() if self.base_address else None + + def set_first_argument_type(self, name): + func_data = idaapi.func_type_data_t() + func_tinfo = self.tinfo.get_pointed_object() + class_tinfo = idaapi.tinfo_t() + if func_tinfo.get_func_details(func_data) and func_tinfo.get_nargs() and \ + class_tinfo.get_named_type(idaapi.cvar.idati, name): + class_tinfo.create_ptr(class_tinfo) + first_arg_tinfo = func_data[0].type + if (first_arg_tinfo.is_ptr() and first_arg_tinfo.get_pointed_object().is_udt()) or \ + Helper.is_legal_type(func_data[0].type): + func_data[0].type = class_tinfo + func_data[0].name = "this" + func_tinfo.create_func(func_data) + func_tinfo.create_ptr(func_tinfo) + if func_tinfo.dstr() != self.tinfo.dstr(): + self.tinfo = func_tinfo + self.tinfo_modified = True + for parent in self.parents: + parent.modified = True + else: + print "[Warning] function {0} probably have wrong type".format(self.name) + + def open_function(self): + if self.address: + if idaapi.decompile(self.address): + idaapi.open_pseudocode(self.address, 0) + else: + idaapi.jumpto(self.address) + + def commit(self): + if self.name_modified: + self.name_modified = False + if self.address: + idaapi.set_name(self.address, self.name) + if self.tinfo_modified: + self.tinfo_modified = False + if self.address: + idaapi.apply_tinfo2(self.address, self.tinfo.get_pointed_object(), idaapi.TINFO_DEFINITE) + + def __eq__(self, other): + return self.address == other.address + + def __repr__(self): + return self.name + + +class VirtualTable(object): + def __init__(self, ordinal, tinfo, class_): + self.ordinal = ordinal + self.tinfo = tinfo + self.class_ = [class_] + self.class_name = None + self.virtual_functions = [] + self.name = self.tinfo.dstr() + self._modified = False + + def update(self): + if self.modified: + vtable_tinfo = idaapi.tinfo_t() + udt_data = idaapi.udt_type_data_t() + vtable_tinfo.get_numbered_type(idaapi.cvar.idati, self.ordinal) + vtable_tinfo.get_udt_details(udt_data) + self.tinfo = vtable_tinfo + self.name = vtable_tinfo.dstr() + self.modified = False + if len(self.virtual_functions) == len(udt_data): + for current_function, other_function in zip(self.virtual_functions, udt_data): + current_function.update(other_function.name, other_function.type) + else: + print "[ERROR] Something have been modified in Local types. Please refresh this view" + + def update_local_type(self): + if self.modified: + final_tinfo = idaapi.tinfo_t() + udt_data = idaapi.udt_type_data_t() + self.tinfo.get_udt_details(udt_data) + if len(udt_data) == len(self.virtual_functions): + for udt_member, virtual_function in zip(udt_data, self.virtual_functions): + udt_member.name = virtual_function.name + udt_member.type = virtual_function.tinfo + virtual_function.commit() + final_tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) + final_tinfo.set_numbered_type(idaapi.cvar.idati, self.ordinal, idaapi.NTF_REPLACE, self.name) + self.modified = False + else: + print "[ERROR] Something have been modified in Local types. Please refresh this view" + + def set_first_argument_type(self, class_name): + for function in self.virtual_functions: + function.set_first_argument_type(class_name) + + @property + def modified(self): + return self._modified + + @modified.setter + def modified(self, value): + self._modified = value + if value: + for class_ in self.class_: + class_.modified = True + + @staticmethod + def create(tinfo, class_): + ordinal = idaapi.get_type_ordinal(idaapi.cvar.idati, tinfo.dstr()) + result = all_virtual_tables.get(ordinal) + if result: + result.class_.append(class_) + else: + udt_data = idaapi.udt_type_data_t() + tinfo.get_udt_details(udt_data) + result = VirtualTable(ordinal, tinfo, class_) + virtual_functions = [VirtualMethod.create(func.type, func.name, result) for func in udt_data] + result.virtual_functions = virtual_functions + all_virtual_functions[ordinal] = result + return result + + def get_class_tinfo(self): + if len(self.class_) == 1: + return self.class_.tinfo + + def setData(self, column, value): + if column == 0: + if idaapi.isident(value) and self.name != value: + self.name = value + self.modified = True + return True + return False + + def data(self, column): + if column == 0: + return self.name + + @property + def color(self): + return QtGui.QColor("#d5f4e6") + + @property + def tooltip(self): + pass + + def font(self, column): + if self.modified: + return QtGui.QFont("Consolas", 12, italic=True) + return QtGui.QFont("Consolas", 12) + + def flags(self, column): + if column == 0: + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + + @property + def children(self): + return self.virtual_functions + + def __repr__(self): + return str(self.virtual_functions) + + +class Class(object): + def __init__(self, name, tinfo, ordinal): + self.name = name + self.class_name = name + self.ordinal = ordinal + self.parent = None + self.vtables = {} + self.modified = False + + @staticmethod + def create_class(ordinal): + tinfo = idaapi.tinfo_t() + tinfo.get_numbered_type(idaapi.cvar.idati, ordinal) + vtables = {} + if tinfo.is_struct(): + udt_data = idaapi.udt_type_data_t() + tinfo.get_udt_details(udt_data) + for field_udt in udt_data: + if field_udt.type.is_ptr(): + possible_vtable = field_udt.type.get_pointed_object() + if possible_vtable.is_struct(): + v_udt_data = idaapi.udt_type_data_t() + possible_vtable.get_udt_details(v_udt_data) + for possible_func_udt in v_udt_data: + if not possible_func_udt.type.is_funcptr(): + break + else: + vtables[field_udt.offset / 8] = possible_vtable + if vtables: + class_ = Class(tinfo.dstr(), tinfo, ordinal) + for offset, vtable_tinfo in vtables.iteritems(): + vtables[offset] = VirtualTable.create(vtable_tinfo, class_) + class_.vtables = vtables + return class_ + + def update_from_local_types(self): + try: + if self.modified: + class_ = self.create_class(self.ordinal) + if class_: + self.name = class_.name + self.modified = False + for offset, vtable in class_.vtables.iteritems(): + self.vtables[offset].update() + else: + # TODO: drop class + raise IndexError + except IndexError: + print "[ERROR] Something have been modified in Local types. Please refresh this view" + + def update_local_type(self): + if self.modified: + for vtable in self.vtables.values(): + vtable.update_local_type() + udt_data = idaapi.udt_type_data_t() + tinfo = idaapi.tinfo_t() + self.tinfo.get_udt_details(udt_data) + tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) + tinfo.set_numbered_type(idaapi.cvar.idati, self.ordinal, idaapi.NTF_REPLACE, self.name) + self.modified = False + + def set_first_argument_type(self, class_name): + if 0 in self.vtables: + self.vtables[0].set_first_argument_type(class_name) + + def has_function(self, regexp): + for vtable in self.vtables.values(): + if filter(lambda func: regexp.indexIn(func.name) >= 0, vtable.virtual_functions): + return True + return False + + def data(self, column): + if column == 0: + return self.name + + def setData(self, column, value): + if column == 0: + if idaapi.isident(value) and self.name != value: + self.name = value + self.modified = True + return True + return False + + def font(self, column): + if self.modified: + return QtGui.QFont("Consolas", 12, QtGui.QFont.Bold, italic=True) + return QtGui.QFont("Consolas", 12, QtGui.QFont.Bold) + + @property + def color(self): + # return QtGui.QBrush(QtGui.QColor("#ffb3ff")): + return QtGui.QColor("#80ced6") + + @property + def tooltip(self): + return None + + def flags(self, column): + if column == 0: + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEditable | QtCore.Qt.ItemIsEnabled + return QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled + + @property + def children(self): + return self.vtables.values() + + @property + def tinfo(self): + tinfo = idaapi.tinfo_t() + tinfo.get_numbered_type(idaapi.cvar.idati, self.ordinal) + return tinfo + + def open_function(self): + pass + + def __repr__(self): + return self.name + " ^_^ " + str(self.vtables) + + +class TreeItem: + def __init__(self, item, row, parent): + self.item = item + self.parent = parent + self.row = row + self.children = [] + + def __repr__(self): + return str(self.item) + + +class TreeModel(QtCore.QAbstractItemModel): + # TODO: Add higlighting if eip in function, consider setting breakpoints + + refreshed = QtCore.Signal() + + def __init__(self): + super(TreeModel, self).__init__() + self.tree_data = [] + self.headers = ["Name", "Declaration", "Address"] + + self.init() + # import pydevd + # pydevd.settrace("localhost", port=12345, stdoutToServer=True, stderrToServer=True) + + def init(self): + idaapi.show_wait_box("Looking for classes...") + all_virtual_functions.clear() + all_virtual_tables.clear() + + classes = [] + for ordinal in xrange(1, idaapi.get_ordinal_qty(idaapi.cvar.idati)): + result = Class.create_class(ordinal) + if result: + classes.append(result) + + for class_row, class_ in enumerate(classes): + class_item = TreeItem(class_, class_row, None) + for vtable_row, vtable in class_.vtables.iteritems(): + vtable_item = TreeItem(vtable, vtable_row, class_item) + vtable_item.children = [TreeItem(function, 0, vtable_item) for function in vtable.virtual_functions] + class_item.children.append(vtable_item) + self.tree_data.append(class_item) + + idaapi.hide_wait_box() + + def flags(self, index): + if index.isValid(): + return index.internalPointer().item.flags(index.column()) + + def index(self, row, column, parent=QtCore.QModelIndex()): + if parent.isValid(): + node = parent.internalPointer() + return self.createIndex(row, column, node.children[row]) + else: + return self.createIndex(row, column, self.tree_data[row]) + + def parent(self, index): + if index.isValid(): + node = index.internalPointer() + if node.parent: + return self.createIndex(0, 0, node.parent) + return QtCore.QModelIndex() + + def rowCount(self, index=QtCore.QModelIndex()): + if index.isValid(): + node = index.internalPointer() + if node: + return len(node.children) + return len(self.tree_data) + + def columnCount(self, index=QtCore.QModelIndex()): + return 3 + + def data(self, index, role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: + node = index.internalPointer() + return node.item.data(index.column()) + elif role == QtCore.Qt.FontRole: + return index.internalPointer().item.font(index.column()) + elif role == QtCore.Qt.ToolTipRole: + return index.internalPointer().item.tooltip + elif role == QtCore.Qt.BackgroundRole: + return index.internalPointer().item.color + elif role == QtCore.Qt.ForegroundRole: + return QtGui.QBrush(QtGui.QColor("#191919")) + return None + + def setData(self, index, value, role=QtCore.Qt.DisplayRole): + result = False + if role == QtCore.Qt.EditRole and value != "": + node = index.internalPointer() + result = node.item.setData(index.column(), str(value)) + return result + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal: + return self.headers[section] + + def set_first_argument_type(self, indexes): + indexes = filter(lambda x: x.column() == 0, indexes) + class_name = indexes[0].internalPointer().item.class_name + if not class_name: + classes = [[x.item.name] for x in self.tree_data] + class_chooser = HexRaysPyTools.Forms.MyChoose(classes, "Select Class", [["Name", 25]]) + idx = class_chooser.Show(True) + if idx != -1: + class_name = classes[idx][0] + if class_name: + for index in indexes: + index.internalPointer().item.set_first_argument_type(class_name) + + def refresh(self): + self.tree_data = [] + self.modelReset.emit() + self.init() + self.refreshed.emit() + + def rollback(self): + for class_item in self.tree_data: + class_item.item.update_from_local_types() + self.dataChanged.emit(QtCore.QModelIndex(), QtCore.QModelIndex()) + + def commit(self): + for class_item in self.tree_data: + if class_item.item.modified: + class_item.item.update_local_type() + + def open_function(self, index): + if index.column() == 2: + index.internalPointer().item.open_function() + + +class ProxyModel(QtGui.QSortFilterProxyModel): + def __init__(self): + super(ProxyModel, self).__init__() + self.filter_by_function = False + + def set_regexp_filter(self, regexp): + if regexp and regexp[0] == '!': + self.filter_by_function = True + self.setFilterRegExp(regexp[1:]) + else: + self.filter_by_function = False + self.setFilterRegExp(regexp) + + def filterAcceptsRow(self, row, parent): + if not parent.isValid() and self.filterRegExp(): + if self.filter_by_function: + return self.sourceModel().tree_data[row].item.has_function(self.filterRegExp()) + return self.filterRegExp().indexIn(self.sourceModel().tree_data[row].item.class_name) >= 0 + return True diff --git a/plugins/HexRaysPyTools/Core/Const.py b/plugins/HexRaysPyTools/Core/Const.py new file mode 100644 index 0000000..f27f019 --- /dev/null +++ b/plugins/HexRaysPyTools/Core/Const.py @@ -0,0 +1,58 @@ +import idaapi +import idc + +EA64 = idc.__EA64__ +EA_SIZE = 8 if EA64 else 4 + +COT_ARITHMETIC = (idaapi.cot_num, idaapi.cot_fnum, idaapi.cot_add, idaapi.cot_fadd, idaapi.cot_sub, idaapi.cot_fsub, + idaapi.cot_mul, idaapi.cot_fmul, idaapi.cot_fdiv) + +VOID_TINFO = None +PVOID_TINFO = idaapi.tinfo_t() +CONST_VOID_TINFO = None +CONST_PVOID_TINFO = idaapi.tinfo_t() +CHAR_TINFO = None +PCHAR_TINFO = idaapi.tinfo_t() +CONST_PCHAR_TINFO = idaapi.tinfo_t() +BYTE_TINFO = None +PBYTE_TINFO = None + +WORD_TINFO = None +PWORD_TINFO = idaapi.tinfo_t() + +X_WORD_TINFO = None # DWORD for x32 and QWORD for x64 +PX_WORD_TINFO = None + +DUMMY_FUNC = None + +LEGAL_TYPES = [] + + +def init(): + """ All tinfo should be reinitialized between session. Otherwise they could have wrong type """ + global VOID_TINFO, PVOID_TINFO, CONST_PVOID_TINFO, BYTE_TINFO, PBYTE_TINFO, LEGAL_TYPES, X_WORD_TINFO, \ + PX_WORD_TINFO, DUMMY_FUNC, CONST_PCHAR_TINFO, CHAR_TINFO, PCHAR_TINFO, CONST_VOID_TINFO, \ + WORD_TINFO, PWORD_TINFO + + VOID_TINFO = idaapi.tinfo_t(idaapi.BT_VOID) + PVOID_TINFO.create_ptr(VOID_TINFO) + CONST_VOID_TINFO = idaapi.tinfo_t(idaapi.BT_VOID | idaapi.BTM_CONST) + CONST_PVOID_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BT_VOID | idaapi.BTM_CONST)) + CONST_PCHAR_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BTF_CHAR | idaapi.BTM_CONST)) + CHAR_TINFO = idaapi.tinfo_t(idaapi.BTF_CHAR) + PCHAR_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BTF_CHAR)) + BYTE_TINFO = idaapi.tinfo_t(idaapi.BTF_BYTE) + PBYTE_TINFO = idaapi.dummy_ptrtype(1, False) + X_WORD_TINFO = idaapi.get_unk_type(EA_SIZE) + PX_WORD_TINFO = idaapi.dummy_ptrtype(EA_SIZE, False) + + WORD_TINFO = idaapi.tinfo_t(idaapi.BT_UNK_WORD) + PWORD_TINFO.create_ptr(idaapi.tinfo_t(idaapi.BT_UNK_WORD)) + + func_data = idaapi.func_type_data_t() + func_data.rettype = PVOID_TINFO + func_data.cc = idaapi.CM_CC_UNKNOWN + DUMMY_FUNC = idaapi.tinfo_t() + DUMMY_FUNC.create_func(func_data, idaapi.BT_FUNC) + + LEGAL_TYPES = [PVOID_TINFO, PX_WORD_TINFO, PWORD_TINFO, PBYTE_TINFO, X_WORD_TINFO] diff --git a/plugins/HexRaysPyTools/Core/Helper.py b/plugins/HexRaysPyTools/Core/Helper.py new file mode 100644 index 0000000..7cd7e7d --- /dev/null +++ b/plugins/HexRaysPyTools/Core/Helper.py @@ -0,0 +1,312 @@ +import collections +import logging + +import idaapi +import idc + +import HexRaysPyTools.Core.Cache as Cache +import HexRaysPyTools.Core.Const as Const +import HexRaysPyTools.Settings as Settings + +logger = logging.getLogger(__name__) + + +def is_imported_ea(ea): + if idc.get_segm_name(ea) == ".plt": + return True + return ea in Cache.imported_ea + + +def is_code_ea(ea): + flags = idaapi.getFlags(ea) # flags_t + return idaapi.isCode(flags) + + +def is_rw_ea(ea): + seg = idaapi.getseg(ea) + return seg.perm & idaapi.SEGPERM_WRITE and seg.perm & idaapi.SEGPERM_READ + + +def get_virtual_func_address(name, tinfo=None, offset=None): + """ + :param name: method name + :param tinfo: class tinfo + :param offset: virtual table offset + :return: address of the method + """ + + address = idc.LocByName(name) + + if address != idaapi.BADADDR: + return address + + address = Cache.demangled_names.get(name, idaapi.BADADDR) + if address != idaapi.BADADDR: + return address + idaapi.get_imagebase() + + if tinfo is None or offset is None: + return + + offset *= 8 + udt_member = idaapi.udt_member_t() + while tinfo.is_struct(): + address = Cache.demangled_names.get(tinfo.dstr() + '::' + name, idaapi.BADADDR) + if address != idaapi.BADADDR: + return address + idaapi.get_imagebase() + udt_member.offset = offset + tinfo.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) + tinfo = udt_member.type + offset = offset - udt_member.offset + + +def get_func_argument_info(function, expression): + """ + Function is cexpr with opname == 'cot_call', expression is any son. Returns index of argument and it's type + + :param function: idaapi.cexpr_t + :param expression: idaapi.cexpr_t + :return: (int, idaapi.tinfo_t) + """ + for idx, argument in enumerate(function.a): + if expression == argument.cexpr: + func_tinfo = function.x.type + if idx < func_tinfo.get_nargs(): + return idx, func_tinfo.get_nth_arg(idx) + return idx, None + print "[ERROR] Wrong usage of 'Helper.get_func_argument_info()'" + + +def set_func_argument(func_tinfo, index, arg_tinfo): + func_data = idaapi.func_type_data_t() + func_tinfo.get_func_details(func_data) + func_data[index].type = arg_tinfo + func_tinfo.create_func(func_data) + + +def set_funcptr_argument(funcptr_tinfo, index, arg_tinfo): + func_tinfo = funcptr_tinfo.get_pointed_object() + set_func_argument(func_tinfo, index, arg_tinfo) + funcptr_tinfo.create_ptr(func_tinfo) + + +def get_nice_pointed_object(tinfo): + """ + Returns nice pointer name (if exist) or None. + For example if tinfo is PKSPIN_LOCK which is typedef of unsigned int *, then if in local types exist KSPIN_LOCK with + type unsigned int, this function returns KSPIN_LOCK + """ + try: + name = tinfo.dstr() + if name[0] == 'P': + pointed_tinfo = idaapi.tinfo_t() + if pointed_tinfo.get_named_type(idaapi.cvar.idati, name[1:]): + if tinfo.get_pointed_object().equals_to(pointed_tinfo): + return pointed_tinfo + except TypeError: + pass + + +def get_fields_at_offset(tinfo, offset): + """ + Given tinfo and offset of the structure or union, returns list of all tinfo at that offset. + This function helps to find appropriate structures by type of the offset + """ + result = [] + if offset == 0: + result.append(tinfo) + udt_data = idaapi.udt_type_data_t() + tinfo.get_udt_details(udt_data) + udt_member = idaapi.udt_member_t() + udt_member.offset = offset * 8 + idx = tinfo.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) + if idx != -1: + while idx < tinfo.get_udt_nmembers() and udt_data[idx].offset <= offset * 8: + udt_member = udt_data[idx] + if udt_member.offset == offset * 8: + if udt_member.type.is_ptr(): + result.append(idaapi.get_unk_type(Const.EA_SIZE)) + result.append(udt_member.type) + result.append(idaapi.dummy_ptrtype(Const.EA_SIZE, False)) + elif not udt_member.type.is_udt(): + result.append(udt_member.type) + if udt_member.type.is_array(): + if (offset - udt_member.offset / 8) % udt_member.type.get_array_element().get_size() == 0: + result.append(udt_member.type.get_array_element()) + elif udt_member.type.is_udt(): + result.extend(get_fields_at_offset(udt_member.type, offset - udt_member.offset / 8)) + idx += 1 + return result + + +def is_legal_type(tinfo): + tinfo.clr_const() + if tinfo.is_ptr() and tinfo.get_pointed_object().is_forward_decl(): + return tinfo.get_pointed_object().get_size() == idaapi.BADSIZE + return Settings.SCAN_ANY_TYPE or bool(filter(lambda x: x.equals_to(tinfo), Const.LEGAL_TYPES)) + + +def search_duplicate_fields(udt_data): + # Returns list of lists with duplicate fields + + default_dict = collections.defaultdict(list) + for idx, udt_member in enumerate(udt_data): + default_dict[udt_member.name].append(idx) + return [indices for indices in default_dict.values() if len(indices) > 1] + + +def get_member_name(tinfo, offset): + udt_member = idaapi.udt_member_t() + udt_member.offset = offset * 8 + tinfo.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) + return udt_member.name + + +def change_member_name(struct_name, offset, name): + return idc.set_member_name(idc.get_struc_id(struct_name), offset, name) + + +def import_structure(name, tinfo): + cdecl_typedef = idaapi.print_tinfo(None, 4, 5, idaapi.PRTYPE_MULTI | idaapi.PRTYPE_TYPE | idaapi.PRTYPE_SEMI, + tinfo, name, None) + if idc.parse_decl(cdecl_typedef, idaapi.PT_TYP) is None: + return 0 + + previous_ordinal = idaapi.get_type_ordinal(idaapi.cvar.idati, name) + if previous_ordinal: + idaapi.del_numbered_type(idaapi.cvar.idati, previous_ordinal) + ordinal = idaapi.idc_set_local_type(previous_ordinal, cdecl_typedef, idaapi.PT_TYP) + else: + ordinal = idaapi.idc_set_local_type(-1, cdecl_typedef, idaapi.PT_TYP) + return ordinal + + +def get_funcs_calling_address(ea): + """ Returns all addresses of functions which make call to a function at `ea`""" + xref_ea = idaapi.get_first_cref_to(ea) + xrefs = set() + while xref_ea != idaapi.BADADDR: + xref_func_ea = idc.GetFunctionAttr(xref_ea, idc.FUNCATTR_START) + if xref_func_ea != idaapi.BADADDR: + xrefs.add(xref_func_ea) + else: + print "[Warning] Function not found at 0x{0:08X}".format(xref_ea) + xref_ea = idaapi.get_next_cref_to(ea, xref_ea) + return xrefs + + +class FunctionTouchVisitor(idaapi.ctree_parentee_t): + def __init__(self, cfunc): + super(FunctionTouchVisitor, self).__init__() + self.functions = set() + self.cfunc = cfunc + + def visit_expr(self, expression): + if expression.op == idaapi.cot_call: + self.functions.add(expression.x.obj_ea) + return 0 + + def touch_all(self): + diff = self.functions.difference(Cache.touched_functions) + for address in diff: + if is_imported_ea(address): + continue + try: + cfunc = idaapi.decompile(address) + if cfunc: + FunctionTouchVisitor(cfunc).process() + except idaapi.DecompilationFailure: + logger.warn("IDA failed to decompile function at {}".format(to_hex(address))) + Cache.touched_functions.add(address) + idaapi.decompile(self.cfunc.entry_ea) + + def process(self): + if self.cfunc.entry_ea not in Cache.touched_functions: + Cache.touched_functions.add(self.cfunc.entry_ea) + self.apply_to(self.cfunc.body, None) + self.touch_all() + return True + return False + + +def to_hex(ea): + """ Formats address so it could be double clicked at console """ + if Const.EA64: + return "0x{:016X}".format(ea) + return "0x{:08X}".format(ea) + + +def to_nice_str(ea): + """ Shows address as function name + offset """ + func_start_ea = idc.get_func_attr(ea, idc.FUNCATTR_START) + func_name = idc.Name(func_start_ea) + offset = ea - func_start_ea + return "{}+0x{:X}".format(func_name, offset) + + +def save_long_str_to_idb(array_name, value): + """ Overwrites old array completely in process """ + id = idc.get_array_id(array_name) + if id != -1: + idc.delete_array(id) + id = idc.create_array(array_name) + r = [] + for idx in xrange(len(value) / 1024 + 1): + s = value[idx * 1024: (idx + 1) * 1024] + r.append(s) + idc.set_array_string(id, idx, s) + + +def load_long_str_from_idb(array_name): + id = idc.get_array_id(array_name) + if id == -1: + return None + max_idx = idc.get_last_index(idc.AR_STR, id) + result = [idc.get_array_element(idc.AR_STR, id, idx) for idx in xrange(max_idx + 1)] + return "".join(result) + + +# ====================================================================== +# Functions that extends IDA Pro capabilities +# ====================================================================== + + +def _find_asm_address(self, cexpr): + """ Returns most close virtual address corresponding to cexpr """ + + ea = cexpr.ea + if ea != idaapi.BADADDR: + return ea + + for p in reversed(self.parents): + if p.ea != idaapi.BADADDR: + return p.ea + + +def my_cexpr_t(*args, **kwargs): + """ Replacement of bugged cexpr_t() function """ + + if len(args) == 0: + return idaapi.cexpr_t() + + if len(args) != 1: + raise NotImplementedError + + cexpr = idaapi.cexpr_t() + cexpr.thisown = False + if type(args[0]) == idaapi.cexpr_t: + cexpr.assign(args[0]) + else: + op = args[0] + cexpr._set_op(op) + + if 'x' in kwargs: + cexpr._set_x(kwargs['x']) + if 'y' in kwargs: + cexpr._set_y(kwargs['y']) + if 'z' in kwargs: + cexpr._set_z(kwargs['z']) + return cexpr + + +def extend_ida(): + idaapi.ctree_parentee_t._find_asm_address = _find_asm_address diff --git a/plugins/HexRaysPyTools/Core/NegativeOffsets.py b/plugins/HexRaysPyTools/Core/NegativeOffsets.py new file mode 100644 index 0000000..567b8be --- /dev/null +++ b/plugins/HexRaysPyTools/Core/NegativeOffsets.py @@ -0,0 +1,255 @@ +import re +import logging +import idaapi +import idc +import Helper + +logger = logging.getLogger(__name__) + + +def parse_lvar_comment(lvar): + if lvar.type().is_ptr(): + m = re.search('```(.+)```', lvar.cmt) + if m: + structure_name, offset = m.group(1).split('+') + offset = int(offset) + parent_tinfo = idaapi.tinfo_t() + if parent_tinfo.get_named_type(idaapi.cvar.idati, structure_name) and parent_tinfo.get_size() > offset: + member_name = dict(find_deep_members(parent_tinfo, lvar.type().get_pointed_object())).get(offset, None) + if member_name: + return NegativeLocalInfo(lvar.type().get_pointed_object(), parent_tinfo, offset, member_name) + return None + + +def find_deep_members(parent_tinfo, target_tinfo): + udt_data = idaapi.udt_type_data_t() + parent_tinfo.get_udt_details(udt_data) + result = [] + for udt_member in udt_data: + if udt_member.type.equals_to(target_tinfo): + result.append((udt_member.offset / 8, udt_member.name)) + elif udt_member.type.is_udt(): + for offset, name in find_deep_members(udt_member.type, target_tinfo): + final_name = udt_member.name + '.' + name if udt_member.name else name + result.append((udt_member.offset / 8 + offset, final_name)) + return result + + +class NegativeLocalInfo: + def __init__(self, tinfo, parent_tinfo, offset, member_name): + self.tinfo = tinfo + self.size = tinfo.get_size() if tinfo.is_udt else 0 + self.parent_tinfo = parent_tinfo + self.offset = offset + self.member_name = member_name + + def __repr__(self): + return "Type - {0}, parent type - {1}, offset - {2}, member_name - {3}".format( + self.tinfo.dstr(), + self.parent_tinfo.dstr(), + self.offset, + self.member_name + ) + + +class NegativeLocalCandidate: + def __init__(self, tinfo, offset): + """ + Tinfo - type of the structure tha local variable points to. So it's stripped from pointer. Offset - is first + found offset that points outside of the structure. + :param tinfo: idaapi.tinfo_t + :param offset: int + """ + self.tinfo = tinfo + self.offsets = [offset] + + def __repr__(self): + return self.tinfo.dstr() + ' ' + str(self.offsets) + + def is_structure_offset(self, tinfo, offset): + # Checks if structure tinfo contains a member at given offset + # TODO:array checking + udt_member = idaapi.udt_member_t() + udt_member.offset = offset * 8 + if offset >= 0 and tinfo.find_udt_member(idaapi.STRMEM_OFFSET, udt_member) != -1: + if udt_member.type.is_udt(): + return self.is_structure_offset(udt_member.type, offset - udt_member.offset / 8) + return udt_member.offset == offset * 8 + return False + + def find_containing_structures(self, type_library): + """ + Given the type library creates a list of structures from this library, that contains this structure and + satisfy offset conditions. + :param type_library: idaapi.til_t + :returns: ordinal, offset, member_name, containing structure name + """ + + min_offset = min(self.offsets) + min_offset = min_offset if min_offset < 0 else 0 + max_offset = max(self.offsets) + max_offset = max_offset if max_offset > 0 else self.tinfo.get_size() + # TODO: Check if all offsets are legal + + # Least acceptable size of the containing structure + min_struct_size = max_offset - min_offset + result = [] + parent_tinfo = idaapi.tinfo_t() + target_tinfo = idaapi.tinfo_t() + if not target_tinfo.get_named_type(type_library, self.tinfo.dstr()): + print "[Warning] Such type doesn't exist in '{0}' library".format(type_library.name) + return result + for ordinal in xrange(1, idaapi.get_ordinal_qty(type_library)): + parent_tinfo.create_typedef(type_library, ordinal) + if parent_tinfo.get_size() >= min_struct_size: + for offset, name in find_deep_members(parent_tinfo, target_tinfo): + # print "[DEBUG] Found {0} at {1} in {2}".format(name, offset, parent_tinfo.dstr()) + if offset + min_offset >= 0 and offset + max_offset <= parent_tinfo.get_size(): + result.append((ordinal, offset, name, parent_tinfo.dstr())) + return result + + +class ReplaceVisitor(idaapi.ctree_parentee_t): + + def __init__(self, negative_lvars): + super(ReplaceVisitor, self).__init__() + self.negative_lvars = negative_lvars + self.pvoid_tinfo = idaapi.tinfo_t(idaapi.BT_VOID) + self.pvoid_tinfo.create_ptr(self.pvoid_tinfo) + + def visit_expr(self, expression): + if expression.op == idaapi.cot_add and expression.x.op == idaapi.cot_var and expression.y.op == idaapi.cot_num: + index = expression.x.v.idx + if index in self.negative_lvars: + offset = expression.y.numval() + if offset >= self.negative_lvars[index].size: + self.create_containing_record(expression, index, offset) + elif expression.op == idaapi.cot_sub and expression.x.op == idaapi.cot_var and expression.y.op == idaapi.cot_num: + index = expression.x.v.idx + if index in self.negative_lvars: + offset = -expression.y.n.value(idaapi.tinfo_t(idaapi.BT_INT)) + self.create_containing_record(expression, index, offset) + return 0 + + def create_containing_record(self, expression, index, offset): + negative_lvar = self.negative_lvars[index] + logger.debug("Creating CONTAINING_RECORD macro, offset: {}, negative offset: {}, TYPE: {}".format( + negative_lvar.offset, + offset, + negative_lvar.parent_tinfo.dstr() + )) + + arg_address = idaapi.carg_t() + if expression.op == idaapi.cot_var: + arg_address.assign(expression) + else: + arg_address.assign(expression.x) + + arg_type = idaapi.carg_t() + cexpr_helper = idaapi.create_helper(True, self.pvoid_tinfo, negative_lvar.parent_tinfo.dstr()) + arg_type.assign(cexpr_helper) + + arg_field = idaapi.carg_t() + cexpr_helper = idaapi.create_helper( + True, + self.pvoid_tinfo, + negative_lvar.member_name + ) + arg_field.assign(cexpr_helper) + return_tinfo = idaapi.tinfo_t(negative_lvar.parent_tinfo) + return_tinfo.create_ptr(return_tinfo) + new_cexpr_call = idaapi.call_helper(return_tinfo, None, "CONTAINING_RECORD") + new_cexpr_call.a.push_back(arg_address) + new_cexpr_call.a.push_back(arg_type) + new_cexpr_call.a.push_back(arg_field) + new_cexpr_call.thisown = False + + parent = reversed(self.parents).next().cexpr + + diff = negative_lvar.offset + offset + if diff: + number = idaapi.make_num(diff) + number.thisown = False + new_cexpr_add = Helper.my_cexpr_t(idaapi.cot_add, x=new_cexpr_call, y=number) + new_cexpr_add.type = return_tinfo + + if parent.op == idaapi.cot_ptr: + tmp_tinfo = idaapi.tinfo_t() + tmp_tinfo.create_ptr(parent.type) + new_cexpr_cast = Helper.my_cexpr_t(idaapi.cot_cast, x=new_cexpr_add) + new_cexpr_cast.thisown = False + new_cexpr_cast.type = tmp_tinfo + expression.assign(new_cexpr_cast) + else: + expression.assign(new_cexpr_add) + else: + if parent.op == idaapi.cot_ptr: + tmp_tinfo = idaapi.tinfo_t() + tmp_tinfo.create_ptr(parent.type) + new_cexpr_cast = Helper.my_cexpr_t(idaapi.cot_cast, x=new_cexpr_call) + new_cexpr_cast.thisown = False + new_cexpr_cast.type = tmp_tinfo + expression.assign(new_cexpr_cast) + else: + expression.assign(new_cexpr_call) + + +class SearchVisitor(idaapi.ctree_parentee_t): + def __init__(self, cfunc): + super(SearchVisitor, self).__init__() + self.cfunc = cfunc + self.result = {} + + def visit_expr(self, expression): + if expression.op == idaapi.cot_call and expression.x.op == idaapi.cot_helper and len(expression.a) == 3: + if expression.x.helper == "CONTAINING_RECORD": + if expression.a[0].op == idaapi.cot_var: + idx = expression.a[0].v.idx + if expression.a[1].op == idaapi.cot_helper and expression.a[2].op == idaapi.cot_helper: + parent_name = expression.a[1].helper + member_name = expression.a[2].helper + parent_tinfo = idaapi.tinfo_t() + if not parent_tinfo.get_named_type(idaapi.cvar.idati, parent_name): + return 0 + udt_data = idaapi.udt_type_data_t() + parent_tinfo.get_udt_details(udt_data) + udt_member = filter(lambda x: x.name == member_name, udt_data) + if udt_member: + tinfo = udt_member[0].type + self.result[idx] = NegativeLocalInfo( + tinfo, + parent_tinfo, + udt_member[0].offset / 8, + member_name + ) + return 1 + return 0 + + +class AnalyseVisitor(idaapi.ctree_parentee_t): + def __init__(self, candidates, potential_negatives): + super(AnalyseVisitor, self).__init__() + self.candidates = candidates + self.potential_negatives = potential_negatives + self.potential_negatives.clear() + + def visit_expr(self, expression): + if expression.op == idaapi.cot_add and expression.y.op == idaapi.cot_num: + if expression.x.op == idaapi.cot_var and expression.x.v.idx in self.candidates: + idx = expression.x.v.idx + number = expression.y.numval() + if self.candidates[idx].get_size() <= number: + if idx in self.potential_negatives: + self.potential_negatives[idx].offsets.append(number) + else: + self.potential_negatives[idx] = NegativeLocalCandidate(self.candidates[idx], number) + elif expression.op == idaapi.cot_sub and expression.y.op == idaapi.cot_num: + if expression.x.op == idaapi.cot_var and expression.x.v.idx in self.candidates: + idx = expression.x.v.idx + number = -expression.y.numval() + if idx in self.potential_negatives: + self.potential_negatives[idx].offsets.append(number) + else: + self.potential_negatives[idx] = NegativeLocalCandidate(self.candidates[idx], number) + + return 0 diff --git a/plugins/HexRaysPyTools/Core/SpaghettiCode.py b/plugins/HexRaysPyTools/Core/SpaghettiCode.py new file mode 100644 index 0000000..10633ee --- /dev/null +++ b/plugins/HexRaysPyTools/Core/SpaghettiCode.py @@ -0,0 +1,120 @@ +import idaapi +import idc + + +def inverse_if_condition(cif): + # cexpr_t has become broken but fortunately still exist `assing` method which copies one expr into another + cit_if_condition = cif.expr + tmp_cexpr = idaapi.cexpr_t() + tmp_cexpr.assign(cit_if_condition) + new_if_condition = idaapi.lnot(tmp_cexpr) + cif.expr.swap(new_if_condition) + del cit_if_condition + + +def inverse_if(cif): + inverse_if_condition(cif) + idaapi.qswap(cif.ithen, cif.ielse) + + +class InversionInfo(object): + ARRAY_NAME = "$HexRaysPyTools:IfThenElse:" + + def __init__(self, func_ea): + self.__name = InversionInfo.ARRAY_NAME + hex(int(func_ea)) + self.__id = idc.GetArrayId(self.__name) + + def get_inverted(self): + if self.__id != -1: + array = idc.GetArrayElement(idc.AR_STR, self.__id, 0) + return set(map(int, array.split())) + return set() + + def switch_inverted(self, address): + if self.__id == -1: + self.__id = idc.CreateArray(self.__name) + idc.SetArrayString(self.__id, 0, str(address)) + else: + inverted = self.get_inverted() + try: + inverted.remove(address) + if not inverted: + idc.DeleteArray(self.__id) + + except KeyError: + inverted.add(address) + + idc.SetArrayString(self.__id, 0, " ".join(map(str, inverted))) + + +class SpaghettiVisitor(idaapi.ctree_parentee_t): + def __init__(self): + super(SpaghettiVisitor, self).__init__() + + def visit_insn(self, instruction): + if instruction.op != idaapi.cit_block: + return 0 + + while True: + cblock = instruction.cblock + size = cblock.size() + # Find block that has "If" and "return" as last 2 statements + if size < 2: + break + + if cblock.at(size - 2).op != idaapi.cit_if: + break + + cif = cblock.at(size - 2).cif + if cblock.back().op != idaapi.cit_return or cif.ielse: + break + + cit_then = cif.ithen + + # Skip if only one (not "if") statement in "then" branch + if cit_then.cblock.size() == 1 and cit_then.cblock.front().op != idaapi.cit_if: + return 0 + + inverse_if_condition(cif) + + # Take return from list of statements and later put it back + cit_return = idaapi.cinsn_t() + cit_return.assign(instruction.cblock.back()) + cit_return.thisown = False + instruction.cblock.pop_back() + + # Fill main block with statements from "Then" branch + while cit_then.cblock: + instruction.cblock.push_back(cit_then.cblock.front()) + cit_then.cblock.pop_front() + + # Put back main return if there's no another return or "GOTO" already + if instruction.cblock.back().op not in (idaapi.cit_return, idaapi.cit_goto): + new_return = idaapi.cinsn_t() + new_return.thisown = False + new_return.assign(cit_return) + instruction.cblock.push_back(new_return) + + # Put return into "Then" branch + cit_then.cblock.push_back(cit_return) + return 0 + + +class SwapThenElseVisitor(idaapi.ctree_parentee_t): + def __init__(self, func_ea): + super(SwapThenElseVisitor, self).__init__() + self.__inversion_info = InversionInfo(func_ea) + self.__inverted = self.__inversion_info.get_inverted() + + def visit_insn(self, insn): + if insn.op != idaapi.cit_if or insn.cif.ielse is None: + return 0 + + if insn.ea in self.__inverted: + inverse_if(insn.cif) + + return 0 + + def apply_to(self, *args): + if self.__inverted: + super(SwapThenElseVisitor, self).apply_to(*args) diff --git a/plugins/HexRaysPyTools/Core/StructXrefs.py b/plugins/HexRaysPyTools/Core/StructXrefs.py new file mode 100644 index 0000000..9d0deb5 --- /dev/null +++ b/plugins/HexRaysPyTools/Core/StructXrefs.py @@ -0,0 +1,181 @@ +import time +import logging +from collections import namedtuple +import json + +import idaapi +import Helper +import HexRaysPyTools.Settings as Settings + + +logger = logging.getLogger(__name__) + +XrefInfo = namedtuple('XrefInfo', ['func_ea', 'offset', 'line', 'type']) + + +def singleton(cls): + instances = {} + + def get_instance(): + if cls not in instances: + instances[cls] = cls() + return instances[cls] + return get_instance + + +@singleton +class XrefStorage(object): + ARRAY_NAME = "$HexRaysPyTools:XrefStorage" + + def __init__(self): + self.storage = None + + def open(self): + if not Settings.STORE_XREFS: + self.storage = {} + return + + result = Helper.load_long_str_from_idb(self.ARRAY_NAME) + if result: + try: + self.storage = json.loads(result, object_hook=self.json_keys_to_str) + return + except ValueError: + logger.error("Failed to read previous info about Xrefs. Try Ctrl+F5 to cache data") + self.storage = {} + + def close(self): + self.save() + self.storage = None + + def save(self): + if not Settings.STORE_XREFS: + return + + if self.storage: + Helper.save_long_str_to_idb(self.ARRAY_NAME, json.dumps(self.storage)) + + def update_structure_info(self, ordinal, function_address, data): + """ Accepts data in form dictionary {structure offset -> list(offsets within function with field appealing) """ + if ordinal not in self.storage: + self.storage[ordinal] = {} + + image_base = idaapi.get_imagebase() + function_offset = function_address - image_base + self.storage[ordinal][function_offset] = data + + def get_structure_info(self, ordinal, struct_offset): + """ By given ordinal and offset within a structure returns dictionary {func_address -> list(offsets)} """ + result = [] + + if ordinal not in self.storage: + return result + + for func_offset, data in self.storage[ordinal].items(): + if struct_offset in data: + func_ea = func_offset + idaapi.get_imagebase() + for xref_info in data[struct_offset]: + offset, line, usage_type = xref_info + result.append(XrefInfo(func_ea, offset, line, usage_type)) + return result + + @staticmethod + def json_keys_to_str(x): + if isinstance(x, dict): + return {int(k): v for k, v in x.items()} + return x + + def __len__(self): + return len(str(self.storage)) + + +class StructXrefVisitor(idaapi.ctree_parentee_t): + def __init__(self, cfunc): + super(StructXrefVisitor, self).__init__() + self.__cfunc = cfunc + self.__image_base = idaapi.get_imagebase() + self.__function_address = cfunc.entry_ea + self.__result = {} + self.__storage = XrefStorage() + + def visit_expr(self, expression): + # Checks if expression is reference by pointer or by value + if expression.op == idaapi.cot_memptr: + struct_type = expression.x.type.get_pointed_object() + elif expression.op == idaapi.cot_memref: + struct_type = expression.x.type + else: + return 0 + + # Getting information about structure, field offset, address and one line corresponding to code + ordinal = struct_type.get_ordinal() + if ordinal == 0: + t = idaapi.tinfo_t() + struct_name = struct_type.dstr().split()[-1] # Get rid of `struct` prefix or something else + t.get_named_type(idaapi.cvar.idati, struct_name) + ordinal = t.get_ordinal() + + field_offset = expression.m + ea = self.__find_ref_address(expression) + usage_type = self.__get_type(expression) + + if ea == idaapi.BADADDR or not ordinal: + logger.warning("Failed to parse at address {0}, ordinal - {1}, type - {2}".format( + Helper.to_hex(ea), ordinal, struct_type.dstr() + )) + + one_line = self.__get_line() + + occurrence_offset = ea - self.__function_address + xref_info = (occurrence_offset, one_line, usage_type) + + # Saving results + if ordinal not in self.__result: + self.__result[ordinal] = {field_offset: [xref_info]} + elif field_offset not in self.__result[ordinal]: + self.__result[ordinal][field_offset] = [xref_info] + else: + self.__result[ordinal][field_offset].append(xref_info) + return 0 + + def process(self): + t = time.time() + self.apply_to(self.__cfunc.body, None) + for ordinal, data in self.__result.items(): + self.__storage.update_structure_info(ordinal, self.__function_address, data) + + storage_mb_size = len(self.__storage) * 1.0 / 1024 ** 2 + logger.debug("Xref processing: %f seconds passed, storage size - %.2f MB ", (time.time() - t), storage_mb_size) + + def __find_ref_address(self, cexpr): + """ Returns most close virtual address corresponding to cexpr """ + + ea = cexpr.ea + if ea != idaapi.BADADDR: + return ea + + for p in reversed(self.parents): + if p.ea != idaapi.BADADDR: + return p.ea + + def __get_type(self, cexpr): + """ Returns one of the following types: 'R' - read value, 'W' - write value, 'A' - function argument""" + child = cexpr + for p in reversed(self.parents): + assert p, "Failed to get type at " + Helper.to_hex(self.__function_address) + + if p.cexpr.op == idaapi.cot_call: + return 'Arg' + if not p.is_expr(): + return 'R' + if p.cexpr.op == idaapi.cot_asg: + if p.cexpr.x == child: + return 'W' + return 'R' + child = p.cexpr + + def __get_line(self): + for p in reversed(self.parents): + if not p.is_expr(): + return idaapi.tag_remove(p.print1(self.__cfunc)) + AssertionError("Parent instruction is not found") diff --git a/plugins/HexRaysPyTools/Core/StructureGraph.py b/plugins/HexRaysPyTools/Core/StructureGraph.py new file mode 100644 index 0000000..a7a0a30 --- /dev/null +++ b/plugins/HexRaysPyTools/Core/StructureGraph.py @@ -0,0 +1,184 @@ +import idaapi +import idc + + +class LocalType: + def __init__(self, name, members_ordinals, hint, is_selected=False, is_typedef=False, is_enum=False, is_union=False): + self.name = name + self.members_ordinals = members_ordinals + self.hint = hint + self.is_selected = is_selected + self.is_typedef = is_typedef + self.is_enum = is_enum + self.is_union = is_union + + def __call__(self): + return self.name, self.members_ordinals + + def __str__(self): + return "<{0}, {1}>".format(self.name, self.members_ordinals) + + def __repr__(self): + return self.__str__() + + @property + def name_and_color(self): + if self.is_selected: + return self.name, 0x0000FF + elif self.is_typedef: + return self.name, 0x99FFFF + elif self.is_enum: + return self.name, 0x33FF33 + elif self.is_union: + return self.name, 0xCCCC00 + return self.name, 0xffdd99 + + +class StructureGraph: + # TODO:Enum types display + def __init__(self, ordinal_list=None): + self.ordinal_list = ordinal_list if ordinal_list else xrange(1, idc.GetMaxLocalType()) + self.local_types = {} + self.edges = [] + self.final_edges = [] + self.visited_downward = [] + self.visited_upward = [] + self.downward_edges = {} + self.upward_edges = {} + self.initialize_nodes() + self.calculate_edges() + + def change_selected(self, selected): + self.visited_downward = [] + self.visited_upward = [] + self.final_edges = [] + for ordinal in self.ordinal_list: + self.local_types[ordinal].is_selected = False + self.ordinal_list = set(self.local_types).intersection(selected) + for ordinal in self.ordinal_list: + self.local_types[ordinal].is_selected = True + + @staticmethod + def get_ordinal(tinfo): + while tinfo.is_ptr() or tinfo.is_array(): + tinfo.remove_ptr_or_array() + if tinfo.is_udt(): + return tinfo.get_ordinal() + elif tinfo.is_enum(): + return tinfo.get_ordinal() + elif tinfo.is_typeref(): + typeref_ordinal = tinfo.get_ordinal() + if typeref_ordinal: + typeref_tinfo = StructureGraph.get_tinfo_by_ordinal(typeref_ordinal) + if typeref_tinfo.is_typeref() or typeref_tinfo.is_udt() or typeref_tinfo.is_ptr(): + return typeref_ordinal + else: + return 0 + + @staticmethod + def get_members_ordinals(tinfo): + ordinals = [] + if tinfo.is_udt(): + udt_data = idaapi.udt_type_data_t() + tinfo.get_udt_details(udt_data) + for udt_member in udt_data: + ordinal = StructureGraph.get_ordinal(udt_member.type) + if ordinal: + ordinals.append(ordinal) + return ordinals + + @staticmethod + def get_tinfo_by_ordinal(ordinal): + local_typestring = idc.GetLocalTinfo(ordinal) + if local_typestring: + p_type, fields = local_typestring + local_tinfo = idaapi.tinfo_t() + local_tinfo.deserialize(idaapi.cvar.idati, p_type, fields) + return local_tinfo + return None + + def initialize_nodes(self): + for ordinal in xrange(1, idc.GetMaxLocalType()): + # if ordinal == 15: + # import pydevd + # pydevd.settrace("localhost", port=12345, stdoutToServer=True, stderrToServer=True) + + local_tinfo = StructureGraph.get_tinfo_by_ordinal(ordinal) + if not local_tinfo: + return + name = idc.GetLocalTypeName(ordinal) + + if local_tinfo.is_typeref(): + typeref_ordinal = local_tinfo.get_ordinal() + members_ordinals = [] + if typeref_ordinal: + typeref_tinfo = StructureGraph.get_tinfo_by_ordinal(typeref_ordinal) + if typeref_tinfo.is_typeref() or typeref_tinfo.is_udt() or typeref_tinfo.is_ptr(): + members_ordinals = [typeref_ordinal] + cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x3, local_tinfo, None, None) + self.local_types[ordinal] = LocalType(name, members_ordinals, cdecl_typedef, is_typedef=True) + elif local_tinfo.is_udt(): + # udt_data = idaapi.udt_type_data_t() + # local_tinfo.get_udt_details(udt_data) + members_ordinals = StructureGraph.get_members_ordinals(local_tinfo) + cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x1, local_tinfo, None, None) + self.local_types[ordinal] = LocalType(name, members_ordinals, cdecl_typedef, is_union=local_tinfo.is_union()) + elif local_tinfo.is_ptr(): + typeref_ordinal = StructureGraph.get_ordinal(local_tinfo) + members_ordinals = [typeref_ordinal] if typeref_ordinal else [] + cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x2, local_tinfo, None, None) + self.local_types[ordinal] = LocalType( + name, + members_ordinals, + cdecl_typedef + ' *', + is_typedef=True + ) + elif local_tinfo.is_enum(): + cdecl_typedef = idaapi.print_tinfo(None, 4, 5, 0x21, local_tinfo, None, None) + self.local_types[ordinal] = LocalType(name, [], cdecl_typedef, is_enum=True) + + self.ordinal_list = set(self.ordinal_list).intersection(self.local_types) + for ordinal in self.ordinal_list: + self.local_types[ordinal].is_selected = True + + def calculate_edges(self): + for first in self.local_types.keys(): + for second in self.local_types[first].members_ordinals: + self.edges.append((first, second)) + + self.downward_edges = {key: [] for key in self.local_types.keys()} + self.upward_edges = {key: [] for key in self.local_types.keys()} + + for key, value in self.edges: + self.downward_edges[key].append(value) + self.upward_edges[value].append(key) + + def generate_final_edges_down(self, node): + if node not in self.visited_downward: + self.visited_downward.append(node) + else: + return + for next_node in self.downward_edges[node]: + self.final_edges.append((node, next_node)) + for next_node in self.downward_edges[node]: + self.generate_final_edges_down(next_node) + + def generate_final_edges_up(self, node): + if node not in self.visited_upward: + self.visited_upward.append(node) + else: + return + for next_node in self.upward_edges[node]: + self.final_edges.append((next_node, node)) + for next_node in self.upward_edges[node]: + self.generate_final_edges_up(next_node) + + def get_nodes(self): + for ordinal in self.ordinal_list: + if ordinal in self.local_types: + self.generate_final_edges_down(ordinal) + self.generate_final_edges_up(ordinal) + return set([node for nodes in self.final_edges for node in nodes]) + + def get_edges(self): + return self.final_edges diff --git a/plugins/HexRaysPyTools/Core/TemporaryStructure.py b/plugins/HexRaysPyTools/Core/TemporaryStructure.py new file mode 100644 index 0000000..7e5415d --- /dev/null +++ b/plugins/HexRaysPyTools/Core/TemporaryStructure.py @@ -0,0 +1,864 @@ +import bisect +import idc +import idaapi +import re +import itertools +# import PySide.QtCore as QtCore +# import PySide.QtGui as QtGui +import Cache +from HexRaysPyTools.Cute import * +import Const +import Helper +import VariableScanner +import HexRaysPyTools.Api as Api +from HexRaysPyTools.Forms import MyChoose + + +SCORE_TABLE = dict((v, k) for k, v in enumerate( + ['unsigned __int8 *', 'unsigned __int8', '__int8 *', '__int8', '_BYTE', '_BYTE *', '_BYTE **', 'const char **', + 'signed __int16', 'unsigned __int16', '__int16', 'signed __int16 *', 'unsigned __int16 *', '__int16 *', + '_WORD *', '_WORD **', + 'signed int*', 'signed int', 'unsigned int *', 'unsigned int', 'int **', 'char **', 'int *', 'void **', + 'int', '_DWORD *', 'char', '_DWORD', '_WORD', 'void *', 'char *'] +)) + + +def parse_vtable_name(address): + name = idaapi.get_short_name(address) + if idaapi.is_valid_typename(name): + if name[0:3] == 'off': + # off_XXXXXXXX case + return "Vtable" + name[3:], False + elif "table" in name: + return name, True + print "[Warning] Weird virtual table name -", name + return "Vtable_" + name + else: + # Attempt to make nice and valid name from demangled RTTI name + try: + name = re.sub("^const ", "", name) + sliced_names = name.split("::") + name, for_part = "_for_".join(sliced_names[:-1]), sliced_names[-1] + print name, for_part + templates = re.search("<(.*)>", name) + if templates: + templates = templates.group(1) + name = re.sub("<.*>", "", name) + templates = re.sub("[^a-zA-Z0-9_*]", "_", templates) + templates = re.sub("\*", "PTR", templates) + name += '_' + templates + + for_part = re.search("\{for `(.*)'\}", for_part) + if for_part: + for_part = for_part.group(1) + name += '_' + for_part + + return 'Vtable_' + name, True + + except (AttributeError, IndexError): + print "[Warning] Unable to parse virtual table name - " + + return "Vtable_{0:X}".format(address), False + + +class AbstractMember: + def __init__(self, offset, scanned_variable, origin): + """ + Offset is the very very base of the structure + Origin is from which offset of the base structure the variable have been scanned + scanned_variable - information about context in which this variable was scanned. This is necessary for final + applying type after packing or finalizing structure. + + :param offset: int + :param scanned_variable: ScannedVariable + :param origin: int + """ + self.offset = offset + self.origin = origin + self.enabled = True + self.is_array = False + self.scanned_variables = {scanned_variable} if scanned_variable else set() + self.tinfo = None + + def type_equals_to(self, tinfo): + return self.tinfo.equals_to(tinfo) + + def switch_array_flag(self): + self.is_array ^= True + + def activate(self): + pass + + def set_enabled(self, enable): + self.enabled = enable + self.is_array = False + + def has_collision(self, other): + if self.offset <= other.offset: + return self.offset + self.size > other.offset + return other.offset + other.size >= self.offset + + @property + def score(self): + """ More score of the member - it better suits as candidate for this offset """ + try: + return SCORE_TABLE[self.type_name] + except KeyError: + if self.tinfo and self.tinfo.is_funcptr(): + return 0x1000 + len(self.tinfo.dstr()) + return 0xFFFF + + @property + def type_name(self): + return self.tinfo.dstr() + + @property + def size(self): + size = self.tinfo.get_size() + return size if size != idaapi.BADSIZE else 1 + + @property + def font(self): + return None + + def __repr__(self): + return hex(self.offset) + ' ' + self.type_name + + def __eq__(self, other): + """ I'm aware that it's dirty but have no time to refactor whole file to nice one """ + + if self.offset == other.offset and self.type_name == other.type_name: + self.scanned_variables |= other.scanned_variables + return True + return False + + __ne__ = lambda self, other: self.offset != other.offset or self.type_name != other.type_name + __lt__ = lambda self, other: self.offset < other.offset or \ + (self.offset == other.offset and self.type_name < other.type_name) + __le__ = lambda self, other: self.offset <= other.offset + __gt__ = lambda self, other: self.offset > other.offset or \ + (self.offset == other.offset and self.type_name < other.type_name) + __ge__ = lambda self, other: self.offset >= other.offset + + +class VirtualFunction: + def __init__(self, address, offset): + self.address = address + self.offset = offset + self.visited = False + + def get_ptr_tinfo(self): + # print self.tinfo.dstr() + ptr_tinfo = idaapi.tinfo_t() + ptr_tinfo.create_ptr(self.tinfo) + return ptr_tinfo + + def get_udt_member(self): + udt_member = idaapi.udt_member_t() + udt_member.type = self.get_ptr_tinfo() + udt_member.offset = self.offset + udt_member.name = self.name + udt_member.size = Const.EA_SIZE + return udt_member + + def get_information(self): + return [Helper.to_hex(self.address), self.name, self.tinfo.dstr()] + + @property + def name(self): + name = idaapi.get_short_name(self.address) + name = name.split('(')[0] + result = re.search(r"(\[thunk\]:)?([^`]*)(.*\{(\d+)}.*)?", name) + name, adjuster = result.group(2), result.group(4) + if adjuster: + name += "_adj_" + adjuster + name = name.translate(None, "`'").replace(':', '_').replace(' ', '_').replace(',', '_').replace('~', 'DESTR__') + name = name.replace("==", "__eq__") + name = name.replace("=", "__asg__") + name = re.sub(r'[<>]', '_t_', name) + return name + + @property + def tinfo(self): + try: + decompiled_function = idaapi.decompile(self.address) + if decompiled_function: + return idaapi.tinfo_t(decompiled_function.type) + return Const.DUMMY_FUNC + except idaapi.DecompilationFailure: + pass + print "[ERROR] Failed to decompile function at 0x{0:08X}".format(self.address) + return Const.DUMMY_FUNC + + def show_location(self): + idaapi.open_pseudocode(self.address, 1) + + +class ImportedVirtualFunction(VirtualFunction): + def __init__(self, address, offset): + VirtualFunction.__init__(self, address, offset) + + @property + def tinfo(self): + print "[INFO] Ignoring import function at 0x{0:08X}".format(self.address) + tinfo = idaapi.tinfo_t() + if idaapi.guess_tinfo2(self.address, tinfo): + return tinfo + return Const.DUMMY_FUNC + + def show_location(self): + idaapi.jumpto(self.address) + + +class VirtualTable(AbstractMember): + class VirtualTableChoose(MyChoose): + def __init__(self, items, temp_struct, virtual_table): + MyChoose.__init__( + self, + items, + "Select Virtual Function", + [["Address", 10], ["Name", 15], ["Declaration", 45]], + 13 + ) + self.popup_names = ["Scan All", "-", "Scan", "-"] + self.__temp_struct = temp_struct + self.__virtual_table = virtual_table + + def OnGetLineAttr(self, n): + return [0xd9d9d9, 0x0] if self.__virtual_table.virtual_functions[n].visited else [0xffffff, 0x0] + + def OnGetIcon(self, n): + return 32 if self.__virtual_table.virtual_functions[n].visited else 160 + + def OnInsertLine(self): + """ Scan All Functions menu """ + self.__virtual_table.scan_virtual_functions() + + def OnEditLine(self, n): + """ Scan menu """ + self.__virtual_table.scan_virtual_function(n) + + def __init__(self, offset, address, scanned_variable=None, origin=0): + AbstractMember.__init__(self, offset + origin, scanned_variable, origin) + self.address = address + self.virtual_functions = [] + self.name = "vtable" + ("_{0:X}".format(self.offset) if self.offset else '') + self.vtable_name, self.have_nice_name = parse_vtable_name(address) + self.populate() + + def populate(self): + address = self.address + while True: + if Const.EA64: + func_address = idaapi.get_64bit(address) + else: + func_address = idaapi.get_32bit(address) + + if Helper.is_code_ea(func_address): + self.virtual_functions.append(VirtualFunction(func_address, address - self.address)) + elif Helper.is_imported_ea(func_address): + self.virtual_functions.append(ImportedVirtualFunction(func_address, address - self.address)) + else: + break + address += Const.EA_SIZE + + if idaapi.get_first_dref_to(address) != idaapi.BADADDR: + break + + def create_tinfo(self): + # print "(Virtual table) at address: 0x{0:08X} name: {1}".format(self.address, self.name) + udt_data = idaapi.udt_type_data_t() + for function in self.virtual_functions: + udt_data.push_back(function.get_udt_member()) + + for duplicates in Helper.search_duplicate_fields(udt_data): + first_entry_idx = duplicates.pop(0) + print "[Warning] Found duplicate virtual functions", udt_data[first_entry_idx].name + for num, dup in enumerate(duplicates): + udt_data[dup].name = "duplicate_{0}_{1}".format(first_entry_idx, num + 1) + tinfo = idaapi.tinfo_t() + tinfo.create_ptr(Const.DUMMY_FUNC) + udt_data[dup].type = tinfo + + final_tinfo = idaapi.tinfo_t() + if final_tinfo.create_udt(udt_data, idaapi.BTF_STRUCT): + # print "\n\t(Final structure)\n" + idaapi.print_tinfo('\t', 4, 5, idaapi.PRTYPE_MULTI | idaapi.PRTYPE_TYPE + # | idaapi.PRTYPE_SEMI, final_tinfo, self.name, None) + return final_tinfo + print "[ERROR] Virtual table creation failed" + + def import_to_structures(self, ask=False): + """ + Imports virtual tables and returns tid_t of new structure + + :return: idaapi.tid_t + """ + cdecl_typedef = idaapi.print_tinfo(None, 4, 5, idaapi.PRTYPE_MULTI | idaapi.PRTYPE_TYPE | idaapi.PRTYPE_SEMI, + self.create_tinfo(), self.vtable_name, None) + if ask: + cdecl_typedef = idaapi.asktext(0x10000, cdecl_typedef, "The following new type will be created") + if not cdecl_typedef: + return + previous_ordinal = idaapi.get_type_ordinal(idaapi.cvar.idati, self.vtable_name) + if previous_ordinal: + idaapi.del_numbered_type(idaapi.cvar.idati, previous_ordinal) + ordinal = idaapi.idc_set_local_type(previous_ordinal, cdecl_typedef, idaapi.PT_TYP) + else: + ordinal = idaapi.idc_set_local_type(-1, cdecl_typedef, idaapi.PT_TYP) + + if ordinal: + print "[Info] Virtual table " + self.vtable_name + " added to Local Types" + return idaapi.import_type(idaapi.cvar.idati, -1, self.vtable_name) + else: + print "[Error] Failed to create virtual table " + self.vtable_name + print "*" * 100 + print cdecl_typedef + print "*" * 100 + + def show_virtual_functions(self): + function_chooser = self.VirtualTableChoose( + [function.get_information() for function in self.virtual_functions], Cache.temporary_structure, self) + + idx = function_chooser.Show(True) + if idx != -1: + virtual_function = self.virtual_functions[idx] + virtual_function.visited = True + virtual_function.show_location() + + def scan_virtual_function(self, index): + if Helper.is_imported_ea(self.virtual_functions[index].address): + print "[INFO] Ignoring import function at 0x{0:08X}".format(self.address) + return + try: + function = idaapi.decompile(self.virtual_functions[index].address) + except idaapi.DecompilationFailure: + print "[ERROR] Failed to decompile function at 0x{0:08X}".format(self.address) + return + if Helper.FunctionTouchVisitor(function).process(): + function = idaapi.decompile(self.virtual_functions[index].address) + if function.arguments and function.arguments[0].is_arg_var and Helper.is_legal_type(function.arguments[0].tif): + print "[Info] Scanning virtual function at 0x{0:08X}".format(function.entry_ea) + # TODO: Remove usage `temporary_structure' as global + obj = Api.VariableObject(function.get_lvars()[0], 0) + scanner = VariableScanner.NewDeepSearchVisitor(function, self.offset, obj, Cache.temporary_structure) + scanner.process() + else: + print "[Warning] Bad type of first argument in virtual function at 0x{0:08X}".format(function.entry_ea) + + def scan_virtual_functions(self): + for idx in xrange(len(self.virtual_functions)): + self.scan_virtual_function(idx) + + def get_udt_member(self, offset=0): + udt_member = idaapi.udt_member_t() + tid = self.import_to_structures() + if tid != idaapi.BADADDR: + udt_member.name = self.name + tmp_tinfo = idaapi.create_typedef(self.vtable_name) + tmp_tinfo.create_ptr(tmp_tinfo) + udt_member.type = tmp_tinfo + udt_member.offset = self.offset - offset + udt_member.size = Const.EA_SIZE + return udt_member + + def type_equals_to(self, tinfo): + udt_data = idaapi.udt_type_data_t() + if tinfo.is_ptr() and tinfo.get_pointed_object().get_udt_details(udt_data): + if udt_data[0].type.is_funcptr(): + return True + return False + + def switch_array_flag(self): + pass + + def activate(self): + self.show_virtual_functions() + + @staticmethod + def check_address(address): + # Checks if given address contains virtual table. Returns True if more than 2 function pointers found + # Also if table's addresses point to code in executable section, than tries to make functions at that addresses + functions_count = 0 + while True: + func_address = idaapi.get_64bit(address) if Const.EA64 else idaapi.get_32bit(address) + # print "[INFO] Address 0x{0:08X}".format(func_address) + if Helper.is_code_ea(func_address) or Helper.is_imported_ea(func_address): + functions_count += 1 + address += Const.EA_SIZE + else: + segment = idaapi.getseg(func_address) + if segment and segment.perm & idaapi.SEGPERM_EXEC: + idc.MakeUnknown(func_address, 1, idaapi.DOUNK_SIMPLE) + if idc.MakeFunction(func_address): + functions_count += 1 + address += Const.EA_SIZE + continue + break + idaapi.autoWait() + return functions_count + + @property + def type_name(self): + return self.vtable_name + " *" + + @property + def font(self): + return QtGui.QFont("Consolas", 10, QtGui.QFont.Bold) + + @property + def size(self): + return Const.EA_SIZE + + +class Member(AbstractMember): + def __init__(self, offset, tinfo, scanned_variable, origin=0): + AbstractMember.__init__(self, offset + origin, scanned_variable, origin) + self.tinfo = tinfo + self.name = "field_{0:X}".format(self.offset) + + def get_udt_member(self, array_size=0, offset=0): + udt_member = idaapi.udt_member_t() + udt_member.name = "field_{0:X}".format(self.offset - offset) if self.name[:6] == "field_" else self.name + udt_member.type = self.tinfo + if array_size: + tmp = idaapi.tinfo_t(self.tinfo) + tmp.create_array(self.tinfo, array_size) + udt_member.type = tmp + udt_member.offset = self.offset - offset + udt_member.size = self.size + return udt_member + + def activate(self): + new_type_declaration = idaapi.askstr(0x100, self.type_name, "Enter type:") + if new_type_declaration is None: + return + + result = idc.ParseType(new_type_declaration, 0) + if result is None: + return + _, tp, fld = result + tinfo = idaapi.tinfo_t() + tinfo.deserialize(idaapi.cvar.idati, tp, fld, None) + self.tinfo = tinfo + self.is_array = False + + +class VoidMember(Member): + def __init__(self, offset, scanned_variable, origin=0, char=False): + tinfo = Const.CHAR_TINFO if char else Const.BYTE_TINFO + Member.__init__(self, offset, tinfo, scanned_variable, origin) + self.is_array = True + + def type_equals_to(self, tinfo): + return True + + def switch_array_flag(self): + pass + + def set_enabled(self, enable): + self.enabled = enable + + @property + def font(self): + return QtGui.QFont("Consolas", 10, italic=True) + + +class TemporaryStructureModel(QtCore.QAbstractTableModel): + + def __init__(self, *args): + """ + Keeps information about currently found fields in possible structure + main_offset - is the base from where variables scanned. Can be set to different value if some field is passed by + reverence + items - array of candidates to fields + """ + super(TemporaryStructureModel, self).__init__(*args) + self.main_offset = 0 + self.headers = ["Offset", "Type", "Name"] + self.items = [] + self.collisions = [] + self.structure_name = "CHANGE_MY_NAME" + + # OVERLOADED METHODS # + + def rowCount(self, *args): + return len(self.items) + + def columnCount(self, *args): + return len(self.headers) + + def data(self, index, role): + row, col = index.row(), index.column() + item = self.items[row] + if role == QtCore.Qt.DisplayRole: + if col == 0: + return "0x{0:08X}".format(item.offset) + elif col == 1: + if item.is_array and item.size > 0: + array_size = self.calculate_array_size(row) + if array_size: + return item.type_name + "[{}]".format(array_size) + return item.type_name + elif col == 2: + return item.name + elif role == QtCore.Qt.ToolTipRole: + if col == 0: + return self.items[row].offset + elif col == 1: + return self.items[row].size * (self.calculate_array_size(row) if self.items[row].is_array else 1) + elif role == QtCore.Qt.EditRole: + if col == 2: + return self.items[row].name + elif role == QtCore.Qt.FontRole: + if col == 1: + return item.font + elif role == QtCore.Qt.BackgroundRole: + if not item.enabled: + return QtGui.QColor(QtCore.Qt.gray) + if item.offset == self.main_offset: + if col == 0: + return QtGui.QBrush(QtGui.QColor("#ff8080")) + if self.have_collision(row): + return QtGui.QBrush(QtGui.QColor("#ffff99")) + elif role == QtCore.Qt.ForegroundRole: + if self.have_collision(row): + return QtGui.QBrush(QtGui.QColor("#191919")) + + def setData(self, index, value, role): + row, col = index.row(), index.column() + if role == QtCore.Qt.EditRole and idaapi.isident(str(value)): + self.items[row].name = str(value) + self.dataChanged.emit(index, index) + return True + return False + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole and orientation == QtCore.Qt.Horizontal: + return self.headers[section] + + def flags(self, index): + if index.column() == 2: + return super(TemporaryStructureModel, self).flags(index) | QtGui.QAbstractItemView.DoubleClicked + return super(TemporaryStructureModel, self).flags(index) + + # HELPER METHODS # + + def pack(self, start=0, stop=None): + if self.collisions[start:stop].count(True): + print "[Warning] Collisions detected" + return + + final_tinfo = idaapi.tinfo_t() + udt_data = idaapi.udt_type_data_t() + origin = self.items[start].offset if start else 0 + offset = origin + + for item in filter(lambda x: x.enabled, self.items[start:stop]): # Filter disabled members + gap_size = item.offset - offset + if gap_size: + udt_data.push_back(TemporaryStructureModel.get_padding_member(offset - origin, gap_size)) + if item.is_array: + array_size = self.calculate_array_size(bisect.bisect_left(self.items, item)) + if array_size: + udt_data.push_back(item.get_udt_member(array_size, offset=origin)) + offset = item.offset + item.size * array_size + continue + udt_data.push_back(item.get_udt_member(offset=origin)) + offset = item.offset + item.size + + final_tinfo.create_udt(udt_data, idaapi.BTF_STRUCT) + cdecl = idaapi.print_tinfo(None, 4, 5, idaapi.PRTYPE_MULTI | idaapi.PRTYPE_TYPE | idaapi.PRTYPE_SEMI, + final_tinfo, self.structure_name, None) + cdecl = idaapi.asktext(0x10000, '#pragma pack(push, 1)\n' + cdecl, "The following new type will be created") + + if cdecl: + structure_name = idaapi.idc_parse_decl(idaapi.cvar.idati, cdecl, idaapi.PT_TYP)[0] + previous_ordinal = idaapi.get_type_ordinal(idaapi.cvar.idati, structure_name) + + if previous_ordinal: + reply = QtGui.QMessageBox.question( + None, + "HexRaysPyTools", + "Structure already exist. Do you want to overwrite it?", + QtGui.QMessageBox.Yes | QtGui.QMessageBox.No + ) + if reply == QtGui.QMessageBox.Yes: + idaapi.del_numbered_type(idaapi.cvar.idati, previous_ordinal) + ordinal = idaapi.idc_set_local_type(previous_ordinal, cdecl, idaapi.PT_TYP) + else: + return + else: + ordinal = idaapi.idc_set_local_type(-1, cdecl, idaapi.PT_TYP) + if ordinal: + print "[Info] New type {0} was added to Local Types".format(structure_name) + tid = idaapi.import_type(idaapi.cvar.idati, -1, structure_name) + if tid: + tinfo = idaapi.create_typedef(structure_name) + ptr_tinfo = idaapi.tinfo_t() + ptr_tinfo.create_ptr(tinfo) + for scanned_var in self.get_unique_scanned_variables(origin): + scanned_var.apply_type(ptr_tinfo) + return tinfo + else: + print "[ERROR] Structure {0} probably already exist".format(structure_name) + + def have_member(self, member): + if self.items: + idx = bisect.bisect_left(self.items, member) + if idx < self.rowCount(): + return self.items[idx] == member + return False + + def have_collision(self, row): + return self.collisions[row] + + def refresh_collisions(self): + self.collisions = [False for _ in xrange(len(self.items))] + if (len(self.items)) > 1: + curr = 0 + while curr < len(self.items): + if self.items[curr].enabled: + break + curr += 1 + next = curr + 1 + while next < len(self.items): + if self.items[next].enabled: + if self.items[curr].offset + self.items[curr].size > self.items[next].offset: + self.collisions[curr] = True + self.collisions[next] = True + if self.items[curr].offset + self.items[curr].size < self.items[next].offset + self.items[next].size: + curr = next + else: + curr = next + next += 1 + + def add_row(self, member): + if not self.have_member(member): + bisect.insort(self.items, member) + self.refresh_collisions() + self.modelReset.emit() + + def get_unique_scanned_variables(self, origin=0): + scan_objects = itertools.chain.from_iterable( + [list(item.scanned_variables) for item in self.items if item.origin == origin]) + return dict(((item.function_name, item.name), item) for item in scan_objects).values() + + def get_next_enabled(self, row): + row += 1 + while row < self.rowCount(): + if self.items[row].enabled: + return row + row += 1 + return None + + def calculate_array_size(self, row): + next_row = self.get_next_enabled(row) + if next_row: + return (self.items[next_row].offset - self.items[row].offset) / self.items[row].size + return 0 + + def get_recognized_shape(self, start=0, stop=-1): + if not self.items: + return None + result = [] + if stop != -1: + base = self.items[start].offset + enabled_items = filter(lambda x: x.enabled, self.items[start:stop]) + else: + base = 0 + enabled_items = filter(lambda x: x.enabled, self.items) + offsets = set(map(lambda x: x.offset, enabled_items)) + if not enabled_items: + return + min_size = enabled_items[-1].offset + enabled_items[-1].size - base + tinfo = idaapi.tinfo_t() + for ordinal in xrange(1, idaapi.get_ordinal_qty(idaapi.cvar.idati)): + tinfo.get_numbered_type(idaapi.cvar.idati, ordinal) + if tinfo.is_udt() and tinfo.get_size() >= min_size: + is_found = False + for offset in offsets: + is_found = False + items = filter(lambda x: x.offset == offset, enabled_items) + potential_members = Helper.get_fields_at_offset(tinfo, offset - base) + for item in items: + for potential_member in potential_members: + if item.type_equals_to(potential_member): + is_found = True + break + if is_found: + break + if not is_found: + break + if is_found: + result.append((ordinal, idaapi.tinfo_t(tinfo))) + chooser = MyChoose( + [[str(x), "0x{0:08X}".format(y.get_size()), y.dstr()] for x, y in result], + "Select Structure", + [["Ordinal", 5], ["Size", 10], ["Structure name", 50]] + ) + idx = chooser.Show(modal=True) + if idx != -1: + return result[idx][1] + return None + + @staticmethod + def get_padding_member(offset, size): + udt_member = idaapi.udt_member_t() + if size == 1: + udt_member.name = "gap_{0:X}".format(offset) + udt_member.type = Const.BYTE_TINFO + udt_member.size = Const.BYTE_TINFO.get_size() + udt_member.offset = offset + return udt_member + + array_data = idaapi.array_type_data_t() + array_data.base = 0 + array_data.elem_type = Const.BYTE_TINFO + array_data.nelems = size + tmp_tinfo = idaapi.tinfo_t() + tmp_tinfo.create_array(array_data) + + udt_member.name = "gap_{0:X}".format(offset) + udt_member.type = tmp_tinfo + udt_member.size = size + udt_member.offset = offset + return udt_member + + # SLOTS # + + def finalize(self): + if self.pack(): + self.clear() + + def disable_rows(self, indices): + for idx in indices: + if self.items[idx.row()].enabled: + self.items[idx.row()].set_enabled(False) + self.refresh_collisions() + self.modelReset.emit() + + def enable_rows(self, indices): + for idx in indices: + if not self.items[idx.row()].enabled: + self.items[idx.row()].enabled = True + self.refresh_collisions() + self.modelReset.emit() + + def set_origin(self, indices): + if indices: + self.main_offset = self.items[indices[0].row()].offset + self.modelReset.emit() + + def make_array(self, indices): + if indices: + self.items[indices[0].row()].switch_array_flag() + self.dataChanged.emit(indices[0], indices[0]) + + def pack_substructure(self, indices): + if indices: + indices = sorted(indices) + self.dataChanged.emit(indices[0], indices[-1]) + start, stop = indices[0].row(), indices[-1].row() + 1 + tinfo = self.pack(start, stop) + if tinfo: + offset = self.items[start].offset + self.items = self.items[0:start] + self.items[stop:] + self.add_row(Member(offset, tinfo, None)) + + def unpack_substructure(self, indices): + + if indices is None or len(indices) != 1: + return + + item = self.items[indices[0].row()] + if item.tinfo is not None and item.tinfo.is_udt(): + + self.remove_items(indices) + offset = item.offset + udt_data = idaapi.udt_type_data_t() + if item.tinfo.get_udt_details(udt_data): + for udt_item in udt_data: + member = Member(offset + udt_item.offset / 8, udt_item.type, None) + member.name = udt_item.name + self.add_row(member) + + def resolve_types(self): + current_item = None + current_item_score = 0 + + for item in self.items: + if not item.enabled: + continue + + if current_item is None: + current_item = item + current_item_score = current_item.score + continue + + item_score = item.score + if current_item.has_collision(item): + if item_score <= current_item_score: + item.set_enabled(False) + continue + elif item_score > current_item_score: + current_item.set_enabled(False) + + current_item = item + current_item_score = item_score + + self.refresh_collisions() + self.modelReset.emit() + + def remove_items(self, indices): + rows = map(lambda x: x.row(), indices) + if rows: + self.items = [item for item in self.items if self.items.index(item) not in rows] + self.modelReset.emit() + + def clear(self): + self.items = [] + self.main_offset = 0 + self.modelReset.emit() + + def recognize_shape(self, indices): + min_idx = max_idx = None + if indices: + min_idx, max_idx = min(indices), max(indices, key=lambda x: (x.row(), x.column())) + + if min_idx == max_idx: + tinfo = self.get_recognized_shape() + if tinfo: + tinfo.create_ptr(tinfo) + for scanned_var in self.get_unique_scanned_variables(origin=0): + scanned_var.apply_type(tinfo) + self.clear() + else: + # indices = sorted(indices) + start, stop = min_idx.row(), max_idx.row() + 1 + base = self.items[start].offset + tinfo = self.get_recognized_shape(start, stop) + if tinfo: + ptr_tinfo = idaapi.tinfo_t() + ptr_tinfo.create_ptr(tinfo) + for scanned_var in self.get_unique_scanned_variables(base): + scanned_var.apply_type(ptr_tinfo) + self.items = filter(lambda x: x.offset < base or x.offset >= base + tinfo.get_size(), self.items) + self.add_row(Member(base, tinfo, None)) + + def activated(self, index): + # Double click on offset, opens window with variables + if index.column() == 0: + item = self.items[index.row()] + scanned_variables = list(item.scanned_variables) + variable_chooser = MyChoose( + map(lambda x: x.to_list(), scanned_variables), + "Select Variable", + [["Origin", 4], ["Function name", 25], ["Variable name", 25], ["Expression address", 10]] + ) + row = variable_chooser.Show(modal=True) + if row != -1: + idaapi.open_pseudocode(scanned_variables[row].expression_address, 0) + + # Double click on type. If type is virtual table than opens windows with virtual methods + elif index.column() == 1: + self.items[index.row()].activate() diff --git a/plugins/HexRaysPyTools/Core/VariableScanner.py b/plugins/HexRaysPyTools/Core/VariableScanner.py new file mode 100644 index 0000000..524b4c2 --- /dev/null +++ b/plugins/HexRaysPyTools/Core/VariableScanner.py @@ -0,0 +1,339 @@ +import logging +import idaapi +import idc +import Const +import Helper +import TemporaryStructure +import HexRaysPyTools.Api as Api + +logger = logging.getLogger(__name__) + +# If disabled then recursion will be triggered only for variable passed as first argument to function +SETTING_SCAN_ALL_ARGUMENTS = True + +# Global set which is populated when deep scanning and cleared after completion +scanned_functions = set() +debug_scan_tree = [] + + +class ScannedObject(object): + def __init__(self, name, expression_address, origin, applicable=True): + """ + :param name: Object name + :param expression_address: ea_t + :param origin: which offset had structure at scan moment + :param applicable: whether to apply type after creating structure + """ + self.name = name + self.expression_address = expression_address + self.func_ea = idc.get_func_attr(self.expression_address, idc.FUNCATTR_START) + self.origin = origin + self._applicable = applicable + + @property + def function_name(self): + return idaapi.get_short_name(self.func_ea) + + def apply_type(self, tinfo): + """ Finally apply Class'es tinfo to this variable """ + raise NotImplemented + + @staticmethod + def create(obj, expression_address, origin, applicable): + """ Creates suitable instance of ScannedObject depending on obj """ + if obj.id == Api.SO_GLOBAL_OBJECT: + return ScannedGlobalObject(obj.ea, obj.name, expression_address, origin, applicable) + elif obj.id == Api.SO_LOCAL_VARIABLE: + return ScannedVariableObject(obj.lvar, obj.name, expression_address, origin, applicable) + elif obj.id in (Api.SO_STRUCT_REFERENCE, Api.SO_STRUCT_POINTER): + return ScannedStructureMemberObject(obj.struct_name, obj.offset, expression_address, origin, applicable) + else: + raise AssertionError + + def to_list(self): + """ Creates list that is acceptable to MyChoose2 viewer """ + return [ + "0x{0:04X}".format(self.origin), + self.function_name, + self.name, + Helper.to_hex(self.expression_address) + ] + + def __eq__(self, other): + return self.func_ea == other.func_ea and self.name == other.name and \ + self.expression_address == other.expression_address + + def __hash__(self): + return hash((self.func_ea, self.name, self.expression_address)) + + def __repr__(self): + return "{} : {}".format(self.name, Helper.to_hex(self.expression_address)) + + +class ScannedGlobalObject(ScannedObject): + def __init__(self, obj_ea, name, expression_address, origin, applicable=True): + super(ScannedGlobalObject, self).__init__(name, expression_address, origin, applicable) + self.__obj_ea = obj_ea + + def apply_type(self, tinfo): + if self._applicable: + idaapi.set_tinfo2(self.__obj_ea, tinfo) + + +class ScannedVariableObject(ScannedObject): + def __init__(self, lvar, name, expression_address, origin, applicable=True): + super(ScannedVariableObject, self).__init__(name, expression_address, origin, applicable) + self.__lvar = idaapi.lvar_locator_t(lvar.location, lvar.defea) + + def apply_type(self, tinfo): + if not self._applicable: + return + + hx_view = idaapi.open_pseudocode(self.func_ea, -1) + if hx_view: + logger.debug("Applying tinfo to variable {0} in function {1}".format(self.name, self.function_name)) + # Finding lvar of new window that have the same name that saved one and applying tinfo_t + lvar = filter(lambda x: x == self.__lvar, hx_view.cfunc.get_lvars()) + if lvar: + logger.debug("Successful") + hx_view.set_lvar_type(lvar[0], tinfo) + else: + logger.warn("Failed to find previously scanned local variable {} from {}".format( + self.name, Helper.to_hex(self.expression_address))) + + +class ScannedStructureMemberObject(ScannedObject): + def __init__(self, struct_name, struct_offset, name, expression_address, origin, applicable=True): + super(ScannedStructureMemberObject, self).__init__(name, expression_address, origin, applicable) + self.__struct_name = struct_name + self.__struct_offset = struct_offset + + def apply_type(self, tinfo): + if self._applicable: + logger.warn("Changing type of structure field is not yet implemented. Address - {}".format( + Helper.to_hex(self.expression_address))) + + +class SearchVisitor(Api.ObjectVisitor): + def __init__(self, cfunc, origin, obj, temporary_structure): + super(SearchVisitor, self).__init__(cfunc, obj, None, True) + self.__origin = origin + self.__temporary_structure = temporary_structure + + def _manipulate(self, cexpr, obj): + super(SearchVisitor, self)._manipulate(cexpr, obj) + + if obj.tinfo and not Helper.is_legal_type(obj.tinfo): + logger.warn("Variable obj.name has weird type at {}".format(Helper.to_hex(self._find_asm_address(cexpr)))) + return + if cexpr.type.is_ptr(): + member = self.__extract_member_from_pointer(cexpr, obj) + else: + member = self.__extract_member_from_xword(cexpr, obj) + if member: + logger.debug("\tCreating member with type {}, {}, offset - {}".format( + member.type_name, member.scanned_variables, member.offset)) + self.__temporary_structure.add_row(member) + + def _get_member(self, offset, cexpr, obj, tinfo=None, obj_ea=None): + if offset < 0: + logger.error("Considered to be imposible: offset - {}, obj - {}".format( + offset, Helper.to_hex(self._find_asm_address(cexpr)))) + raise AssertionError + + applicable = not self.crippled + cexpr_ea = self._find_asm_address(cexpr) + scan_obj = ScannedObject.create(obj, cexpr_ea, self.__origin, applicable) + if obj_ea: + if TemporaryStructure.VirtualTable.check_address(obj_ea): + return TemporaryStructure.VirtualTable(offset, obj_ea, scan_obj, self.__origin) + if Helper.is_code_ea(obj_ea): + cfunc = Api.decompile_function(obj_ea) + if cfunc: + tinfo = cfunc.type + tinfo.create_ptr(tinfo) + else: + tinfo = Const.DUMMY_FUNC + return TemporaryStructure.Member(offset, tinfo, scan_obj, self.__origin) + # logger.warn("Want to see this ea - {},".format(Helper.to_hex(cexpr_ea))) + + if not tinfo or tinfo.equals_to(Const.VOID_TINFO) or tinfo.equals_to(Const.CONST_VOID_TINFO): + return TemporaryStructure.VoidMember(offset, scan_obj, self.__origin) + + if tinfo.equals_to(Const.CHAR_TINFO): + return TemporaryStructure.VoidMember(offset, scan_obj, self.__origin, char=True) + + if tinfo.equals_to(Const.CONST_PCHAR_TINFO): + tinfo = Const.PCHAR_TINFO + elif tinfo.equals_to(Const.CONST_PVOID_TINFO): + tinfo = Const.PVOID_TINFO + else: + tinfo.clr_const() + return TemporaryStructure.Member(offset, tinfo, scan_obj, self.__origin) + + def _parse_call(self, call_cexpr, arg_cexpr, offset): + _, tinfo = Helper.get_func_argument_info(call_cexpr, arg_cexpr) + if tinfo: + return self.__deref_tinfo(tinfo) + # TODO: Find example with UTF-16 strings + return Const.CHAR_TINFO + + def _parse_left_assignee(self, cexpr, offset): + pass + + def __extract_member_from_pointer(self, cexpr, obj): + parents_type = map(lambda x: idaapi.get_ctype_name(x.cexpr.op), list(self.parents)[:0:-1]) + parents = map(lambda x: x.cexpr, list(self.parents)[:0:-1]) + + logger.debug("Parsing expression {}. Parents - {}".format(obj.name, parents_type)) + + # Extracting offset and removing expression parents making this offset + if parents_type[0] in ('idx', 'add'): + # `obj[idx]' or `(TYPE *) + x' + if parents[0].y.op != idaapi.cot_num: + # There's no way to handle with dynamic offset + return + offset = parents[0].y.numval() * cexpr.type.get_ptrarr_objsize() + cexpr = self.parent_expr() + if parents_type[0] == 'add': + del parents_type[0] + del parents[0] + elif parents_type[0:2] == ['cast', 'add']: + # (TYPE *)obj + offset or (TYPE)obj + offset + if parents[1].y.op != idaapi.cot_num: + return + if parents[0].type.is_ptr(): + size = parents[0].type.get_ptrarr_objsize() + else: + size = 1 + offset = parents[1].theother(parents[0]).numval() * size + cexpr = parents[1] + del parents_type[0:2] + del parents[0:2] + else: + offset = 0 + + return self.__extract_member(cexpr, obj, offset, parents, parents_type) + + def __extract_member_from_xword(self, cexpr, obj): + parents_type = map(lambda x: idaapi.get_ctype_name(x.cexpr.op), list(self.parents)[:0:-1]) + parents = map(lambda x: x.cexpr, list(self.parents)[:0:-1]) + + logger.debug("Parsing expression {}. Parents - {}".format(obj.name, parents_type)) + + if parents_type[0] == 'add': + if parents[0].theother(cexpr).op != idaapi.cot_num: + return + offset = parents[0].theother(cexpr).numval() + cexpr = self.parent_expr() + del parents_type[0] + del parents[0] + else: + offset = 0 + + return self.__extract_member(cexpr, obj, offset, parents, parents_type) + + def __extract_member(self, cexpr, obj, offset, parents, parents_type): + if parents_type[0] == 'cast': + default_tinfo = parents[0].type + cexpr = parents[0] + del parents_type[0] + del parents[0] + else: + default_tinfo = Const.PX_WORD_TINFO + + if parents_type[0] in ('idx', 'ptr'): + if parents_type[1] == 'cast': + default_tinfo = parents[1].type + cexpr = parents[0] + del parents_type[0] + del parents[0] + else: + default_tinfo = self.__deref_tinfo(default_tinfo) + + if parents_type[1] == 'asg': + if parents[1].x == parents[0]: + # *(TYPE *)(var + x) = ??? + obj_ea = self.__extract_obj_ea(parents[1].y) + return self._get_member(offset, cexpr, obj, default_tinfo, obj_ea) + return self._get_member(offset, cexpr, obj, parents[1].x.type) + elif parents_type[1] == 'call': + if parents[1].x == parents[0]: + # ((type (__some_call *)(..., ..., ...)var[idx])(..., ..., ...) + # ((type (__some_call *)(..., ..., ...)*(TYPE *)(var + x))(..., ..., ...) + return self._get_member(offset, cexpr, obj, parents[0].type) + _, tinfo = Helper.get_func_argument_info(parents[1], parents[0]) + if tinfo is None: + tinfo = Const.PCHAR_TINFO + return self._get_member(offset, cexpr, obj, tinfo) + return self._get_member(offset, cexpr, obj, default_tinfo) + + elif parents_type[0] == 'call': + # call(..., (TYPE)(var + x), ...) + tinfo = self._parse_call(parents[0], cexpr, offset) + return self._get_member(offset, cexpr, obj, tinfo) + + elif parents_type[0] == 'asg': + if parents[0].y == cexpr: + # other_obj = (TYPE) (var + offset) + self._parse_left_assignee(parents[1].x, offset) + return self._get_member(offset, cexpr, obj, self.__deref_tinfo(default_tinfo)) + + @staticmethod + def __extract_obj_ea(cexpr): + if cexpr.op == idaapi.cot_ref: + cexpr = cexpr.x + if cexpr.op == idaapi.cot_obj: + if cexpr.obj_ea != idaapi.BADADDR: + return cexpr.obj_ea + + @staticmethod + def __deref_tinfo(tinfo): + if tinfo.is_ptr(): + if tinfo.get_ptrarr_objsize() == 1: + if tinfo.equals_to(Const.PCHAR_TINFO) or tinfo.equals_to(Const.CONST_PCHAR_TINFO): + return Const.CHAR_TINFO + return None # Turns into VoidMember + return tinfo.get_pointed_object() + return tinfo + + +class NewShallowSearchVisitor(SearchVisitor, Api.ObjectDownwardsVisitor): + def __init__(self, cfunc, origin, obj, temporary_structure): + super(NewShallowSearchVisitor, self).__init__(cfunc, origin, obj, temporary_structure) + + +class NewDeepSearchVisitor(SearchVisitor, Api.RecursiveObjectDownwardsVisitor): + def __init__(self, cfunc, origin, obj, temporary_structure): + super(NewDeepSearchVisitor, self).__init__(cfunc, origin, obj, temporary_structure) + + +class DeepReturnVisitor(NewDeepSearchVisitor): + def __init__(self, cfunc, origin, obj, temporary_structure): + super(DeepReturnVisitor, self).__init__(cfunc, origin, obj, temporary_structure) + self.__callers_ea = Helper.get_funcs_calling_address(cfunc.entry_ea) + self.__call_obj = obj + + def _start(self): + for ea in self.__callers_ea: + self._add_scan_tree_info(ea, -1) + assert self.__prepare_scanner() + + def _finish(self): + if self.__prepare_scanner(): + self._recursive_process() + + def __prepare_scanner(self): + try: + cfunc = self.__iter_callers().next() + except StopIteration: + return False + + self.prepare_new_scan(cfunc, -1, self.__call_obj) + return True + + def __iter_callers(self): + for ea in self.__callers_ea: + cfunc = Api.decompile_function(ea) + if cfunc: + yield cfunc diff --git a/plugins/HexRaysPyTools/Core/__init__.py b/plugins/HexRaysPyTools/Core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/plugins/HexRaysPyTools/Cute.py b/plugins/HexRaysPyTools/Cute.py new file mode 100644 index 0000000..23466c5 --- /dev/null +++ b/plugins/HexRaysPyTools/Cute.py @@ -0,0 +1,116 @@ +""" +Cute - a crossQt compatibility module for IDAPython. +Feel free to copy this code into your own projects. +Latest version can be found at https://github.com/tmr232/Cute +The MIT License (MIT) +Copyright (c) 2015 Tamir Bahar +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +""" + +import os +import sys + +import idaapi + +# This nasty piece of code is here to force the loading of IDA's PySide. +# Without it, Python attempts to load PySide from the site-packages directory, +# and failing, as it does not play nicely with IDA. +old_path = sys.path[:] +try: + ida_python_path = os.path.dirname(idaapi.__file__) + sys.path.insert(0, ida_python_path) + if idaapi.IDA_SDK_VERSION >= 690: + from PyQt5 import QtGui, QtCore, QtWidgets + import sip + + QtCore.Signal = QtCore.pyqtSignal + QtCore.Slot = QtCore.pyqtSlot + QtGui.QTreeView = QtWidgets.QTreeView + QtGui.QTableView = QtWidgets.QTableView + QtGui.QAction = QtWidgets.QAction + QtGui.QMenu = QtWidgets.QMenu + QtGui.QLabel = QtWidgets.QLabel + QtGui.QMessageBox = QtWidgets.QMessageBox + + QtGui.QHeaderView = QtWidgets.QHeaderView + QtGui.QAbstractItemView = QtWidgets.QAbstractItemView + QtGui.QVBoxLayout = QtWidgets.QVBoxLayout + QtGui.QHBoxLayout = QtWidgets.QHBoxLayout + QtGui.QGridLayout = QtWidgets.QGridLayout + QtGui.QPushButton = QtWidgets.QPushButton + QtGui.QSpacerItem = QtWidgets.QSpacerItem + QtGui.QSizePolicy = QtWidgets.QSizePolicy + QtGui.QSortFilterProxyModel = QtCore.QSortFilterProxyModel + QtGui.QLineEdit = QtWidgets.QLineEdit + + use_qt5 = True + else: + from PySide import QtGui, QtCore + from PySide import QtGui as QtWidgets + + QtGui.QHeaderView.setSectionResizeMode = QtGui.QHeaderView.setResizeMode + + use_qt5 = False +finally: + sys.path = old_path + + +def connect(sender, signal, callback): + """Connect a signal. + Use this function only in cases where code should work with both Qt5 and Qt4, as it is an ugly hack. + Args: + sender: The Qt object emitting the signal + signal: A string, containing the signal signature (as in Qt4 and PySide) + callback: The function to be called upon receiving the signal + """ + if use_qt5: + return getattr(sender, signal.split('(', 1)[0]).connect(callback) + else: + return sender.connect(QtCore.SIGNAL(signal), callback) + + +def disconnect(sender, signal, callback): + """Disconnect a signal. + Use this function only in cases where code should work with both Qt5 and Qt4, as it is an ugly hack. + Args: + sender: The Qt object emitting the signal + signal: A string, containing the signal signature (as in Qt4 and PySide) + callback: The function to be called upon receiving the signal + """ + if use_qt5: + return getattr(sender, signal.split('(', 1)[0]).disconnect(callback) + else: + return sender.disconnect(QtCore.SIGNAL(signal), callback) + + +def form_to_widget(tform): + """Get the tform's widget. + IDA has two different form-to-widget functions, one for PyQt and one for PySide. + This function uses the relevant one based on the version of IDA you are using. + Args: + tform: The IDA TForm to get the widget from. + """ + class Ctx(object): + QtGui = QtGui + if use_qt5: + QtWidgets = QtWidgets + sip = sip + + if use_qt5: + return idaapi.PluginForm.FormToPyQtWidget(tform, ctx=Ctx()) + else: + return idaapi.PluginForm.FormToPySideWidget(tform, ctx=Ctx()) \ No newline at end of file diff --git a/plugins/HexRaysPyTools/Forms.py b/plugins/HexRaysPyTools/Forms.py new file mode 100644 index 0000000..ebc3cc4 --- /dev/null +++ b/plugins/HexRaysPyTools/Forms.py @@ -0,0 +1,246 @@ +import idaapi +# import PySide.QtGui as QtGui +# import PySide.QtCore as QtCore +from HexRaysPyTools.Cute import * +import Core.Classes + + +class MyChoose(idaapi.Choose2): + def __init__(self, items, title, cols, icon=-1): + idaapi.Choose2.__init__(self, title, cols, flags=idaapi.Choose2.CH_MODAL, icon=icon) + self.items = items + + def OnClose(self): + pass + + def OnGetLine(self, n): + return self.items[n] + + def OnGetSize(self): + return len(self.items) + + +class StructureBuilder(idaapi.PluginForm): + def __init__(self, structure_model): + super(StructureBuilder, self).__init__() + self.structure_model = structure_model + self.parent = None + + def OnCreate(self, form): + self.parent = form_to_widget(form) + self.init_ui() + + def init_ui(self): + self.parent.setStyleSheet( + "QTableView {background-color: transparent; selection-background-color: #87bdd8;}" + "QHeaderView::section {background-color: transparent; border: 0.5px solid;}" + "QPushButton {width: 50px; height: 20px;}" + # "QPushButton::pressed {background-color: #ccccff}" + ) + self.parent.resize(400, 600) + self.parent.setWindowTitle('Structure Builder') + + btn_finalize = QtGui.QPushButton("&Finalize") + btn_disable = QtGui.QPushButton("&Disable") + btn_enable = QtGui.QPushButton("&Enable") + btn_origin = QtGui.QPushButton("&Origin") + btn_array = QtGui.QPushButton("&Array") + btn_pack = QtGui.QPushButton("&Pack") + btn_unpack = QtGui.QPushButton("&Unpack") + btn_remove = QtGui.QPushButton("&Remove") + btn_resolve = QtGui.QPushButton("Resolve") + btn_clear = QtGui.QPushButton("Clear") # Clear button doesn't have shortcut because it can fuck up all work + btn_recognize = QtGui.QPushButton("Recognize Shape") + btn_recognize.setStyleSheet("QPushButton {width: 100px; height: 20px;}") + + btn_finalize.setShortcut("f") + btn_disable.setShortcut("d") + btn_enable.setShortcut("e") + btn_origin.setShortcut("o") + btn_array.setShortcut("a") + btn_pack.setShortcut("p") + btn_unpack.setShortcut("u") + btn_remove.setShortcut("r") + + struct_view = QtGui.QTableView() + struct_view.setModel(self.structure_model) + # struct_view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + + struct_view.verticalHeader().setVisible(False) + struct_view.verticalHeader().setDefaultSectionSize(24) + struct_view.horizontalHeader().setStretchLastSection(True) + struct_view.horizontalHeader().setSectionResizeMode(QtGui.QHeaderView.ResizeToContents) + + grid_box = QtGui.QGridLayout() + grid_box.setSpacing(0) + grid_box.addWidget(btn_finalize, 0, 0) + grid_box.addWidget(btn_enable, 0, 1) + grid_box.addWidget(btn_disable, 0, 2) + grid_box.addWidget(btn_origin, 0, 3) + grid_box.addItem(QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Expanding), 0, 5) + grid_box.addWidget(btn_array, 1, 0) + grid_box.addWidget(btn_pack, 1, 1) + grid_box.addWidget(btn_unpack, 1, 2) + grid_box.addWidget(btn_remove, 1, 3) + grid_box.addWidget(btn_resolve, 0, 4) + grid_box.addItem(QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Expanding), 1, 5) + grid_box.addWidget(btn_recognize, 0, 6) + grid_box.addWidget(btn_clear, 1, 6) + + vertical_box = QtGui.QVBoxLayout() + vertical_box.addWidget(struct_view) + vertical_box.addLayout(grid_box) + self.parent.setLayout(vertical_box) + + btn_finalize.clicked.connect(lambda: self.structure_model.finalize()) + btn_disable.clicked.connect(lambda: self.structure_model.disable_rows(struct_view.selectedIndexes())) + btn_enable.clicked.connect(lambda: self.structure_model.enable_rows(struct_view.selectedIndexes())) + btn_origin.clicked.connect(lambda: self.structure_model.set_origin(struct_view.selectedIndexes())) + btn_array.clicked.connect(lambda: self.structure_model.make_array(struct_view.selectedIndexes())) + btn_pack.clicked.connect(lambda: self.structure_model.pack_substructure(struct_view.selectedIndexes())) + btn_unpack.clicked.connect(lambda: self.structure_model.unpack_substructure(struct_view.selectedIndexes())) + btn_remove.clicked.connect(lambda: self.structure_model.remove_items(struct_view.selectedIndexes())) + btn_resolve.clicked.connect(lambda: self.structure_model.resolve_types()) + btn_clear.clicked.connect(lambda: self.structure_model.clear()) + btn_recognize.clicked.connect(lambda: self.structure_model.recognize_shape(struct_view.selectedIndexes())) + struct_view.activated[QtCore.QModelIndex].connect(self.structure_model.activated) + self.structure_model.dataChanged.connect(struct_view.clearSelection) + + def OnClose(self, form): + pass + + def Show(self, caption=None, options=0): + return idaapi.PluginForm.Show(self, caption, options=options) + + +class StructureGraphViewer(idaapi.GraphViewer): + def __init__(self, title, graph): + idaapi.GraphViewer.__init__(self, title) + self.graph = graph + self.nodes_id = {} + + def OnRefresh(self): + self.Clear() + self.nodes_id.clear() + for node in self.graph.get_nodes(): + self.nodes_id[node] = self.AddNode(node) + for first, second in self.graph.get_edges(): + self.AddEdge(self.nodes_id[first], self.nodes_id[second]) + return True + + def OnGetText(self, node_id): + return self.graph.local_types[self[node_id]].name_and_color + + def OnHint(self, node_id): + """ Try-catch clause because IDA sometimes attempts to use old information to get hint """ + try: + ordinal = self[node_id] + return self.graph.local_types[ordinal].hint + except KeyError: + return + + def OnDblClick(self, node_id): + self.change_selected([self[node_id]]) + + def change_selected(self, ordinals): + self.graph.change_selected(ordinals) + self.Refresh() + self.Select(self.nodes_id[ordinals[0]]) + + +class ClassViewer(idaapi.PluginForm): + def __init__(self): + super(ClassViewer, self).__init__() + self.parent = None + self.class_tree = QtGui.QTreeView() + self.line_edit_filter = QtGui.QLineEdit() + + self.action_collapse = QtGui.QAction("Collapse all", self.class_tree) + self.action_expand = QtGui.QAction("Expand all", self.class_tree) + self.action_set_arg = QtGui.QAction("Set First Argument Type", self.class_tree) + self.action_rollback = QtGui.QAction("Rollback", self.class_tree) + self.action_refresh = QtGui.QAction("Refresh", self.class_tree) + self.action_commit = QtGui.QAction("Commit", self.class_tree) + + self.menu = QtGui.QMenu(self.parent) + + self.proxy_model = Core.Classes.ProxyModel() + + def OnCreate(self, form): + # self.parent = self.FormToPySideWidget(form) + self.parent = form_to_widget(form) + self.init_ui() + + def init_ui(self): + self.parent.setWindowTitle('Classes') + self.parent.setStyleSheet( + # "QTreeView::item:!has-children { background-color: #fefbd8; border: 0.5px solid lightgray ;}" + # "QTreeView::item:has-children { background-color: #80ced6; border-top: 1px solid black ;}" + # "QTreeView::item:selected { background-color: #618685; show-decoration-selected: 1;}" + "QTreeView {background-color: transparent; }" + "QHeaderView::section {background-color: transparent; border: 1px solid;}" + ) + + hbox_layout = QtGui.QHBoxLayout() + label_filter = QtGui.QLabel("&Filter:") + label_filter.setBuddy(self.line_edit_filter) + hbox_layout.addWidget(label_filter) + hbox_layout.addWidget(self.line_edit_filter) + + class_model = Core.Classes.TreeModel() + self.proxy_model.setSourceModel(class_model) + self.proxy_model.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) + self.class_tree.setModel(self.proxy_model) + self.class_tree.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.class_tree.expandAll() + self.class_tree.header().setStretchLastSection(True) + self.class_tree.header().setSectionResizeMode(QtGui.QHeaderView.ResizeToContents) + self.class_tree.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection) + + self.action_collapse.triggered.connect(self.class_tree.collapseAll) + self.action_expand.triggered.connect(self.class_tree.expandAll) + self.action_set_arg.triggered.connect( + lambda: class_model.set_first_argument_type( + map(self.proxy_model.mapToSource, self.class_tree.selectedIndexes()) + ) + ) + self.action_rollback.triggered.connect(lambda: class_model.rollback()) + self.action_refresh.triggered.connect(lambda: class_model.refresh()) + self.action_commit.triggered.connect(lambda: class_model.commit()) + class_model.refreshed.connect(self.class_tree.expandAll) + + self.menu.addAction(self.action_collapse) + self.menu.addAction(self.action_expand) + self.menu.addAction(self.action_refresh) + self.menu.addAction(self.action_set_arg) + self.menu.addAction(self.action_rollback) + self.menu.addAction(self.action_commit) + + vertical_box = QtGui.QVBoxLayout() + vertical_box.addWidget(self.class_tree) + vertical_box.addLayout(hbox_layout) + self.parent.setLayout(vertical_box) + + self.class_tree.activated[QtCore.QModelIndex].connect( + lambda x: class_model.open_function(self.proxy_model.mapToSource(x)) + ) + self.class_tree.customContextMenuRequested[QtCore.QPoint].connect(self.show_menu) + self.line_edit_filter.textChanged[str].connect(self.proxy_model.set_regexp_filter) + # proxy_model.rowsInserted[object].connect(lambda: self.class_tree.setExpanded(object, True)) + + def OnClose(self, form): + pass + + def Show(self, caption=None, options=0): + return idaapi.PluginForm.Show(self, caption, options=options) + + def show_menu(self, point): + self.action_set_arg.setEnabled(True) + indexes = map( + self.proxy_model.mapToSource, + filter(lambda x: x.column() == 0, self.class_tree.selectedIndexes()) + ) + if len(indexes) > 1: + if filter(lambda x: len(x.internalPointer().children) > 0, indexes): + self.action_set_arg.setEnabled(False) + self.menu.exec_(self.class_tree.mapToGlobal(point)) diff --git a/plugins/HexRaysPyTools/Settings.py b/plugins/HexRaysPyTools/Settings.py new file mode 100644 index 0000000..9a3141a --- /dev/null +++ b/plugins/HexRaysPyTools/Settings.py @@ -0,0 +1,57 @@ +import os +import ConfigParser +import idc + +import logging + +CONFIG_FILE_PATH = os.path.join(idc.GetIdaDirectory(), 'cfg', 'HexRaysPyTools.cfg') + +DEBUG_MESSAGE_LEVEL = logging.INFO +# Whether propagate names (Propagate name feature) through all names or only defaults like v11, a3, this, field_4 +PROPAGATE_THROUGH_ALL_NAMES = False +# Store Xref information in database. I don't know how much size it consumes yet +STORE_XREFS = True +# There're some types that can be pointers to structures like int, PVOID etc and by default plugin scans only them +# Full list can be found in `Const.LEGAL_TYPES`. +# But if set this option to True than variable of every type could be possible to scan +SCAN_ANY_TYPE = False + + +def add_default_settings(config): + updated = False + if not config.has_option("DEFAULT", "DEBUG_MESSAGE_LEVEL"): + print DEBUG_MESSAGE_LEVEL + config.set(None, 'DEBUG_MESSAGE_LEVEL', str(DEBUG_MESSAGE_LEVEL)) + updated = True + if not config.has_option("DEFAULT", "PROPAGATE_THROUGH_ALL_NAMES"): + config.set(None, 'PROPAGATE_THROUGH_ALL_NAMES', str(PROPAGATE_THROUGH_ALL_NAMES)) + updated = True + if not config.has_option("DEFAULT", "STORE_XREFS"): + config.set(None, 'STORE_XREFS', str(STORE_XREFS)) + updated = True + if not config.has_option("DEFAULT", "SCAN_ANY_TYPE"): + config.set(None, 'SCAN_ANY_TYPE', str(SCAN_ANY_TYPE)) + updated = True + + if updated: + try: + with open(CONFIG_FILE_PATH, "w") as f: + config.write(f) + except IOError: + print "[ERROR] Failed to write or update config file at {}. Default settings will be used instead.\n" \ + "Consider running IDA Pro under administrator once".format(CONFIG_FILE_PATH) + + +def load_settings(): + global DEBUG_MESSAGE_LEVEL, PROPAGATE_THROUGH_ALL_NAMES, STORE_XREFS, SCAN_ANY_TYPE + + config = ConfigParser.ConfigParser() + if os.path.isfile(CONFIG_FILE_PATH): + config.read(CONFIG_FILE_PATH) + + add_default_settings(config) + + DEBUG_MESSAGE_LEVEL = config.getint("DEFAULT", 'DEBUG_MESSAGE_LEVEL') + PROPAGATE_THROUGH_ALL_NAMES = config.getboolean("DEFAULT", 'PROPAGATE_THROUGH_ALL_NAMES') + STORE_XREFS = config.getboolean("DEFAULT", 'STORE_XREFS') + SCAN_ANY_TYPE = config.getboolean("DEFAULT", 'SCAN_ANY_TYPE') diff --git a/plugins/HexRaysPyTools/__init__.py b/plugins/HexRaysPyTools/__init__.py new file mode 100644 index 0000000..0545fcc --- /dev/null +++ b/plugins/HexRaysPyTools/__init__.py @@ -0,0 +1,7 @@ +import logging +import Settings + +Settings.load_settings() +logging.basicConfig(format='[%(levelname)s] %(message)s\t(%(module)s:%(funcName)s)') +logging.root.setLevel(Settings.DEBUG_MESSAGE_LEVEL) + diff --git a/plugins/HexraysInvertIf32.dll b/plugins/HexraysInvertIf32.dll deleted file mode 100644 index ccbd6c5..0000000 Binary files a/plugins/HexraysInvertIf32.dll and /dev/null differ diff --git a/plugins/HexraysInvertIf64.dll b/plugins/HexraysInvertIf64.dll deleted file mode 100644 index f36e1d7..0000000 Binary files a/plugins/HexraysInvertIf64.dll and /dev/null differ