model: Import idb

This commit is contained in:
Djordje Todorovic
2022-08-29 14:32:59 +02:00
committed by Alessandro Di Federico
parent 9193618fab
commit a3b44ae65a
4 changed files with 703 additions and 1 deletions
+3 -1
View File
@@ -102,7 +102,9 @@ set(REVNG_CLI_COMMANDS_MODULE_FILES
revng/cli/_commands/opt.py revng/cli/_commands/pipeline_tools.py
revng/cli/_commands/llvm_pipeline.py
revng/cli/_commands/override_by_name.py revng/cli/_commands/daemon.py
revng/cli/_commands/download_pdb.py revng/cli/_commands/hard_purge.py)
revng/cli/_commands/download_pdb.py revng/cli/_commands/hard_purge.py
revng/cli/_commands/import_idb.py revng/cli/_commands/idb_converter.py)
python_module(TARGET_NAME revng-python-cli-commands MODULE_FILES
${REVNG_CLI_COMMANDS_MODULE_FILES})
+3
View File
@@ -27,3 +27,6 @@ psutil
# Requirements for pdb-download
pefile
# revng.import_idb dependencies
python-idb
+591
View File
@@ -0,0 +1,591 @@
#!/usr/bin/env python3
#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
import sys
from typing import Dict, List, Optional, Set, Tuple, Union
import idb
import idb.analysis
import idb.fileformat
import idb.typeinf
import idb.typeinf_flags
import revng.model.v1 as m
from revng.cli.support import log_error
from revng.model.metaaddress import MetaAddressType
RevngTypes = Union[
m.UnionType,
m.StructType,
m.PrimitiveType,
m.EnumType,
m.TypedefType,
m.RawFunctionType,
m.CABIFunctionType,
]
idb_procname_to_revng_arch = {
"x86_64": m.Architecture.x86_64,
"x86": m.Architecture.x86,
"arm": m.Architecture.arm,
"aarch64": m.Architecture.aarch64,
"mips": m.Architecture.mips,
"mipsb": m.Architecture.mips,
"mipsl": m.Architecture.mipsel,
"s390x": m.Architecture.systemz,
}
revng_arch_to_metaaddr_code_type = {
m.Architecture.x86: MetaAddressType.Code_x86,
m.Architecture.x86_64: MetaAddressType.Code_x86_64,
m.Architecture.arm: MetaAddressType.Code_arm,
m.Architecture.aarch64: MetaAddressType.Code_aarch64,
m.Architecture.mips: MetaAddressType.Code_mips,
m.Architecture.mipsel: MetaAddressType.Code_mipsel,
m.Architecture.systemz: MetaAddressType.Code_systemz,
}
revng_arch_to_abi = {
m.Architecture.arm: m.ABI.AAPCS,
m.Architecture.aarch64: m.ABI.AAPCS64,
m.Architecture.x86: m.ABI.SystemV_x86,
m.Architecture.x86_64: m.ABI.SystemV_x86_64,
m.Architecture.mips: m.ABI.SystemV_MIPS_o32,
m.Architecture.mipsel: m.ABI.SystemV_MIPSEL_o32,
m.Architecture.systemz: m.ABI.SystemZ_s390x,
}
revng_arch_to_abiname = {
m.Architecture.arm: "AAPCS",
m.Architecture.aarch64: "AAPCS64",
m.Architecture.x86: "SystemV_x86",
m.Architecture.x86_64: "SystemV_x86_64",
m.Architecture.mips: "SystemV_MIPS_o32",
m.Architecture.mipsel: "SystemV_MIPSEL_o32",
m.Architecture.systemz: "SystemZ_s390x",
}
CONST_QUALIFIER = m.Qualifier(Kind=m.QualifierKind.Const)
class IDBConverter:
def __init__(self, input_idb: idb.fileformat.IDB, base_addr, verbose):
self.idb: idb.fileformat.IDB = input_idb
self.api = idb.IDAPython(self.idb)
self.arch: m.Architecture = self._get_arch()
self.is64bit: bool = self._is_64_bit()
self.segments: List[m.Segment] = []
self.revng_types_by_id: Dict[int, RevngTypes] = {}
self.idb_types_to_revng_types: Dict[int, m.QualifiedType] = {}
self.functions: Set[m.Function] = set()
self.dynamic_functions: List[m.DynamicFunction] = []
self.imported_libraries: List[str] = []
self.base_addr = base_addr
self.verbose = verbose
self._structs_to_fixup: Set[Tuple[m.StructType, idb.typeinf.TInfo]] = set()
self._ordinal_types_to_fixup: Set[Tuple[m.QualifiedType, int]] = set()
self._unions_to_fixup: Set[Tuple[m.UnionType, idb.typeinf.TInfo]] = set()
self._import_types()
self._fixup_structs()
self._fixup_unions()
self._fixup_ordinal_types()
self._import_functions()
self._collect_imports()
def log(self, message):
if self.verbose:
sys.stderr.write(message + "\n")
def _import_types(self):
"""Imports initial types from the IDB. The types will be incomplete and need to be fixed"""
til = self.idb.til
for type_definition in til.types.defs:
assert isinstance(type_definition, idb.typeinf.TILTypeInfo)
the_type = type_definition.type
assert isinstance(the_type, idb.typeinf.TInfo)
self._convert_idb_type_to_revng_type(the_type, ordinal=type_definition.ordinal)
def _import_functions(self):
metaaddr_type = revng_arch_to_metaaddr_code_type[self.arch]
if self.arch == m.Architecture.arm and self.api.idc.ItemSize(0x0) == 2:
# If the instrucitons are 16-bit long, it is a Thumb mode.
metaaddr_type = MetaAddressType.Code_arm_thumb
for function_start_addr in self.api.idautils.Functions():
function = idb.analysis.Function(self.idb, function_start_addr)
function_name = function.get_name()
idb_function_type = function.get_signature()
function_attributes: List[m.FunctionAttribute] = []
if self.api.ida_nalt.is_noret(function_start_addr):
function_attributes.insert(0, m.FunctionAttribute.NoReturn)
function_start_addr += self.base_addr
if idb_function_type is not None:
qualified_revng_function_type = self._convert_idb_type_to_revng_type(
idb_function_type
)
revng_function_type = self.unwrap_qualified(qualified_revng_function_type)
revng_function = m.Function(
OriginalName=function_name,
Entry=m.MetaAddress(Address=function_start_addr, Type=metaaddr_type),
Attributes=function_attributes,
Prototype=m.Reference.create(m.Binary, revng_function_type),
)
else:
self.log(f"warning: Function {function_name} without a signature.")
revng_function = m.Function(
OriginalName=function_name,
Entry=m.MetaAddress(Address=function_start_addr, Type=metaaddr_type),
Attributes=function_attributes,
)
self.functions.add(revng_function)
def _get_arch(self):
inf_structure = self.api.idaapi.get_inf_structure()
procname = inf_structure.procname.lower()
if procname == "arm" and inf_structure.is_64bit():
procname = "aarch64"
elif procname == "metapc" and inf_structure.is_64bit():
procname = "x86_64"
elif procname == "metapc" and inf_structure.is_32bit():
procname = "x86"
return idb_procname_to_revng_arch[procname]
def _is_64_bit(self):
inf_structure = self.api.idaapi.get_inf_structure()
return inf_structure.is_64bit()
def _import_names_helper(self, function_addr, function_name):
function = idb.analysis.Function(self.idb, function_addr)
if not function:
log_error(f"Unable to find function {function_name}.")
return True
function_name = function.get_name()
function_type = None
try:
function_type = function.get_signature()
except Exception as exception:
log_error(f"Unable to parse function type for {function_name}.")
log_error(str(exception))
function_type = None
return True
revng_function_type = None
if function_type is not None:
qualified_revng_function_type = self._convert_idb_type_to_revng_type(function_type)
revng_function_type = self.unwrap_qualified(qualified_revng_function_type)
else:
abi = revng_arch_to_abiname[self.arch]
qualified_return_type = self._get_primitive_type(m.PrimitiveTypeKind.Void, 0)
revng_function_type = m.CABIFunctionType(
OriginalName=function_name,
ABI=abi,
ReturnType=qualified_return_type,
Arguments=[],
)
self.revng_types_by_id[revng_function_type.ID] = revng_function_type
dynamic_function = m.DynamicFunction(
OriginalName=function_name,
Prototype=m.Reference.create(m.Binary, revng_function_type),
)
self.dynamic_functions.append(dynamic_function)
return True
def _collect_imports(self):
# NOTE: If @plt was used, the functions are in Functions() already, but the name starts
# with ".", and we do import types for them during parsing of regular/local functions.
# NOTE: We cannot use api.ida_nalt.get_import_module_qty() here, since it relies
# on `library names`, but IDA prints `.dynsym` as library for each library in case of ELF.
for mod_index in range(self.api.ida_nalt.get_import_module_qty() + 1):
def import_names_callback(function_addr, function_name, ignored_always_none):
assert ignored_always_none is None
return self._import_names_helper(function_addr, function_name)
try:
mod_name = self.api.ida_nalt.get_import_module_name(mod_index)
except KeyError as exception:
# TODO: Due to the bug mentioned bellow, we try sometimes to
# find imported modules that do not exist.
if mod_index != 0:
# It would be very strange if the mod_index is not 0.
log_error(f"Unable to find module with index: {mod_index}.")
log_error(str(exception))
continue
# NOTE: IDA 7 has a bug when reporting imported library names - prints `.dynsym` for
# each import.
if mod_name and mod_name != ".dynsym":
self.imported_libraries.append(mod_name)
self.api.ida_nalt.enum_import_names(mod_index, import_names_callback)
def _fixup_ordinal_types(self):
"""Fixes some types that were referring an ordinal that was not observed yet."""
while self._ordinal_types_to_fixup:
quialified_type, idb_ordinal_type = self._ordinal_types_to_fixup.pop()
assert idb_ordinal_type in self.idb_types_to_revng_types
real_quialified_type = self.idb_types_to_revng_types[idb_ordinal_type]
the_revng_type = self.unwrap_qualified(real_quialified_type)
revng_type_to_fix = self.unwrap_qualified(quialified_type)
# TODO: For now, we found out that structs can be affected by this only.
assert isinstance(the_revng_type, m.StructType) and isinstance(
revng_type_to_fix, m.StructType
)
revng_type_to_fix.Fields = the_revng_type.Fields
revng_type_to_fix.OriginalName = the_revng_type.OriginalName
revng_type_to_fix.Size = the_revng_type.Size
def _fixup_structs(self):
while self._structs_to_fixup:
revng_type, idb_type = self._structs_to_fixup.pop()
assert isinstance(revng_type, m.StructType)
assert idb_type.is_decl_struct()
fields = []
committed_size = 0
# TODO: For now, we are ignoring structs that contain bitfields.
# We should also improve the python-idb package to pickup the struct size directly from
# the IDB files (e.g. to populate the `type.type_details.storage_size` for structs).
for member in idb_type.type_details.members:
if member.type.is_decl_bitfield():
self.log(
f"warning: Ignoring {revng_type.OriginalName} struct that contains a "
"bitfield."
)
return
for member in idb_type.type_details.members:
underlying_type = self._convert_idb_type_to_revng_type(member.type)
revng_member = m.StructField(
OriginalName=member.name,
Type=underlying_type,
Offset=committed_size,
)
member_size = member.type.get_size()
if member_size == 0:
self.log(
f"warning: Dropping zero-sized field {member.name} of struct "
f"{revng_type.OriginalName}."
)
else:
fields.append(revng_member)
committed_size += member_size
revng_type.Fields = fields
revng_type.Size = committed_size
def _fixup_unions(self):
while self._unions_to_fixup:
revng_type, idb_type = self._unions_to_fixup.pop()
assert isinstance(revng_type, m.UnionType)
assert idb_type.is_decl_union()
fields = []
for idx, member in enumerate(idb_type.type_details.members):
qualified_type = self._convert_idb_type_to_revng_type(member.type)
revng_member = m.UnionField(
OriginalName=member.name,
Type=qualified_type,
Index=idx,
)
fields.append(revng_member)
revng_type.Fields = fields
def _convert_idb_type_to_revng_type(
self,
type: idb.typeinf.TInfo, # noqa: A002
ordinal=None,
) -> m.QualifiedType:
"""Converts the given TInfo obtained from python-idb to the corresponding revng
QualifiedType. If available, the integer identifying the type in the IDB (ordinal) should
be supplied to allow handling circular references. If a type with the given ordinal was
already converted the same instance is returned.
"""
assert isinstance(type, idb.typeinf.TInfo)
# Check if we already converted this type, and if so return the existing type.
# Fundamental to handle circular dependencies.
existing_revng_type = self.idb_types_to_revng_types.get(ordinal)
if existing_revng_type is not None:
return existing_revng_type
type_name = type.get_name()
revng_type_qualifiers: List[m.Qualifier] = []
if type.is_decl_typedef():
aliased_type = type.get_final_tinfo()
# IDA's types could be generated/identified in two ways: by ordinal and by names,
# so we handle both ways here.
aliased_type_ordinal = None
if type.type_details.is_ordref:
aliased_type_ordinal = type.type_details.ordinal
else:
aliased_tiltypeinfo = type.til.types.find_by_name(aliased_type.name)
if aliased_tiltypeinfo:
aliased_type_ordinal = aliased_tiltypeinfo.ordinal
qualified_type = self._convert_idb_type_to_revng_type(
aliased_type, ordinal=aliased_type_ordinal
)
revng_type = m.TypedefType(OriginalName=type_name, UnderlyingType=qualified_type)
elif type.is_decl_enum():
revng_underlying_type = self._get_primitive_type(
m.PrimitiveTypeKind.Unsigned, type.type_details.storage_size
)
entries = []
for member in type.type_details.members:
member_value = member.value
size_of_underlying_type_in_bits = type.type_details.storage_size * 8
if member_value >= 2**size_of_underlying_type_in_bits:
self.log(
f"warning: Value {hex(member_value)} for enum member "
f"{type_name}.{member.name} out of range, ignoring it."
)
continue
# TODO: We should keep the user comment which might exist in member.cmt.
enum_entry = m.EnumEntry(
OriginalName=member.name,
Value=member_value,
)
entries.append(enum_entry)
if len(entries) == 0:
self.log(f"warning: An empty enum type: {type_name}.")
revng_type = m.TypedefType(
OriginalName=type_name, UnderlyingType=revng_underlying_type
)
else:
revng_type = m.EnumType(
OriginalName=type_name,
Entries=entries,
UnderlyingType=revng_underlying_type,
)
elif type.is_decl_struct():
if type.type_details.ref is not None and type.type_details.ref.type_details.is_ordref:
if type.type_details.ref.type_details.ordinal not in self.idb_types_to_revng_types:
# Make a placeholder for this, since we did not observe the type yet.
revng_type = m.StructType(OriginalName="", Size=0, Fields=[])
qualified_type = m.QualifiedType(
UnqualifiedType=m.Reference.create(m.Binary, revng_type),
Qualifiers=revng_type_qualifiers,
)
self.revng_types_by_id[revng_type.ID] = revng_type
self._ordinal_types_to_fixup.add(
(qualified_type, type.type_details.ref.type_details.ordinal)
)
return qualified_type
else:
return self.idb_types_to_revng_types[type.type_details.ref.type_details.ordinal]
else:
revng_type = m.StructType(OriginalName=type_name, Size=0, Fields=[])
# Empty structs will be considered as invalid types.
if len(type.type_details.members) == 0:
self.log(
f"warning: Found invalid empty struct {type_name}, replacing with a "
"void* typedef."
)
else:
# Struct members and size will be computed later.
self._structs_to_fixup.add((revng_type, type))
elif type.is_decl_union():
# Union members will be computed later.
revng_type = m.UnionType(OriginalName=type_name, Fields=[])
self._unions_to_fixup.add((revng_type, type))
elif type.is_decl_ptr():
underlying_type = self._convert_idb_type_to_revng_type(type.type_details.obj_type)
revng_type = self.resolve_typeref(underlying_type.UnqualifiedType)
revng_type_qualifiers = list(underlying_type.Qualifiers)
elif type.is_decl_array():
underlying_type = self._convert_idb_type_to_revng_type(type.type_details.elem_type)
revng_type = self.resolve_typeref(underlying_type.UnqualifiedType)
revng_type_qualifiers = list(underlying_type.Qualifiers)
elif type.is_decl_bool():
size = type.get_size()
revng_type = self.unwrap_qualified(
self._get_primitive_type(m.PrimitiveTypeKind.Unsigned, size)
)
elif type.is_decl_int() or type.is_decl_floating():
size = type.get_size()
primitive_kind = get_primitive_kind(type)
revng_type = self.unwrap_qualified(self._get_primitive_type(primitive_kind, size))
elif type.is_decl_void():
primitive_kind = m.PrimitiveTypeKind.Void
size = 0
if type.get_name() != "":
# Treat this case as `typedef void someothername`.
revng_void_type = self._get_primitive_type(primitive_kind, size)
revng_type = m.TypedefType(OriginalName=type_name, UnderlyingType=revng_void_type)
else:
revng_type = self.unwrap_qualified(self._get_primitive_type(primitive_kind, size))
elif type.is_decl_func():
# TODO: handle non C-ABI functions.
# We cannot handle stack arguments at the moment.
assert type.type_details.stkargs is None
idb_return_type = type.get_rettype()
revng_return_type = self._convert_idb_type_to_revng_type(idb_return_type)
arguments = []
for idx, argument in enumerate(type.type_details.args):
argument_qualified_type = self._convert_idb_type_to_revng_type(argument.type)
revng_argument = m.Argument(
Index=idx,
Type=argument_qualified_type,
OriginalName=argument.name,
)
arguments.append(revng_argument)
revng_type = m.CABIFunctionType(
ABI=revng_arch_to_abi[self.arch],
ReturnType=revng_return_type,
Arguments=arguments,
OriginalName=type_name,
)
elif type.is_decl_partial():
# Represents an unknown or void type with a known size.
assert type.get_size() != 0
# The type should be compatible with being a primitive type.
# NOTE: If we find a case where this is not satisifed, we can produce a char[].
assert (
type.get_size() == 1
or type.get_size() == 2
or type.get_size() == 4
or type.get_size() == 8
or type.get_size() == 10
or type.get_size() == 16
)
revng_type = self.unwrap_qualified(
self._get_primitive_type(
m.PrimitiveTypeKind.Generic,
type.get_size(),
)
)
else:
# IDA does not know anything about this type.
# TODO: In some cases we should emit a void type (when the type is always used as a
# pointer).
size = type.get_size()
if size == 0:
revng_type = self.unwrap_qualified(
self._get_primitive_type(m.PrimitiveTypeKind.Void, 0)
)
else:
kind = m.PrimitiveTypeKind.PointerOrNumber
revng_type = self.unwrap_qualified(self._get_primitive_type(kind, size))
existing_revng_type = self.revng_types_by_id.get(revng_type.ID)
if existing_revng_type:
# A type with this ID was already emitted, ensure we are returning the same instance.
assert revng_type is existing_revng_type
qualified_type = m.QualifiedType(
UnqualifiedType=m.Reference.create(m.Binary, revng_type),
Qualifiers=revng_type_qualifiers,
)
if type.is_decl_ptr():
qualified_type.Qualifiers.insert(
0, m.Qualifier(Kind=m.QualifierKind.Pointer, Size=type.get_size())
)
if type.is_decl_array():
n_elements = type.type_details.n_elems
if n_elements == 0:
self.log(f"warning: Array {type_name} has invalid zero size.")
qualified_type.Qualifiers.insert(
0, m.Qualifier(Kind=m.QualifierKind.Array, Size=n_elements)
)
if type.is_decl_const():
qualified_type.Qualifiers.insert(0, CONST_QUALIFIER)
if ordinal is not None:
self.idb_types_to_revng_types[ordinal] = qualified_type
self.revng_types_by_id[revng_type.ID] = revng_type
return qualified_type
def _get_primitive_type(self, kind: m.PrimitiveTypeKind, size: int) -> m.QualifiedType:
"""Gets a primitive type, taking care to register it"""
revng_type = m.PrimitiveType(PrimitiveKind=kind, Size=size)
if revng_type.ID not in self.revng_types_by_id:
self.revng_types_by_id[revng_type.ID] = revng_type
return m.QualifiedType(UnqualifiedType=m.Reference.create(m.Binary, revng_type))
def get_model(self) -> m.Binary:
return m.Binary(
# NOTE: We assume that the EntryPoint can be obtained from binary itself, so we use an
# invalid address for it here.
EntryPoint=m.MetaAddress(Address=0x0, Type=MetaAddressType.Invalid),
Functions=list(self.functions),
ImportedDynamicFunctions=list(self.dynamic_functions),
Types=list(self.revng_types_by_id.values()),
Architecture=self.arch,
Segments=self.segments,
ImportedLibraries=self.imported_libraries,
)
def resolve_typeref(self, typeref: m.Reference) -> Optional[RevngTypes]:
return self.revng_types_by_id.get(typeref.id)
def get_revng_type_by_name(self, name):
for revng_type in self.revng_types_by_id.values():
if revng_type.OriginalName == name:
return revng_type
return None
def unwrap_qualified(self, qt: m.QualifiedType):
if qt.Qualifiers:
raise ValueError("Trying to unwrap qualified type with non empty qualifiers list!")
return self.resolve_typeref(qt.UnqualifiedType)
def get_primitive_kind(idb_type: idb.typeinf.TInfo) -> m.PrimitiveTypeKind:
if idb_type.is_decl_void():
return m.PrimitiveTypeKind.Void
elif idb_type.is_decl_floating():
return m.PrimitiveTypeKind.Float
elif idb.typeinf_flags.is_type_integral(idb_type.get_decltype()):
if idb_type.is_signed():
return m.PrimitiveTypeKind.Signed
elif idb_type.is_unsigned():
return m.PrimitiveTypeKind.Unsigned
else:
return m.PrimitiveTypeKind.Number
elif idb_type.is_decl_ptr():
return m.PrimitiveTypeKind.PointerOrNumber
return m.PrimitiveTypeKind.Generic
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env python3
#
# This file is distributed under the MIT License. See LICENSE.md for details.
#
from tempfile import NamedTemporaryFile
import idb
import yaml
from revng.cli.commands_registry import Command, Options, commands_registry
from revng.cli.revng import run_revng_command
from revng.model import YamlDumper
from .idb_converter import IDBConverter
class ImportIDBCommand(Command):
def __init__(self):
super().__init__(
("model", "import", "idb"), "Extract a rev.ng model from an IDB/I64 database"
)
def register_arguments(self, parser):
parser.add_argument("idb", help="Path to the IDB/I64 file to import")
parser.add_argument(
"--output", "-o", default="/dev/stdout", help="Output filepath (default stdout)"
)
parser.add_argument(
"--base", default=0x0, help="base address where dynamic objects should be loaded"
)
def run(self, options: Options):
idb_path = options.parsed_args.idb
output_path = options.parsed_args.output
# NOTE: This should be used for PIC only.
base_address = options.parsed_args.base
with idb.from_file(idb_path) as db:
idb_converter = IDBConverter(db, base_address, options.parsed_args.verbose)
revng_model = idb_converter.get_model()
yaml_model = yaml.dump(revng_model, Dumper=YamlDumper)
def temporary_file(suffix="", mode="w+"):
return NamedTemporaryFile(suffix=suffix, mode=mode, delete=not options.keep_temporaries)
with temporary_file(suffix=".yml") as model_file:
model_file.write(yaml_model)
model_file.flush()
# Fix the model and do the clean-up.
run_revng_command(
[
"model",
"opt",
"-fix",
model_file.name,
"-o",
model_file.name,
],
options,
)
run_revng_command(
[
"model",
"opt",
"-deduplicate-equivalent-types",
model_file.name,
"-o",
model_file.name,
],
options,
)
run_revng_command(
[
"model",
"opt",
"-promote-original-name",
model_file.name,
"-o",
model_file.name,
],
options,
)
run_revng_command(
[
"model",
"opt",
"-purge-unnamed-and-unreachable-types",
model_file.name,
"-o",
output_path,
],
options,
)
return 0
commands_registry.register_command(ImportIDBCommand())