├── .gitignore ├── README.md ├── auto_re.py └── docs ├── auto_rename_dst.png ├── auto_rename_src.png ├── function_rename.png ├── tags_in_unexplored_code.png ├── tags_view_0.png └── tags_view_1.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | /.idea 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Join the chat at https://gitter.im/auto_re/Lobby](https://badges.gitter.im/auto_re/Lobby.svg)](https://gitter.im/auto_re/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 2 | 3 | Features 4 | ======== 5 | 6 | ## 1. Auto-renaming dummy-named functions, which have one API call or jump to the imported API 7 | 8 | ### Before 9 | ![auto_rename_src.png](docs/auto_rename_src.png) 10 | 11 | ### After 12 | ![auto_rename_dst.png](docs/auto_rename_dst.png) 13 | 14 | 15 | ## 2. Assigning TAGS to functions accordingly to called API-indicators inside 16 | 17 | * Sets tags as repeatable function comments and displays TAG tree in the separate view 18 | 19 | 20 | Some screenshots of TAGS view: 21 | 22 | ![tags_view_0.png](docs/tags_view_0.png) 23 | 24 | ![tags_view_1.png](docs/tags_view_1.png) 25 | 26 | How TAGs look in unexplored code: 27 | ![tags_in_unexplored_code.png](docs/tags_in_unexplored_code.png) 28 | 29 | 30 | You can easily rename function using its context menu or just pressing `n` hotkey: 31 | 32 | ![function_rename.png](docs/function_rename.png) 33 | 34 | # Installation 35 | 36 | Just copy `auto_re.py` to the `IDA\plugins` directory and it will be available through `Edit -> Plugins -> Auto RE` menu -------------------------------------------------------------------------------- /auto_re.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | __author__ = 'Trafimchuk Aliaksandr' 3 | __version__ = '1.9' 4 | 5 | from collections import defaultdict 6 | import idaapi 7 | from idautils import FuncItems, CodeRefsTo 8 | from idaapi import o_reg, o_imm, o_far, o_near, o_mem, o_displ 9 | import os 10 | import re 11 | import sys 12 | import traceback 13 | 14 | 15 | HAS_PYSIDE = idaapi.IDA_SDK_VERSION < 690 16 | if HAS_PYSIDE: 17 | from PySide import QtGui, QtCore 18 | from PySide.QtGui import QTreeView, QVBoxLayout, QLineEdit, QMenu, QInputDialog, QAction, QTabWidget 19 | else: 20 | from PyQt5 import QtGui, QtCore 21 | from PyQt5.QtWidgets import QTreeView, QVBoxLayout, QLineEdit, QMenu, QInputDialog, QAction, QTabWidget 22 | 23 | 24 | try: 25 | # Python 2. 26 | xrange 27 | except NameError: 28 | # Python 3. 29 | xrange = range 30 | 31 | 32 | # enable to allow PyCharm remote debug 33 | RDEBUG = False 34 | # adjust this value to be a full path to a debug egg 35 | RDEBUG_EGG = r'c:\Program Files\JetBrains\PyCharm 2017.1.4\debug-eggs\pycharm-debug.egg' 36 | RDEBUG_HOST = 'localhost' 37 | RDEBUG_PORT = 12321 38 | 39 | 40 | TAGS_IGNORE_LIST = { 41 | 'OpenProcessToken', 42 | 'DisconnectNamedPipe' 43 | } 44 | 45 | IGNORE_CALL_LIST = { 46 | 'RtlNtStatusToDosError', 47 | 'GetLastError', 48 | 'SetLastError' 49 | } 50 | 51 | TAGS = { 52 | 'net': ['WSAStartup', 'socket', 'recv', 'recvfrom', 'send', 'sendto', 'acccept', 'bind', 'listen', 'select', 53 | 'setsockopt', 'ioctlsocket', 'closesocket', 'WSAAccept', 'WSARecv', 'WSARecvFrom', 'WSASend', 'WSASendTo', 54 | 'WSASocket', 'WSAConnect', 'ConnectEx', 'TransmitFile', 'HTTPOpenRequest', 'HTTPSendRequest', 55 | 'URLDownloadToFile', 'InternetCrackUrl', 'InternetOpen', 'InternetOpen', 'InternetConnect', 56 | 'InternetOpenUrl', 'InternetQueryOption', 'InternetSetOption', 'InternetReadFile', 'InternetWriteFile', 57 | 'InternetGetConnectedState', 'InternetSetStatusCallback', 'DnsQuery', 'getaddrinfo', 'GetAddrInfo', 58 | 'GetAdaptersInfo', 'GetAdaptersAddresses', 'HttpQueryInfo', 'ObtainUserAgentString', 'WNetGetProviderName', 59 | 'GetBestInterfaceEx', 'gethostbyname', 'getsockname', 'connect', 'WinHttpOpen', 'WinHttpSetTimeouts', 60 | 'WinHttpSendRequest', 'WinHttpConnect', 'WinHttpCrackUrl', 'WinHttpReadData', 'WinHttpOpenRequest', 61 | 'WinHttpReceiveResponse', 'WinHttpQueryHeaders', 'HttpSendRequestW', 'HttpSendRequestA', 'HttpAddRequestHeadersW', 'HttpAddRequestHeadersA', 'HttpOpenRequestW', 'HttpOpenRequestA', 'NetServerGetInfo', 'NetApiBufferFree', 'NetWkstaGetInfo'], 62 | 'spawn': ['CreateProcess', 'ShellExecute', 'ShellExecuteEx', 'system', 'CreateProcessInternal', 'NtCreateProcess', 63 | 'ZwCreateProcess', 'NtCreateProcessEx', 'ZwCreateProcessEx', 'NtCreateUserProcess', 'ZwCreateUserProcess', 64 | 'RtlCreateUserProcess', 'NtCreateSection', 'ZwCreateSection', 'NtOpenSection', 'ZwOpenSection', 65 | 'NtAllocateVirtualMemory', 'ZwAllocateVirtualMemory', 'NtWriteVirtualMemory', 'ZwWriteVirtualMemory', 66 | 'NtMapViewOfSection', 'ZwMapViewOfSection', 'OpenSCManager', 'CreateService', 'OpenService', 67 | 'StartService', 'ControlService', 'ShellExecuteExA', 'ShellExecuteExW'], 68 | 'inject': ['OpenProcess-disabled', 'ZwOpenProcess', 'NtOpenProcess', 'WriteProcessMemory', 'NtWriteVirtualMemory', 69 | 'ZwWriteVirtualMemory', 'CreateRemoteThread', 'QueueUserAPC', 'ZwUnmapViewOfSection', 'NtUnmapViewOfSection'], 70 | 'com': ['CoCreateInstance', 'CoInitializeSecurity', 'CoGetClassObject', 'OleConvertOLESTREAMToIStorage', 'CreateBindCtx', 'CoSetProxyBlanket', 'VariantClear'], 71 | 'crypto': ['CryptAcquireContext', 'CryptProtectData', 'CryptUnprotectData', 'CryptProtectMemory', 72 | 'CryptUnprotectMemory', 'CryptDecrypt', 'CryptEncrypt', 'CryptHashData', 'CryptDecodeMessage', 73 | 'CryptDecryptMessage', 'CryptEncryptMessage', 'CryptHashMessage', 'CryptExportKey', 'CryptGenKey', 74 | 'CryptCreateHash', 'CryptDecodeObjectEx', 'EncryptMessage', 'DecryptMessage'], 75 | 'kbd': ['SendInput', 'VkKeyScanA', 'VkKeyScanW'], 76 | 'file': ['_open64', 'open64', 'open', 'open64', 'fopen', 'fread', 'fclose', 'fwrite', 'flock', 'read', 'write', 77 | 'fstat', 'lstat', 'stat', 'chmod', 'chown', 'lchown', 'link', 'symlink', 'readdir', 'readdir64'], 78 | 'reg': ['RegOpenKeyExW', 'RegQueryValueExW', 'RegSetValueExW', 'RegCreateKeyExW', 'RegDeleteValueW', 'RegEnumKeyW', 'RegCloseKey', 'RegQueryInfoKeyW', 'RegOpenKeyExA', 'RegQueryValueExA', 'RegSetValueExA', 'RegCreateKeyExA', 'RegDeleteValueA', 'RegEnumKeyA', 'RegQueryInfoKeyA'], 79 | 'dev': ['DeviceIoControl'], 80 | 'wow': ['Wow64DisableWow64FsRedirection', 'Wow64RevertWow64FsRedirection'] 81 | } 82 | 83 | STRICT_TAG_NAME_CHECKING = {'file'} 84 | 85 | blacklist = {'@__security_check_cookie@4', '__SEH_prolog4', '__SEH_epilog4'} 86 | replacements = [ 87 | ('??3@YAXPAX@Z', 'alloc'), 88 | ('?', '') 89 | ] 90 | 91 | def inf_is_64bit(): 92 | return (idaapi.inf_is_64bit if idaapi.IDA_SDK_VERSION >= 900 else idaapi.cvar.inf.is_64bit)() 93 | 94 | 95 | def get_addr_width(): 96 | return '16' if inf_is_64bit() else '8' 97 | 98 | 99 | def decode_insn(ea): 100 | if idaapi.IDA_SDK_VERSION >= 700 and sys.maxsize > 2**32: 101 | insn = idaapi.insn_t() 102 | if idaapi.decode_insn(insn, ea) > 0: 103 | return insn 104 | else: 105 | if idaapi.decode_insn(ea): 106 | return idaapi.cmd.copy() 107 | 108 | 109 | def force_name(ea, new_name): 110 | if not ea or ea == idaapi.BADADDR: 111 | return 112 | if idaapi.IDA_SDK_VERSION >= 700: 113 | return idaapi.force_name(ea, new_name, idaapi.SN_NOCHECK) 114 | return idaapi.do_name_anyway(ea, new_name, 0) 115 | 116 | 117 | class AutoReIDPHooks(idaapi.IDP_Hooks): 118 | """ 119 | Hooks to keep view updated if some function is updated 120 | """ 121 | def __init__(self, view, *args): 122 | super(AutoReIDPHooks, self).__init__(*args) 123 | self._view = view 124 | 125 | def __on_rename(self, ea, new_name): 126 | if not self._view: 127 | return 128 | items = self._view._model.findItems(('%0' + get_addr_width() + 'X') % ea, QtCore.Qt.MatchRecursive) 129 | if len(items) != 1: 130 | return 131 | 132 | item = items[0] 133 | index = self._view._model.indexFromItem(item) 134 | if not index.isValid(): 135 | return 136 | 137 | name_index = index.sibling(index.row(), 1) 138 | if not name_index.isValid(): 139 | return 140 | 141 | self._view._model.setData(name_index, new_name) 142 | 143 | def ev_rename(self, ea, new_name): 144 | """ callback for IDA >= 700 """ 145 | self.__on_rename(ea, new_name) 146 | return super(AutoReIDPHooks, self).ev_rename(ea, new_name) 147 | 148 | def rename(self, ea, new_name): 149 | """ callback for IDA < 700 """ 150 | self.__on_rename(ea, new_name) 151 | return super(AutoReIDPHooks, self).rename(ea, new_name) 152 | 153 | 154 | class AutoREView(idaapi.PluginForm): 155 | ADDR_ROLE = QtCore.Qt.UserRole + 1 156 | 157 | OPT_FORM_PERSIST = idaapi.PluginForm.FORM_PERSIST if hasattr(idaapi.PluginForm, 'FORM_PERSIST') else idaapi.PluginForm.WOPN_PERSIST 158 | OPT_FORM_NO_CONTEXT = idaapi.PluginForm.FORM_NO_CONTEXT if hasattr(idaapi.PluginForm, 'FORM_NO_CONTEXT') else idaapi.PluginForm.WCLS_NO_CONTEXT 159 | 160 | def __init__(self, data): 161 | super(AutoREView, self).__init__() 162 | self._data = data 163 | self.tv = None 164 | self._model = None 165 | self._idp_hooks = None 166 | 167 | def Show(self): 168 | return idaapi.PluginForm.Show(self, 'AutoRE', options=self.OPT_FORM_PERSIST) 169 | 170 | def _get_parent_widget(self, form): 171 | if HAS_PYSIDE: 172 | return self.FormToPySideWidget(form) 173 | return self.FormToPyQtWidget(form) 174 | 175 | def OnCreate(self, form): 176 | self.parent = self._get_parent_widget(form) 177 | 178 | self._idp_hooks = AutoReIDPHooks(self) 179 | if not self._idp_hooks.hook(): 180 | print('IDP_Hooks.hook() failed') 181 | 182 | self.tv = QTreeView() 183 | self.tv.setExpandsOnDoubleClick(False) 184 | 185 | root_layout = QVBoxLayout(self.parent) 186 | # self.le_filter = QLineEdit(self.parent) 187 | 188 | # root_layout.addWidget(self.le_filter) 189 | root_layout.addWidget(self.tv) 190 | 191 | self.parent.setLayout(root_layout) 192 | 193 | self._model = QtGui.QStandardItemModel() 194 | self._init_model() 195 | self.tv.setModel(self._model) 196 | 197 | self.tv.setColumnWidth(0, 200) 198 | self.tv.setColumnWidth(1, 300) 199 | self.tv.header().setStretchLastSection(True) 200 | 201 | self.tv.expandAll() 202 | 203 | self.tv.doubleClicked.connect(self.on_navigate_to_method_requested) 204 | # self.le_filter.textChanged.connect(self.on_filter_text_changed) 205 | self.tv.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 206 | self.tv.customContextMenuRequested.connect(self._tree_customContextMenuRequesssted) 207 | 208 | rename_action = QAction('Rename...', self.tv) 209 | rename_action.setShortcut('n') 210 | rename_action.triggered.connect(self._tv_rename_action_triggered) 211 | self.tv.addAction(rename_action) 212 | 213 | def _tree_customContextMenuRequesssted(self, pos): 214 | idx = self.tv.indexAt(pos) 215 | if not idx.isValid(): 216 | return 217 | 218 | addr = idx.data(role=self.ADDR_ROLE) 219 | if not addr: 220 | return 221 | 222 | name_idx = idx.sibling(idx.row(), 1) 223 | old_name = name_idx.data() 224 | 225 | menu = QMenu() 226 | rename_action = menu.addAction('Rename `%s`...' % old_name) 227 | rename_action.setShortcut('n') 228 | action = menu.exec_(self.tv.mapToGlobal(pos)) 229 | if action == rename_action: 230 | return self._rename_ea_requested(addr, name_idx) 231 | 232 | def _tv_rename_action_triggered(self): 233 | selected = self.tv.selectionModel().selectedIndexes() 234 | if not selected: 235 | return 236 | 237 | idx = selected[0] 238 | if not idx.isValid(): 239 | return 240 | 241 | addr = idx.data(role=self.ADDR_ROLE) 242 | if not addr: 243 | return 244 | 245 | name_idx = idx.sibling(idx.row(), 1) 246 | if not name_idx.isValid(): 247 | return 248 | 249 | return self._rename_ea_requested(addr, name_idx) 250 | 251 | def _rename_ea_requested(self, addr, name_idx): 252 | old_name = name_idx.data() 253 | 254 | if idaapi.IDA_SDK_VERSION >= 700: 255 | new_name = idaapi.ask_str(str(old_name), 0, 'New name:') 256 | else: 257 | new_name = idaapi.askstr(0, str(old_name), 'New name:') 258 | 259 | if new_name is None: 260 | return 261 | 262 | force_name(addr, new_name) 263 | renamed_name = idaapi.get_ea_name(addr) 264 | name_idx.model().setData(name_idx, renamed_name) 265 | 266 | def OnClose(self, form): 267 | if self._idp_hooks: 268 | self._idp_hooks.unhook() 269 | 270 | def _tv_init_header(self, model): 271 | item_header = QtGui.QStandardItem("EA") 272 | item_header.setToolTip("Address") 273 | model.setHorizontalHeaderItem(0, item_header) 274 | 275 | item_header = QtGui.QStandardItem("Function name") 276 | model.setHorizontalHeaderItem(1, item_header) 277 | 278 | item_header = QtGui.QStandardItem("API called") 279 | model.setHorizontalHeaderItem(2, item_header) 280 | 281 | # noinspection PyMethodMayBeStatic 282 | def _tv_make_tag_item(self, name): 283 | rv = QtGui.QStandardItem(name) 284 | 285 | rv.setEditable(False) 286 | return [rv, QtGui.QStandardItem(), QtGui.QStandardItem()] 287 | 288 | def _tv_make_ref_item(self, tag, ref): 289 | ea_item = QtGui.QStandardItem(('%0' + get_addr_width() + 'X') % ref['ea']) 290 | ea_item.setEditable(False) 291 | ea_item.setData(ref['ea'], self.ADDR_ROLE) 292 | 293 | name_item = QtGui.QStandardItem(ref['name']) 294 | name_item.setEditable(False) 295 | name_item.setData(ref['ea'], self.ADDR_ROLE) 296 | 297 | apis = ', '.join(ref['tags'][tag]) 298 | api_name = QtGui.QStandardItem(apis) 299 | api_name.setEditable(False) 300 | api_name.setData(ref['ea'], self.ADDR_ROLE) 301 | api_name.setToolTip(apis) 302 | 303 | return [ea_item, name_item, api_name] 304 | 305 | def _init_model(self): 306 | self._model.clear() 307 | 308 | root_node = self._model.invisibleRootItem() 309 | self._tv_init_header(self._model) 310 | 311 | for tag, refs in self._data.items(): 312 | item_tag_list = self._tv_make_tag_item(tag) 313 | item_tag = item_tag_list[0] 314 | 315 | root_node.appendRow(item_tag_list) 316 | 317 | for ref in refs: 318 | ref_item_list = self._tv_make_ref_item(tag, ref) 319 | 320 | item_tag.appendRow(ref_item_list) 321 | 322 | def on_navigate_to_method_requested(self, index): 323 | addr = index.data(role=self.ADDR_ROLE) 324 | if addr is not None: 325 | idaapi.jumpto(addr) 326 | 327 | # def on_filter_text_changed(self, text): 328 | # print('on_text_changed: %s' % text) 329 | 330 | 331 | class auto_re_t(idaapi.plugin_t): 332 | flags = idaapi.PLUGIN_UNL 333 | comment = "" 334 | 335 | help = "" 336 | wanted_name = "Auto RE" 337 | wanted_hotkey = "Ctrl+Shift+M" 338 | 339 | _PREFIX_NAME = 'au_re_' 340 | _MIN_MAX_MATH_OPS_TO_ALLOW_RENAME = 10 341 | 342 | _CALLEE_NODE_NAMES = { 343 | idaapi.PLFM_MIPS: '$ mips', 344 | idaapi.PLFM_ARM: '$ arm' 345 | } 346 | _DEFAULT_CALLEE_NODE_NAME = '$ vmm functions' 347 | 348 | _JMP_TYPES = {idaapi.NN_jmp, idaapi.NN_jmpni, idaapi.NN_jmpfi, idaapi.NN_jmpshort} 349 | 350 | def __init__(self): 351 | super(auto_re_t, self).__init__() 352 | self._data = None 353 | self.view = None 354 | 355 | def init(self): 356 | # self._cfg = None 357 | self.view = None 358 | # self._load_config() 359 | 360 | return idaapi.PLUGIN_OK 361 | 362 | # def _load_config(self): 363 | # self._cfg = {'auto_rename': False} 364 | 365 | # def _store_config(self, cfg): 366 | # pass 367 | 368 | def _handle_tags(self, fn, fn_an, known_refs): 369 | if known_refs: 370 | known_refs = dict(known_refs) 371 | for k, names in known_refs.items(): 372 | existing = set(fn_an['tags'][k]) 373 | new = set(names) - existing 374 | if new: 375 | fn_an['tags'][k] += list(new) 376 | 377 | tags = dict(fn_an['tags']) 378 | if not tags: 379 | return 380 | print('fn: %#08x tags: %s' % (self.start_ea_of(fn), tags)) 381 | cmt = idaapi.get_func_cmt(fn, True) 382 | if cmt: 383 | cmt += '\n' 384 | s = str(tags.keys()) 385 | name = idaapi.get_ea_name(self.start_ea_of(fn)) 386 | item = {'ea': self.start_ea_of(fn), 'name': name, 'tags': tags} 387 | if not cmt or s not in cmt: 388 | idaapi.set_func_cmt(fn, '%sTAGS: %s' % (cmt or '', s), True) 389 | # self.mark_position(self.start_ea_of(fn), 'TAGS: %s' % s) 390 | for tag in tags: 391 | if tag not in self._data: 392 | self._data[tag] = list() 393 | self._data[tag].append(item) 394 | 395 | def _handle_calls(self, fn, fn_an): 396 | num_calls = len(fn_an['calls']) 397 | if num_calls != 1: 398 | return 399 | 400 | dis = fn_an['calls'][0] 401 | if dis.Op1.type not in (o_imm, o_far, o_near, o_mem): 402 | return 403 | 404 | ea = dis.Op1.value 405 | if not ea and dis.Op1.addr: 406 | ea = dis.Op1.addr 407 | 408 | if idaapi.has_dummy_name(self.get_flags_at(ea)): 409 | return 410 | 411 | # TODO: check is there jmp, push+retn then don't rename the func 412 | if fn_an['strange_flow']: 413 | return 414 | 415 | possible_name = idaapi.get_ea_name(ea) 416 | if not possible_name or possible_name in blacklist: 417 | return 418 | 419 | normalized = self.normalize_name(possible_name) 420 | 421 | # if self._cfg.get('auto_rename'): 422 | if len(fn_an['math']) < self._MIN_MAX_MATH_OPS_TO_ALLOW_RENAME: 423 | force_name(self.start_ea_of(fn), normalized) 424 | # TODO: add an API to the view 425 | print('fn: %#08x: %d calls, %d math%s possible name: %s, normalized: %s' % ( 426 | self.start_ea_of(fn), len(fn_an['calls']), len(fn_an['math']), 'has bads' if fn_an['has_bads'] else '', 427 | possible_name, normalized)) 428 | 429 | # noinspection PyMethodMayBeStatic 430 | def _check_is_jmp_wrapper(self, dis): 431 | # checks instructions like `jmp API` 432 | if dis.itype not in self._JMP_TYPES: 433 | return 434 | 435 | # handle call wrappers like jmp GetProcAddress 436 | if dis.Op1.type == idaapi.o_mem and dis.Op1.addr: 437 | # TODO: check is there better way to determine is the function a wrapper 438 | v = dis.Op1.addr 439 | flags = self.get_flags_at(v) 440 | if v and dis.itype == idaapi.NN_jmpni and self.is_data(flags) and self.__is_ptr_val(flags): 441 | v = self.__get_ptr_val(v) 442 | return v 443 | 444 | # noinspection PyMethodMayBeStatic 445 | def _check_is_push_retn_wrapper(self, dis0, dis1): 446 | """ 447 | Checks for sequence of push IMM32/retn 448 | :param dis0: the first insn 449 | :param dis1: the second insn 450 | :return: value of IMM32 451 | """ 452 | if dis0.itype != idaapi.NN_push or dis0.Op1.type != idaapi.o_imm or not dis0.Op1.value: 453 | return 454 | 455 | if dis1.itype not in (idaapi.NN_retn,): 456 | return 457 | 458 | return dis0.Op1.value 459 | 460 | def _preprocess_api_wrappers(self, fnqty): 461 | rv = defaultdict(dict) 462 | 463 | for i in xrange(fnqty): 464 | fn = idaapi.getn_func(i) 465 | items = list(FuncItems(self.start_ea_of(fn))) 466 | if len(items) not in (1, 2): 467 | continue 468 | 469 | dis0 = decode_insn(items[0]) 470 | if dis0 is None: 471 | continue 472 | addr = self._check_is_jmp_wrapper(dis0) 473 | 474 | if not addr and len(items) > 1: 475 | dis1 = decode_insn(items[1]) 476 | if dis1 is not None: 477 | addr = self._check_is_push_retn_wrapper(dis0, dis1) 478 | 479 | if not addr: 480 | continue 481 | 482 | name = idaapi.get_ea_name(addr) 483 | name = name.replace(idaapi.FUNC_IMPORT_PREFIX, '') 484 | if not name: 485 | continue 486 | 487 | imp_stripped_name = name.lstrip('_') 488 | 489 | for tag, names in TAGS.items(): 490 | for tag_api in names: 491 | if tag in STRICT_TAG_NAME_CHECKING: 492 | match = tag_api in (name, imp_stripped_name) 493 | else: 494 | match = tag_api in name 495 | if not match: 496 | continue 497 | 498 | refs = list(CodeRefsTo(self.start_ea_of(fn), 1)) 499 | 500 | for ref in refs: 501 | ref_fn = idaapi.get_func(ref) 502 | if not ref_fn: 503 | # idaapi.msg('AutoRE: there is no func for ref: %08x for api: %s' % (ref, name)) 504 | continue 505 | if tag not in rv[self.start_ea_of(ref_fn)]: 506 | rv[self.start_ea_of(ref_fn)][tag] = list() 507 | if name not in rv[self.start_ea_of(ref_fn)][tag]: 508 | rv[self.start_ea_of(ref_fn)][tag].append(name) 509 | return dict(rv) 510 | 511 | def run(self, arg): 512 | if RDEBUG and RDEBUG_EGG: 513 | if not os.path.isfile(RDEBUG_EGG): 514 | idaapi.msg('AutoRE: Remote debug is enabled, but I cannot find the debug egg: %s' % RDEBUG_EGG) 515 | else: 516 | import sys 517 | 518 | if RDEBUG_EGG not in sys.path: 519 | sys.path.append(RDEBUG_EGG) 520 | 521 | import pydevd 522 | pydevd.settrace(RDEBUG_HOST, port=RDEBUG_PORT, stdoutToServer=True, stderrToServer=True) 523 | 524 | try: 525 | self._data = dict() 526 | count = idaapi.get_func_qty() 527 | 528 | # pre-process of api wrapper functions 529 | known_refs_tags = self._preprocess_api_wrappers(count) 530 | 531 | for i in xrange(count): 532 | fn = idaapi.getn_func(i) 533 | fn_an = self.analyze_func(fn) 534 | 535 | # if fn_an['math']: 536 | # print('fn: %#08x has math' % self.start_ea_of(fn)) 537 | 538 | if idaapi.has_dummy_name(self.get_flags_at(self.start_ea_of(fn))): 539 | self._handle_calls(fn, fn_an) 540 | 541 | known_refs = known_refs_tags.get(self.start_ea_of(fn)) 542 | self._handle_tags(fn, fn_an, known_refs) 543 | 544 | if self.view: 545 | self.view.Close(AutoREView.OPT_FORM_NO_CONTEXT) 546 | self.view = AutoREView(self._data) 547 | self.view.Show() 548 | except: 549 | idaapi.msg('AutoRE: error: %s\n' % traceback.format_exc()) 550 | 551 | def term(self): 552 | self._data = None 553 | 554 | @classmethod 555 | def disasm_func(cls, fn): 556 | rv = list() 557 | items = list(FuncItems(cls.start_ea_of(fn))) 558 | for item_ea in items: 559 | obj = {'ea': item_ea, 'fn_ea': cls.start_ea_of(fn), 'dis': None} 560 | insn = decode_insn(item_ea) 561 | if insn is not None: 562 | obj['dis'] = insn 563 | rv.append(obj) 564 | return rv 565 | 566 | @classmethod 567 | def get_callee_netnode(cls): 568 | node_name = cls._CALLEE_NODE_NAMES.get(idaapi.ph.id, cls._DEFAULT_CALLEE_NODE_NAME) 569 | n = idaapi.netnode(node_name) 570 | return n 571 | 572 | @classmethod 573 | def get_callee(cls, ea): 574 | n = cls.get_callee_netnode() 575 | v = n.altval(ea) 576 | v -= 1 577 | if v == idaapi.BADNODE: 578 | return 579 | return v 580 | 581 | @classmethod 582 | def _analysis_handle_call_insn(cls, dis, rv): 583 | rv['calls'].append(dis) 584 | if dis.Op1.type != o_mem or not dis.Op1.addr: 585 | callee = cls.get_callee(dis.ip) 586 | if not callee: 587 | return 588 | else: 589 | callee = dis.Op1.addr 590 | 591 | cls._apply_tag_on_callee(callee, rv, is_call=True) 592 | 593 | @classmethod 594 | def _apply_tag_on_callee(cls, callee_ea, rv, is_call=False): 595 | name = idaapi.get_ea_name(callee_ea) 596 | name = name.replace(idaapi.FUNC_IMPORT_PREFIX, '') 597 | 598 | if '@' in name: 599 | name = name.split('@')[0] 600 | 601 | if not name: 602 | return 603 | 604 | if name in IGNORE_CALL_LIST: 605 | if is_call: 606 | rv['calls'].pop() 607 | return 608 | 609 | if name in TAGS_IGNORE_LIST: 610 | return 611 | 612 | for tag, names in TAGS.items(): 613 | for tag_api in names: 614 | if tag in STRICT_TAG_NAME_CHECKING: 615 | match = tag_api in (name, name.lstrip('_')) 616 | else: 617 | match = tag_api in name 618 | if not match or name in rv['tags'][tag]: 619 | continue 620 | 621 | # print('%#08x: %s, tag: %s' % (dis.ea, name, tag)) 622 | rv['tags'][tag].append(name) 623 | break 624 | 625 | @classmethod 626 | def __is_ptr_val(cls, flags): 627 | if idaapi.IDA_SDK_VERSION >= 700: 628 | return (idaapi.is_qword if inf_is_64bit() else idaapi.is_dword)(flags) 629 | return (idaapi.isQwrd if inf_is_64bit() else idaapi.isDwrd)(flags) 630 | 631 | @classmethod 632 | def __get_ptr_val(cls, ea): 633 | if inf_is_64bit(): 634 | return idaapi.get_qword(ea) 635 | 636 | return (idaapi.get_dword if idaapi.IDA_SDK_VERSION >= 700 else idaapi.get_long)(ea) 637 | 638 | @classmethod 639 | def start_ea_of(cls, o): 640 | return getattr(o, 'start_ea' if idaapi.IDA_SDK_VERSION >= 700 else 'startEA') 641 | 642 | @classmethod 643 | def end_ea_of(cls, o): 644 | return getattr(o, 'end_ea' if idaapi.IDA_SDK_VERSION >= 700 else 'endEA') 645 | 646 | @classmethod 647 | def get_flags_at(cls, ea): 648 | return getattr(idaapi, 'get_flags' if idaapi.IDA_SDK_VERSION >= 700 else 'getFlags')(ea) 649 | 650 | @classmethod 651 | def is_data(cls, flags): 652 | return getattr(idaapi, 'is_data' if idaapi.IDA_SDK_VERSION >= 700 else 'isData')(flags) 653 | 654 | @classmethod 655 | def analyze_func(cls, fn): 656 | rv = { 657 | 'fn': fn, 658 | 'calls': [], 659 | 'math': [], 660 | 'has_bads': False, 661 | 'strange_flow': False, 662 | 'tags': defaultdict(list) 663 | } 664 | items = cls.disasm_func(fn) 665 | items_set = set(map(lambda x: x['ea'], items)) 666 | 667 | for item in items: 668 | dis = item['dis'] 669 | if dis is None: 670 | rv['has_bads'] = True 671 | continue 672 | 673 | if dis.itype in (idaapi.NN_call, idaapi.NN_callfi, idaapi.NN_callni): 674 | cls._analysis_handle_call_insn(dis, rv) 675 | elif dis.itype == idaapi.NN_xor: 676 | if dis.Op1.type == o_reg and dis.Op2.type == o_reg and dis.Op1.reg == dis.Op2.reg: 677 | continue 678 | rv['math'].append(dis) 679 | elif dis.itype in (idaapi.NN_shr, idaapi.NN_shl, idaapi.NN_sal, idaapi.NN_sar, idaapi.NN_ror, 680 | idaapi.NN_rol, idaapi.NN_rcl, idaapi.NN_rcl): 681 | # TODO 682 | rv['math'].append(dis) 683 | elif dis.itype in cls._JMP_TYPES: 684 | if dis.Op1.type not in (o_far, o_near, o_mem, o_displ): 685 | continue 686 | 687 | if dis.Op1.type == o_displ: 688 | rv['strange_flow'] = True 689 | continue 690 | 691 | ea = dis.Op1.value 692 | if not ea and dis.Op1.addr: 693 | ea = dis.Op1.addr 694 | if ea not in items_set: 695 | rv['strange_flow'] = True 696 | 697 | # flags = self.get_flags_at(ea) 698 | # if dis.itype == idaapi.NN_jmpni and dis.Op1.type == o_mem and ea and self.is_data(flags): 699 | # if cls.__is_ptr_val(flags): 700 | # val = cls.__get_ptr_val(ea) 701 | # if val: 702 | # cls._apply_tag_on_callee(val, rv, is_call=False) 703 | 704 | return rv 705 | 706 | @classmethod 707 | def normalize_name(cls, n): 708 | for repl in replacements: 709 | n = n.replace(*repl) 710 | if '@' in n: 711 | n = n.split('@')[0] 712 | if len(n) < 3: 713 | return '' 714 | if not n.startswith(cls._PREFIX_NAME): 715 | n = cls._PREFIX_NAME + n 716 | return n 717 | 718 | # @classmethod 719 | # def mark_position(cls, ea, name, slot=[0]): 720 | # curloc = idaapi.curloc() 721 | # curloc.ea = ea 722 | # curloc.lnnum = 0 723 | # curloc.x = 0 724 | # curloc.y = 0 725 | # slot[0] += 1 726 | # curloc.mark(slot[0], name, name) 727 | 728 | 729 | # noinspection PyPep8Naming 730 | def PLUGIN_ENTRY(): 731 | return auto_re_t() 732 | -------------------------------------------------------------------------------- /docs/auto_rename_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/2ea88f0c18aa06ccb2fe073ab5aece27736dce15/docs/auto_rename_dst.png -------------------------------------------------------------------------------- /docs/auto_rename_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/2ea88f0c18aa06ccb2fe073ab5aece27736dce15/docs/auto_rename_src.png -------------------------------------------------------------------------------- /docs/function_rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/2ea88f0c18aa06ccb2fe073ab5aece27736dce15/docs/function_rename.png -------------------------------------------------------------------------------- /docs/tags_in_unexplored_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/2ea88f0c18aa06ccb2fe073ab5aece27736dce15/docs/tags_in_unexplored_code.png -------------------------------------------------------------------------------- /docs/tags_view_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/2ea88f0c18aa06ccb2fe073ab5aece27736dce15/docs/tags_view_0.png -------------------------------------------------------------------------------- /docs/tags_view_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/2ea88f0c18aa06ccb2fe073ab5aece27736dce15/docs/tags_view_1.png --------------------------------------------------------------------------------