Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support IDA 8.4 changes #101

Merged
merged 3 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/change_watcher_plugin/bs_change_watcher/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from pathlib import Path

from libbs.artifacts import Typedef
from libbs.plugin_installer import LibBSPluginInstaller

__version__ = "0.0.1"
Expand Down Expand Up @@ -28,7 +29,7 @@ def create_plugin(*args, **kwargs):
# register the callback for all the types we want to print
deci.artifact_change_callbacks = {
typ: [decompiler_printer] for typ in (
FunctionHeader, StackVariable, Enum, Struct, GlobalVariable, Comment, Context
FunctionHeader, StackVariable, Enum, Struct, GlobalVariable, Comment, Typedef, Context
)
}

Expand Down
2 changes: 1 addition & 1 deletion libbs/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__version__ = "1.18.4"
__version__ = "1.19.0"


import logging
Expand Down
12 changes: 12 additions & 0 deletions libbs/api/decompiler_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -691,6 +691,18 @@ def enum_changed(self, enum: Enum, deleted=False, **kwargs) -> Enum:

return lifted_enum

def typedef_changed(self, typedef: Typedef, deleted=False, **kwargs) -> Typedef:
kwargs["deleted"] = deleted
lifted_typedef = self.art_lifter.lift(typedef)
for callback_func in self.artifact_change_callbacks[Typedef]:
args = (lifted_typedef,)
if self._thread_artifact_callbacks:
threading.Thread(target=callback_func, args=args, kwargs=kwargs, daemon=True).start()
else:
callback_func(*args, **kwargs)

return lifted_typedef

def global_variable_changed(self, gvar: GlobalVariable, **kwargs) -> GlobalVariable:
lifted_gvar = self.art_lifter.lift(gvar)
for callback_func in self.artifact_change_callbacks[GlobalVariable]:
Expand Down
125 changes: 116 additions & 9 deletions libbs/decompilers/ida/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

import libbs
from libbs.artifacts import (
Struct, FunctionHeader, FunctionArgument, StackVariable, Function, GlobalVariable, Enum, Artifact, Context, Typedef
Struct, FunctionHeader, FunctionArgument, StackVariable, Function, GlobalVariable, Enum, Artifact, Context, Typedef,
StructMember
)

from PyQt5.Qt import QObject
Expand All @@ -37,6 +38,7 @@
idaapi.BWN_FUNCS: "functions",
idaapi.BWN_STRUCTS: "structs",
idaapi.BWN_ENUMS: "enums",
idaapi.BWN_TILIST: "types",
}

FUNC_FORMS = {"decompilation", "disassembly"}
Expand Down Expand Up @@ -150,7 +152,82 @@ def set_func_ret_type(ea, return_type_str):
return True

#
# Data Type Converters
# Types
#


@execute_write
def get_types(structs=True, enums=True, typedefs=True) -> typing.Dict[str, Artifact]:
types = {}
idati = idaapi.get_idati()

for ord_num in range(ida_typeinf.get_ordinal_qty(idati)):
tif = ida_typeinf.tinfo_t()
success = tif.get_numbered_type(idati, ord_num)
if not success:
continue

is_typedef, name, type_name = typedef_info(tif, use_new_check=True)
# must check typedefs first, since they can be structs
if is_typedef:
if typedefs:
types[name] = Typedef(name, type_name)
continue

if structs and tif.is_struct():
bs_struct = bs_struct_from_tif(tif)
types[bs_struct.name] = bs_struct
elif enums and tif.is_enum():
# TODO: implement this for 9.0
_l.warning("Enums are not supported in this API until IDA 9.0")

return types


@execute_write
def get_ord_to_type_names() -> typing.Dict[int, typing.Tuple[str, typing.Type[Artifact]]]:
idati = idaapi.get_idati()
ord_to_name = {}
for ord_num in range(ida_typeinf.get_ordinal_qty(idati)):
tif = ida_typeinf.tinfo_t()
success = tif.get_numbered_type(idati, ord_num)
if not success:
continue

type_name = tif.get_type_name()
if tif.is_typedef():
type_type = Typedef
elif tif.is_struct():
type_type = Struct
elif tif.is_enum():
type_type = Enum
else:
type_type = None

if type_name:
ord_to_name[ord_num] = (type_name, type_type)

return ord_to_name


def get_ida_type(ida_ord=None, name=None):
tif = ida_typeinf.tinfo_t()
idati = idaapi.get_idati()
if ida_ord is not None:
success = tif.get_numbered_type(idati, ida_ord)
if not success:
return None
elif name is not None:
success = tif.get_named_type(idati, name)
if not success:
return None
else:
return None

return tif

#
# Type Converters
#

@execute_write
Expand Down Expand Up @@ -716,17 +793,46 @@ def ida_get_frame(func_addr):
# IDA Struct r/w
#

def bs_struct_from_tif(tif):
if not tif.is_struct():
return None

size = tif.get_size()
name = tif.get_type_name()

# get members
members = {}
if size > 0:
udt_data = ida_typeinf.udt_type_data_t()
if tif.get_udt_details(udt_data):
for udt_memb in udt_data:
offset = udt_memb.offset
m_name = udt_memb.name
m_type = udt_memb.type
type_name = m_type.get_type_name() or str(m_type)
m_size = m_type.get_size()
members[offset] = StructMember(name=m_name, type_=type_name, size=m_size, offset=offset)

return Struct(name=name, size=size, members=members)


@execute_write
def structs():
_structs = {}
for struct_item in idautils.Structs():
idx, sid, name = struct_item[:]
sptr = ida_struct.get_struc(sid)
size = ida_struct.get_struc_size(sptr)
_structs[name] = Struct(name, size, {})

dec_version = get_decompiler_version()
if dec_version < Version("8.4"):
# TODO: in IDA 9, we will deprecate this block of code
_structs = {}
for struct_item in idautils.Structs():
idx, sid, name = struct_item[:]
sptr = ida_struct.get_struc(sid)
size = ida_struct.get_struc_size(sptr)
_structs[name] = Struct(name, size, {})
else:
_structs = get_types(structs=True, enums=False, typedefs=False)

return _structs


@execute_write
def struct(name):
sid = ida_struct.get_struc_id(name)
Expand Down Expand Up @@ -1195,6 +1301,7 @@ def get_decompiler_version() -> typing.Optional[Version]:

return vers


def view_to_bs_context(view, get_var=True) -> typing.Optional[Context]:
form_type = idaapi.get_widget_type(view)
if form_type is None:
Expand Down
53 changes: 50 additions & 3 deletions libbs/decompilers/ida/hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import functools
import logging
from typing import TYPE_CHECKING
from packaging.version import Version

from PyQt5 import QtCore
from PyQt5.QtGui import QKeyEvent
Expand All @@ -41,7 +42,7 @@
from . import compat
from libbs.artifacts import (
FunctionHeader, StackVariable,
Comment, GlobalVariable, Enum, Struct, Context
Comment, GlobalVariable, Enum, Struct, Context, Typedef, StructMember
)


Expand Down Expand Up @@ -132,8 +133,54 @@ def __init__(self, interface):
self.interface: "IDAInterface" = interface
self._seen_function_prototypes = {}

@while_should_watch
def local_types_changed(self):
def bs_type_deleted(self, ordinal):
old_name, old_type = self.interface.cached_ord_to_type_names[ordinal]
if old_type == Struct:
self.interface.struct_changed(Struct(old_name, -1, members={}), deleted=True)
elif old_type == Enum:
self.interface.enum_changed(Enum(old_name, members={}), deleted=True)
elif old_type == Typedef:
self.interface.typedef_changed(Typedef(nam=old_name), deleted=True)

del self.interface.cached_ord_to_type_names[ordinal]

def local_types_changed(self, ltc, ordinal, name):
# this can't be a decorator for this function due to how IDA implements these overrides
if not self.interface.should_watch_artifacts():
return 0

tif = compat.get_ida_type(ida_ord=ordinal, name=name)
# was the type deleted?
if tif is None:
if ltc == ida_idp.LTC_DELETED and ordinal in self.interface.cached_ord_to_type_names:
self.bs_type_deleted(ordinal)

return 0

# was the type renamed?
if ordinal in self.interface.cached_ord_to_type_names:
old_name, _ = self.interface.cached_ord_to_type_names[ordinal]
if old_name != name:
self.bs_type_deleted(ordinal)

# at this point, the type is either completely new or renamed from an existing type.
# in either case we need to just gather the new info and trigger an update
#
# check if it's a typedef first since this these can also trigger strucs
is_typedef, name, type_name = compat.typedef_info(tif, use_new_check=True)
new_type_type = None
if is_typedef:
self.interface.typedef_changed(Typedef(nam=name, type_=type_name))
new_type_type = Typedef
elif tif.is_struct():
bs_struct = compat.bs_struct_from_tif(tif)
self.interface.struct_changed(bs_struct)
new_type_type = Struct
elif tif.is_enum():
# TODO: handle enum changes in IDA 9
pass

self.interface.cached_ord_to_type_names[ordinal] = (name, new_type_type)
return 0

@while_should_watch
Expand Down
21 changes: 18 additions & 3 deletions libbs/decompilers/ida/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ def __init__(self, **kwargs):
self._artifact_watcher_hooks = []
self._gui_active_context = None
self._deleted_artifacts = defaultdict(set)
self.cached_ord_to_type_names = {}

super().__init__(
name="ida", qt_version="PyQt5", artifact_lifter=IDAArtifactLifter(self),
Expand Down Expand Up @@ -61,6 +62,13 @@ def _init_gui_hooks(self):
def _init_gui_plugin(self, *args, **kwargs):
return compat.GenericIDAPlugin(*args, name=self._plugin_name, interface=self, **kwargs)

@property
def dec_version(self):
if self._dec_version is None:
self._dec_version = compat.get_decompiler_version()

return self._dec_version

#
# GUI
#
Expand Down Expand Up @@ -198,8 +206,16 @@ def fast_get_function(self, func_addr) -> Optional[Function]:

def start_artifact_watchers(self):
super().start_artifact_watchers()
# TODO: this is a hack for backwards compatibility and should be removed in IDA 9
idb_hook = IDBHooks(self)
if self.dec_version < Version("8.4"):
idb_hook.local_types_changed = lambda: 0
else:
# this code in this block must exist in 9.0, so don't delete it!
self.cached_ord_to_type_names = compat.get_ord_to_type_names()

self._artifact_watcher_hooks = [
IDBHooks(self),
idb_hook,
# this hook is special because it relies on the decompiler being present, which can only be checked
# after the plugin loading phase. this means the user will need to manually init this hook in the UI
# either through scripting or a UI.
Expand Down Expand Up @@ -290,9 +306,8 @@ def _global_vars(self, **kwargs) -> Dict[int, GlobalVariable]:

# structs
def _set_struct(self, struct: Struct, header=True, members=True, **kwargs) -> bool:
self._dec_version = compat.get_decompiler_version() if self._dec_version is None else self._dec_version
data_changed = False
if (self._dec_version is not None and self._dec_version < Version("8.3")) and "gcc_va_list" in struct.name:
if (self.dec_version < Version("8.3")) and "gcc_va_list" in struct.name:
_l.critical(f"Syncing the struct {struct.name} in IDA Pro 8.2 <= will cause a crash. Skipping...")
return False

Expand Down
Loading