Skip to content

Commit 5b0e5b4

Browse files
committed
Added a lot of features
- Appending a class name prefix to its virtual methods automatically - Renaming possible constructors and destructors - Creating class name folders, and move virtual methods, possible constructors and destructors into the folders (<= IDA 7.7) - Displaying a dirtree to check vftables, possible constructors and destrutctors, and class hierarchy at once (<= IDA 7.7) - Adding FUNC_LIB flag to virtual methods of known classes such as STL and MFC to make IDA recognize they are a part of legitimate static linked libraries - Added a config form when launching this - Better handling of class hierarchy - Coloring vftables of known classes in the PyClassInformer result view (<= IDA 8.3) - Added automatic dark mode detection (<= IDA 8.3) - Code-refactored a bit
1 parent ac8dcb7 commit 5b0e5b4

8 files changed

Lines changed: 312 additions & 571 deletions

File tree

README.md

Lines changed: 85 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,113 @@
1-
# PyClassInformer #
2-
#### Yet Another RTTI Parsing IDA plugin ####
1+
# PyClassInformer
2+
## Yet Another RTTI Parsing IDA plugin
33
![PyClassInformer Icon](/pyclassinformer/pci_icon.png)
44

55
PyClassInformer is an RTTI parser. Although there are several RTTI parsers such as Class Informer and SusanRTTI, and even IDA can also parse RTTI, I created this tool. It is because they cannot be used as libraries for parsing RTTI. IDA cannot easily manage class hierarchies such as checking them as a list and filtering the information, either.
66

7-
**PyClassInformer can parse RTTI on binaries compiled by MSVC++ on x86 and x64**. Since it is written in IDAPython, you can run it on IDA for Mac OS and Linux as well as Windows. You can also use results of parsing RTTI in your python code by importing this tool as a library.
7+
**PyClassInformer can parse RTTI on PE formatted binaries compiled by MSVC++ for x86, x64, ARM and ARM64**. Since it is written in IDAPython, you can run it on IDA for Mac OS and Linux as well as Windows. You can also use results of parsing RTTI in your python code by importing this tool as a library.
88

9-
### Usage ###
9+
## Usage
1010
Launch it by pressing Alt+Shift+L. Or navigate to Edit -> Plugins -> PyClassInformer.
11+
Then, select the options. In most cases, the default options should remain unchanged.
1112

12-
### Installation ###
13+
## Installation
1314
Put "pyclassinformer_plugin.py" and "pyclassinformer" folder including the files under it into the "plugins" folder of IDA's user directory ($IDAUSR).
1415

1516
See the URL if you don't know about "$IDAUSR".
1617
[https://hex-rays.com/blog/igors-tip-of-the-week-33-idas-user-directory-idausr/](https://hex-rays.com/blog/igors-tip-of-the-week-33-idas-user-directory-idausr/)
1718
[https://www.hex-rays.com/products/ida/support/idadoc/1375.shtml](https://www.hex-rays.com/products/ida/support/idadoc/1375.shtml)
1819

19-
### Requirements ###
20-
- IDA Pro 7.4 or later (I tested on 7.4 SP1, 7.5 SP3, 8.0, 9.0 SP1 and 9.1)
20+
## Requirements
21+
- IDA Pro 7.4 or later (I tested on 7.4 SP1 to 9.1)
2122
- Python 3.x (I tested on Python 3.8 and 3.10)
2223

23-
You will need at least IDA Pro 7.4 or later because of the APIs that I use.
24+
You will need at least IDA Pro 7.4 or later because of the APIs that I use. If you want to use full features, use IDA 8.3 or later. Otherwise, some features will be limited to use or skipped.
2425

25-
### Example Results ###
26+
## Features (short)
27+
- Display class names, vftables and class hierarchies as a list
28+
- Display RTTI parsed results on the Output window
29+
- Display vftables, class names, virtual methods, possilbe constructors and destructors, and class hierarchies as a dir tree (IDA 7.7 or later)
30+
- Create directories for classes and move virtual methods to them in Functions and Names subviews (IDA 7.7 or later)
31+
- Move functions refer vftables to "possible ctors or dtors" folder under each class directory in Functions and Names subviews (IDA 7.7 or later)
32+
- Rename virtual methods by appending class names to them
33+
- Add the FUNC_LIB flag to methods that known classes own
34+
- Rename possible constructors and destructors
35+
- Coloring known class names and their methods on the list and the tree widgets
36+
37+
## Features in detail
38+
### Default output
2639
![PyClassInformer Result](/images/result.png)
27-
The figure above is an example of PyClassInformer result. And the figure below is an example of the original Class Informer result.
28-
As you see, almost all columns are matched with the original ones.
40+
The image above is an example of PyClassInformer result. And the image below is an example of the original Class Informer result.
41+
42+
![Original ClassInformer Result](/images/orig_class_informer.png)
43+
44+
As you see, almost all columns match the original ones.
2945

3046
In addition, PyClassInformer has two more columns. One is "offset", which shows the offset of a vftable in a class layout.
3147

32-
Another one named "Hierarchy Order" shows class hierarchy information related to a vftable of a line. The column shows the order of inheritance from the class to the top-most super class.
48+
Another one named "Hierarchy Order" shows class hierarchy information related to a vftable of a line. The column shows the order of inheritance from the class to the top-most super class.
3349

34-
These are useful for grasping class layouts and class hierarchies. Double-clicking a line navigates to its vftable address as weiil.
35-
36-
![Original ClassInformer Result](/images/orig_class_informer.png)
37-
If you check the Output subview, you will also see parsed RTTI information such as Complete Object Locator as COL, Class Hierarchy Descriptor as CHD and Base Class Descriptor as BCD with their addresses. They are useful for checking more details and debugging.
50+
These are useful for grasping class layouts and class hierarchies. Double-clicking a line navigates to its vftable address as well.
3851

52+
### RTTI parsed results
53+
If you check the Output window, you will also see parsed RTTI information such as Complete Object Locator as COL, Class Hierarchy Descriptor as CHD and Base Class Descriptor as BCD with their addresses. They are useful for checking more details and debugging.
54+
3955
![Class Hierarchy](/images/class_hierarchy.png)
56+
4057
You will also see class hierarchies by checking indents of BCDs. For example, CMFCComObject, which is the class for the vftable at 0x530fcc, inherits ATL::CAccessibleProxy. And ATL::CAccessibleProxy inherits three super classes, ATL::CComObjectRootEx, ATL::IAccessibleProxyImpl and IOleWindow. Like this, you can get class hierarchy information as a form of a tree.
4158

42-
### Note ###
59+
### Automatic renaming
60+
PyClassInformer can automatically append class names to their virtual method names. Therefore, you can easily find them by filtering the class name. The image below is a result appending a class name "CDC" to its methods.
61+
62+
![automatically renaming virtual methods](/images/auto_renmaing.png)
63+
64+
PyClassInformer can also rename functions that refer to vftables to "class name" + "_possible_ctor_or_dtor". The image below is a result. Although some false positives will occur due to inlined ctors and dtors, and dynamic initializers, this feature is still useful to find them.
65+
66+
![automatically renaming possible ctors and dtors](/images/auto_renmaing2.png)
67+
68+
### Virtual method classification (<= IDA 7.7)
69+
The detected methods are moved to each class folder in Functions and Names subviews.
70+
> [!NOTE]
71+
> This is only available IDA 7.7 or later.
72+
73+
![method classifications](/images/classification.png)
74+
75+
PyClassInformer also displays a new widget named "Method Classifier". It lists all detected classes, vftables, virtual methods and possible constructors and destructors, and class herarchies at once as a form of a tree.
76+
77+
![method classifier](/images/method_classifier.png)
78+
79+
> [!TIP]
80+
> Class hierarchies are represented as directories in Method Classifier.
81+
> Unfortunaltely, IDA's quick filter feature cannot filter directory contents.
82+
> To search them, use text search feture (Ctrl+T (find first text) and Alt+T (Find next text)).
83+
> For example, input a class name, a single space, and a parenthesis like "CWinApp (".
84+
85+
> [!NOTE]
86+
> This is only available IDA 7.7 or later.
87+
88+
### Known classes detection (<= IDA 8.3)
89+
PyClassInformer can color known class names for easily finding user-defined classes.
90+
The image below is an example of a coloring result.
91+
You can easily find CSimpleTestApp, CSimpleTestDoc, CSimpleTestView and CSimpleTestCtrlItem are user-defined classes. So you can forcus on checking them.
92+
93+
![Class coloring](/images/coloring.png)
94+
95+
The coloring is also applied to Method Classifier widget. Therefore, you can easily find overridden virtual methods like the image below.
96+
![Methods coloring](/images/overridden_methods.png)
97+
98+
> [!NOTE]
99+
> The coloring feature is only available IDA 8.3 or later.
100+
101+
Known class names are defined in "lib_classes.json". I added many patterns related to STL, which starts with "std::", and several versions of MFC Application with MFC Application Wizard.
102+
If you find some additional legitimate classes, you can add them to it.
103+
104+
PyClassInformer also adds the FUNC_LIB flag to the methods that match the list. Therefore, you can recognize they are a part of static linked libraries.
105+
The following images are before and after PyClassInformer execution. Many known class methods are found and IDA can recognize them a part of static linked libraries.
106+
107+
![Methods coloring](/images/before_libflag_applied.png)
108+
![Methods coloring](/images/after_libflag_applied.png)
109+
110+
## Note
43111
- I **WILL NOT** support parsing GCC's RTTI. **DO NOT** open an issue about it.
44112
- I **WILL NOT** support beta versions of IDA. **DO NOT** open an issue about it.
45113
- Some code is from SusanRTTI and the output table is similar to Class Informer.

images/result.png

16.4 KB
Loading

pyclassinformer/pci_chooser.py

Lines changed: 34 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -7,64 +7,67 @@
77

88
class pci_chooser_t(ida_kernwin.Choose):
99

10-
def __init__(self, title, data, icon=-1):
10+
def __init__(self, title, data, icon=-1, libcolor=0xffffffe9, defcolor=0xffffffff):
1111
ida_kernwin.Choose.__init__(
1212
self,
1313
title,
1414
[
15-
["Vftable", 10 | ida_kernwin.Choose.CHCOL_HEX],
15+
["Vftable", 10 | ida_kernwin.Choose.CHCOL_EA],
1616
["Methods", 4 | ida_kernwin.Choose.CHCOL_DEC],
1717
["Flags", 4 | ida_kernwin.Choose.CHCOL_PLAIN],
1818
["Type", 30 | ida_kernwin.Choose.CHCOL_PLAIN],
1919
["Hierarchy", 50 | ida_kernwin.Choose.CHCOL_PLAIN],
2020
["Offset", 4 | ida_kernwin.Choose.CHCOL_HEX],
2121
["Hierarchy Order", 50 | ida_kernwin.Choose.CHCOL_PLAIN],
2222
],
23-
flags=ida_kernwin.CH_MULTI,
23+
flags=ida_kernwin.CH_MULTI|ida_kernwin.CH_ATTRS,
2424
icon=icon
2525
)
2626
self.items = [
2727
[
2828
hex(vftable_ea),
29-
"{}".format(len([x for x in pci_utils.get_vtbl_methods(vftable_ea)])),
29+
"{}".format(len(data[vftable_ea].vfeas)),
3030
data[vftable_ea].chd.flags,
3131
data[vftable_ea].name,
3232
self.get_hierarychy(data, vftable_ea),
3333
hex(data[vftable_ea].offset),
3434
self.get_hierarychy_order(data, vftable_ea),
35+
data[vftable_ea].libflag,
3536
vftable_ea
3637
] for vftable_ea in data
3738
]
39+
self.libcolor = libcolor
40+
self.defcolor = defcolor
41+
self.libflag = data[next(iter(data))].LIBLIB
3842

3943
def get_hierarychy(self, data, vftable_ea):
4044
col = data[vftable_ea]
41-
col_offs, curr_off = u.get_col_offs(col, data)
45+
# get the actual base classes mainly for multiple inheritance
46+
bases = u.get_mdisp_bases(col, data)
47+
4248
result = "{}: ".format(col.name)
43-
if len(col.chd.bca.bases) > 0:
44-
idx = 0
45-
if col.chd.bca.bases[0].name == col.name:
46-
idx = 1
49+
if len(bases) > 0:
50+
# replace the class name with the first BCD's class name if they are different from each other.
51+
# it occurs when the class is multiple inheritance with multiple vftables
52+
i = 1
53+
if bases[0].name != col.name:
54+
result = "{}: ".format(bases[0].name)
55+
#i = 0
56+
4757
# get the result related to the offset of the COL
48-
result += ", ".join([x.name for x in col.chd.bca.bases[idx:] if u.does_bcd_append(col_offs, x, curr_off)]) + ";" if len(col.chd.bca.bases) > 1 else ""
58+
result += ", ".join([x.name for x in bases][i:]) + ";" if len(bases) > 1 else ""
4959
return result
5060

5161
def get_hierarychy_order(self, data, vftable_ea):
5262
col = data[vftable_ea]
53-
col_offs, curr_off = u.get_col_offs(col, data)
5463
result = []
5564
if len(col.chd.bca.bases) > 0:
5665
for off in col.chd.bca.paths:
57-
target_off = off
58-
if off not in col_offs:
59-
# sometimes, mdisp is not included in COLs.
60-
# in those cases, get the least offset in COLs and it is treated as the offset.
61-
target_off = 0
62-
if len(col_offs) > 0:
63-
target_off = sorted(col_offs)[0]
66+
# output each path
6467
for p in col.chd.bca.paths[off]:
65-
#if curr_off == target_off or col.chd.flags.find("V") >= 0 or len(list(filter(lambda x: x.pdisp >= 0, p))) > 0:
66-
if curr_off == target_off:
67-
result.append("{:#x}: ".format(off) + " -> ".join([x.name + " ({},{},{})".format(x.mdisp, x.pdisp, x.vdisp) for x in p]))
68+
# get only the target COL related result
69+
if col.offset == off:
70+
result.append(" -> ".join([x.name + " ({},{},{})".format(x.mdisp, x.pdisp, x.vdisp) for x in p]))
6871

6972
return ", ".join(result)
7073

@@ -93,8 +96,16 @@ def OnGetEA(self, n):
9396
n = n[0]
9497
return self.items[n][-1]
9598

99+
def OnGetLineAttr(self, n):
100+
# change the line color if a class is a part of static linked libraries
101+
vftable_ea = self.items[n][-1]
102+
color = self.defcolor
103+
if self.items[n][-2] == self.libflag:
104+
color = self.libcolor
105+
return (color, 0)
106+
96107

97-
def show_pci_chooser_t(data, icon=-1, modal=False):
98-
c = pci_chooser_t("[PyClassInformer]", data, icon)
108+
def show_pci_chooser_t(data, icon=-1, modal=False, libcolor=0xffffffe9, defcolor=0xffffffff):
109+
c = pci_chooser_t("[PyClassInformer]", data, icon, libcolor, defcolor)
99110
c.Show(modal=modal)
100111

pyclassinformer/pci_icon.png

100644100755
File mode changed.

pyclassinformer/pci_utils.py

Lines changed: 45 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,15 @@ def __init__(self):
4949
self.data = ida_segment.get_segm_by_name(".data")
5050
self.rdata = ida_segment.get_segm_by_name(".rdata")
5151
# try to use rdata if there actually is an rdata segment, otherwise just use data
52-
if self.rdata is not None:
52+
if self.rdata is not None and self.data is not None:
5353
self.valid_ranges = [(self.rdata.start_ea, self.rdata.end_ea), (self.data.start_ea, self.data.end_ea)]
54+
# fail safe for renaming segment names
5455
else:
55-
self.valid_ranges = [(self.data.start_ea, self.data.end_ea)]
56+
self.valid_ranges = []
57+
for n in range(ida_segment.get_segm_qty()):
58+
seg = ida_segment.getnseg(n)
59+
if seg and ida_segment.get_segm_class(seg) == "DATA" and seg and not seg.is_header_segm():
60+
self.valid_ranges.append((seg.start_ea, seg.end_ea))
5661

5762
self.x64 = (ida_segment.getnseg(0).bitness == 2)
5863
if self.x64:
@@ -65,6 +70,18 @@ def __init__(self):
6570
self.REF_OFF = ida_nalt.REF_OFF32
6671
self.PTR_SIZE = 4
6772
self.get_ptr = ida_bytes.get_32bit
73+
74+
@staticmethod
75+
def get_data_segments():
76+
for n in range(ida_segment.get_segm_qty()):
77+
seg = ida_segment.getnseg(n)
78+
if seg and ida_segment.get_segm_class(seg) == "DATA" and seg and not seg.is_header_segm():
79+
yield seg
80+
81+
def update_valid_ranges(self):
82+
self.valid_ranges = []
83+
for seg in utils.get_data_segments():
84+
self.valid_ranges.append((seg.start_ea, seg.end_ea))
6885

6986
# for 32-bit binaries, the RTTI structs contain absolute addresses, but for
7087
# 64-bit binaries, they're offsets from the image base.
@@ -73,31 +90,7 @@ def x64_imagebase(self):
7390
return ida_nalt.get_imagebase()
7491
else:
7592
return 0
76-
77-
def mt_rva(self):
78-
ri = ida_nalt.refinfo_t()
79-
ri.flags = self.REF_OFF|ida_nalt.REFINFO_RVAOFF
80-
ri.target = 0
81-
mt = ida_nalt.opinfo_t()
82-
mt.ri = ri
83-
return mt
84-
85-
def mt_address(self):
86-
ri = ida_nalt.refinfo_t()
87-
ri.flags = self.REF_OFF
88-
ri.target = 0
89-
mt = ida_nalt.opinfo_t()
90-
mt.ri = ri
91-
return mt
92-
93-
def mt_ascii(self):
94-
ri = ida_nalt.refinfo_t()
95-
ri.flags = ida_nalt.STRTYPE_C
96-
ri.target = -1
97-
mt = ida_nalt.opinfo_t()
98-
mt.ri = ri
99-
return mt
100-
93+
10194
def get_strlen(self, addr, max_len=500):
10295
# 50 is sometimes too short. I increased a number here.
10396
strlen = 0
@@ -165,9 +158,8 @@ def set_ptr_or_rva_member(self, sid, mname, mtype_name, array=False, idx=-1):
165158
tif = ida_typeinf.tinfo_t()
166159
tif.get_named_type(None, sname)
167160
udt = ida_typeinf.udt_type_data_t()
168-
if tif.get_udt_details(udt):
169-
tif.set_udm_type(idx, mtif, 0, r)
170-
tif.get_udt_details(udt)
161+
tif.set_udm_type(idx, mtif, 0, r)
162+
tif.get_udt_details(udt)
171163
else:
172164
s = ida_struct.get_struc(sid)
173165
ida_struct.set_member_tinfo(s, s.get_member(idx), 0, mtif, 0)
@@ -181,23 +173,7 @@ def get_moff_by_name(struc, name):
181173
# for ida 9.0
182174
offset = get_member_by_name(struc, name).offset // 8
183175
return offset
184-
185-
@staticmethod
186-
def does_bcd_append(col_offs, bcd, curr_off):
187-
append = False
188-
# for single inheritance
189-
if len(col_offs) <= 1:
190-
append = True
191-
# for multiple inheritance
192-
elif bcd.mdisp in col_offs:
193-
if bcd.mdisp == curr_off:
194-
append = True
195-
# for items that are not matched with COL offsets
196-
# they are treated as a part of COL offset 0
197-
elif curr_off == 0:
198-
append = True
199-
return append
200-
176+
201177
@staticmethod
202178
def get_col_offs(col, vftables):
203179
# get offsets in COLs by finding xrefs for multiple inheritance
@@ -209,8 +185,27 @@ def get_col_offs(col, vftables):
209185
cols = list(filter(lambda x: x.ea in coleas, vftables.values()))
210186
# get the offsets in COLs
211187
col_offs = [ida_bytes.get_32bit(x.ea+utils.get_moff_by_name(x.struc, "offset")) for x in cols]
212-
curr_off = col.offset
213-
return col_offs, curr_off
188+
return col_offs
189+
190+
@staticmethod
191+
def get_mdisp_bases(col, vftables):
192+
# for checking if a class has multiple vftables or not
193+
col_offs = utils.get_col_offs(col, vftables)
194+
195+
bases = []
196+
for path in col.chd.bca.paths[col.offset]:
197+
append = False
198+
for bcd in path:
199+
# for SI and MI but there is only a vftable
200+
if len(col_offs) < 2:
201+
append = True
202+
# for MI and there are multiple vftables
203+
elif bcd.mdisp == col.offset:
204+
append = True
205+
# if append flag is enabled, append it and subsequent BCDs after it
206+
if append and bcd not in bases:
207+
bases.append(bcd)
208+
return bases
214209

215210

216211
def add_struc_by_name_and_def(name, struc_def):
@@ -237,6 +232,7 @@ def build_udm(name, msize=0, mtype=ida_typeinf.BTF_INT, moffset=-1, vrepr=None):
237232

238233
return udm
239234

235+
240236
def add_struc(name):
241237
tif = ida_typeinf.tinfo_t()
242238
udt = ida_typeinf.udt_type_data_t()

0 commit comments

Comments
 (0)