├── .gitignore ├── img ├── ida_ifl_dark.png └── ida_ifl_default.png ├── ida-plugin.json ├── README.md └── ifl.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.bak 2 | .vs/ 3 | 4 | -------------------------------------------------------------------------------- /img/ida_ifl_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasherezade/ida_ifl/HEAD/img/ida_ifl_dark.png -------------------------------------------------------------------------------- /img/ida_ifl_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hasherezade/ida_ifl/HEAD/img/ida_ifl_default.png -------------------------------------------------------------------------------- /ida-plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "IDAMetadataDescriptorVersion": 1, 3 | "plugin": { 4 | "name": "IFL", 5 | "entryPoint": "ifl.py", 6 | "version": "1.5.2", 7 | "description": "Interactive Functions List with navigation and PE-sieve import support", 8 | "license": "CC-BY-3.0", 9 | "categories": [ 10 | "ui-ux-and-visualization", 11 | "api-scripting-and-automation" 12 | ], 13 | "urls": { 14 | "repository": "https://github.com/hasherezade/ida_ifl" 15 | }, 16 | "authors": [{ 17 | "name": "hasherezade", 18 | "email": "hasherezade@gmail.com" 19 | }], 20 | "keywords": [ 21 | "function-navigation", 22 | "function-analysis", 23 | "references", 24 | "pe-sieve", 25 | "interactive-ui", 26 | "function-listing", 27 | "import-export", 28 | "malware-analysis" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IFL - Interactive Functions List 2 | 3 | [![GitHub release](https://img.shields.io/github/release/hasherezade/ida_ifl.svg)](https://github.com/hasherezade/ida_ifl/releases) 4 | [![GitHub release date](https://img.shields.io/github/release-date/hasherezade/ida_ifl?color=blue)](https://github.com/hasherezade/ida_ifl/releases) 5 | 6 | [![Commit activity](https://img.shields.io/github/commit-activity/m/hasherezade/ida_ifl)](https://github.com/hasherezade/ida_ifl/commits) 7 | [![Last Commit](https://img.shields.io/github/last-commit/hasherezade/ida_ifl/master)](https://github.com/hasherezade/ida_ifl/commits) 8 | 9 | 10 | License: CC-BY (https://creativecommons.org/licenses/by/3.0/) 11 | 12 | A small plugin with a goal to provide user-friendly way to navigate between functions and their references.
13 | Additionally, it allows to import reports generated by i.e. [PE-sieve](https://github.com/hasherezade/pe-sieve/wiki/1.-FAQ) into IDA. Supports: 14 | + [`.tag` format](https://github.com/hasherezade/tiny_tracer/wiki/Using-the-TAGs-with-disassemblers-and-debuggers) (generated by [PE-sieve](https://github.com/hasherezade/pe-sieve), [Tiny Tracer](https://github.com/hasherezade/tiny_tracer), [PE-bear](https://github.com/hasherezade/pe-bear-releases)) 15 | + [`.imports.txt` format](https://github.com/hasherezade/pe-sieve/wiki/4.3.-Import-table-reconstruction-(imp)) (generated by [PE-sieve](https://github.com/hasherezade/pe-sieve)) 16 | 17 | *A legacy version for Python 2 available via branch [python2](https://github.com/hasherezade/ida_ifl/tree/python2)* 18 | 19 | #### For Binary Ninja version check: https://github.com/leandrofroes/bn_ifl 20 | 21 | Demo 22 | == 23 | 24 | Dark theme: 25 | 26 | ![](https://github.com/hasherezade/ida_ifl/blob/master/img/ida_ifl_dark.png) 27 | 28 | Light theme: 29 | 30 | ![](https://github.com/hasherezade/ida_ifl/blob/master/img/ida_ifl_default.png) 31 | 32 | 📖 More info on [Wiki](https://github.com/hasherezade/ida_ifl/wiki) 33 | -------------------------------------------------------------------------------- /ifl.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # 3 | # IFL - Interactive Functions List 4 | # 5 | # how to install: copy the script into plugins directory, i.e: C:\Program Files\IDA \plugins 6 | # then: 7 | # run from IDA menu: View -> PLUGIN_NAME 8 | # or press: PLUGIN_HOTKEY 9 | # 10 | """ 11 | CC-BY: hasherezade, run via IDA Pro >= 8.0 12 | """ 13 | __VERSION__ = "1.5.2" 14 | __AUTHOR__ = "hasherezade" 15 | 16 | PLUGIN_NAME = "IFL - Interactive Functions List" 17 | PLUGIN_HOTKEY = "Ctrl-Alt-F" 18 | import dataclasses 19 | import functools 20 | import string 21 | from typing import Any, List, Optional, Tuple, Union 22 | 23 | import ida_bytes 24 | import ida_kernwin 25 | import idaapi # type: ignore 26 | import idc # type: ignore 27 | from idaapi import PluginForm # type: ignore 28 | from idaapi import ( 29 | BADADDR, 30 | SN_NOWARN, 31 | jumpto, 32 | next_addr, 33 | o_void, 34 | prev_addr, 35 | print_insn_mnem, 36 | set_cmt, 37 | set_name, 38 | ) 39 | from idautils import Functions, XrefsFrom, XrefsTo # type: ignore 40 | from idc import ( 41 | CIC_ITEM, 42 | FUNCATTR_END, 43 | FUNCATTR_FRAME, 44 | FUNCATTR_START, 45 | INF_SHORT_DN, 46 | GetDisasm, 47 | demangle_name, 48 | get_func_attr, 49 | get_func_name, 50 | get_inf_attr, 51 | get_operand_type, 52 | get_operand_value, 53 | get_type, 54 | set_color, 55 | ) 56 | 57 | @functools.total_ordering 58 | @dataclasses.dataclass(frozen=True) 59 | class IDAVersionInfo: 60 | major: int 61 | minor: int 62 | sdk_version: int 63 | 64 | def __eq__(self, other): 65 | if isinstance(other, IDAVersionInfo): 66 | return (self.major, self.minor) == (other.major, other.minor) 67 | if isinstance(other, tuple): 68 | return (self.major, self.minor) == tuple(other[:2]) 69 | return NotImplemented 70 | 71 | def __lt__(self, other): 72 | if isinstance(other, IDAVersionInfo): 73 | return (self.major, self.minor) < (other.major, other.minor) 74 | if isinstance(other, tuple): 75 | return (self.major, self.minor) < tuple(other[:2]) 76 | return NotImplemented 77 | 78 | @staticmethod 79 | @functools.cache 80 | def ida_version(): 81 | """ 82 | Returns an IDAVersionInfo instance for the current IDA kernel version. 83 | 84 | The returned object supports comparison with tuples, e.g.: 85 | if IDAVersionInfo.ida_version() >= (9, 2): 86 | ... 87 | """ 88 | version_str: str = idaapi.get_kernel_version() # e.g. "9.1" 89 | sdk_version: int = idaapi.IDA_SDK_VERSION 90 | major, minor = map(int, version_str.split(".")) 91 | return IDAVersionInfo(major, minor, sdk_version) 92 | 93 | 94 | ida_version = IDAVersionInfo.ida_version 95 | 96 | if ida_version() >= (9, 2): 97 | from PySide6 import QtCore, QtGui, QtWidgets 98 | from PySide6.QtCore import QObject, Signal as pyqtSignal 99 | QT_VERSION = 6 100 | else: 101 | from PyQt5 import QtCore, QtGui, QtWidgets # type: ignore 102 | from PyQt5.QtCore import QObject, pyqtSignal # type: ignore 103 | QT_VERSION = 5 104 | 105 | VERSION_INFO = ( 106 | f"IFL v{__VERSION__} - check for updates: https://github.com/hasherezade/ida_ifl" 107 | ) 108 | 109 | # Qt5/Qt6 compatibility 110 | if QT_VERSION == 6: 111 | BackgroundColorRole = QtCore.Qt.ItemDataRole.BackgroundRole 112 | PaletteBackground = QtGui.QPalette.ColorRole.Window 113 | else: 114 | BackgroundColorRole = QtCore.Qt.BackgroundColorRole 115 | PaletteBackground = QtGui.QPalette.Background 116 | 117 | transp_l = 230 118 | light_theme = [ 119 | QtGui.QColor(173, 216, 230, transp_l), 120 | QtGui.QColor(255, 165, 0, transp_l), 121 | QtGui.QColor(240, 230, 140, transp_l), 122 | ] 123 | transp_d = 70 124 | dark_theme = [ 125 | QtGui.QColor(173, 216, 240, transp_d), 126 | QtGui.QColor(255, 0, 255, transp_d), 127 | QtGui.QColor(255, 130, 130, transp_d), 128 | ] 129 | 130 | COLOR_HILIGHT_FUNC = 0xFFDDBB # BBGGRR 131 | COLOR_HILIGHT_REFTO = 0xBBFFBB 132 | COLOR_HILIGHT_REFFROM = 0xDDBBFF 133 | 134 | IS_ALTERNATE_ROW = False 135 | 136 | # -------------------------------------------------------------------------- 137 | # custom functions: 138 | # -------------------------------------------------------------------------- 139 | 140 | # Theme 141 | 142 | 143 | def get_bg_color(): 144 | w = ida_kernwin.get_current_widget() 145 | if not w: 146 | return None 147 | widget = ida_kernwin.PluginForm.FormToPyQtWidget(w) 148 | if not widget: 149 | return None 150 | color = widget.palette().color(PaletteBackground) 151 | return color 152 | 153 | 154 | def is_darker(color1, color2): 155 | if QtGui.QColor(color2).lightness() > QtGui.QColor(color1).lightness(): 156 | return False 157 | return True 158 | 159 | 160 | def color_to_val(color): 161 | return (((color.red() << 8) | color.green()) << 8) | color.blue() 162 | 163 | 164 | def get_theme(): 165 | bgcolor = get_bg_color() 166 | if bgcolor is None: 167 | return None 168 | if is_darker(bgcolor, COLOR_HILIGHT_FUNC): 169 | return light_theme 170 | return dark_theme 171 | 172 | 173 | # Addressing 174 | 175 | 176 | def rva_to_va(rva: int) -> int: 177 | base = idaapi.get_imagebase() 178 | return rva + base 179 | 180 | 181 | def va_to_rva(va: int) -> int: 182 | base = idaapi.get_imagebase() 183 | return va - base 184 | 185 | 186 | def _is_hex_str(s): 187 | # Check if the string starts with "0x" or is a valid hex string 188 | if s.startswith("0x"): 189 | s = s[2:] 190 | return all(c in string.hexdigits for c in s) 191 | 192 | 193 | # Functions and args 194 | 195 | 196 | def function_at(ea: int) -> Optional[int]: 197 | return next(Functions(ea), None) 198 | 199 | 200 | def parse_function_args(ea: int) -> str: 201 | arguments = [] 202 | frame = idc.get_func_attr(ea, FUNCATTR_FRAME) 203 | 204 | if frame is None: 205 | return "" 206 | 207 | if 760 <= idaapi.IDA_SDK_VERSION < 900: 208 | 209 | class frame_members_iterator: 210 | def __init__(self, f, max_count=10000): 211 | self.frame = f 212 | self.max_count = max_count 213 | self.start = idc.get_first_member(f) 214 | self.end = idc.get_last_member(f) 215 | self.count = 0 216 | 217 | def __iter__(self): 218 | while self.start <= self.end and self.count <= self.max_count: 219 | size = idc.get_member_size(self.frame, self.start) 220 | self.count += 1 221 | 222 | if size is None: 223 | self.start += 1 224 | continue # Skip invalid members 225 | 226 | name = idc.get_member_name(self.frame, self.start) 227 | self.start += size 228 | 229 | yield name 230 | skip_names = [" r", " s"] 231 | udt_data_iter = frame_members_iterator(frame) 232 | else: 233 | import operator 234 | 235 | import ida_typeinf 236 | 237 | tif = ida_typeinf.tinfo_t() 238 | tif.get_type_by_tid(ea) 239 | udt_data = ida_typeinf.udt_type_data_t() 240 | if not tif.get_udt_details(udt_data): 241 | return "" 242 | skip_names = ["__return_address", "__saved_registers"] 243 | udt_data_iter = map(operator.attrgetter("name"), udt_data) 244 | 245 | arguments = [arg for arg in udt_data_iter if arg not in skip_names] 246 | if len(arguments) == 0: 247 | args_str = "void" 248 | else: 249 | args_str = ", ".join(arguments) 250 | return f"({args_str})" 251 | 252 | 253 | def parse_function_type(ea: int, end: Optional[int] = None) -> str: 254 | frame = idc.get_func_attr(ea, FUNCATTR_FRAME) 255 | if frame is None: 256 | return "" 257 | if end is None: # try to find end 258 | func = function_at(ea) 259 | if not func: 260 | return "?" 261 | end = prev_addr(idc.get_func_attr(func, FUNCATTR_END)) 262 | end_addr = end 263 | mnem = GetDisasm(end_addr) 264 | 265 | if "ret" not in mnem: 266 | # it's not a real end, get instruction before... 267 | end_addr = prev_addr(end) 268 | if end_addr == BADADDR: 269 | # cannot get the real end 270 | return "" 271 | mnem = GetDisasm(end_addr) 272 | 273 | if "ret" not in mnem: 274 | # cannot get the real end 275 | return "" 276 | 277 | op = get_operand_type(end_addr, 0) 278 | if op == o_void: 279 | # retn has NO parameters 280 | return "__cdecl" 281 | # retn has parameters 282 | return "__stdcall" 283 | 284 | 285 | def _getFunctionType(start: int, end: Optional[int] = None) -> str: 286 | type = get_type(start) 287 | if type is None: 288 | return parse_function_type(start, end) 289 | args_start = type.find("(") 290 | if args_start is not None: 291 | type = type[:args_start] 292 | return type 293 | 294 | 295 | def _isFunctionMangled(ea: int) -> bool: 296 | name = get_func_name(ea) 297 | disable_mask = get_inf_attr(INF_SHORT_DN) 298 | if demangle_name(name, disable_mask) is None: 299 | return False 300 | return True 301 | 302 | 303 | def _getFunctionNameAt(ea: int) -> str: 304 | name = get_func_name(ea) 305 | disable_mask = get_inf_attr(INF_SHORT_DN) 306 | demangled_name = demangle_name(name, disable_mask) 307 | if demangled_name is None: 308 | return name 309 | args_start = demangled_name.find("(") 310 | if args_start is None: 311 | return demangled_name 312 | return demangled_name[:args_start] 313 | 314 | 315 | def _getArgsDescription(ea: int) -> str: 316 | name = demangle_name( 317 | get_func_name(ea), get_inf_attr(INF_SHORT_DN) 318 | ) # get from mangled name 319 | if not name: 320 | name = get_type(ea) # get from type 321 | if not name: 322 | return parse_function_args(ea) # cannot get params from the mangled name 323 | args_start = name.find("(") 324 | if args_start is not None and args_start != (-1): 325 | return name[args_start:] 326 | return "" 327 | 328 | 329 | def _getArgsNum(ea: int) -> int: 330 | args = _getArgsDescription(ea) 331 | if not args or len(args) == 0: 332 | return 0 333 | delimiter = "," 334 | args_list = args.split(delimiter) 335 | args_num = 0 336 | for arg in args_list: 337 | if arg == "()" or arg == "(void)": 338 | continue 339 | args_num += 1 340 | return args_num 341 | 342 | 343 | # -------------------------------------------------------------------------- 344 | # custom data types: 345 | # -------------------------------------------------------------------------- 346 | 347 | 348 | # Global DataManager 349 | class DataManager(QObject): 350 | """Keeps track on the changes in data and signalizies them.""" 351 | 352 | updateSignal = pyqtSignal() 353 | 354 | def __init__(self, parent=None) -> None: 355 | QtCore.QObject.__init__(self, parent=parent) 356 | self.currentRva = BADADDR 357 | 358 | def setFunctionName(self, start: int, func_name: str) -> bool: 359 | flags = idaapi.SN_NOWARN | idaapi.SN_NOCHECK 360 | if idc.set_name(start, func_name, flags): 361 | self.updateSignal.emit() 362 | return True 363 | return False 364 | 365 | def setCurrentRva(self, rva: Optional[int]) -> None: 366 | if rva is None: 367 | rva = BADADDR 368 | self.currentRva = rva 369 | self.updateSignal.emit() 370 | 371 | def refreshData(self) -> None: 372 | self.updateSignal.emit() 373 | 374 | 375 | # -------------------------------------------------------------------------- 376 | 377 | 378 | class FunctionInfo_t: 379 | """A class representing a single function's record.""" 380 | 381 | def __init__( 382 | self, start: int, end: int, refs_list, called_list, is_import: bool = False 383 | ) -> None: 384 | self.start = start 385 | self.end = end 386 | self.args_num = _getArgsNum(start) 387 | self.type = _getFunctionType(start, end) 388 | self.is_import = is_import 389 | self.refs_list = refs_list 390 | self.called_list = called_list 391 | 392 | def contains(self, addr: int) -> bool: 393 | """Check if the given address lies inside the function.""" 394 | bgn = self.start 395 | end = self.end 396 | # swap if order is opposite: 397 | if self.start > self.end: 398 | end = self.start 399 | bgn = self.end 400 | if addr >= bgn and addr < end: 401 | return True 402 | return False 403 | 404 | 405 | # -------------------------------------------------------------------------- 406 | # custom models: 407 | # -------------------------------------------------------------------------- 408 | 409 | 410 | class TableModel_t(QtCore.QAbstractTableModel): 411 | """The model for the top view: storing all the functions.""" 412 | 413 | COL_START = 0 414 | COL_END = 1 415 | COL_NAME = 2 416 | COL_TYPE = 3 417 | COL_ARGS = 4 418 | COL_REFS = 5 419 | COL_CALLED = 6 420 | COL_IMPORT = 7 421 | COL_COUNT = 8 422 | header_names = [ 423 | "Start", 424 | "End", 425 | "Name", 426 | "Type", 427 | "Args", 428 | "Is referred by", 429 | "Refers to", 430 | "Imported?", 431 | ] 432 | 433 | # private: 434 | 435 | def _displayHeader( 436 | self, orientation: QtCore.Qt.Orientation, col: int 437 | ) -> Optional[str]: 438 | if orientation == QtCore.Qt.Vertical: 439 | return None 440 | if col == self.COL_START: 441 | return self.header_names[self.COL_START] 442 | if col == self.COL_END: 443 | return self.header_names[self.COL_END] 444 | if col == self.COL_TYPE: 445 | return self.header_names[self.COL_TYPE] 446 | if col == self.COL_ARGS: 447 | return self.header_names[self.COL_ARGS] 448 | if col == self.COL_NAME: 449 | return self.header_names[self.COL_NAME] 450 | if col == self.COL_REFS: 451 | return self.header_names[self.COL_REFS] 452 | if col == self.COL_CALLED: 453 | return self.header_names[self.COL_CALLED] 454 | if col == self.COL_IMPORT: 455 | return self.header_names[self.COL_IMPORT] 456 | return None 457 | 458 | def _displayData(self, row: int, col: int) -> Optional[Union[int, str]]: 459 | func_info = self.function_info_list[row] 460 | if col == self.COL_START: 461 | return "%08x" % func_info.start 462 | if col == self.COL_END: 463 | return "%08x" % func_info.end 464 | if col == self.COL_TYPE: 465 | return func_info.type 466 | if col == self.COL_ARGS: 467 | return _getArgsDescription(func_info.start) 468 | if col == self.COL_NAME: 469 | return _getFunctionNameAt(func_info.start) 470 | if col == self.COL_REFS: 471 | return len(func_info.refs_list) 472 | if col == self.COL_CALLED: 473 | return len(func_info.called_list) 474 | if col == self.COL_IMPORT: 475 | if func_info.is_import: 476 | return "+" 477 | return "-" 478 | return None 479 | 480 | def _displayToolTip(self, row: int, col: int) -> str: 481 | func_info = self.function_info_list[row] 482 | if col == self.COL_START or col == self.COL_END: 483 | return "Double Click to follow" 484 | if col == self.COL_NAME: 485 | return "Double Click to edit" 486 | if col == self.COL_REFS: 487 | return self._listRefs(func_info.refs_list) 488 | if col == self.COL_CALLED: 489 | return self._listRefs(func_info.called_list) 490 | return "" 491 | 492 | def _displayBackground(self, row: int, col: int) -> Any: 493 | curr_theme = get_theme() 494 | if curr_theme is not None: 495 | self.theme = curr_theme 496 | 497 | func_info = self.function_info_list[row] 498 | if col == self.COL_START or col == self.COL_END: 499 | return QtGui.QColor(self.theme[0]) 500 | if col == self.COL_NAME: 501 | if func_info.is_import: 502 | return QtGui.QColor(self.theme[1]) 503 | return QtGui.QColor(self.theme[2]) 504 | return None 505 | 506 | def _listRefs(self, refs_list: List[Tuple[int, int]]) -> str: 507 | str_list = [] 508 | for ea, ea_to in refs_list: 509 | str = "%08x @ %s" % (ea, _getFunctionNameAt(ea_to)) 510 | str_list.append(str) 511 | return "\n".join(str_list) 512 | 513 | # public: 514 | def __init__(self, function_info_list, parent=None, *args) -> None: 515 | super(TableModel_t, self).__init__() 516 | self.function_info_list = function_info_list 517 | self.theme = light_theme 518 | 519 | def isFollowable(self, col: int) -> bool: 520 | if col == self.COL_START: 521 | return True 522 | if col == self.COL_END: 523 | return True 524 | return False 525 | 526 | # Qt API 527 | def rowCount(self, parent) -> int: 528 | return len(self.function_info_list) 529 | 530 | def columnCount(self, parent) -> int: 531 | return self.COL_COUNT 532 | 533 | def setData(self, index, content, role) -> bool: 534 | if not index.isValid(): 535 | return False 536 | func_info = self.function_info_list[index.row()] 537 | if index.column() == self.COL_NAME: 538 | set_name(func_info.start, str(content), SN_NOWARN) 539 | g_DataManager.refreshData() 540 | return True 541 | 542 | def data(self, index, role) -> Any: 543 | if not index.isValid(): 544 | return None 545 | col = index.column() 546 | row = index.row() 547 | 548 | func_info = self.function_info_list[row] 549 | 550 | if role == QtCore.Qt.UserRole: 551 | if col == self.COL_END: 552 | return func_info.end 553 | return func_info.start 554 | elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: 555 | return self._displayData(row, col) 556 | elif role == QtCore.Qt.ToolTipRole: 557 | return self._displayToolTip(row, col) 558 | elif role == BackgroundColorRole: 559 | return self._displayBackground(row, col) 560 | else: 561 | return None 562 | 563 | def flags(self, index) -> Any: 564 | if not index.isValid(): 565 | return None 566 | flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable 567 | if index.column() == self.COL_NAME: 568 | return flags | QtCore.Qt.ItemIsEditable 569 | return flags 570 | 571 | def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole) -> Any: 572 | if role == QtCore.Qt.DisplayRole: 573 | return self._displayHeader(orientation, section) 574 | else: 575 | return None 576 | 577 | 578 | # -------------------------------------------------------------------------- 579 | 580 | 581 | class RefsTableModel_t(QtCore.QAbstractTableModel): 582 | """The model for the bottom view: the references to the functions.""" 583 | 584 | COL_NAME = 0 585 | COL_ADDR = 1 586 | COL_TOADDR = 2 587 | COL_COUNT = 3 588 | 589 | # private: 590 | def _displayHeader(self, orientation, col: int) -> Optional[str]: 591 | """Retrieves a field description to be displayed in the header.""" 592 | 593 | if orientation == QtCore.Qt.Vertical: 594 | return None 595 | if col == self.COL_ADDR: 596 | return "From Address" 597 | if col == self.COL_TOADDR: 598 | return "To Address" 599 | if col == self.COL_NAME: 600 | return "Foreign Val." 601 | return None 602 | 603 | def _getTargetAddr(self, row: int) -> int: 604 | """Retrieves the address from which function was referenced, or to which it references.""" 605 | 606 | curr_ref_fromaddr = self.refs_list[row][0] # fromaddr 607 | curr_ref_addr = self.refs_list[row][1] # toaddr 608 | target_addr = BADADDR 609 | if self.is_refs_to: 610 | target_addr = curr_ref_fromaddr 611 | else: 612 | target_addr = curr_ref_addr 613 | return target_addr 614 | 615 | def _getForeignFuncName(self, row: int) -> str: 616 | """Retrieves a name of the foreign function or the details on the referenced address.""" 617 | 618 | # curr_ref_fromaddr = self.refs_list[row][0] # fromaddr 619 | # curr_ref_addr = self.refs_list[row][1] # toaddr 620 | 621 | target_addr = self._getTargetAddr(row) 622 | if print_insn_mnem(target_addr) != "": 623 | func_name = _getFunctionNameAt(target_addr) 624 | if func_name: 625 | return func_name 626 | 627 | addr_str = "[%08lx]" % target_addr 628 | # target_name = GetDisasm(target_addr) 629 | return f"{addr_str} : {GetDisasm(target_addr)}" 630 | 631 | def _displayData(self, row: int, col: int) -> Optional[str]: 632 | """Retrieves the data to be displayed. appropriately to the row and column.""" 633 | 634 | if len(self.refs_list) <= row: 635 | return None 636 | curr_ref_fromaddr = self.refs_list[row][0] # fromaddr 637 | curr_ref_addr = self.refs_list[row][1] # toaddr 638 | if col == self.COL_ADDR: 639 | return "%08x" % curr_ref_fromaddr 640 | if col == self.COL_TOADDR: 641 | return "%08x" % curr_ref_addr 642 | if col == self.COL_NAME: 643 | return self._getForeignFuncName(row) 644 | return None 645 | 646 | def _getAddrToFollow(self, row: int, col: int) -> int: 647 | """Retrieves the address that can be followed on click.""" 648 | 649 | if col == self.COL_ADDR: 650 | return self.refs_list[row][0] 651 | if col == self.COL_TOADDR: 652 | return self.refs_list[row][1] 653 | return BADADDR 654 | 655 | def _displayBackground(self, row: int, col: int) -> Any: 656 | """Retrieves a background color appropriate for the data.""" 657 | curr_theme = get_theme() 658 | if curr_theme is not None: 659 | self.theme = curr_theme 660 | 661 | if self.isFollowable(col): 662 | return QtGui.QColor(self.theme[0]) 663 | return None 664 | 665 | # public: 666 | def __init__(self, function_info_list, is_refs_to=True, parent=None, *args) -> None: 667 | super(RefsTableModel_t, self).__init__() 668 | self.function_info_list = function_info_list 669 | self.curr_index = -1 670 | self.refs_list = [] 671 | self.is_refs_to = is_refs_to 672 | self.theme = light_theme 673 | 674 | def isFollowable(self, col: int) -> bool: 675 | """Is the address possible to follow in the disassembly view?""" 676 | if col == self.COL_ADDR: 677 | return True 678 | if col == self.COL_TOADDR: 679 | return True 680 | return False 681 | 682 | def findOffsetIndex(self, data: int) -> int: 683 | """Serches the given address on the list of functions and returns it if found.""" 684 | 685 | index = 0 686 | for func_info in self.function_info_list: 687 | if data >= func_info.start and data <= func_info.end: 688 | return index 689 | index += 1 690 | return -1 691 | 692 | def setCurrentIndex(self, curr_index: int) -> None: 693 | self.curr_index = curr_index 694 | if self.curr_index == (-1) or self.curr_index >= len(self.function_info_list): 695 | # reset list 696 | self.refs_list = [] 697 | else: 698 | if self.is_refs_to: 699 | self.refs_list = self.function_info_list[self.curr_index].refs_list 700 | else: 701 | self.refs_list = self.function_info_list[self.curr_index].called_list 702 | self.reset() 703 | 704 | def reset(self) -> None: 705 | self.beginResetModel() 706 | self.endResetModel() 707 | 708 | # Qt API 709 | def rowCount(self, parent=None) -> int: 710 | return len(self.refs_list) 711 | 712 | def columnCount(self, parent) -> int: 713 | return self.COL_COUNT 714 | 715 | def data(self, index, role) -> Any: 716 | if not index.isValid(): 717 | return None 718 | col = index.column() 719 | row = index.row() 720 | 721 | # curr_ref_addr = self.refs_list[row][0] 722 | 723 | if role == QtCore.Qt.UserRole: 724 | if self.isFollowable(col): 725 | return self._getAddrToFollow(row, col) 726 | elif role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole: 727 | return self._displayData(row, col) 728 | elif role == BackgroundColorRole: 729 | return self._displayBackground(row, col) 730 | else: 731 | return None 732 | 733 | def flags(self, index) -> Any: 734 | if not index.isValid(): 735 | return None 736 | flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable 737 | return flags 738 | 739 | def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole) -> Any: 740 | if role == QtCore.Qt.DisplayRole: 741 | return self._displayHeader(orientation, section) 742 | else: 743 | return None 744 | 745 | 746 | # -------------------------------------------------------------------------- 747 | # custom views: 748 | 749 | 750 | class FunctionsView_t(QtWidgets.QTableView): 751 | """The top view: listing all the functions.""" 752 | 753 | # private 754 | def _get_default_color(self) -> None: 755 | return idc.DEFCOLOR 756 | 757 | def _set_segment_color(self, ea, color) -> None: 758 | seg = idaapi.getseg(ea) 759 | seg.color = color 760 | seg.update() 761 | 762 | def _set_item_color(self, addr: int, color: int) -> None: 763 | ea = addr 764 | # reset to default 765 | self._set_segment_color(ea, self.color_normal) 766 | # set desired item color 767 | set_color(addr, CIC_ITEM, color) 768 | 769 | def _get_themed_color(self, color: int, theme: list) -> int: 770 | if theme == dark_theme: 771 | color_hilight = color_to_val(QtGui.QColor(color).darker(200)) 772 | else: 773 | color_hilight = color 774 | return color_hilight 775 | 776 | # public 777 | def __init__(self, dataManager, color_hilight, func_model, parent=None) -> None: 778 | super(FunctionsView_t, self).__init__(parent=parent) 779 | self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) 780 | # 781 | self.color_normal = self._get_default_color() 782 | self.prev_addr = BADADDR 783 | self.color_hilight = color_hilight 784 | self.func_model = func_model 785 | self.dataManager = dataManager 786 | 787 | self.setMouseTracking(True) 788 | self.setAutoFillBackground(True) 789 | self.theme = light_theme 790 | 791 | # Qt API 792 | def currentChanged(self, current, previous) -> None: 793 | index_data = self.get_index_data(current) 794 | self.dataManager.setCurrentRva(index_data) 795 | 796 | def hilight_addr(self, addr: int) -> None: 797 | curr_theme = get_theme() 798 | if curr_theme is not None: 799 | self.theme = curr_theme 800 | 801 | color_hilight = self._get_themed_color(self.color_hilight, self.theme) 802 | 803 | if self.prev_addr != BADADDR: 804 | self._set_item_color(self.prev_addr, self.color_normal) 805 | if addr != BADADDR: 806 | self._set_item_color(addr, color_hilight) 807 | self.prev_addr = addr 808 | 809 | def get_index_data(self, index: Any) -> Any: 810 | if not index.isValid(): 811 | return None 812 | try: 813 | data_val = index.data(QtCore.Qt.UserRole) 814 | if data_val is None: 815 | return None 816 | index_data = data_val 817 | except ValueError: 818 | return None 819 | return index_data 820 | 821 | def mousePressEvent(self, event: Any) -> None: 822 | event.accept() 823 | # index = self.indexAt(event.pos()) 824 | # data = self.get_index_data(index) 825 | super(QtWidgets.QTableView, self).mousePressEvent(event) 826 | 827 | def mouseDoubleClickEvent(self, event: Any) -> None: 828 | event.accept() 829 | index = self.indexAt(event.pos()) 830 | if not index.isValid(): 831 | return 832 | data = self.get_index_data(index) 833 | if not data: 834 | super(QtWidgets.QTableView, self).mouseDoubleClickEvent(event) 835 | return 836 | col = index.column() 837 | if self.func_model.isFollowable(col): 838 | self.hilight_addr(data) 839 | jumpto(data) 840 | super(QtWidgets.QTableView, self).mouseDoubleClickEvent(event) 841 | 842 | def mouseMoveEvent(self, event: Any) -> None: 843 | index = self.indexAt(event.pos()) 844 | if not index.isValid(): 845 | return 846 | col = index.column() 847 | if self.func_model.isFollowable(col): 848 | self.setCursor(QtCore.Qt.PointingHandCursor) 849 | else: 850 | self.setCursor(QtCore.Qt.ArrowCursor) 851 | 852 | def leaveEvent(self, event: Any) -> None: 853 | self.setCursor(QtCore.Qt.ArrowCursor) 854 | 855 | def OnDestroy(self) -> None: 856 | self.hilight_addr(BADADDR) 857 | 858 | 859 | # -------------------------------------------------------------------------- 860 | 861 | 862 | class FunctionsMapper_t(QObject): 863 | """The class keeping the mapping of all the functions.""" 864 | 865 | # private 866 | 867 | def _isImportStart(self, start: int) -> bool: 868 | """Check if the given function is imported or internal.""" 869 | 870 | if start in self._importsSet: 871 | return True 872 | if print_insn_mnem(start) == "call": 873 | return False 874 | # print(print_insn_mnem(start)) 875 | op = get_operand_value(start, 0) 876 | if op in self._importsSet: 877 | return True 878 | return False 879 | 880 | def imports_names_callback(self, ea: int, name: str, ord: Any) -> bool: 881 | """A callback adding a particular name and offset to the internal set of the imported functions.""" 882 | 883 | self._importsSet.add(ea) 884 | self._importNamesSet.add(name) 885 | # True -> Continue enumeration 886 | return True 887 | 888 | def _loadImports(self) -> None: 889 | """Enumerates imported functions with the help of IDA API and adds them to the internal sets.""" 890 | 891 | self._importsSet = set() 892 | self._importNamesSet = set() 893 | nimps = idaapi.get_import_module_qty() 894 | for i in range(0, nimps): 895 | try: 896 | idaapi.enum_import_names(i, self.imports_names_callback) 897 | except: 898 | idaapi.msg("Failed to fetch function name") 899 | 900 | def _isImportName(self, name: str) -> bool: 901 | """Checks if the given name belongs to the imported function with the help of internal set.""" 902 | 903 | if name in self._importNamesSet: 904 | return True 905 | return False 906 | 907 | def _listRefsTo(self, start: int) -> List[Tuple[int, int]]: 908 | """Make a list of all the references to the given function. 909 | Args: 910 | func : The function references to which we are searching. 911 | start : The function's start offset. 912 | 913 | Returns: 914 | list : A list of tuples. 915 | Each tuple represents: the offsets: 916 | 0 : the offset from where the given function was referenced by the foreign function 917 | 1 : the function's start address 918 | """ 919 | 920 | func_refs_to = XrefsTo(start, 1) 921 | refs_list = [] 922 | for ref in func_refs_to: 923 | if idc.print_insn_mnem(ref.frm) == "": 924 | continue 925 | refs_list.append((ref.frm, start)) 926 | return refs_list 927 | 928 | def _getCallingOffset(self, func, called_list) -> List[Tuple[int, int]]: 929 | """Lists the offsets from where the given function references the list of other function.""" 930 | 931 | start = get_func_attr(func, FUNCATTR_START) 932 | end = prev_addr(get_func_attr(func, FUNCATTR_END)) 933 | # func_name = _getFunctionNameAt(start) 934 | curr = start 935 | calling_list = [] 936 | while True: 937 | if curr >= end: 938 | break 939 | op = get_operand_value(curr, 0) 940 | if op in called_list: 941 | calling_list.append((curr, op)) 942 | curr = next_addr(curr) 943 | return calling_list 944 | 945 | def _listRefsFrom(self, func, start: int, end: int) -> List[Tuple[int, int]]: 946 | """Make a list of all the references made from the given function. 947 | 948 | Args: 949 | func : The function inside of which we are searching. 950 | start : The function's start offset. 951 | end : The function's end offset. 952 | 953 | Returns: 954 | list : A list of tuples. Each tuple represents: 955 | 0 : the offset from where the given function referenced the other entity 956 | 1 : the address that was referenced 957 | """ 958 | 959 | dif = end - start 960 | called_list = [] 961 | func_name = _getFunctionNameAt(start) 962 | 963 | for indx in range(0, dif): 964 | addr = start + indx 965 | func_refs_from = XrefsFrom(addr, 1) 966 | for ref in func_refs_from: 967 | if _getFunctionNameAt(ref.to) == func_name: 968 | # skip jumps inside self 969 | continue 970 | called_list.append(ref.to) 971 | calling_list = self._getCallingOffset(func, called_list) 972 | return calling_list 973 | 974 | def _loadLocals(self) -> None: 975 | """Enumerates functions using IDA API and loads them into the internal mapping.""" 976 | self._loadImports() 977 | for func in Functions(): 978 | start = get_func_attr(func, FUNCATTR_START) 979 | end = prev_addr(get_func_attr(func, FUNCATTR_END)) 980 | 981 | is_import = self._isImportStart(start) 982 | 983 | refs_list = self._listRefsTo(start) 984 | calling_list = self._listRefsFrom(func, start, end) 985 | 986 | func_info = FunctionInfo_t(start, end, refs_list, calling_list, is_import) 987 | self._functionsMap[va_to_rva(start)] = func_info 988 | self._functionsMap[va_to_rva(end)] = func_info 989 | self.funcList.append(func_info) 990 | 991 | # public 992 | def __init__(self, parent=None) -> None: 993 | super(FunctionsMapper_t, self).__init__(parent=parent) 994 | self._functionsMap = dict() 995 | self.funcList = [] # public 996 | self._loadLocals() 997 | 998 | def funcAt(self, rva: int) -> FunctionInfo_t: 999 | func_info = self._functionsMap[rva] 1000 | return func_info 1001 | 1002 | 1003 | class FunctionsListForm_t(PluginForm): 1004 | """The main form of the IFL plugin.""" 1005 | 1006 | # private 1007 | _LIVE_FILTER = True 1008 | 1009 | def _listFunctionsAddr(self) -> List[int]: 1010 | """Lists all the starting addresses of the functions using IDA API.""" 1011 | 1012 | fn_list = list() 1013 | for func in Functions(): 1014 | start = get_func_attr(func, FUNCATTR_START) 1015 | fn_list.append(start) 1016 | return fn_list 1017 | 1018 | def _saveFunctionsNames( 1019 | self, file_name: Optional[str], ext: str, skip_unnamed: bool 1020 | ) -> bool: 1021 | """Saves functions names and offsets from the internal mappings into a file. 1022 | Fromats: CSV (default), or TAG (PE-bear, PE-sieve compatibile). 1023 | """ 1024 | 1025 | if file_name is None or len(file_name) == 0: 1026 | return False 1027 | delim = "," 1028 | if ".tag" in ext: # a TAG format was chosen 1029 | delim = ";" 1030 | fn_list = list() 1031 | for func in Functions(): 1032 | start = get_func_attr(func, FUNCATTR_START) 1033 | func_name = _getFunctionNameAt(start) 1034 | if skip_unnamed: 1035 | if func_name.startswith("sub_"): 1036 | continue 1037 | start_rva = va_to_rva(start) 1038 | line = "%lx%c%s" % (start_rva, delim, func_name) 1039 | fn_list.append(line) 1040 | idaapi.msg(str(file_name)) 1041 | with open(file_name, "w") as f: 1042 | for item in fn_list: 1043 | f.write("%s\n" % item) 1044 | return True 1045 | return False 1046 | 1047 | def _stripImportName(self, func_name) -> str: 1048 | """Keep only ImportName, without the DLL name, and the ordinal.""" 1049 | 1050 | fn1 = func_name.split(".") 1051 | if len(fn1) >= 2: 1052 | func_name = fn1[1].strip() 1053 | fn1 = func_name.split("#") 1054 | if len(fn1) >= 2: 1055 | func_name = fn1[0].strip() 1056 | return func_name 1057 | 1058 | def _getBitness(self): 1059 | if idaapi.IDA_SDK_VERSION >= 900: 1060 | if idaapi.inf_is_64bit(): 1061 | return 64 1062 | elif idaapi.inf_is_32bit_exactly(): 1063 | return 32 1064 | else: 1065 | info = idaapi.get_inf_structure() 1066 | if info.is_64bit(): 1067 | return 64 1068 | elif info.is_32bit(): 1069 | return 32 1070 | return None 1071 | 1072 | def _defineImportThunk(self, start, thunk_val): 1073 | """If the binary has the Import Thunk filled, define it as a data chunk of appropriate size.""" 1074 | 1075 | bitness = self._getBitness() 1076 | if bitness is None: 1077 | return False 1078 | if bitness == 64: 1079 | curr_val = idc.get_qword(start) 1080 | if curr_val == thunk_val: 1081 | return ida_bytes.create_data(start, idaapi.FF_QWORD, 8, idaapi.BADADDR) 1082 | elif bitness == 32: 1083 | curr_val = ida_bytes.get_dword(start) 1084 | if curr_val == thunk_val: 1085 | return ida_bytes.create_data(start, idaapi.FF_DWORD, 4, idaapi.BADADDR) 1086 | return False 1087 | 1088 | def _loadFunctionsNames( 1089 | self, file_name: Optional[str], ext: str, loadBase: int 1090 | ) -> Optional[Tuple[int, int]]: 1091 | """Loads functions names from the given file into the internal mappings. 1092 | Fromats: CSV (default), or TAG (PE-bear, PE-sieve compatibile). 1093 | """ 1094 | 1095 | if file_name is None or len(file_name) == 0: 1096 | return None 1097 | curr_functions = self._listFunctionsAddr() 1098 | delim = "," # new delimiter (for CSV format) 1099 | delim2 = ":" # old delimiter 1100 | rva_indx = 0 1101 | cmt_indx = 1 1102 | is_imp_list = False 1103 | if ".imports.txt" in ext: 1104 | is_imp_list = True 1105 | cmt_indx = 2 1106 | if ".tag" in ext: # a TAG format was chosen 1107 | delim2 = ";" 1108 | functions = 0 1109 | comments = 0 1110 | with open(file_name, "r") as f: 1111 | for line in f.readlines(): 1112 | line = line.strip() 1113 | fn = line.split(delim) 1114 | if len(fn) < 2: 1115 | fn = line.split(delim2) # try old delimiter 1116 | if len(fn) < 2: 1117 | continue 1118 | start = 0 1119 | addr_chunk = fn[rva_indx].strip() 1120 | if not _is_hex_str(addr_chunk): 1121 | continue 1122 | try: 1123 | start = int(addr_chunk, 16) 1124 | except ValueError: 1125 | # this line doesn't start from an offset, so skip it 1126 | continue 1127 | func_name = fn[cmt_indx].strip() 1128 | if start < loadBase: # it is RVA 1129 | start = start + loadBase # convert to VA 1130 | 1131 | if is_imp_list or (start in curr_functions): 1132 | if is_imp_list: 1133 | func_name = self._stripImportName(func_name) 1134 | thunk_val = int(fn[1].strip(), 16) 1135 | self._defineImportThunk(start, thunk_val) 1136 | 1137 | if self.subDataManager.setFunctionName(start, func_name): 1138 | functions += 1 1139 | continue 1140 | 1141 | set_cmt(start, func_name, 1) # set the name as a repeatable comment 1142 | set_cmt( 1143 | start, func_name, 0 1144 | ) # make sure to overwrite the default IDA comments 1145 | comments += 1 1146 | return (functions, comments) 1147 | 1148 | def _setup_sorted_model(self, view, model) -> QtCore.QSortFilterProxyModel: 1149 | """Connects the given sorted data model with the given view.""" 1150 | 1151 | sorted_model = QtCore.QSortFilterProxyModel() 1152 | sorted_model.setDynamicSortFilter(True) 1153 | sorted_model.setSourceModel(model) 1154 | view.setModel(sorted_model) 1155 | view.setSortingEnabled(True) 1156 | # 1157 | sorted_model.setParent(view) 1158 | model.setParent(sorted_model) 1159 | return sorted_model 1160 | 1161 | def _update_current_offset(self, view, refs_model, offset) -> None: 1162 | """Update the given data model to follow given offset.""" 1163 | 1164 | if offset: 1165 | index = refs_model.findOffsetIndex(offset) 1166 | else: 1167 | index = -1 1168 | refs_model.setCurrentIndex(index) 1169 | refs_model.reset() 1170 | view.reset() 1171 | view.repaint() 1172 | 1173 | def _update_function_name(self, ea: int) -> None: 1174 | """Sets on the displayed label the name of the function and it's arguments.""" 1175 | 1176 | try: 1177 | func_info = self.funcMapper.funcAt(va_to_rva(ea)) 1178 | except KeyError: 1179 | return 1180 | 1181 | func_type = func_info.type 1182 | func_args = _getArgsDescription(ea) 1183 | func_name = _getFunctionNameAt(ea) 1184 | self.refs_label.setText(f"{func_type} {func_name} {func_args}") 1185 | 1186 | def _update_ref_tabs(self, ea: int) -> None: 1187 | """Sets on the tabs headers the numbers of references to the selected function.""" 1188 | 1189 | tocount = 0 1190 | fromcount = 0 1191 | try: 1192 | func_info = self.funcMapper.funcAt(va_to_rva(ea)) 1193 | tocount = len(func_info.refs_list) 1194 | fromcount = len(func_info.called_list) 1195 | except KeyError: 1196 | pass 1197 | self.refs_tabs.setTabText(0, "Is referred by %d:" % tocount) 1198 | self.refs_tabs.setTabText(1, "Refers to %d:" % fromcount) 1199 | 1200 | def adjustColumnsToContents(self) -> None: 1201 | """Adjusts columns' sizes to fit the data.""" 1202 | 1203 | self.addr_view.resizeColumnToContents(0) 1204 | self.addr_view.resizeColumnToContents(1) 1205 | self.addr_view.resizeColumnToContents(2) 1206 | # 1207 | self.addr_view.resizeColumnToContents(5) 1208 | self.addr_view.resizeColumnToContents(6) 1209 | self.addr_view.resizeColumnToContents(7) 1210 | 1211 | # public 1212 | # @pyqtSlot() 1213 | 1214 | def longoperationcomplete(self) -> None: 1215 | """A callback executed when the current RVA has changed.""" 1216 | global g_DataManager 1217 | 1218 | data = g_DataManager.currentRva 1219 | self.setRefOffset(data) 1220 | 1221 | def setRefOffset(self, data: Any) -> None: 1222 | """Updates the views to follow to the given RVA.""" 1223 | 1224 | if not data: 1225 | return 1226 | self._update_current_offset(self.refs_view, self.refsto_model, data) 1227 | self._update_current_offset(self.refsfrom_view, self.refsfrom_model, data) 1228 | self._update_ref_tabs(data) 1229 | self._update_function_name(data) 1230 | 1231 | def filterByColumn(self, col_num, str) -> None: 1232 | """Applies a filter defined by the string on data model.""" 1233 | 1234 | filter_type = QtCore.QRegExp.FixedString 1235 | sensitivity = QtCore.Qt.CaseInsensitive 1236 | if self.criterium_id != 0: 1237 | filter_type = QtCore.QRegExp.RegExp 1238 | self.addr_sorted_model.setFilterRegExp( 1239 | QtCore.QRegExp(str, sensitivity, filter_type) 1240 | ) 1241 | self.addr_sorted_model.setFilterKeyColumn(col_num) 1242 | 1243 | def filterChanged(self) -> None: 1244 | """A wrapper for the function: filterByColumn(self, col_num, str)""" 1245 | 1246 | self.filterByColumn(self.filter_combo.currentIndex(), self.filter_edit.text()) 1247 | 1248 | def filterKeyEvent(self, event: Any = None) -> None: 1249 | if event is not None: 1250 | QtWidgets.QLineEdit.keyReleaseEvent(self.filter_edit, event) 1251 | if event and ( 1252 | not self.is_livefilter 1253 | and event.key() != QtCore.Qt.Key_Enter 1254 | and event.key() != QtCore.Qt.Key_Return 1255 | ): 1256 | return 1257 | self.filterChanged() 1258 | 1259 | def criteriumChanged(self) -> None: 1260 | """A callback executed when the criterium of sorting has changed and the data has to be sorted again.""" 1261 | 1262 | self.criterium_id = self.criterium_combo.currentIndex() 1263 | if self.criterium_id == 0: 1264 | self.filter_edit.setPlaceholderText("keyword") 1265 | else: 1266 | self.filter_edit.setPlaceholderText("regex") 1267 | self.filterChanged() 1268 | 1269 | def liveSearchCheckBox(self) -> None: 1270 | self.is_livefilter = self.livefilter_box.isChecked() 1271 | if self.is_livefilter: 1272 | self.filterByColumn( 1273 | self.filter_combo.currentIndex(), self.filter_edit.text() 1274 | ) 1275 | 1276 | def alternateRowColors(self, enable): 1277 | self.refsfrom_view.setAlternatingRowColors(enable) 1278 | self.addr_view.setAlternatingRowColors(enable) 1279 | self.refs_view.setAlternatingRowColors(enable) 1280 | 1281 | def OnCreate(self, form) -> None: 1282 | """Called when the plugin form is created""" 1283 | 1284 | QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True) 1285 | QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) 1286 | 1287 | # init data structures: 1288 | self.funcMapper = FunctionsMapper_t() 1289 | self.criterium_id = 0 1290 | 1291 | # Get parent widget 1292 | self.parent = self.FormToPyQtWidget(form) 1293 | 1294 | # Create models 1295 | self.subDataManager = DataManager() 1296 | 1297 | self.table_model = TableModel_t(self.funcMapper.funcList) 1298 | 1299 | # init 1300 | self.addr_sorted_model = QtCore.QSortFilterProxyModel() 1301 | self.addr_sorted_model.setDynamicSortFilter(True) 1302 | self.addr_sorted_model.setSourceModel(self.table_model) 1303 | self.addr_view = FunctionsView_t( 1304 | g_DataManager, COLOR_HILIGHT_FUNC, self.table_model 1305 | ) 1306 | self.addr_view.setModel(self.addr_sorted_model) 1307 | self.addr_view.setSortingEnabled(True) 1308 | self.addr_view.setWordWrap(False) 1309 | self.addr_view.horizontalHeader().setStretchLastSection(False) 1310 | self.addr_view.verticalHeader().show() 1311 | 1312 | self.adjustColumnsToContents() 1313 | # 1314 | self.refsto_model = RefsTableModel_t(self.funcMapper.funcList, True) 1315 | self.refs_view = FunctionsView_t( 1316 | self.subDataManager, COLOR_HILIGHT_REFTO, self.refsto_model 1317 | ) 1318 | self._setup_sorted_model(self.refs_view, self.refsto_model) 1319 | self.refs_view.setColumnHidden(RefsTableModel_t.COL_TOADDR, True) 1320 | self.refs_view.setWordWrap(False) 1321 | 1322 | self.refsfrom_model = RefsTableModel_t(self.funcMapper.funcList, False) 1323 | self.refsfrom_view = FunctionsView_t( 1324 | self.subDataManager, COLOR_HILIGHT_REFFROM, self.refsfrom_model 1325 | ) 1326 | self._setup_sorted_model(self.refsfrom_view, self.refsfrom_model) 1327 | self.refsfrom_view.setColumnHidden(RefsTableModel_t.COL_TOADDR, True) 1328 | self.refsfrom_view.setWordWrap(False) 1329 | 1330 | # add a box to enable/disable live filtering 1331 | self.livefilter_box = QtWidgets.QCheckBox("Live filtering") 1332 | self.livefilter_box.setToolTip( 1333 | "If live filtering is enabled, functions are searched as you type in the edit box.\nOtherwise they are searched when you press Enter." 1334 | ) 1335 | self.livefilter_box.setChecked(self._LIVE_FILTER) 1336 | self.is_livefilter = self._LIVE_FILTER 1337 | # connect SIGNAL 1338 | self.livefilter_box.stateChanged.connect(self.liveSearchCheckBox) 1339 | 1340 | # important for proper order of objects destruction: 1341 | self.table_model.setParent(self.addr_sorted_model) 1342 | self.addr_sorted_model.setParent(self.addr_view) 1343 | 1344 | # connect SIGNAL 1345 | g_DataManager.updateSignal.connect(self.longoperationcomplete) 1346 | 1347 | # Create a Tab widget for references: 1348 | self.refs_tabs = QtWidgets.QTabWidget() 1349 | self.refs_tabs.insertTab(0, self.refs_view, "Is refered by") 1350 | self.refs_tabs.insertTab(1, self.refsfrom_view, "Refers to") 1351 | 1352 | # Create filter 1353 | self.filter_edit = QtWidgets.QLineEdit() 1354 | self.filter_edit.setPlaceholderText("keyword") 1355 | self.filter_edit.keyReleaseEvent = self.filterKeyEvent 1356 | 1357 | self.filter_combo = QtWidgets.QComboBox() 1358 | self.filter_combo.addItems(TableModel_t.header_names) 1359 | self.filter_combo.setCurrentIndex(TableModel_t.COL_NAME) 1360 | # connect SIGNAL 1361 | self.filter_combo.activated.connect(self.filterChanged) 1362 | 1363 | self.criterium_combo = QtWidgets.QComboBox() 1364 | criteria = ["contains", "matches"] 1365 | self.criterium_combo.addItems(criteria) 1366 | self.criterium_combo.setCurrentIndex(0) 1367 | # connect SIGNAL 1368 | self.criterium_combo.activated.connect(self.criteriumChanged) 1369 | 1370 | filter_panel = QtWidgets.QFrame() 1371 | filter_layout = QtWidgets.QHBoxLayout() 1372 | filter_layout.addWidget(QtWidgets.QLabel("Where ")) 1373 | filter_layout.addWidget(self.filter_combo) 1374 | filter_layout.addWidget(self.criterium_combo) 1375 | filter_layout.addWidget(self.filter_edit) 1376 | 1377 | filter_panel.setLayout(filter_layout) 1378 | filter_panel.setAutoFillBackground(True) 1379 | 1380 | self.refs_label = QtWidgets.QLabel("Function") 1381 | self.refs_label.setTextFormat(QtCore.Qt.RichText) 1382 | self.refs_label.setWordWrap(True) 1383 | 1384 | panel1 = QtWidgets.QFrame() 1385 | layout1 = QtWidgets.QVBoxLayout() 1386 | panel1.setLayout(layout1) 1387 | 1388 | layout1.addWidget(filter_panel) 1389 | layout1.addWidget(self.livefilter_box) 1390 | layout1.addWidget(self.addr_view) 1391 | layout1.setContentsMargins(0, 0, 0, 0) 1392 | 1393 | panel2 = QtWidgets.QFrame() 1394 | layout2 = QtWidgets.QVBoxLayout() 1395 | layout2.addWidget(self.refs_label) 1396 | layout2.addWidget(self.refs_tabs) 1397 | layout2.addWidget(self._makeButtonsPanel()) 1398 | layout2.setContentsMargins(0, 10, 0, 0) 1399 | panel2.setLayout(layout2) 1400 | 1401 | self.main_splitter = QtWidgets.QSplitter() 1402 | self.main_splitter.setOrientation(QtCore.Qt.Vertical) 1403 | self.main_splitter.addWidget(panel1) 1404 | self.main_splitter.addWidget(panel2) 1405 | 1406 | # Populate PluginForm 1407 | layout = QtWidgets.QVBoxLayout() 1408 | layout.addWidget(self.main_splitter) 1409 | layout.setSpacing(0) 1410 | layout.setContentsMargins(0, 0, 0, 0) 1411 | self.parent.setLayout(layout) 1412 | self.alternateRowColors(IS_ALTERNATE_ROW) 1413 | 1414 | idaapi.set_dock_pos(PLUGIN_NAME, None, idaapi.DP_RIGHT) 1415 | 1416 | def _makeButtonsPanel(self) -> QtWidgets.QFrame: 1417 | """Creates on the form's bottom the panel with buttons.""" 1418 | 1419 | buttons_panel = QtWidgets.QFrame() 1420 | buttons_layout = QtWidgets.QHBoxLayout() 1421 | buttons_panel.setLayout(buttons_layout) 1422 | 1423 | importButton = QtWidgets.QPushButton("Load names") 1424 | importButton.clicked.connect(self.importNames) 1425 | buttons_layout.addWidget(importButton) 1426 | 1427 | exportButton = QtWidgets.QPushButton("Save names") 1428 | exportButton.clicked.connect(self.exportNames) 1429 | buttons_layout.addWidget(exportButton) 1430 | return buttons_panel 1431 | 1432 | def importNames(self) -> None: 1433 | """Imports functions list from a file.""" 1434 | 1435 | file_name, ext = QtWidgets.QFileDialog.getOpenFileName( 1436 | None, 1437 | "Import functions names", 1438 | QtCore.QDir.homePath(), 1439 | "CSV Files (*.csv);;TAG Files: PE-bear, PE-sieve compatibile (*.tag);;IMPORTS.TXT: generated by PE-sieve (*.imports.txt);;All files (*)", 1440 | ) 1441 | if file_name is None or len(file_name) == 0: 1442 | return 1443 | default_base = idaapi.get_imagebase() 1444 | base_str, ok = QtWidgets.QInputDialog.getText( 1445 | None, 1446 | "Rebase tags:", 1447 | "Current module base:", 1448 | QtWidgets.QLineEdit.Normal, 1449 | hex(default_base), 1450 | ) 1451 | if not ok: 1452 | return 1453 | if _is_hex_str(base_str): 1454 | load_base = int(base_str, 16) 1455 | else: 1456 | load_base = default_base 1457 | names = self._loadFunctionsNames(file_name, ext, load_base) 1458 | if names is None: 1459 | idaapi.warning(f"Malformed file %s" % file_name) 1460 | return 1461 | (loaded, comments) = names 1462 | if loaded == 0 and comments == 0: 1463 | idaapi.warning("Failed importing functions names! Not matching offsets!") 1464 | else: 1465 | idaapi.info( 1466 | "Imported %d function names and %d comments" % (loaded, comments) 1467 | ) 1468 | 1469 | def exportNames(self) -> None: 1470 | """Exports functions list into a file.""" 1471 | 1472 | file_name, ext = QtWidgets.QFileDialog.getSaveFileName( 1473 | None, 1474 | "Export functions names", 1475 | QtCore.QDir.homePath(), 1476 | "CSV Files (*.csv);;TAG Files (*.tag)", 1477 | ) 1478 | skip_unnamed = True 1479 | if file_name is not None and len(file_name) > 0: 1480 | if not self._saveFunctionsNames(file_name, ext, skip_unnamed): 1481 | idaapi.warning("Failed exporting functions names!") 1482 | else: 1483 | idaapi.info("Exported to: " + file_name) 1484 | 1485 | def OnClose(self, form: Any) -> None: 1486 | """Called when the plugin form is closed""" 1487 | 1488 | # clear last selection 1489 | self.addr_view.hilight_addr(BADADDR) 1490 | self.refs_view.hilight_addr(BADADDR) 1491 | self.refsfrom_view.hilight_addr(BADADDR) 1492 | del self 1493 | 1494 | def Show(self) -> PluginForm.Show: 1495 | """Creates the form if not created or sets the focus if the form already exits.""" 1496 | 1497 | title = PLUGIN_NAME + " v" + __VERSION__ 1498 | return PluginForm.Show(self, title, options=PluginForm.WOPN_PERSIST) 1499 | 1500 | 1501 | # -------------------------------------------------------------------------- 1502 | class IFLMenuHandler(idaapi.action_handler_t): 1503 | """Manages menu items belonging to IFL.""" 1504 | 1505 | def __init__(self) -> None: 1506 | idaapi.action_handler_t.__init__(self) 1507 | 1508 | def activate(self, ctx: Any) -> int: 1509 | open_form() 1510 | return 1 1511 | 1512 | def update(self, ctx: Any) -> int: 1513 | return idaapi.AST_ENABLE_ALWAYS 1514 | 1515 | 1516 | # -------------------------------------------------------------------------- 1517 | def open_form() -> None: 1518 | global m_functionInfoForm 1519 | global g_DataManager 1520 | # ----- 1521 | try: 1522 | g_DataManager 1523 | except Exception: 1524 | g_DataManager = DataManager() 1525 | # ----- 1526 | try: 1527 | m_functionInfoForm 1528 | except Exception: 1529 | idaapi.msg("%s\nLoading Interactive Function List...\n" % VERSION_INFO) 1530 | m_functionInfoForm = FunctionsListForm_t() 1531 | 1532 | m_functionInfoForm.Show() 1533 | 1534 | 1535 | # -------------------------------------------------------------------------- 1536 | # IDA api: 1537 | 1538 | 1539 | class funclister_t(idaapi.plugin_t): 1540 | flags = idaapi.PLUGIN_UNL 1541 | comment = "Interactive Functions List" 1542 | 1543 | help = ( 1544 | "Interactive Function List. Comments? Remarks? Mail to: hasherezade@gmail.com" 1545 | ) 1546 | wanted_name = PLUGIN_NAME 1547 | wanted_hotkey = "" 1548 | 1549 | def init(self) -> None: 1550 | idaapi.register_action( 1551 | idaapi.action_desc_t( 1552 | "ifl:open", # action name 1553 | PLUGIN_NAME, 1554 | IFLMenuHandler(), 1555 | PLUGIN_HOTKEY, 1556 | "Opens Interactive Function List Pane", 1557 | ) 1558 | ) 1559 | 1560 | idaapi.attach_action_to_menu("View/", "ifl:open", idaapi.SETMENU_APP) 1561 | 1562 | return idaapi.PLUGIN_OK 1563 | 1564 | def run(self, arg: Any) -> None: 1565 | open_form() 1566 | 1567 | 1568 | def PLUGIN_ENTRY() -> funclister_t: 1569 | return funclister_t() 1570 | 1571 | 1572 | if __name__ == "__main__": 1573 | PLUGIN_ENTRY().init() 1574 | --------------------------------------------------------------------------------