├── .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 | [](https://github.com/hasherezade/ida_ifl/releases)
4 | [](https://github.com/hasherezade/ida_ifl/releases)
5 |
6 | [](https://github.com/hasherezade/ida_ifl/commits)
7 | [](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 | 
27 |
28 | Light theme:
29 |
30 | 
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 |
--------------------------------------------------------------------------------