diff --git a/README.md b/README.md index 7c6e3e5..5f65891 100644 --- a/README.md +++ b/README.md @@ -4,10 +4,16 @@ Phrank helps with structure analysis and function pointers. Phrank works on top of HexRays ctrees. -## Installation +## Installation (IDA 9.x) -1) Copy/link phrank_plugin.py to IDAPRO/plugins/ -2) Copy/link pyphrank and phrank.py to IDAPRO/python/3/ folder +- 사용자 설치(권장): `C:\Users\\AppData\Roaming\Hex-Rays\IDA Pro\plugins` +- 전역 설치(관리자): `C:\Program Files\IDA Pro 9.x\plugins` + +다음 파일/폴더를 같은 `plugins` 폴더 바로 아래에 둡니다. +- `phrank.py` +- `pyphrank/` 전체 폴더 + +참고: IDA 9에서는 `python/3` 경로가 필요하지 않습니다. `phrank.py`가 플러그인 엔트리(PLUGIN_ENTRY)를 제공합니다. ## Capabilities @@ -21,4 +27,18 @@ Phrank helps with structure analysis and function pointers. Phrank works on top ## How to use -There are currently two ways to use phrank: hotkey actions (described [here](https://github.com/Mizari/phrank/wiki/Phrank-plugin-and-actions) and in comments [here](https://github.com/Mizari/phrank/blob/master/phrank_plugin.py)) and phrank api (described in docstring comments [here](https://github.com/Mizari/phrank/blob/master/phrank.py)). +There are two ways to use phrank: +1) Hex-Rays pseudocode popup menu: right-click in pseudocode and use the "Phrank" submenu. +2) Hotkeys and API: hotkeys below, and phrank API documented in `phrank.py` docstrings. + +### Keyboard Shortcuts + +Phrank provides the following keyboard shortcuts: +- `Shift-A`: Analyze item under cursor and its dependencies +- `Alt-T`: Print TypeFlowGraph for variable/function under cursor + +### Context Menu (Hex-Rays) + +In pseudocode view (F5), right-click to open the popup menu: +- Phrank → Analyze item under cursor and its dependencies +- Phrank → Print TypeFlowGraph for variable/function under cursor diff --git a/phrank.py b/phrank.py index 41027cd..a782adc 100644 --- a/phrank.py +++ b/phrank.py @@ -79,7 +79,7 @@ def phrank_help(): funcs = {} modules = {} classes = {} - skips = {"sys", "idaapi", "typing", "ida_struct", "re", "logging", "idautils", "idc", "Any", "utils"} + skips = {"sys", "idaapi", "typing", "re", "logging", "idautils", "idc", "Any", "utils"} for k, v in vars(mod).items(): if k.startswith("__"): continue if k in skips: @@ -99,4 +99,9 @@ def phrank_help(): __help_objects("MODULES", modules) __help_objects("CLASSES", classes) - __help_objects("FUNCTIONS", funcs) \ No newline at end of file + __help_objects("FUNCTIONS", funcs) + + +def PLUGIN_ENTRY(): + """IDA plugin entry point when this file is loaded as a plugin.""" + return get_plugin_instance() \ No newline at end of file diff --git a/phrank_plugin.py b/phrank_plugin.py deleted file mode 100644 index 4a9b59d..0000000 --- a/phrank_plugin.py +++ /dev/null @@ -1,5 +0,0 @@ -import phrank - - -def PLUGIN_ENTRY(): - return phrank.get_plugin_instance() \ No newline at end of file diff --git a/pyphrank/cfunction_factory.py b/pyphrank/cfunction_factory.py index 0ee425e..15a665a 100644 --- a/pyphrank/cfunction_factory.py +++ b/pyphrank/cfunction_factory.py @@ -33,17 +33,18 @@ def get_cfunc(self, func_ea:int) -> idaapi.cfunc_t|None: cfunc = self.cached_cfuncs.get(func_ea) if isinstance(cfunc, idaapi.cfunc_t): return cfunc + # Check if it's our sentinel value (-1) indicating a failed decompilation + if isinstance(cfunc, int) and cfunc == -1: + return None if not settings.DECOMPILE_RECURSIVELY: cfunc = utils.decompile_function(func_ea) # -1 (instead of None) to cache failed decompilation if cfunc is None: - cfunc = -1 - self.cached_cfuncs[func_ea] = cfunc - if cfunc == -1: + self.cached_cfuncs[func_ea] = -1 return None - else: - return cfunc + self.cached_cfuncs[func_ea] = cfunc + return cfunc decompilation_queue = [func_ea] while len(decompilation_queue) != 0: @@ -59,15 +60,17 @@ def get_cfunc(self, func_ea:int) -> idaapi.cfunc_t|None: if len(new_functions_to_decompile) == 0: cfunc = utils.decompile_function(func_ea) if cfunc is None: - cfunc = -1 - self.cached_cfuncs[func_ea] = cfunc + self.cached_cfuncs[func_ea] = -1 + else: + self.cached_cfuncs[func_ea] = cfunc decompilation_queue.pop() else: decompilation_queue += list(new_functions_to_decompile) cfunc = self.cached_cfuncs.get(func_ea) - if cfunc == -1: - cfunc = None + # Check if it's our sentinel value (-1) indicating a failed decompilation + if isinstance(cfunc, int) and cfunc == -1: + return None return cfunc def clear_cfunc(self, func_ea:int) -> None: diff --git a/pyphrank/containers/ida_struc_wrapper.py b/pyphrank/containers/ida_struc_wrapper.py index 4110bf3..e00dea1 100644 --- a/pyphrank/containers/ida_struc_wrapper.py +++ b/pyphrank/containers/ida_struc_wrapper.py @@ -2,7 +2,7 @@ import idaapi import idc -import ida_struct +import ida_typeinf import pyphrank.utils as utils @@ -42,15 +42,20 @@ def get(cls, struc_info:str|idaapi.tinfo_t): @property def sptr(self) -> idaapi.struc_t: + # Note: kept for compatibility; primarily use tinfo_t for IDA 9 APIs return idaapi.get_struc(self.strucid) @property def name(self) -> str: - return idc.get_struc_name(self.strucid, 0) + return idc.get_struc_name(self.strucid) @property def size(self) -> int: - return ida_struct.get_struc_size(self.strucid) + # IDA 9: ida_struct API 제거. tinfo_t를 통해 사이즈 계산 + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return 0 + return t.get_size() @property def tinfo(self) -> idaapi.tinfo_t: @@ -65,53 +70,103 @@ def ptr_tinfo(self) -> idaapi.tinfo_t: return ptr_tinfo def is_union(self) -> bool: - return idaapi.is_union(self.strucid) + # IDA 9: use idc.is_union(tid) + return idc.is_union(self.strucid) def delete(self): if self.strucid == -1: return - - if idc.get_struc_idx(self.strucid) == idaapi.BADADDR: - self.strucid = -1 - return - - idc.del_struc(self.strucid) + # IDA 9: delete via del_named_type + name = idc.get_struc_name(self.strucid) + if name: + ida_typeinf.del_named_type(None, name, ida_typeinf.NTF_TYPE) self.strucid = -1 def get_member_size(self, offset:int) -> int: - return idc.get_member_size(self.strucid, offset) + # IDA 9: use tinfo_t to get member size + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return 0 + udm = idaapi.udm_t() + udm.offset = offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: + return 0 + return udm.size // 8 def get_member_name(self, offset:int) -> str: - return idc.get_member_name(self.strucid, offset) + # IDA 9: read from udm + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return "" + udm = idaapi.udm_t() + udm.offset = offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: + return "" + return udm.name def rename(self, newname:str): - return idc.set_struc_name(self.strucid, newname) + # IDA 9: rename via tinfo + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return False + code = t.rename_type(newname) + return code >= 0 def set_member_comment(self, offset:int, cmt:str): - rv = idc.set_member_cmt(self.strucid, offset, cmt, 0) - if rv == 0: + # IDA 9: set via tinfo_t.set_udm_cmt + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return 0 + udm = idaapi.udm_t() + udm.offset = offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: + return 0 + code = t.set_udm_cmt(idx, cmt) + if code < 0: utils.log_warn(f"failed to set member comment in {self.name} at {hex(offset)}") - return rv + return code def get_member_comment(self, offset:int): - return idc.get_member_cmt(self.strucid, offset, 0) + # IDA 9: read from udm + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return None + udm = idaapi.udm_t() + udm.offset = offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: + return None + return udm.cmt def set_member_name(self, member_offset:int, member_name:str) -> int: - rv = idc.set_member_name(self.strucid, member_offset, member_name) - if rv == 0: + # IDA 9: rename via tinfo_t.rename_udm + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return 0 + udm = idaapi.udm_t() + udm.offset = member_offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: + return 0 + code = t.rename_udm(idx, member_name) + if code < 0: utils.log_warn(f"failed to set member name {str(member_name)} in {self.name} at {hex(member_offset)}") - return rv + return 1 if code >= 0 else 0 def member_offsets(self, skip_holes=True): - sptr = self.sptr - size = self.size - off = ida_struct.get_struc_first_offset(sptr) - while off != idaapi.BADADDR and off < size: - if skip_holes and not self.get_member_name(off): - off = ida_struct.get_struc_next_offset(sptr, off) - else: - yield off - off = ida_struct.get_struc_next_offset(sptr, off) + # IDA 9: UDT 디테일을 통해 멤버 나열 + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return + udt = idaapi.udt_type_data_t() + if not t.get_udt_details(udt): + return + for m in udt: + moff = m.begin() // 8 + yield moff def unset_members(self, offset_from:int, unset_size:int): unset_offsets = [] @@ -123,7 +178,15 @@ def unset_members(self, offset_from:int, unset_size:int): self.del_member(mo) def del_member(self, offset:int): - idc.del_struc_member(self.strucid, offset) + # IDA 9: 멤버 삭제는 tinfo_t.del_udm 사용 + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return + udm = idaapi.udm_t() + udm.offset = offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx != -1: + t.del_udm(idx) def get_member_type(self, member_offset:int) -> idaapi.tinfo_t|None: # TODO add ability to get member by offset or by name @@ -137,39 +200,108 @@ def get_member_type(self, member_offset:int) -> idaapi.tinfo_t|None: if member_offset >= self.size: raise BaseException("Offset too big") - mptr = ida_struct.get_member(self.sptr, member_offset) - # member is unset - if mptr is None: + # IDA 9: tinfo_t.get_udm_by_offset 사용 + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): return None - - tif = idaapi.tinfo_t() - # member has no type - if not ida_struct.get_member_tinfo(tif, mptr): + udm = idaapi.udm_t() + udm.offset = member_offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: return None - return tif + return udm.type def set_member_type(self, member_offset: int, member_type: idaapi.tinfo_t): #if member_type.get_size() != self.get_member_size(member_offset): # self.unset_members(member_offset + self.get_member_size(member_offset), member_type.get_size() - self.get_member_size(member_offset)) - mptr = ida_struct.get_member(self.sptr, member_offset) - rv = ida_struct.set_member_tinfo(self.sptr, mptr, member_offset, member_type, ida_struct.SET_MEMTI_COMPATIBLE | ida_struct.SET_MEMTI_MAY_DESTROY) - if rv == 0: + # IDA 9: tinfo_t.set_udm_type 사용 + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + utils.log_err(f"failed to change member type in {self.name}: bad tid") + return 0 + udm = idaapi.udm_t() + udm.offset = member_offset * 8 + idx = t.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: + utils.log_err(f"failed to change member type in {self.name}: no member at {hex(member_offset)}") + return 0 + code = t.set_udm_type(idx, member_type) + if code < 0: utils.log_err(f"failed to change member type in {self.name} to {str(member_type)} at {hex(member_offset)}") - return rv - return rv + return code def add_member(self, member_offset:int, name=None) -> bool: if name is None: name = "field_" + hex(member_offset)[2:] - ret = idc.add_struc_member(self.strucid, name, member_offset, idaapi.FF_DATA | idaapi.FF_BYTE, -1, 1) - self.handle_addstrucmember_ret(ret) - return ret >= 0 + # IDA 9: add_struc_member 대체: add_udm + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + return False + # ensure struct is not fixed, and has enough size to accept the member + try: + if hasattr(t, 'set_fixed_struct'): + t.set_fixed_struct(False) + except Exception: + pass + needed = member_offset + 1 + curr = t.get_size() + if curr == idaapi.BADSIZE: + curr = 0 + if needed > curr: + t.set_struct_size(needed) + t.save_type() + udm = idaapi.udm_t() + udm.name = name + udm.offset = member_offset * 8 + # 1바이트 배열로 패딩 멤버 생성해 명확한 크기 보장 + arr_tif = idaapi.tinfo_t() + idaapi.parse_decl(arr_tif, idaapi.get_idati(), "unsigned char[1];", 0) + udm.type = arr_tif + udm.size = arr_tif.get_size() * 8 + code = t.add_udm(udm) + if code < 0: + self.handle_addstrucmember_ret(code) + return False + return True def append_member(self, name:str, member_type:idaapi.tinfo_t, member_comment=None): - size = member_type.get_size() - ret = idc.add_struc_member(self.strucid, name, -1, utils.size2dataflags(size), -1, size) - self.handle_addstrucmember_ret(ret) - offset = self.size - size - self.set_member_type(offset, member_type) + # IDA 9: append은 현재 사이즈 위치로 오프셋 산정해 add_udm + offset = self.size + udm = idaapi.udm_t() + udm.name = name + udm.offset = offset * 8 + # ensure member type has a valid, non-zero size + type_to_set = member_type + msz = type_to_set.get_size() + if msz == idaapi.BADSIZE or msz <= 0: + # fallback to void* of correct pointer size + fallback = idaapi.tinfo_t() + ok = idaapi.parse_decl(fallback, idaapi.get_idati(), "void (*)(void);", 0) + if not fallback.is_correct() or fallback.get_size() <= 0: + pvoid = idaapi.tinfo_t() + pvoid.create_ptr(idaapi.tinfo_t(idaapi.BTF_VOID)) + fallback = pvoid + type_to_set = fallback + udm.type = type_to_set + udm.size = max(1, type_to_set.get_size()) * 8 + t = idaapi.tinfo_t() + if not t.get_type_by_tid(self.strucid): + self.handle_addstrucmember_ret(-5) + return + # ensure struct is not fixed, and has enough size to accept the member + try: + if hasattr(t, 'set_fixed_struct'): + t.set_fixed_struct(False) + except Exception: + pass + needed = offset + max(1, udm.type.get_size()) + curr = t.get_size() + if curr == idaapi.BADSIZE: + curr = 0 + if needed > curr: + t.set_struct_size(needed) + t.save_type() + code = t.add_udm(udm) + self.handle_addstrucmember_ret(code) if member_comment is not None: self.set_member_comment(offset, member_comment) \ No newline at end of file diff --git a/pyphrank/containers/structure.py b/pyphrank/containers/structure.py index 6970003..55b43e8 100644 --- a/pyphrank/containers/structure.py +++ b/pyphrank/containers/structure.py @@ -2,7 +2,6 @@ import idaapi import idc -import ida_struct import pyphrank.settings as settings from pyphrank.containers.ida_struc_wrapper import IdaStrucWrapper import pyphrank.utils as utils @@ -15,15 +14,29 @@ def __init__ (self, strucid): @classmethod def new(cls): - strucid = idc.add_struc(idaapi.BADADDR, None, False) - return cls(strucid) + # IDA 9: 구조체 생성은 tinfo_t.create_udt 사용 + udt = idaapi.udt_type_data_t() + tif = idaapi.tinfo_t() + if not tif.create_udt(udt): + return cls(-1) + # 익명 이름 할당 후 tid 조회 + name = utils.get_next_available_strucname("struct_anon") + tif.set_named_type(None, name) + sid = idc.get_struc_id(name) + return cls(sid) @classmethod def create(cls, struc_name:str): - strucid = idc.add_struc(idaapi.BADADDR, struc_name, False) - if strucid == idaapi.BADADDR: + # IDA 9: 구조체 생성은 tinfo_t.create_udt 사용 + udt = idaapi.udt_type_data_t() + tif = idaapi.tinfo_t() + if not tif.create_udt(udt): return None - return cls(strucid) + tif.set_named_type(None, struc_name) + sid = idc.get_struc_id(struc_name) + if sid == idaapi.BADADDR: + return None + return cls(sid) def member_names(self): for member_offset in self.member_offsets(): @@ -45,10 +58,26 @@ def resize(self, new_size: int): self.expand(new_size - current_size) def expand(self, extra_size: int): + # IDA 9: expand_struc 제거. tinfo_t.set_struct_size 사용 current_size = self.size - membername = 'field_' + hex(extra_size + current_size - 1)[2:] - idc.add_struc_member(self.strucid, membername, current_size, utils.size2dataflags(1), -1, 1) - idc.expand_struc(self.strucid, current_size, extra_size - 1, False) + new_size = current_size + extra_size + ct = idaapi.tinfo_t() + if ct.get_type_by_tid(self.strucid) and ct.is_udt(): + ct.set_struct_size(new_size) + ct.save_type() + return + # fallback: 마지막에 패딩 멤버 추가로 크기 확보 + membername = 'field_' + hex(new_size - 1)[2:] + # add padding as an array of bytes + pad_tif = idaapi.tinfo_t() + idaapi.parse_decl(pad_tif, idaapi.get_idati(), f"unsigned char[{new_size - current_size}];", 0) + udm = idaapi.udm_t() + udm.name = membername + udm.offset = current_size * 8 + udm.type = pad_tif + ct2 = idaapi.tinfo_t() + if ct2.get_type_by_tid(self.strucid): + ct2.add_udm(udm) def is_offset_ok(self, offset:int, size:int): if offset + size <= self.size: @@ -64,28 +93,50 @@ def set_member(self, name:str, offset:int, size:int): if self.size < original_size - 1: self.resize(original_size - 1) - ret = idc.add_struc_member(self.strucid, name, offset, utils.size2dataflags(size), -1, size) - self.handle_addstrucmember_ret(ret) - if ret == idaapi.BADADDR: - raise BaseException("Failed to append structure pointer") + # IDA 9: 원시 바이트 멤버 추가를 tinfo_t.add_udm으로 대체 + ct = idaapi.tinfo_t() + assert ct.get_type_by_tid(self.strucid) and ct.is_udt() + arr_tif = idaapi.tinfo_t() + idaapi.parse_decl(arr_tif, idaapi.get_idati(), f"unsigned char[{size}];", 0) + udm = idaapi.udm_t() + udm.name = name + udm.offset = offset * 8 + udm.type = arr_tif + code = ct.add_udm(udm) + if code < 0: + self.handle_addstrucmember_ret(code) + raise BaseException("Failed to append structure member") def set_struc(self, name:str, offset:int, struc): size = struc.size if not self.is_offset_ok(offset, size): raise BaseException("offset and size are too big") self.unset_members(offset, size) - ret = ida_struct.add_struc_member(self.strucid, name, offset, utils.size2dataflags(1), -1, 1) - self.handle_addstrucmember_ret(ret) - idc.SetType(ida_struct.get_member_id(self.strucid, offset), struc.get_name()) + # IDA 9: add_struc_member 제거. tinfo_t.add_udm 사용 + ct = idaapi.tinfo_t() + assert ct.get_type_by_tid(self.strucid) and ct.is_udt() + udm = idaapi.udm_t() + udm.name = name + udm.offset = offset * 8 + udm.type = struc.tinfo + code = ct.add_udm(udm) + if code < 0: + self.handle_addstrucmember_ret(code) def set_strucptr(self, name:str, offset:int, struc): PTRSIZE = settings.PTRSIZE if not self.is_offset_ok(offset, PTRSIZE): raise BaseException("offset and size are too big") self.unset_members(offset, PTRSIZE) - ret = ida_struct.add_struc_member(self.strucid, name, offset, utils.size2dataflags(PTRSIZE), -1, PTRSIZE) - self.handle_addstrucmember_ret(ret) - idc.SetType(ida_struct.get_member_id(self.strucid, offset), struc.get_name() + "*") + ct = idaapi.tinfo_t() + assert ct.get_type_by_tid(self.strucid) and ct.is_udt() + udm = idaapi.udm_t() + udm.name = name + udm.offset = offset * 8 + udm.type = struc.ptr_tinfo + code = ct.add_udm(udm) + if code < 0: + self.handle_addstrucmember_ret(code) def member_exists(self, offset:int) -> bool: if self.strucid == idaapi.BADADDR: @@ -93,32 +144,42 @@ def member_exists(self, offset:int) -> bool: if offset < 0 or offset >= self.size: return False - sptr = ida_struct.get_struc(self.strucid) - mptr = ida_struct.get_member(sptr, offset) - return mptr is not None + ct = idaapi.tinfo_t() + if not ct.get_type_by_tid(self.strucid): + return False + udm = idaapi.udm_t() + udm.offset = offset * 8 + return ct.find_udm(udm, idaapi.STRMEM_OFFSET) != -1 def get_next_member_offset(self, offset:int) -> int: if offset < 0 or offset > self.size: return -1 - sptr = ida_struct.get_struc(self.strucid) - offset = ida_struct.get_struc_next_offset(sptr, offset) - while offset != idaapi.BADADDR and not self.get_member_name(offset): - offset = ida_struct.get_struc_next_offset(sptr, offset) - - if offset == idaapi.BADADDR: - offset = -1 - return offset + ct = idaapi.tinfo_t() + if not ct.get_type_by_tid(self.strucid): + return -1 + udt = idaapi.udt_type_data_t() + if not ct.get_udt_details(udt): + return -1 + for m in udt: + moff = m.begin() // 8 + if moff > offset: + return moff + return -1 def get_member_start(self, offset:int) -> int: if offset < 0 or offset > self.size: return -1 - sptr = ida_struct.get_struc(self.strucid) - member = ida_struct.get_member(sptr, offset) - if member is None: + ct = idaapi.tinfo_t() + if not ct.get_type_by_tid(self.strucid): + return -1 + udm = idaapi.udm_t() + udm.offset = offset * 8 + idx = ct.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: return -1 - return member.soff + return udm.offset // 8 def is_member_start(self, offset:int) -> bool: if offset < 0 or offset > self.size: diff --git a/pyphrank/containers/union.py b/pyphrank/containers/union.py index 408d87a..132e099 100644 --- a/pyphrank/containers/union.py +++ b/pyphrank/containers/union.py @@ -10,5 +10,14 @@ def __init__(self, strucid): @classmethod def create(cls, struc_name=None): - strucid = idc.add_struc(idaapi.BADADDR, struc_name, True) - return cls(strucid) \ No newline at end of file + # IDA 9: create union via tinfo_t.create_udt with is_union + udt = idaapi.udt_type_data_t() + udt.is_union = True + tif = idaapi.tinfo_t() + if not tif.create_udt(udt, idaapi.BTF_UNION): + return cls(-1) + if struc_name is None: + struc_name = "union_anon" + tif.set_named_type(None, struc_name) + sid = idc.get_struc_id(struc_name) + return cls(sid) \ No newline at end of file diff --git a/pyphrank/containers/vtable.py b/pyphrank/containers/vtable.py index cc38743..52b6979 100644 --- a/pyphrank/containers/vtable.py +++ b/pyphrank/containers/vtable.py @@ -3,7 +3,7 @@ import idaapi import idautils import idc -import ida_struct +import ida_typeinf import pyphrank.settings as settings import pyphrank.utils as utils @@ -25,7 +25,9 @@ def from_data(cls, addr:int): if vtbl is None: return None - voidptr_tif = utils.str2tif("void*") + # Prefer function pointer type for vtable entries + funcptr_tif = idaapi.tinfo_t() + idaapi.parse_decl(funcptr_tif, idaapi.get_idati(), "void (*)(void);", 0) for func_addr in vfcs: member_name = idaapi.get_name(func_addr) if member_name is None: @@ -34,7 +36,13 @@ def from_data(cls, addr:int): member_name = utils.get_next_available_membername(vtbl.strucid, member_name, Vtable.REUSE_DELIM) - vtbl.append_member(member_name, voidptr_tif, hex(func_addr)) + # 보장: 타입 크기 > 0 + use_tif = funcptr_tif if funcptr_tif.is_correct() and funcptr_tif.get_size() > 0 else utils.str2tif("void*") + if use_tif.get_size() <= 0: + pvoid = idaapi.tinfo_t() + pvoid.create_ptr(idaapi.tinfo_t(idaapi.BTF_VOID)) + use_tif = pvoid + vtbl.append_member(member_name, use_tif, hex(func_addr)) return vtbl def add_member(self, member_offset: int, name=None) -> bool: @@ -57,10 +65,14 @@ def get_member_name(self, moffset:int) -> str: @staticmethod def is_strucid_vtable(strucid:int): - if ida_struct.is_union(strucid): + if idc.is_union(strucid): return False - if ida_struct.get_struc_size(strucid) % settings.PTRSIZE != 0: + # IDA 9: 구조체 크기는 tinfo_t.get_size + ct = idaapi.tinfo_t() + if not ct.get_type_by_tid(strucid): + return False + if ct.get_size() % settings.PTRSIZE != 0: return False # vtable has one data xref max diff --git a/pyphrank/containers/vtables_union.py b/pyphrank/containers/vtables_union.py index 44aa764..05da4f9 100644 --- a/pyphrank/containers/vtables_union.py +++ b/pyphrank/containers/vtables_union.py @@ -2,7 +2,7 @@ import idaapi import idc -import ida_struct +import ida_typeinf from pyphrank.containers.union import Union from pyphrank.containers.vtable import Vtable @@ -28,13 +28,15 @@ def is_vtables_union(vtbl_info:idaapi.tinfo_t|str|int) -> bool: if not idc.is_union(vtbl_info): return False - sptr = ida_struct.get_struc(vtbl_info) - for member_offset in range(idc.get_member_qty(vtbl_info)): - mptr = ida_struct.get_member(sptr, member_offset) - mtif = idaapi.tinfo_t() - # member has no type - if not ida_struct.get_member_tinfo(mtif, mptr): - return False + # IDA 9: tinfo에서 UDT 멤버 타입 확인 + ct = idaapi.tinfo_t() + if not ct.get_type_by_tid(vtbl_info): + return False + udt = idaapi.udt_type_data_t() + if not ct.get_udt_details(udt): + return False + for m in udt: + mtif = m.type if not mtif.is_ptr(): return False diff --git a/pyphrank/ida_plugin.py b/pyphrank/ida_plugin.py index c78e4eb..6b69317 100644 --- a/pyphrank/ida_plugin.py +++ b/pyphrank/ida_plugin.py @@ -11,6 +11,17 @@ from pyphrank.type_analyzer import TypeAnalyzer +def _safe_get_vdui(widget): + """Return vdui if Hex-Rays is loaded; otherwise None without triggering load/warnings.""" + try: + import ida_hexrays + if not ida_hexrays.init_hexrays_plugin(): + return None + return idaapi.get_widget_vdui(widget) + except Exception: + return None + + def get_lvar_id(cfunc, lvar_arg): for lvar_id, lvar in enumerate(cfunc.lvars): @@ -20,7 +31,7 @@ def get_lvar_id(cfunc, lvar_arg): class PluginActionHandler(idaapi.action_handler_t): - def __init__(self, action_name, label, plugin:IDAPlugin, hotkey=None): + def __init__(self, action_name, label, plugin: 'IDAPlugin', hotkey=None): idaapi.action_handler_t.__init__(self) self.action_name = action_name self.hotkey = hotkey @@ -31,8 +42,9 @@ def _get_analyzer(self): if self.plugin is None: raise ValueError("Plugin is not set in its action") return self.plugin.type_analyzer - + def can_activate(self, ctx): + # default: enable only in pseudocode widgets if ctx.widget_type != idaapi.BWN_PSEUDOCODE: return False return True @@ -134,13 +146,37 @@ def activate_item(self, cfunc, citem) -> int: class ItemAnalyzer(PluginActionHandler): + def _is_var_selected(self, widget) -> bool: + hx_view = _safe_get_vdui(widget) + if hx_view is None: + return False + citem = hx_view.item + if citem.citype == idaapi.VDI_LVAR: + return True + if citem.citype == idaapi.VDI_EXPR: + it = citem.it.to_specific_type + # local or temp var + if getattr(idaapi, 'cot_var', None) is not None and it.op == idaapi.cot_var: + return True + # global/data object but not a function start + if getattr(idaapi, 'cot_obj', None) is not None and it.op == idaapi.cot_obj: + return not utils.is_func_start(it.obj_ea) + return False + + def update(self, ctx): + if not self.can_activate(ctx): + return idaapi.AST_DISABLE_FOR_WIDGET + return idaapi.AST_ENABLE_FOR_WIDGET if self._is_var_selected(ctx.widget) else idaapi.AST_DISABLE_FOR_WIDGET def handle_var(self, var:Var): analyzer = self._get_analyzer() start = time.time() - analyzer.analyze_var(var) + try: + analyzer.analyze_var(var) + except Exception as e: + utils.log_err(f"[phrank] analyze_var exception: {e}") + raise if self.plugin.should_apply_analysis: analyzer.apply_analysis() - utils.log_info(f"Analysis completed in {time.time() - start}") def handle_function(self, func_ea:int): """ @@ -225,6 +261,7 @@ def __init__(self) -> None: self.actions: list[PluginActionHandler] = [] self.type_analyzer = TypeAnalyzer() self.should_apply_analysis = True + self._ui_hooks = None @classmethod def get_instance(cls): @@ -240,18 +277,54 @@ def init(self): settings.PTRSIZE = utils.get_pointer_size() self.actions.append( - ItemAnalyzer("phrank::item_analyzer", "analyze item under cursor and its dependencies", self) + ItemAnalyzer("phrank::item_analyzer", "analyze item under cursor and its dependencies", self, "Shift-A") ) self.actions.append( - TFGPrinter("phrank::tfg_printer", "print TypeFlowGraph for variable/function under cursor", self) + TFGPrinter("phrank::tfg_printer", "print TypeFlowGraph for variable/function under cursor", self, "Alt-T") ) for action in self.actions: action.register() + # Attach actions to Hex-Rays pseudocode popup (conditional context menu) + class PhrankUIHooks(idaapi.UI_Hooks): + def __init__(self, plugin: 'IDAPlugin'): + super().__init__() + self._plugin = plugin + + def finish_populating_widget_popup(self, widget, popup_handle): + # Only add actions for pseudocode views + if idaapi.get_widget_type(widget) != idaapi.BWN_PSEUDOCODE: + return + # Attach item_analyzer only when a variable is selected + hx_view = _safe_get_vdui(widget) + if hx_view is not None: + citem = hx_view.item + attach_item = False + if citem.citype == idaapi.VDI_LVAR: + attach_item = True + elif citem.citype == idaapi.VDI_EXPR: + it = citem.it.to_specific_type + if getattr(idaapi, 'cot_var', None) is not None and it.op == idaapi.cot_var: + attach_item = True + elif getattr(idaapi, 'cot_obj', None) is not None and it.op == idaapi.cot_obj and not utils.is_func_start(it.obj_ea): + attach_item = True + if attach_item: + idaapi.attach_action_to_popup(widget, popup_handle, "phrank::item_analyzer", "Phrank/") + idaapi.attach_action_to_popup(widget, popup_handle, "phrank::tfg_printer", "Phrank/") + + self._ui_hooks = PhrankUIHooks(self) + self._ui_hooks.hook() + return idaapi.PLUGIN_KEEP def run(self, arg): return def term(self): + for action in self.actions: + idaapi.unregister_action(action.action_name) + self.actions = [] + if self._ui_hooks is not None: + self._ui_hooks.unhook() + self._ui_hooks = None return \ No newline at end of file diff --git a/pyphrank/util_tif.py b/pyphrank/util_tif.py index eef041a..0a983d6 100644 --- a/pyphrank/util_tif.py +++ b/pyphrank/util_tif.py @@ -1,7 +1,6 @@ from __future__ import annotations import idaapi -import ida_struct import idc from functools import lru_cache as _lru_cache @@ -41,11 +40,12 @@ def str2strucid(s:str) -> int: if s.startswith("struct "): s = s[7:] - rv = idaapi.get_struc_id(s) + rv = idc.get_struc_id(s) if rv != idaapi.BADADDR: return rv - rv = idaapi.import_type(idaapi.get_idati(), -1, s) + # IDA 9: ida_typeinf.import_type 제거 -> idc.import_type 사용 + rv = idc.import_type(idaapi.get_idati(), -1, s) if rv == idaapi.BADNODE: return -1 return rv @@ -135,7 +135,11 @@ def __init__(self, strucid:int, offset:int): self.offset = offset def bad_offset(self) -> bool: - struc_sz = ida_struct.get_struc_size(self.strucid) + # IDA 9: ida_struct.get_struc_size 제거. tinfo_t를 통해 크기 조회 + tif = idaapi.tinfo_t() + if not tif.get_type_by_tid(self.strucid): + return True + struc_sz = tif.get_size() if self.offset < 0 or self.offset >= struc_sz: return True return False @@ -153,17 +157,16 @@ def name(self) -> str: def tif(self) -> idaapi.tinfo_t: if self.bad_offset(): return UNKNOWN_TYPE - sptr = ida_struct.get_struc(self.strucid) - mptr = ida_struct.get_member(sptr, self.offset) - # member is unset - if mptr is None: + # IDA 9: 멤버 타입 획득은 tinfo_t.get_udm_by_offset 사용 + ct = idaapi.tinfo_t() + if not ct.get_type_by_tid(self.strucid): return UNKNOWN_TYPE - - tif = idaapi.tinfo_t() - # member has no type - if not ida_struct.get_member_tinfo(tif, mptr): + udm = idaapi.udm_t() + udm.offset = self.offset * 8 + idx = ct.find_udm(udm, idaapi.STRMEM_OFFSET) + if idx == -1: return UNKNOWN_TYPE - return tif + return udm.type if udm.type.is_correct() else UNKNOWN_TYPE @property def comment(self) -> str: diff --git a/pyphrank/utils.py b/pyphrank/utils.py index 8dff276..5301e76 100644 --- a/pyphrank/utils.py +++ b/pyphrank/utils.py @@ -20,7 +20,7 @@ def split_list(list_to_split:list, cond) -> tuple[list,list]: return on_true, on_false def get_next_available_strucname(strucname:str, delimiter='__') -> str: - while idaapi.get_struc_id(strucname) != idaapi.BADADDR: + while idc.get_struc_id(strucname) != idaapi.BADADDR: splitted = strucname.rsplit(delimiter, 1) if len(splitted) == 1: strucname = strucname + delimiter + '0' @@ -65,9 +65,10 @@ def iterate_segments(): yield idc.get_segm_start(segea), idc.get_segm_end(segea) def get_pointer_size() -> int: - if idaapi.get_inf_structure().is_64bit(): + # IDA 9: get_inf_structure 제거. inf_ 구조 접근자 사용 + if idaapi.inf_is_64bit(): return 8 - elif idaapi.get_inf_structure().is_32bit(): + elif idaapi.inf_is_32bit_exactly(): return 4 else: return 2