├── README.md ├── img ├── after.png ├── before.png └── example.png └── plugins └── make_class.py /README.md: -------------------------------------------------------------------------------- 1 | # ClassMaker 2 | A simple IDA plugin to make classes automatically 3 | 4 | 5 | #### Disclaimer 6 | 7 | Don't watch the sources if you don't want your eyes to bleed, this is originally just a script I wrote in a week while learning (debugging and reversing the code source) IDA python for my own personal use, just wanted to share it in case someone would have a use for it. 8 | 9 | If more than me and my colleagues use it, maybe I'll gather the courage to clean the code and document it. 10 | 11 | --- 12 | 13 | ## How it works 14 | 15 | This script uses the pseudocode of IDA to create the structure and reconstruct the vtable(s) that corresponds to the class you're looking at. It means it'll work on every architucture your IDA supports. It's dumb, it just follows the address of the vtable, make the computation if the adress is relative to an offset, then goes 4 by 4 or 8 by 8 depending on your architecture (yes, so far it only supports 32/64, I had no use for something else, but it's 2 lines to add for more, so just ask if I don't have the need for it before) and check if the address in question is the start of a function. It stores everything, create a struct in ida with a random name, and so on until all the vtables in your constructor has been reconstructed. 16 | 17 | It then does some simple "create struct type" that doesn't work as well as create struct type (but there is no possibility to use create struct type in IDA python, I asked, and what a hell it is to re-do a full create struct type in ida python), guess the name of the class thanks to the last assignement in the first vtable, and voilà. If the heuristic for the name appears to be wrong, you're free to change it yourself. I mostly made this script to reconstruct the vtables and gain some time during the reverse of my target. 18 | 19 | I do not believe in current "auto class makers" that tries to reconstruct the whole binary. By essence, it relies in heuristics, heuristics that can fails, and I rather take a bit more time doing it myself than lose a week on a pseudocode I trusted too much. Hence why don't hope for any upgrades pointing that way. I count on people better than me for that. 20 | 21 | ## Limitations 22 | 23 | * The code doesn't dive into the functions inside the constructor. 24 | * I know and I'll update it at some point, if you encounter a constructor that substract an offset from a reference to get to its vtable (`vt0 = refVtable - 2`), it won't work, I don't handle sub asg. Should be very rare though. 25 | * Some member assignations and assignations only made in parameters of functions within the constructor won't be recognized. If there is too much of them, you can use "create new struct type" prior to my script, call the structure the same way my script would call the class, and then apply the plugin. It will keep the existing structure and just edit it. 26 | * Do not work on class constructed into the stack, the variable must be a pointer. 27 | 28 | ## How to use 29 | 30 | Once the script is loaded, locate the variable that will hold the first vtable of your class, click on it, and then press '4'. Click away on the script so IDA refreshes the GUI, and it should be done. If the script asks you for a class name, you either have no symbols whatsoever, or most likely, it failed. 31 | 32 | Before the use of the plugin, note where the cursor his, make sure to make it point on the variable that holds the vtable. 33 | 34 | ![Before use](img/before.png) 35 | 36 | And then you press '4' (or change it within the script for whatever you want) and the class is made. 37 | 38 | ![After use](img/after.png) 39 | ![An example](img/example.png) 40 | 41 | 42 | ## TODO 43 | 44 | * Cross ref every constructor calls to propagate the class everywhere it should be propagated. 45 | * Try to find a reliable way to handle automatically classes within classes. Maybe use that iteration to dive into sub functions also for the class members reconstruction. 46 | 47 | --- 48 | ### Last word 49 | 50 | As it relies on the pseudocode, I may not have thought about all the kind of assignations IDA can produce, and I may not have encounter all of them myself. Feel free to get in touch if you have an IDB that has a case that I don't handle, either by doing a PR or by contacting me through my twitter: https://twitter.com/ShiroPycatchown. 51 | 52 | This is some work that I share for free, please be polite if you have a request as I don't owe anyone anything. That being said I hope it'll make your life easier for some of you as it does for me during my C++ reversing. 53 | -------------------------------------------------------------------------------- /img/after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pycatchown/ClassMaker/f69f7dc67c0f67287d70e0fe31921e2dde057cba/img/after.png -------------------------------------------------------------------------------- /img/before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pycatchown/ClassMaker/f69f7dc67c0f67287d70e0fe31921e2dde057cba/img/before.png -------------------------------------------------------------------------------- /img/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pycatchown/ClassMaker/f69f7dc67c0f67287d70e0fe31921e2dde057cba/img/example.png -------------------------------------------------------------------------------- /plugins/make_class.py: -------------------------------------------------------------------------------- 1 | import idaapi 2 | import ida_ida 3 | import ida_funcs 4 | import idc 5 | import ida_kernwin 6 | import ida_hexrays 7 | import ida_lines 8 | import ida_typeinf 9 | import re 10 | import uuid 11 | 12 | IS_64 = False if ida_ida.inf_is_32bit_exactly() else True #8.4 reliquat 13 | SIZEOF_PTR = 8 if IS_64 else 4 14 | 15 | def read_ptr(ea): 16 | if IS_64: 17 | return idaapi.get_qword(ea) 18 | return idaapi.get_dword(ea) 19 | 20 | def get_type(size): 21 | if size == 1: 22 | return "byte" 23 | elif size == 2: 24 | return "word" 25 | elif size == 4: 26 | return "dword" 27 | elif size == 8: 28 | return "qword" 29 | else: 30 | return get_type(SIZEOF_PTR) 31 | 32 | def get_idasize_from_size(size): 33 | if size == 1: 34 | return idaapi.FF_BYTE 35 | elif size == 2: 36 | return idaapi.FF_WORD 37 | elif size == 4: 38 | return idaapi.FF_DWORD 39 | elif size == 8: 40 | return idaapi.FF_QWORD 41 | else: 42 | return idaapi.DT_TYPE 43 | 44 | def pretty(insn): 45 | print(f"{insn.ea:x}: {insn.opname}") 46 | 47 | 48 | class ClassConstructor(ida_hexrays.ctree_visitor_t): 49 | 50 | def __init__(self, idx = 0, struct = {}): 51 | ida_hexrays.ctree_visitor_t.__init__(self, ida_hexrays.CV_FAST) 52 | 53 | self.idx = idx 54 | self.name_class = "" 55 | self.struct = struct 56 | 57 | def visit_insn(self, ins): 58 | if ins.cexpr: 59 | #self.dump_cexpr(ins.cexpr.ea, ins.cexpr) 60 | if ins.cexpr.op == ida_hexrays.cot_asg: 61 | self.build_struct_fromasg(ins.cexpr) 62 | elif ins.cexpr.op == ida_hexrays.cot_call: 63 | pass#self.build_struct_fromcall(ins.cexpr) 64 | 65 | return 0 66 | 67 | def make_vftable(self, vftable): 68 | vftable_struct_id = idc.get_struc_id(vftable["name"]) 69 | if vftable_struct_id == idaapi.BADADDR: 70 | vftable_struct_id = idc.add_struc(0, vftable["name"], 0) 71 | if vftable_struct_id == idaapi.BADADDR: 72 | print(vftable) 73 | for i in range(len(vftable["methods"])): 74 | suffix = "" 75 | duplicate = 1 76 | while idc.add_struc_member(vftable_struct_id, vftable["methods"][i]["name_func"] + suffix, i * SIZEOF_PTR, get_idasize_from_size(SIZEOF_PTR), -1, SIZEOF_PTR) != 0: 77 | suffix = f"_{duplicate}" 78 | duplicate += 1 79 | proto = idc.get_type(vftable['methods'][i]['ea_func']) 80 | if proto == None: #no typing decl 81 | proto = f"void __thiscall(*{vftable['methods'][i]['name_func']})({self.name_class} *this)" 82 | else: 83 | p = proto.split("(") 84 | print(p) 85 | p[0] += f" (*{vftable['methods'][i]['name_func']})" 86 | args = p[1].split(',') 87 | args[0] = f"{self.name_class} *{')' if len(args) == 1 else ''}" 88 | p[1] = ",".join(args) 89 | proto = "(".join(p) 90 | idc.SetType(vftable['methods'][i]['ea_func'], proto) 91 | idc.SetType(idc.get_member_id(vftable_struct_id, i * SIZEOF_PTR), proto) 92 | return vftable_struct_id 93 | 94 | def make_class_idastruct(self): 95 | if self.name_class == "": 96 | self.name_class = ida_kernwin.ask_str("", 0, "Unfortunately, the class name couldn't be determined, please enter a class name:") 97 | if self.name_class == None: 98 | print("Couldn't determine class name") 99 | return 100 | self.ida_struct_id = idc.get_struc_id(self.name_class) 101 | if self.ida_struct_id == idaapi.BADADDR: 102 | self.ida_struct_id = idc.add_struc(0, self.name_class, 0) 103 | for k, v in self.struct.items(): 104 | idc.add_struc_member(self.ida_struct_id, v["name"], k, get_idasize_from_size(v["size"]), -1, v["size"]) 105 | if (vftable := v.get("vftable")) is not None: 106 | vftable_id = self.make_vftable(vftable) 107 | idc.SetType(idc.get_member_id(self.ida_struct_id, k), f'struct {vftable["name"]} *') 108 | 109 | def get_methods_from_vtable(self, ea): 110 | if ea == idaapi.BADADDR: 111 | return None 112 | methods = [] 113 | while ((addr := read_ptr(ea)) != idaapi.BADADDR): 114 | f = ida_funcs.get_func(addr) 115 | try: 116 | if f == None: 117 | break 118 | except: 119 | pass #stupid fix to prevent none comparison from crashing the script 120 | if addr != f.start_ea: 121 | break 122 | name_func = idaapi.get_ea_name(addr, idaapi.GN_SHORT|idaapi.GN_DEMANGLED).split("(")[0].replace("::", "__").replace("<", "_").replace(">", "_").replace(" ", "_") 123 | name_func = re.sub(r"[^a-zA-Z0-9\_]+",'', name_func) 124 | methods += [{"ea_func":addr, "name_func": name_func}] 125 | ea += SIZEOF_PTR 126 | return methods 127 | 128 | def add_struct_member(self, cexpr, offset, refwidth): 129 | if (cexpr.y and (cexpr.y.op == ida_hexrays.cot_ref or cexpr.y.op == ida_hexrays.cot_obj)) or (cexpr.y and cexpr.y.op == ida_hexrays.cot_add and (cexpr.y.x.op == ida_hexrays.cot_ref or cexpr.y.x.op == ida_hexrays.cot_obj)): 130 | to_add = 0 131 | ref = cexpr.y 132 | if cexpr.y.op == ida_hexrays.cot_add: 133 | ref = cexpr.y.x 134 | to_add = cexpr.y.y.numval() * SIZEOF_PTR 135 | if ref.op == ida_hexrays.cot_ref: 136 | vtable_addr = ref.x.obj_ea + to_add 137 | elif ref.op == ida_hexrays.cot_obj: 138 | vtable_addr = ref.obj_ea + to_add 139 | 140 | methods = self.get_methods_from_vtable(vtable_addr + to_add) # TODO: being careful in case of other refs in class that arn't vtables 141 | name_vtable = "v" + str(uuid.uuid4()).split('-')[0] 142 | while idc.get_struc_id(name_vtable) != idaapi.BADADDR: 143 | name_vtable = "v" + str(uuid.uuid4()).split('-')[0] 144 | self.struct[offset] = {"name": f"vt{offset}", "size": refwidth, "vftable": {"ea": vtable_addr + to_add, "name": f"{name_vtable}", "methods": methods}} 145 | if offset == 0: 146 | self.name_class = idaapi.get_ea_name(vtable_addr, idaapi.GN_SHORT|idaapi.GN_DEMANGLED) 147 | self.name_class = "_".join(re.sub(r"\`.*'",'', self.name_class).split(" ")[1:]).replace("::", "__").replace("<", "_").replace(">", "_").replace(" ", "_").replace("*", "P") 148 | if self.name_class.endswith("::"): 149 | self.name_class = self.name_class[:-2] 150 | 151 | else: 152 | self.struct[offset] = {"name": f"{get_type(refwidth)}{offset}", "size": refwidth} 153 | 154 | def build_struct_fromcall(self, cexpr): 155 | for arg in cexpr.a: 156 | if arg.op == ida_hexrays.cot_add: 157 | if (v := arg.x.find_op(ida_hexrays.cot_var)): 158 | print(v) 159 | if v.v.idx == self.idx: 160 | self.add_struct_member(arg, arg.y.numval(), SIZEOF_PTR) 161 | 162 | 163 | def build_struct_fromasg(self, cexpr): 164 | try: 165 | if cexpr.x: 166 | if cexpr.x.op == ida_hexrays.cot_memptr: 167 | op_idx = cexpr.x.get_ptr_or_array().v.idx 168 | if op_idx != self.idx: 169 | return 170 | self.add_struct_member(cexpr, cexpr.x.m, cexpr.x.refwidth) 171 | elif cexpr.x.op == ida_hexrays.cot_ptr: 172 | cast = cexpr.x.get_ptr_or_array() 173 | if cast.op == ida_hexrays.cot_var: 174 | if cast.v.idx != self.idx: 175 | return 176 | self.add_struct_member(cexpr, 0, cexpr.x.refwidth) 177 | elif cast.x.op == ida_hexrays.cot_var: 178 | if cast.x.v.idx != self.idx: 179 | return 180 | self.add_struct_member(cexpr, 0, cexpr.x.refwidth) 181 | elif cast.x.op == ida_hexrays.cot_add: 182 | if cast.x.x.op == ida_hexrays.cot_cast: 183 | if cast.x.x.x.v.idx != self.idx: 184 | return 185 | self.add_struct_member(cexpr, cast.x.y.numval(), cexpr.x.refwidth) 186 | else: 187 | if cast.x.x.v.idx != self.idx: 188 | return 189 | self.add_struct_member(cexpr, cast.x.y.numval(), cexpr.x.refwidth) 190 | elif cast.x.op == ida_hexrays.cot_cast: 191 | if cast.x.x.v and cast.x.x.v.idx != self.idx: 192 | return 193 | self.add_struct_member(cexpr, cast.y.numval() * cexpr.x.refwidth, cexpr.x.refwidth) 194 | except Exception as e: 195 | print(f"error ({e}) at {cexpr.ea:x}, {cexpr.x.opname}") 196 | 197 | def dump_cexpr(self, ea, cexpr): 198 | # iterate over all block instructions 199 | print("dumping cexpr %x: %s" % (ea, cexpr.opname,)) 200 | if cexpr.x: 201 | if cexpr.x.op == ida_hexrays.cot_idx: 202 | print(f" {cexpr.ea:x}: op {cexpr.x.opname} (var: v{cexpr.x.opname})") 203 | elif cexpr.x.op == ida_hexrays.cot_var: 204 | print(" %x: op %s (var: v%d)" % (cexpr.x.ea, cexpr.x.opname, cexpr.x.v.idx)) 205 | elif cexpr.x.op == ida_hexrays.cot_memptr: 206 | print(" %x: op %s (var: v%d+%d)" % (cexpr.x.ea, cexpr.x.opname, cexpr.x.get_ptr_or_array().v.idx, cexpr.x.m)) 207 | elif cexpr.x.op == ida_hexrays.cot_ptr: 208 | ptr = cexpr.x.get_ptr_or_array() 209 | print(ptr.opname) 210 | if ptr.op == ida_hexrays.cot_var: 211 | print(f" {cexpr.ea:x}: op {cexpr.opname} (var: v{ptr.v.idx})") 212 | elif ptr.x.op == ida_hexrays.cot_var: 213 | print(f" {cexpr.x.ea:x}: op {cexpr.x.opname} (var: v{ptr.x.v.idx})") 214 | elif ptr.x.op == ida_hexrays.cot_add: 215 | print(f" {cexpr.x.ea:x}: op {cexpr.x.opname} (var: v{ptr.x.x.v.idx}+{ptr.x.y.numval()})") 216 | #print(" %x: op %s (var: v%d+%d)" % (cexpr.x.ea, cexpr.x.opname, cexpr.x.get_ptr_or_array().v.idx, cexpr.x.m)) 217 | else: 218 | print(" %x: unknown x op %s" % (cexpr.x.ea, cexpr.x.opname)) 219 | if cexpr.y: 220 | if cexpr.y.op == ida_hexrays.cot_num: 221 | print(" %x: op %s (num: 0x%x)" % (cexpr.y.ea, cexpr.y.opname, cexpr.y.numval())) 222 | else: 223 | print(" %x: unknown y op %s" % (cexpr.y.ea, cexpr.y.opname)) 224 | if cexpr.z: 225 | print("THE OPERAND I HAVE NO CLUE WHAT'S ITS PURPOSE AND NEITHER DO THE DOC : Z FOUND!!!! %x: op %s" % (cexpr.z.ea, cexpr.z.opname)) 226 | 227 | 228 | 229 | def make_class(ea, idx_choosed): 230 | if ida_hexrays.init_hexrays_plugin(): 231 | idx_choosed = idx_choosed 232 | x = ClassConstructor(idx = idx_choosed, struct = {}) 233 | f = ida_funcs.get_func(ea) 234 | cfunc = ida_hexrays.decompile(f) 235 | 236 | tif = idaapi.tinfo_t() 237 | tif.get_named_type(idaapi.get_idati(), "_QWORD" if IS_64 else "_DWORD") 238 | tif.create_ptr(tif) 239 | lst = ida_hexrays.lvar_saved_info_t() 240 | lst.ll = cfunc.lvars[idx_choosed] 241 | lst.type = ida_typeinf.tinfo_t(tif) 242 | ida_hexrays.modify_user_lvar_info(f.start_ea, ida_hexrays.MLI_TYPE, lst) 243 | cfunc = ida_hexrays.decompile(f) 244 | 245 | x.apply_to(cfunc.body, None) 246 | print(x.struct) 247 | 248 | x.make_class_idastruct() 249 | 250 | tif = idaapi.tinfo_t() 251 | tif.get_named_type(idaapi.get_idati(), x.name_class) 252 | tif.create_ptr(tif) 253 | lst = ida_hexrays.lvar_saved_info_t() 254 | lst.ll = cfunc.lvars[idx_choosed] 255 | lst.type = ida_typeinf.tinfo_t(tif) 256 | ida_hexrays.rename_lvar(f.start_ea, cfunc.lvars[idx_choosed].name, "v_" + x.name_class) 257 | ida_hexrays.modify_user_lvar_info(f.start_ea, ida_hexrays.MLI_TYPE, lst) 258 | cfunc = ida_hexrays.decompile(f) 259 | cfunc.refresh_func_ctext() 260 | #idc.SetType(0, f'struct {x.name_class} *') 261 | else: 262 | print('error') 263 | 264 | class CexprFinder(ida_hexrays.ctree_visitor_t): 265 | 266 | def __init__(self, ea): 267 | ida_hexrays.ctree_visitor_t.__init__(self, ida_hexrays.CV_FAST) 268 | 269 | self.ea = ea 270 | self.result = None 271 | 272 | def visit_insn(self, ins): 273 | if ins.cexpr: 274 | if self.result is None and ins.cexpr.ea == self.ea: 275 | self.result = ins.cexpr 276 | return 0 277 | 278 | class MakeClassHandler(idaapi.action_handler_t): 279 | def __init__(self): 280 | idaapi.action_handler_t.__init__(self) 281 | 282 | def activate(self, ctx): 283 | print("activated") 284 | if ctx.cur_ea == idaapi.BADADDR: 285 | print("bad ea") 286 | return 1 287 | 288 | cfunc = ida_hexrays.decompile(ctx.cur_ea) 289 | finder = CexprFinder(ctx.cur_ea) 290 | finder.apply_to(cfunc.body, None) 291 | cexpr = finder.result 292 | 293 | idx = -1 294 | if cexpr.x and cexpr.x.op == ida_hexrays.cot_memptr: 295 | idx = cexpr.x.get_ptr_or_array().v.idx 296 | elif cexpr.x and cexpr.x.op == ida_hexrays.cot_ptr: 297 | cast = cexpr.x.get_ptr_or_array() 298 | if cast.op == ida_hexrays.cot_var: 299 | idx = cast.v.idx 300 | elif cast.x.op == ida_hexrays.cot_var: 301 | idx = cast.x.v.idx 302 | elif cast.x.op == ida_hexrays.cot_add: 303 | idx = cast.x.x.v.idx 304 | elif cast.x.op == ida_hexrays.cot_cast: 305 | idx = cast.x.x.v.idx 306 | 307 | if idx == -1: 308 | print("Couldn't find the variable that points to the class") 309 | return 1 310 | make_class(ctx.cur_ea, idx) 311 | return 1 312 | 313 | def update(self, ctx): 314 | return idaapi.AST_ENABLE_ALWAYS 315 | 316 | action_desc = idaapi.action_desc_t( 317 | 'make:class', 318 | 'Create a class', 319 | MakeClassHandler(), 320 | '4', 321 | 'Make a class' 322 | ) 323 | idaapi.unregister_action('make:class') 324 | idaapi.register_action(action_desc) 325 | --------------------------------------------------------------------------------