├── LICENSE.md ├── README.md ├── functions_plus.py └── tree-example.png /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Arthur Gerkis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Functions+ 2 | 3 | IDA Pro plugin to make functions tree view. Plugin parses function names and groups them by namespaces. 4 | 5 | ![Functions+](https://github.com/ax330d/functions-plus/raw/master/tree-example.png "Functions+") 6 | 7 | Currently does not support search, is not possible to sort, no context menu. 8 | 9 | You can enable displaying function attributes in the script by setting `show_extra_fields` to `True`. 10 | 11 | ## Usage 12 | 13 | Use it as any other plugin: Alt+F7. 14 | -------------------------------------------------------------------------------- /functions_plus.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | ''' 4 | Functions+ IDA Pro plugin -- alternative version of functions window. 5 | 6 | Splits functions names and groups by namespaces. 7 | ''' 8 | 9 | import re 10 | from collections import OrderedDict 11 | 12 | # import cProfile 13 | # import pstats 14 | # import StringIO 15 | 16 | import idaapi 17 | import idc 18 | import idautils 19 | 20 | from idaapi import PluginForm 21 | from PyQt5 import QtWidgets, QtGui 22 | 23 | __author__ = 'xxxzsx, Arthur Gerkis' 24 | __version__ = '1.0.1' 25 | 26 | 27 | class FunctionState(object): 28 | ''' 29 | Holds the state of the current function. 30 | ''' 31 | 32 | def __init__(self): 33 | self._args = '' 34 | self._flags = 0 35 | self._addr = 0 36 | 37 | @property 38 | def args(self): 39 | ''' 40 | Returns args property. 41 | ''' 42 | return self._args 43 | 44 | @args.setter 45 | def args(self, value): 46 | ''' 47 | Sets args property. 48 | ''' 49 | self._args = value 50 | 51 | @property 52 | def flags(self): 53 | ''' 54 | Returns flags property. 55 | ''' 56 | return self._flags 57 | 58 | @flags.setter 59 | def flags(self, value): 60 | ''' 61 | Sets flags property. 62 | ''' 63 | self._flags = value 64 | 65 | @property 66 | def addr(self): 67 | ''' 68 | Returns addr property. 69 | ''' 70 | return self._addr 71 | 72 | @addr.setter 73 | def addr(self, value): 74 | ''' 75 | Sets addr property. 76 | ''' 77 | self._addr = value 78 | 79 | 80 | class FunctionData(object): 81 | ''' 82 | Holds data of the function. 83 | ''' 84 | 85 | def __init__(self, state): 86 | self._args = state.args 87 | self._flags = state.flags 88 | self._addr = state.addr 89 | 90 | @property 91 | def args(self): 92 | ''' 93 | Returns args property. 94 | ''' 95 | return self._args 96 | 97 | @property 98 | def flags(self): 99 | ''' 100 | Returns flags property. 101 | ''' 102 | return self._flags 103 | 104 | @property 105 | def addr(self): 106 | ''' 107 | Returns ea property. 108 | ''' 109 | return self._addr 110 | 111 | 112 | class Cols(object): 113 | ''' 114 | Class which is responsible for handling columns. 115 | ''' 116 | def __init__(self, show_extra_fields): 117 | self.addr = None 118 | self.flags = None 119 | self.show_extra_fields = show_extra_fields 120 | self.names = [ 121 | 'Name', 'Address', 'Segment', 'Length', 'Locals', 'Arguments' 122 | ] 123 | 124 | self.handlers = { 125 | 0: lambda: None, 126 | 1: lambda: self.fmt(self.addr), 127 | 2: lambda: '{}'.format(idc.get_segm_name(self.addr)), 128 | 3: lambda: self.fmt(idc.get_func_attr(self.addr, idc.FUNCATTR_END) - self.addr), 129 | 4: lambda: self.fmt(idc.get_func_attr(self.addr, idc.FUNCATTR_FRSIZE)), 130 | 5: lambda: self.fmt(idc.get_func_attr(self.addr, idc.FUNCATTR_ARGSIZE)) 131 | } 132 | 133 | if self.show_extra_fields: 134 | self.names.extend(['R', 'F', 'L', 'S', 'B', 'T', '=']) 135 | # TODO: add Lumina column info 136 | self.handlers.update({ 137 | 6: lambda: self.is_true(not self.flags & idc.FUNC_NORET, 'R'), 138 | 7: lambda: self.is_true(self.flags & idc.FUNC_FAR, 'F'), 139 | 8: lambda: self.is_true(self.flags & idc.FUNC_LIB, 'L'), 140 | 9: lambda: self.is_true(self.flags & idc.FUNC_STATIC, 'S'), 141 | 10: lambda: self.is_true(self.flags & idc.FUNC_FRAME, 'B'), 142 | 11: lambda: self.is_true(idc.get_type(self.addr), 'T'), 143 | 12: lambda: self.is_true(self.flags & idc.FUNC_BOTTOMBP, '=') 144 | }) 145 | 146 | def set_data(self, addr, flags): 147 | ''' 148 | Sets data actual for the current function. 149 | ''' 150 | self.addr = addr 151 | self.flags = flags 152 | 153 | def item(self, index): 154 | ''' 155 | Gets the data according to requested col index. 156 | ''' 157 | 158 | return self.handlers[index]() 159 | 160 | def is_true(self, flag, char): 161 | ''' 162 | Wrapper to conform IDA default UI view. 163 | ''' 164 | if flag: 165 | return char 166 | return '.' 167 | 168 | def fmt(self, value): 169 | ''' 170 | Wrapper to conform IDA default UI view. 171 | ''' 172 | return '{:08X}'.format(value) 173 | 174 | 175 | class FunctionsTree(object): 176 | ''' 177 | Builds tree of functions with all relevant information. 178 | ''' 179 | def __init__(self): 180 | self.chunks_regexp = re.compile(r'(.*?)(?:|\((.*?)\))$') 181 | self.simple_regexp = re.compile(r'^[a-zA-Z0-9_]*$') 182 | 183 | def get(self): 184 | ''' 185 | Returns functions tree. 186 | ''' 187 | 188 | functions_list = self.get_list_of_functions() 189 | functions_tree = self.build_functions_tree(functions_list) 190 | 191 | return functions_tree 192 | 193 | def get_list_of_functions(self): 194 | ''' 195 | Gets all functions list. 196 | ''' 197 | 198 | functions_list = {} 199 | seg_ea = idc.get_segm_by_sel(idc.SEG_NORM) 200 | 201 | for func_ea in idautils.Functions(idc.get_segm_start(seg_ea), 202 | idc.get_segm_end(seg_ea)): 203 | function_name = idc.get_func_name(func_ea) 204 | functions_list[function_name] = func_ea 205 | 206 | return functions_list 207 | 208 | def build_functions_tree(self, functions_list): 209 | ''' 210 | Builds tree of functions. 211 | ''' 212 | 213 | func_state = FunctionState() 214 | functions_tree = OrderedDict() 215 | 216 | for function_name in sorted(functions_list): 217 | func_state.args = '' 218 | func_state.addr = functions_list[function_name] 219 | func_state.flags = \ 220 | idc.get_func_attr(func_state.addr, idc.FUNCATTR_FLAGS) 221 | demangled_name = self.maybe_demangle(function_name) 222 | chunks = self.get_chunks(demangled_name, func_state) 223 | self.maybe_push(chunks, functions_tree, func_state) 224 | 225 | return functions_tree 226 | 227 | def maybe_push(self, chunks, functions_tree, func_state): 228 | ''' 229 | Adds new function name or properties to the tree. 230 | ''' 231 | 232 | # FIXME: handle duplicate entries properly 233 | if isinstance(functions_tree, FunctionData): 234 | return 235 | 236 | name = chunks.pop(0) 237 | if not len(name): 238 | return 239 | 240 | # If this is the last (or one) chunk 241 | if not len(chunks): 242 | functions_tree[name + func_state.args] = FunctionData(func_state) 243 | return 244 | 245 | # If this is a new namespace, create a tree 246 | if name not in functions_tree: 247 | functions_tree[name] = OrderedDict() 248 | 249 | return self.maybe_push(chunks, functions_tree[name], func_state) 250 | 251 | def get_chunks(self, func_string, func_state): 252 | ''' 253 | Splits function name by namespaces. 254 | ''' 255 | 256 | new_chunks = [] 257 | matches = re.match(self.chunks_regexp, func_string) 258 | if not matches: 259 | return [] 260 | 261 | args = '' 262 | if matches.group(2): 263 | args = '({})'.format(matches.group(2)) 264 | func_state.args = args 265 | 266 | chunks = list(matches.group(1)) 267 | if chunks[0] == '`': 268 | return [matches.group(1)] 269 | 270 | open_left_tpl = 0 271 | tmp_chunk = '' 272 | for chunk in chunks: 273 | if chunk == ':' and open_left_tpl == 0: 274 | if len(tmp_chunk): 275 | new_chunks.append(tmp_chunk) 276 | tmp_chunk = '' 277 | continue 278 | if chunk == '<': 279 | open_left_tpl += 1 280 | if chunk == '>': 281 | open_left_tpl -= 1 282 | tmp_chunk += chunk 283 | new_chunks.append(tmp_chunk) 284 | return new_chunks 285 | 286 | def maybe_demangle(self, function_name): 287 | ''' 288 | Demangles name of required. 289 | ''' 290 | 291 | if function_name.find('@') != -1: 292 | function_name = self.demangle(function_name) 293 | return function_name 294 | 295 | @classmethod 296 | def demangle(cls, name): 297 | ''' 298 | Demangles name. 299 | ''' 300 | 301 | mask = idc.get_inf_attr(idc.INF_SHORT_DN) 302 | demangled = idc.demangle_name(name, mask) 303 | if demangled is None: 304 | return name 305 | return demangled 306 | 307 | 308 | class FunctionsPlus(PluginForm): 309 | '''Functions+ plugin.''' 310 | 311 | def __init__(self): 312 | super(FunctionsPlus, self).__init__() 313 | if idc.get_inf_attr(idc.INF_PROCNAME).lower() != 'metapc': 314 | print('Functions+ warning: not tested in this configuration') 315 | self.tree = None 316 | self.icon = 135 317 | # Enable this if you want to see extra information about function 318 | self.show_extra_fields = False 319 | self.cols = Cols(self.show_extra_fields) 320 | 321 | def _populate_tree(self): 322 | ''' 323 | Populates functions tree. 324 | ''' 325 | self.tree.clear() 326 | self._build_tree(FunctionsTree().get(), self.tree) 327 | return 328 | 329 | def _build_tree(self, function_tree, root): 330 | ''' 331 | Builds Qt Widget tree. 332 | ''' 333 | 334 | if not function_tree: 335 | return 336 | 337 | if isinstance(function_tree, FunctionData): 338 | self._handle_function_data_instance(function_tree, root) 339 | return 340 | 341 | for name, tree in sorted(function_tree.items()): 342 | func_item = QtWidgets.QTreeWidgetItem(root) 343 | if not isinstance(tree, FunctionData): 344 | name = self._handle_class_name(tree, name, func_item) 345 | func_item.setText(0, name) 346 | self._build_tree(tree, func_item) 347 | 348 | def _handle_class_name(self, tree, name, func_item): 349 | ''' 350 | Handles class name. 351 | ''' 352 | 353 | tree_keys_len = len(list(tree.keys())) 354 | name = '{} ({} {})'.\ 355 | format(name, tree_keys_len, self._get_word(tree_keys_len)) 356 | font = QtGui.QFont() 357 | font.setBold(True) 358 | func_item.setFont(0, font) 359 | return name 360 | 361 | def _handle_function_data_instance(self, function_tree, root): 362 | ''' 363 | Handles FunctionData instance. 364 | ''' 365 | 366 | flags = int(function_tree.flags) 367 | addr = function_tree.addr 368 | 369 | self.cols.set_data(addr, flags) 370 | 371 | for index in range(0, len(self.cols.names)): 372 | if index > 0: 373 | root.setText(index, self.cols.item(index)) 374 | if flags & idc.FUNC_THUNK: 375 | root.setBackground(index, QtGui.QColor('#E8DAEF')) 376 | if flags & idc.FUNC_LIB: 377 | root.setBackground(index, QtGui.QColor('#D1F2EB')) 378 | 379 | def _get_word(self, len): 380 | ''' 381 | Gets proper word for number. 382 | ''' 383 | word = 'items' 384 | if len % 10 == 1 and len % 100 != 11: 385 | word = 'item' 386 | return word 387 | 388 | def _dblclick(self, item): 389 | ''' 390 | Handles double click event. 391 | ''' 392 | try: 393 | idaapi.jumpto(int(item.text(1), 16)) 394 | except: 395 | pass 396 | 397 | def OnCreate(self, form): 398 | ''' 399 | Called when the plugin form is created. 400 | ''' 401 | 402 | # pr = cProfile.Profile() 403 | # pr.enable() 404 | 405 | parent = self.FormToPyQtWidget(form) 406 | 407 | self.tree = QtWidgets.QTreeWidget() 408 | self.tree.setColumnCount(len(self.cols.names)) 409 | self.tree.setHeaderLabels(self.cols.names) 410 | self.tree.itemDoubleClicked.connect(self._dblclick) 411 | # self.tree.resizeColumnToContents(True) 412 | 413 | layout = QtWidgets.QVBoxLayout() 414 | layout.addWidget(self.tree) 415 | layout.setSpacing(0) 416 | layout.setContentsMargins(0, 0, 0, 0) 417 | 418 | self._populate_tree() 419 | 420 | self.tree.setColumnWidth(0, 512) 421 | for index in range(6, len(self.cols.names)): 422 | self.tree.setColumnWidth(index, 32) 423 | self.tree.setAlternatingRowColors(True) 424 | 425 | parent.setLayout(layout) 426 | 427 | # pr.disable() 428 | # s = StringIO.StringIO() 429 | # ps = pstats.Stats(pr, stream=s).sort_stats('cumulative') 430 | # ps.print_stats() 431 | # print(s.getvalue()) 432 | 433 | def OnClose(self, form): 434 | ''' 435 | Called when the plugin form is closed. 436 | ''' 437 | del self 438 | 439 | def Show(self): 440 | ''' 441 | Creates the form is not created or focuses it if it was. 442 | ''' 443 | return PluginForm.Show(self, 'Functions+') 444 | 445 | 446 | class FunctionsPlusPlugin(idaapi.plugin_t): 447 | flags = idaapi.PLUGIN_KEEP 448 | comment = "Functions+" 449 | 450 | help = "" 451 | wanted_name = "Functions+" 452 | wanted_hotkey = "" 453 | 454 | # @staticmethod 455 | # def init(): 456 | # return idaapi.PLUGIN_KEEP 457 | 458 | @staticmethod 459 | def init(): 460 | funp = FunctionsPlus() 461 | funp.Show() 462 | return idaapi.PLUGIN_KEEP 463 | 464 | @staticmethod 465 | def run(arg=0): 466 | funp = FunctionsPlus() 467 | funp.Show() 468 | 469 | @staticmethod 470 | def term(): 471 | pass 472 | 473 | def PLUGIN_ENTRY(): 474 | return FunctionsPlusPlugin() 475 | -------------------------------------------------------------------------------- /tree-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ax330d/functions-plus/a0eac3333fbd1a8df86fcdbb0eb0fbbc7a92990f/tree-example.png --------------------------------------------------------------------------------