''' This module defines the generic Type interface for different ABIs.
'''
import idaapi
import idautils
import idc
import pickle
from .utils import *
if VTABLE_ABI == "ITANIUM":
from .itanium import ItaniumTypeInfo as TypeInfo
from .itanium import ItaniumVTableGroup as VTableGroup
else:
raise RuntimeError("Unsupported vtable ABI")
# from .msvc import MSVCTypeInfo as TypeInfo
# from .msvc import MSVCVtable as VTable
[docs]def tables_from_names():
''' Yields addresses of VtableGroups if binary is not stripped
'''
for n in idautils.Names():
seg = idaapi.getseg(n[0])
if seg is None or seg.type != idaapi.SEG_DATA:
continue
if is_vtable_name(n[1]) is True:
yield n[0]
[docs]def tables_from_heuristics(require_rtti=False):
''' Yields addresses of VTableGroups found via heuristic methods
'''
for s in idautils.Segments():
seg = idaapi.getseg(s)
if seg is None:
continue
if seg.type != idaapi.SEG_DATA:
continue
ea = seg.startEA
while ea < seg.endEA:
try:
table = VTableGroup(ea)
if require_rtti is True and ea.typeinfo is not None:
yield ea
elif require_rtti is False:
yield ea
ea += table.size
except ValueError:
# Assume vtables are aligned
ea += TARGET_ADDRESS_SIZE
[docs]def type_matching_typeinfo(types, typeinfo):
''' Get the type in ``types`` that is associated with ``typeinfo``.
'''
if typeinfo is None:
return None
for type in types:
if type.typeinfo is None:
continue
if type.typeinfo.ea == typeinfo.ea:
return type
return None
[docs]def save_type_info():
''' Save the current state/relationships between types. This
essentially 'saves' the Devirtualize plugin.
'''
netnode()["saved_types"] = pickle.dumps(Types())
[docs]def Types(regenerate=False):
''' Returns a memoized list of Type objects for this binary
'''
def add_parents(types, typeinfo):
for parent in typeinfo.parents:
existing_type = type_matching_typeinfo(types, parent)
if existing_type:
continue
else:
types.append(Type(None, parent))
add_parents(types, parent)
def generate_type_relations(types):
# Generate the type relationships
for type in types:
if type.typeinfo is not None:
parents = [type_matching_typeinfo(types, p)
for p in type.typeinfo.parents]
else:
parents = parents_from_destructors(type)
for p in parents:
p.children.append(type)
type.parents.append(p)
if regenerate is False and "saved_types" in netnode() and Types.cache is None:
Types.cache = pickle.loads(netnode()["saved_types"])
elif regenerate is True or Types.cache is None:
Types.cache = []
for table_ea in tables_from_heuristics():
tablegroup = VTableGroup(table_ea)
existing_type = type_matching_typeinfo(Types.cache, tablegroup.typeinfo)
if existing_type:
existing_type.tablegroup = tablegroup
else:
Types.cache.append(Type(tablegroup, tablegroup.typeinfo))
if tablegroup.typeinfo:
add_parents(Types.cache, tablegroup.typeinfo)
generate_type_relations(Types.cache)
save_type_info()
return Types.cache
Types.cache = None
[docs]def get_type_by_name(name):
''' Returns any type object matching ``name``
'''
for t in Types():
if t.name == name:
return t
return None
[docs]def get_type_by_func(ea):
''' Returns a Type with ``ea`` in its vtable. If there are multiple
such types, the least derived type is returned (or the 1st found, if
the multiple types have no known inheritance relationship).
'''
res = None
for t in Types():
if t.tablegroup is None:
continue
for table in t.tablegroup.tables:
for func in table.functions:
if func == ea and (res is None or t.is_ancestor_of(res)):
res = t
return res
[docs]def get_type_by_tinfo(tinfo):
''' Returns the Type that has a struct with the associated ``tinfo``
'''
while tinfo.remove_ptr_or_array():
continue
for t in Types():
if t.tinfo == tinfo:
return t
return None
#TODO:
# 1. Consider inlined destructors (or children of abstract types)
# 2. Multiple inheritance
[docs]def parents_from_destructors(type):
''' Finds the direct parents of the Type associated with ``tablegroup`` by
examining function calls in its destructor.
'''
def get_type_having_destructor(func_ea):
for type in Types():
if func_ea in type.destructors():
return type
return None
class destructor_finder_t(idaapi.ctree_visitor_t):
def __init__(self, ea):
idaapi.ctree_visitor_t.__init__(self, idaapi.CV_FAST)
def visit_expr(self, e):
if e.op == idaapi.cot_call:
# Destructors only take 1 arg
if len(e.a) != 1:
return 0
elif e.a[0].v is None or e.a[0].v.idx != 0:
return 0
addr = e.x.obj_ea
type = get_type_having_destructor(addr)
if type is None:
return 0
parents.append(type)
return 0
elif e.op == idaapi.cot_asg:
pass
return 0
def leave_expr(self, e):
if e.op == idaapi.cot_call:
self.destructor_candidate = None
destructors = type.destructors()
if len(destructors) == 0:
return []
#TODO: consider other candidates
destructor = destructors[0]
parents = []
try:
cfunc = idaapi.decompile(destructor);
except idaapi.DecompilationFailure:
return []
iff = destructor_finder_t(destructor)
iff.apply_to(cfunc.body, None)
return parents
[docs]class Type(object):
''' This is the fundamental type in ``Devirtualize``. `Type` is a flexible
representation of a type that existed during compilation. Such a type may be
discovered via RTTI or the presence of a TableGroup.
'''
def __init__(self, tablegroup=None, typeinfo=None):
if tablegroup is None and typeinfo is None:
raise ValueError("Either 'tablegroup' or 'typeinfo' must be non-None")
#: Handle to the TableGroup backing this type (if any)
self.tablegroup = tablegroup
#: Handle to the RTTI typeinfo for this type (if any)
self.typeinfo = typeinfo
#: A list of this type's parents in inheritance order
self.parents = []
#: A list of this type's children
self.children = []
self.struct = None
if self.typeinfo is None:
if self.tablegroup is not None:
self._name = "type_{:02x}".format(self.tablegroup.ea)
else:
self._name = "type_{:02x}".format(id(self))
else:
if self.typeinfo.name is None:
self._name = "type_{:02x}".format(self.typeinfo.ea)
else:
self._name = demangle(self.typeinfo.name)
def __eq__(self, other):
if self.tablegroup is not None:
if other.tablegroup is None:
return False
return self.tablegroup.ea == other.tablegroup.ea
else:
if other.typeinfo is None:
return False
return self.typeinfo.ea == other.typeinfo.ea
@property
def ancestors(self):
''' A tree of the parent types for this type (and their parents, etc).
For a heirarchy like this::
A B
\ /
C D
\ /
E
E's ancestors will be the nested dictionaries::
{
C: {
A: {},
B: {}
},
D: {}
}
.. warning::
Remember that dictionary traversal is not ordered, so the first item
in the ancestors dictionary is not necessarily the first parent in
the parents list.
'''
ancestors = {}
for p in self.parents:
ancestors[p] = p.ancestors
return ancestors
[docs] def is_descendant_of(self, other):
''' Returns True if ``other`` is a direct or indirect parent of this Type.
'''
for p in self.parents:
if p == other or p.is_descendant_of(other):
return True
return False
@property
def descendants(self):
''' A tree of the child types for this type (and their children, etc).
For a heirarchy like this::
A
/ \
B C
/ / \
D E F
A's descendants will be the nested dictionaries::
{
B: {
D: {},
},
C: {
E: {},
F: {}
}
}
'''
descendants = {}
for c in self.children:
descendants[c] = c.descendants
return descendants
[docs] def is_ancestor_of(self, other):
''' Returns True if ``other`` is a direct or indirect child of this Type.
'''
for c in self.children:
if c == other or c.is_ancestor_of(other):
return True
return False
@property
def family(self):
''' The set of Types that are the direct/indirect children and parents
of this Type, as well as the children and parents of those types, recursively.
'''
#TODO: memoize this
# There really shouldn't be loops in this topology,
# but the user could add one, so lets just assume
# they're allowed.
open_set = set([self])
closed_set = set([])
while len(open_set) > 0:
type = open_set.pop()
closed_set.add(type)
for c in type.children:
if c not in closed_set:
open_set.add(c)
for p in type.parents:
if p not in closed_set:
open_set.add(p)
return closed_set
def _refs_to_tablegroup(self):
from itertools import chain
if self.tablegroup is None:
return []
candidates = []
# For now just use the first table array
primary_table = self.tablegroup.primary_table()
# When debug symbols are present, the decompile will usually
# refer to the function table as an offset from the start
# of the vtable, so also allow references to that.
references = chain(idautils.XrefsTo(primary_table.address_point),
idautils.XrefsTo(self.tablegroup.ea))
for ref in references:
start = as_signed(idc.GetFunctionAttr(ref.frm, idc.FUNCATTR_START),
TARGET_ADDRESS_SIZE)
if start == -1:
continue
candidates.append(start)
return candidates
[docs] def constructors(self):
''' A list of constructors associated with this type. The list will
be empty for types not backed by tablegroups.
'''
if self.tablegroup is None:
return []
candidates = self._refs_to_tablegroup()
return [c for c in candidates
if c not in self.tablegroup.primary_table().functions]
[docs] def destructors(self):
''' A list of destructors associated with this type. The list will
be empty for types not backed by tablegroups.
'''
if self.tablegroup is None:
return []
candidates = self._refs_to_tablegroup()
return [c for c in candidates
if c in self.tablegroup.primary_table().functions]
@property
def name(self):
return self._name
@name.setter
def name(self, newname):
#TODO: rename struct
self._name = newname
@property
def tinfo(self):
tinfo = idaapi.tinfo_t()
tinfo.get_named_type(idaapi.cvar.idati, self.name)
return tinfo
[docs] def table_for_cast(self, parent):
''' Finds the table that would be used for virtual function
lookups if this type was cast to 'parent'.
'''
def traverse_heirarchy(tree, target):
if len(tree.parents) == 0:
return (1, False)
total = 0
found = False
for type in tree.parents:
if type == target:
return (total, True)
count, found = traverse_heirarchy(type, target)
total += count
if found is True:
break
return (total, found)
if self.tablegroup is None:
return None
total, found = traverse_heirarchy(self, parent)
if found is False:
return None
return self.tablegroup.tables[total]
[docs] def build_struct(self):
''' Creates an IDA structure for this Type.
'''
if self.struct is not None:
return
for p in self.parents:
p.build_struct()
self.struct = idc.AddStrucEx(-1, self.name, 0)
if as_signed(self.struct, TARGET_ADDRESS_SIZE) == -1:
raise RuntimeError("Unable to make struct `{}`".format(self.name))
else:
#TODO: either come up with another way of showing this, or
# sync it with the actual function names
cmt = "constructors: "
for c in self.constructors():
cmt += "{}(0x{:02x}), ".format(idc.Name(c), c)
cmt = cmt.strip(", ")
idaapi.set_struc_cmt(self.struct, cmt, False)
if TARGET_ADDRESS_SIZE == 8:
mask = idc.FF_QWRD
else:
mask = idc.FF_DWRD
# Only bases get the magic _vptr member
if len(self.parents) == 0:
idc.AddStrucMember(self.struct,
"_vptr",
0,
idc.FF_DATA|mask,
-1,
TARGET_ADDRESS_SIZE)
idc.SetType(idc.GetMemberId(self.struct, 0),
"_vfunc**");
for i, parent in enumerate(self.parents):
try:
#TODO: for non-itanium ABI, this may not be available
# when RTTI is disabled
offset = self.tablegroup.tables[i].offset_to_top
except:
break
idc.AddStrucMember(self.struct,
"parent_{}".format(i),
-offset,
idc.FF_DATA,
-1,
idc.GetStrucSize(parent.struct))
idc.SetType(idc.GetMemberId(self.struct, -offset),
parent.name);
def __str__(self):
return self.name
def __repr__(self):
return str(self)
[docs]def fixup_this_arg_types(cfunc):
''' Modifies a cfuncptr_t such that its first argument is a pointer
to the Type that has this cfunc in its vtable (and is named 'this')
'''
# Don't do anything if the type has already been set
if idc.GetType(cfunc.entry_ea) is not None:
return
t = get_type_by_func(cfunc.entry_ea)
if t is None:
return
tinfo = t.tinfo
tinfo.create_ptr(tinfo)
#TODO: add missing this argument?
if len(cfunc.arguments) == 0:
return
cfunc.arguments[0].set_lvar_type(tinfo)
cfunc.arguments[0].name = "this"
cfunc.get_func_type(tinfo)
idaapi.set_tinfo2(cfunc.entry_ea, tinfo)
def build_types():
sid = idc.AddStrucEx(-1, "_vfunc", 0)
if sid != -1:
sptr = idaapi.get_struc(sid)
idaapi.set_struc_hidden(sptr, True)
for t in Types():
t.build_struct()
save_type_info()