├── docs ├── tags_view_0.png ├── tags_view_1.png ├── auto_rename_dst.png ├── auto_rename_src.png ├── function_rename.png └── tags_in_unexplored_code.png ├── README.md ├── .gitignore └── auto_re.py /docs/tags_view_0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/HEAD/docs/tags_view_0.png -------------------------------------------------------------------------------- /docs/tags_view_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/HEAD/docs/tags_view_1.png -------------------------------------------------------------------------------- /docs/auto_rename_dst.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/HEAD/docs/auto_rename_dst.png -------------------------------------------------------------------------------- /docs/auto_rename_src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/HEAD/docs/auto_rename_src.png -------------------------------------------------------------------------------- /docs/function_rename.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/HEAD/docs/function_rename.png -------------------------------------------------------------------------------- /docs/tags_in_unexplored_code.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/a1ext/auto_re/HEAD/docs/tags_in_unexplored_code.png -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /auto_re.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -* 2 | __author__ = 'Trafimchuk Aliaksandr' 3 | __version__ = '2.1' 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 | if idaapi.IDA_SDK_VERSION >= 920: 21 | from PySide6 import QtGui, QtCore 22 | from PySide6.QtGui import QAction 23 | from PySide6.QtWidgets import QTreeView, QVBoxLayout, QLineEdit, QMenu, QInputDialog, QTabWidget 24 | else: 25 | from PyQt5 import QtGui, QtCore 26 | from PyQt5.QtWidgets import QTreeView, QVBoxLayout, QLineEdit, QMenu, QInputDialog, QAction, QTabWidget 27 | 28 | 29 | try: 30 | # Python 2. 31 | xrange 32 | except NameError: 33 | # Python 3. 34 | xrange = range 35 | 36 | 37 | # enable to allow PyCharm remote debug 38 | RDEBUG = False 39 | # adjust this value to be a full path to a debug egg 40 | RDEBUG_EGG = r'c:\Program Files\JetBrains\PyCharm 2017.1.4\debug-eggs\pycharm-debug.egg' 41 | RDEBUG_HOST = 'localhost' 42 | RDEBUG_PORT = 12321 43 | 44 | 45 | TAGS_IGNORE_LIST = { 46 | 'OpenProcessToken', 47 | 'DisconnectNamedPipe' 48 | } 49 | 50 | IGNORE_CALL_LIST = { 51 | 'RtlNtStatusToDosError', 52 | 'GetLastError', 53 | 'SetLastError' 54 | } 55 | 56 | TAGS = { 57 | 'net': ['WSAStartup', 'socket', 'recv', 'recvfrom', 'send', 'sendto', 'acccept', 'bind', 'listen', 'select', 58 | 'setsockopt', 'ioctlsocket', 'closesocket', 'WSAAccept', 'WSARecv', 'WSARecvFrom', 'WSASend', 'WSASendTo', 59 | 'WSASocket', 'WSAConnect', 'ConnectEx', 'TransmitFile', 'HTTPOpenRequest', 'HTTPSendRequest', 60 | 'URLDownloadToFile', 'InternetCrackUrl', 'InternetOpen', 'InternetOpen', 'InternetConnect', 61 | 'InternetOpenUrl', 'InternetQueryOption', 'InternetSetOption', 'InternetReadFile', 'InternetWriteFile', 62 | 'InternetGetConnectedState', 'InternetSetStatusCallback', 'DnsQuery', 'getaddrinfo', 'GetAddrInfo', 63 | 'GetAdaptersInfo', 'GetAdaptersAddresses', 'HttpQueryInfo', 'ObtainUserAgentString', 'WNetGetProviderName', 64 | 'GetBestInterfaceEx', 'gethostbyname', 'getsockname', 'connect', 'WinHttpOpen', 'WinHttpSetTimeouts', 65 | 'WinHttpSendRequest', 'WinHttpConnect', 'WinHttpCrackUrl', 'WinHttpReadData', 'WinHttpOpenRequest', 66 | 'WinHttpReceiveResponse', 'WinHttpQueryHeaders', 'HttpSendRequestW', 'HttpSendRequestA', 'HttpAddRequestHeadersW', 67 | 'HttpAddRequestHeadersA', 'HttpOpenRequestW', 'HttpOpenRequestA', 'NetServerGetInfo', 'NetApiBufferFree', 'NetWkstaGetInfo', 68 | 'getnameinfo', 'getpeername', 'socketpair'], 69 | 'spawn': ['CreateProcess', 'ShellExecute', 'ShellExecuteEx', 'system', 'CreateProcessInternal', 'NtCreateProcess', 70 | 'ZwCreateProcess', 'NtCreateProcessEx', 'ZwCreateProcessEx', 'NtCreateUserProcess', 'ZwCreateUserProcess', 71 | 'RtlCreateUserProcess', 'NtCreateSection', 'ZwCreateSection', 'NtOpenSection', 'ZwOpenSection', 72 | 'NtAllocateVirtualMemory', 'ZwAllocateVirtualMemory', 'NtWriteVirtualMemory', 'ZwWriteVirtualMemory', 73 | 'NtMapViewOfSection', 'ZwMapViewOfSection', 'OpenSCManager', 'CreateService', 'OpenService', 74 | 'StartService', 'ControlService', 'ShellExecuteExA', 'ShellExecuteExW', 'execve', 'execvp', 'fork', 'popen', 'execl', 75 | 'posix_spawn'], 76 | 'inject': ['OpenProcess-disabled', 'ZwOpenProcess', 'NtOpenProcess', 'WriteProcessMemory', 'NtWriteVirtualMemory', 77 | 'ZwWriteVirtualMemory', 'CreateRemoteThread', 'QueueUserAPC', 'ZwUnmapViewOfSection', 'NtUnmapViewOfSection'], 78 | 'com': ['CoCreateInstance', 'CoInitializeSecurity', 'CoGetClassObject', 'OleConvertOLESTREAMToIStorage', 'CreateBindCtx', 79 | 'CoSetProxyBlanket', 'VariantClear'], 80 | 'crypto': ['CryptAcquireContext', 'CryptProtectData', 'CryptUnprotectData', 'CryptProtectMemory', 81 | 'CryptUnprotectMemory', 'CryptDecrypt', 'CryptEncrypt', 'CryptHashData', 'CryptDecodeMessage', 82 | 'CryptDecryptMessage', 'CryptEncryptMessage', 'CryptHashMessage', 'CryptExportKey', 'CryptGenKey', 83 | 'CryptCreateHash', 'CryptDecodeObjectEx', 'EncryptMessage', 'DecryptMessage'], 84 | 'kbd': ['SendInput', 'VkKeyScanA', 'VkKeyScanW'], 85 | 'file': ['_open64', 'open64', 'open', 'open64', 'fopen', 'fread', 'fclose', 'fwrite', 'flock', 'read', 'write', 86 | 'fstat', 'lstat', 'stat', 'chmod', 'chown', 'lchown', 'link', 'symlink', 'readdir', 'readdir64', 'sync', 'ftell', 'opendir'], 87 | 'reg': ['RegOpenKeyExW', 'RegQueryValueExW', 'RegSetValueExW', 'RegCreateKeyExW', 'RegDeleteValueW', 'RegEnumKeyW', 'RegCloseKey', 88 | 'RegQueryInfoKeyW', 'RegOpenKeyExA', 'RegQueryValueExA', 'RegSetValueExA', 'RegCreateKeyExA', 'RegDeleteValueA', 89 | 'RegEnumKeyA', 'RegQueryInfoKeyA'], 90 | 'dev': ['DeviceIoControl', 'ioctl'], 91 | 'wow': ['Wow64DisableWow64FsRedirection', 'Wow64RevertWow64FsRedirection'], 92 | 'native': ['syscall'], 93 | 'mem': ['memcpy', 'memset', 'memmove', 'shmget', 'mmap', 'bcopy', 'munmap', 'strncpy'], 94 | 'priv': ['geteuid', 'getuid', 'getgid', 'setreuid', 'setregid', 'getresuid', 'seteuid', 'getlogin_r', 'pam_open_session'], 95 | 'cmp': ['memcmp', 'strcmp', 'strncmp', 'strcasecmp'], 96 | 'fmt': ['vprintf', 'vsnprintf', 'sprintf', 'ssprintf', 'vfprintf', 'spprintf'], 97 | 'parsing': ['sscanf', 'strtok', 'strtol', 'strtoul'], 98 | 'io': ['mkfifo'], 99 | 'ldr': ['LoadLibrary', 'dlopen', 'LdrLoadDLL', 'LdrLoadDriver'], 100 | } 101 | 102 | STRICT_TAG_NAME_CHECKING = {'file'} 103 | 104 | BLACKLIST = frozenset([ 105 | '@__security_check_cookie@4', 106 | '__SEH_prolog4', 107 | '__SEH_epilog4', 108 | 'throw_system_error', 109 | 'chrono::system_clock', 110 | 'QObject::connect', 111 | ]) 112 | REPLACEMENTS = [ 113 | ('??3@YAXPAX@Z', 'alloc'), 114 | ('?', '') 115 | ] 116 | 117 | def inf_is_64bit(): 118 | return (idaapi.inf_is_64bit if idaapi.IDA_SDK_VERSION >= 900 else idaapi.cvar.inf.is_64bit)() 119 | 120 | 121 | def get_addr_width(): 122 | return '16' if inf_is_64bit() else '8' 123 | 124 | 125 | def decode_insn(ea): 126 | if idaapi.IDA_SDK_VERSION >= 700 and sys.maxsize > 2**32: 127 | insn = idaapi.insn_t() 128 | if idaapi.decode_insn(insn, ea) > 0: 129 | return insn 130 | else: 131 | if idaapi.decode_insn(ea): 132 | return idaapi.cmd.copy() 133 | 134 | 135 | def force_name(ea, new_name): 136 | if not ea or ea == idaapi.BADADDR: 137 | return 138 | if idaapi.IDA_SDK_VERSION >= 700: 139 | return idaapi.force_name(ea, new_name, idaapi.SN_NOCHECK) 140 | return idaapi.do_name_anyway(ea, new_name, 0) 141 | 142 | 143 | class AutoReIDPHooks(idaapi.IDP_Hooks): 144 | """ 145 | Hooks to keep view updated if some function is updated 146 | """ 147 | def __init__(self, view, *args): 148 | super(AutoReIDPHooks, self).__init__(*args) 149 | self._view = view 150 | 151 | def __on_rename(self, ea, new_name): 152 | if not self._view: 153 | return 154 | items = self._view._model.findItems(('%0' + get_addr_width() + 'X') % ea, QtCore.Qt.MatchRecursive) 155 | if len(items) != 1: 156 | return 157 | 158 | item = items[0] 159 | index = self._view._model.indexFromItem(item) 160 | if not index.isValid(): 161 | return 162 | 163 | name_index = index.sibling(index.row(), 1) 164 | if not name_index.isValid(): 165 | return 166 | 167 | self._view._model.setData(name_index, new_name) 168 | 169 | def ev_rename(self, ea, new_name): 170 | """ callback for IDA >= 700 """ 171 | self.__on_rename(ea, new_name) 172 | return super(AutoReIDPHooks, self).ev_rename(ea, new_name) 173 | 174 | def rename(self, ea, new_name): 175 | """ callback for IDA < 700 """ 176 | self.__on_rename(ea, new_name) 177 | return super(AutoReIDPHooks, self).rename(ea, new_name) 178 | 179 | 180 | class AutoREView(idaapi.PluginForm): 181 | ADDR_ROLE = QtCore.Qt.UserRole + 1 182 | 183 | OPT_FORM_PERSIST = idaapi.PluginForm.FORM_PERSIST if hasattr(idaapi.PluginForm, 'FORM_PERSIST') else idaapi.PluginForm.WOPN_PERSIST 184 | OPT_FORM_NO_CONTEXT = idaapi.PluginForm.FORM_NO_CONTEXT if hasattr(idaapi.PluginForm, 'FORM_NO_CONTEXT') else idaapi.PluginForm.WCLS_NO_CONTEXT 185 | 186 | def __init__(self, data): 187 | super(AutoREView, self).__init__() 188 | self._data = data 189 | self.tv = None 190 | self._model = None 191 | self._idp_hooks = None 192 | 193 | def Show(self): 194 | return idaapi.PluginForm.Show(self, 'AutoRE', options=self.OPT_FORM_PERSIST) 195 | 196 | def _get_parent_widget(self, form): 197 | if HAS_PYSIDE: 198 | return self.FormToPySideWidget(form) 199 | return self.FormToPyQtWidget(form) 200 | 201 | def OnCreate(self, form): 202 | self.parent = self._get_parent_widget(form) 203 | 204 | self._idp_hooks = AutoReIDPHooks(self) 205 | if not self._idp_hooks.hook(): 206 | print('IDP_Hooks.hook() failed') 207 | 208 | self.tv = QTreeView() 209 | self.tv.setExpandsOnDoubleClick(False) 210 | 211 | root_layout = QVBoxLayout(self.parent) 212 | # self.le_filter = QLineEdit(self.parent) 213 | 214 | # root_layout.addWidget(self.le_filter) 215 | root_layout.addWidget(self.tv) 216 | 217 | self.parent.setLayout(root_layout) 218 | 219 | self._model = QtGui.QStandardItemModel() 220 | self._init_model() 221 | self.tv.setModel(self._model) 222 | 223 | self.tv.setColumnWidth(0, 200) 224 | self.tv.setColumnWidth(1, 300) 225 | self.tv.header().setStretchLastSection(True) 226 | 227 | self.tv.expandAll() 228 | 229 | self.tv.doubleClicked.connect(self.on_navigate_to_method_requested) 230 | # self.le_filter.textChanged.connect(self.on_filter_text_changed) 231 | self.tv.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) 232 | self.tv.customContextMenuRequested.connect(self._tree_customContextMenuRequesssted) 233 | 234 | rename_action = QAction('Rename...', self.tv) 235 | rename_action.setShortcut('n') 236 | rename_action.triggered.connect(self._tv_rename_action_triggered) 237 | self.tv.addAction(rename_action) 238 | 239 | def _tree_customContextMenuRequesssted(self, pos): 240 | idx = self.tv.indexAt(pos) 241 | if not idx.isValid(): 242 | return 243 | 244 | addr = idx.data(role=self.ADDR_ROLE) 245 | if not addr: 246 | return 247 | 248 | name_idx = idx.sibling(idx.row(), 1) 249 | old_name = name_idx.data() 250 | 251 | menu = QMenu() 252 | rename_action = menu.addAction('Rename `%s`...' % old_name) 253 | rename_action.setShortcut('n') 254 | action = menu.exec_(self.tv.mapToGlobal(pos)) 255 | if action == rename_action: 256 | return self._rename_ea_requested(addr, name_idx) 257 | 258 | def _tv_rename_action_triggered(self): 259 | selected = self.tv.selectionModel().selectedIndexes() 260 | if not selected: 261 | return 262 | 263 | idx = selected[0] 264 | if not idx.isValid(): 265 | return 266 | 267 | addr = idx.data(role=self.ADDR_ROLE) 268 | if not addr: 269 | return 270 | 271 | name_idx = idx.sibling(idx.row(), 1) 272 | if not name_idx.isValid(): 273 | return 274 | 275 | return self._rename_ea_requested(addr, name_idx) 276 | 277 | def _rename_ea_requested(self, addr, name_idx): 278 | old_name = name_idx.data() 279 | 280 | if idaapi.IDA_SDK_VERSION >= 700: 281 | new_name = idaapi.ask_str(str(old_name), 0, 'New name:') 282 | else: 283 | new_name = idaapi.askstr(0, str(old_name), 'New name:') 284 | 285 | if new_name is None: 286 | return 287 | 288 | force_name(addr, new_name) 289 | renamed_name = idaapi.get_ea_name(addr) 290 | name_idx.model().setData(name_idx, renamed_name) 291 | 292 | def OnClose(self, form): 293 | if self._idp_hooks: 294 | self._idp_hooks.unhook() 295 | 296 | def _tv_init_header(self, model): 297 | item_header = QtGui.QStandardItem("EA") 298 | item_header.setToolTip("Address") 299 | model.setHorizontalHeaderItem(0, item_header) 300 | 301 | item_header = QtGui.QStandardItem("Function name") 302 | model.setHorizontalHeaderItem(1, item_header) 303 | 304 | item_header = QtGui.QStandardItem("API called") 305 | model.setHorizontalHeaderItem(2, item_header) 306 | 307 | # noinspection PyMethodMayBeStatic 308 | def _tv_make_tag_item(self, name): 309 | rv = QtGui.QStandardItem(name) 310 | 311 | rv.setEditable(False) 312 | return [rv, QtGui.QStandardItem(), QtGui.QStandardItem()] 313 | 314 | def _tv_make_ref_item(self, tag, ref): 315 | ea_item = QtGui.QStandardItem(('%0' + get_addr_width() + 'X') % ref['ea']) 316 | ea_item.setEditable(False) 317 | ea_item.setData(ref['ea'], self.ADDR_ROLE) 318 | 319 | name_item = QtGui.QStandardItem(ref['name']) 320 | name_item.setEditable(False) 321 | name_item.setData(ref['ea'], self.ADDR_ROLE) 322 | 323 | apis = ', '.join(ref['tags'][tag]) 324 | api_name = QtGui.QStandardItem(apis) 325 | api_name.setEditable(False) 326 | api_name.setData(ref['ea'], self.ADDR_ROLE) 327 | api_name.setToolTip(apis) 328 | 329 | return [ea_item, name_item, api_name] 330 | 331 | def _init_model(self): 332 | self._model.clear() 333 | 334 | root_node = self._model.invisibleRootItem() 335 | self._tv_init_header(self._model) 336 | 337 | for tag, refs in self._data.items(): 338 | item_tag_list = self._tv_make_tag_item(tag) 339 | item_tag = item_tag_list[0] 340 | 341 | root_node.appendRow(item_tag_list) 342 | 343 | for ref in refs: 344 | ref_item_list = self._tv_make_ref_item(tag, ref) 345 | 346 | item_tag.appendRow(ref_item_list) 347 | 348 | def on_navigate_to_method_requested(self, index): 349 | addr = index.data(role=self.ADDR_ROLE) 350 | if addr is not None: 351 | idaapi.jumpto(addr) 352 | 353 | # def on_filter_text_changed(self, text): 354 | # print('on_text_changed: %s' % text) 355 | 356 | 357 | class auto_re_t(idaapi.plugin_t): 358 | flags = idaapi.PLUGIN_UNL 359 | comment = "" 360 | 361 | help = "" 362 | wanted_name = "Auto RE" 363 | wanted_hotkey = "Ctrl+Shift+M" 364 | 365 | _PREFIX_NAME = 'au_re_' 366 | _MIN_MAX_MATH_OPS_TO_ALLOW_RENAME = 10 367 | 368 | _CALLEE_NODE_NAMES = { 369 | idaapi.PLFM_MIPS: '$ mips', 370 | idaapi.PLFM_ARM: '$ arm' 371 | } 372 | _DEFAULT_CALLEE_NODE_NAME = '$ vmm functions' 373 | 374 | _JMP_TYPES = {idaapi.NN_jmp, idaapi.NN_jmpni, idaapi.NN_jmpfi, idaapi.NN_jmpshort} 375 | 376 | def __init__(self): 377 | super(auto_re_t, self).__init__() 378 | self._data = None 379 | self.view = None 380 | 381 | def init(self): 382 | # self._cfg = None 383 | self.view = None 384 | # self._load_config() 385 | 386 | return idaapi.PLUGIN_OK 387 | 388 | # def _load_config(self): 389 | # self._cfg = {'auto_rename': False} 390 | 391 | # def _store_config(self, cfg): 392 | # pass 393 | 394 | def _handle_tags(self, fn, fn_an, known_refs): 395 | if known_refs: 396 | known_refs = dict(known_refs) 397 | for k, names in known_refs.items(): 398 | existing = set(fn_an['tags'][k]) 399 | new = set(names) - existing 400 | if new: 401 | fn_an['tags'][k] += list(new) 402 | 403 | tags = dict(fn_an['tags']) 404 | if not tags: 405 | return 406 | print('fn: %#08x tags: %s' % (self.start_ea_of(fn), tags)) 407 | cmt = idaapi.get_func_cmt(fn, True) 408 | if cmt: 409 | cmt += '\n' 410 | s = str(tags.keys()) 411 | name = idaapi.get_long_name(self.start_ea_of(fn)) 412 | item = {'ea': self.start_ea_of(fn), 'name': name, 'tags': tags} 413 | if not cmt or s not in cmt: 414 | idaapi.set_func_cmt(fn, '%sTAGS: %s' % (cmt or '', s), True) 415 | # self.mark_position(self.start_ea_of(fn), 'TAGS: %s' % s) 416 | for tag in tags: 417 | if tag not in self._data: 418 | self._data[tag] = list() 419 | self._data[tag].append(item) 420 | 421 | def _handle_calls(self, fn, fn_an): 422 | num_calls = len(fn_an['calls']) 423 | if num_calls != 1: 424 | return 425 | 426 | dis = fn_an['calls'][0] 427 | if dis.Op1.type not in (o_imm, o_far, o_near, o_mem): 428 | return 429 | 430 | ea = dis.Op1.value 431 | if not ea and dis.Op1.addr: 432 | ea = dis.Op1.addr 433 | 434 | if idaapi.has_dummy_name(self.get_flags_at(ea)): 435 | return 436 | 437 | # TODO: check is there jmp, push+retn then don't rename the func 438 | if fn_an['strange_flow']: 439 | return 440 | 441 | possible_name = idaapi.get_ea_name(ea) 442 | if not possible_name or possible_name in BLACKLIST: 443 | return 444 | 445 | normalized = self.normalize_name(possible_name) 446 | 447 | # if self._cfg.get('auto_rename'): 448 | if len(fn_an['math']) < self._MIN_MAX_MATH_OPS_TO_ALLOW_RENAME: 449 | force_name(self.start_ea_of(fn), normalized) 450 | # TODO: add an API to the view 451 | print('fn: %#08x: %d calls, %d math%s possible name: %s, normalized: %s' % ( 452 | self.start_ea_of(fn), len(fn_an['calls']), len(fn_an['math']), 'has bads' if fn_an['has_bads'] else '', 453 | possible_name, normalized)) 454 | 455 | # noinspection PyMethodMayBeStatic 456 | def _check_is_jmp_wrapper(self, dis): 457 | # checks instructions like `jmp API` 458 | if dis.itype not in self._JMP_TYPES: 459 | return 460 | 461 | # handle call wrappers like jmp GetProcAddress 462 | if dis.Op1.type == idaapi.o_mem and dis.Op1.addr: 463 | # TODO: check is there better way to determine is the function a wrapper 464 | v = dis.Op1.addr 465 | flags = self.get_flags_at(v) 466 | if v and dis.itype == idaapi.NN_jmpni and self.is_data(flags) and self.__is_ptr_val(flags): 467 | v = self.__get_ptr_val(v) 468 | return v 469 | 470 | # noinspection PyMethodMayBeStatic 471 | def _check_is_push_retn_wrapper(self, dis0, dis1): 472 | """ 473 | Checks for sequence of push IMM32/retn 474 | :param dis0: the first insn 475 | :param dis1: the second insn 476 | :return: value of IMM32 477 | """ 478 | if dis0.itype != idaapi.NN_push or dis0.Op1.type != idaapi.o_imm or not dis0.Op1.value: 479 | return 480 | 481 | if dis1.itype not in (idaapi.NN_retn,): 482 | return 483 | 484 | return dis0.Op1.value 485 | 486 | def _check_is_mipsl_jmp(self, dis0, dis1, rest_items): 487 | """ 488 | Checks for sequence like: 489 | .plt:009F63E0 lui $t7, 0xA1 490 | .plt:009F63E4 lw $t9, off_A08884 491 | .plt:009F63E8 jr $t9 492 | """ 493 | 494 | if (not dis0 or dis0.itype != idaapi.NN_popaw or dis0.Op1.type != idaapi.o_reg or 495 | dis0.Op2.type != idaapi.o_imm or not dis0.Op2.value): 496 | return 497 | if (not dis1 or dis1.itype != idaapi.NN_or or dis1.Op1.type != idaapi.o_reg or 498 | dis1.Op2.type != idaapi.o_mem or not dis1.Op2.addr or 499 | not idaapi.is_data(idaapi.get_flags(dis1.Op2.addr))): 500 | return 501 | if not rest_items: return 502 | 503 | dis2 = decode_insn(rest_items[0]) 504 | if not dis2 or dis2.itype != idaapi.NN_loopwne or dis2.Op1.type != idaapi.o_reg: 505 | return 506 | 507 | addr = dis1.Op2.addr 508 | seg = idaapi.getseg(addr) 509 | if not seg: return # TODO: check if imagebase check is required 510 | if idaapi.get_segm_name(seg) not in ('.got.plt',): return 511 | 512 | offs = idaapi.get_dword(addr) 513 | if not offs: 514 | offs = idaapi.get_word(addr + idaapi.get_imagebase()) 515 | if not offs: return 516 | 517 | offs_seg = idaapi.getseg(offs) 518 | if not offs_seg: 519 | offs_seg = idaapi.getseg(offs + idaapi.get_imagebase()) 520 | if not offs_seg or idaapi.get_segm_name(offs_seg) not in ('extern',): 521 | return 522 | 523 | return offs 524 | 525 | def _check_is_armle_jmp(self, dis0, dis1, rest_items): 526 | """ 527 | Check for sequence like: 528 | ADDRL R12, 0x606A4 529 | LDR PC, [R12, #(sprintf_ptr - 0x606A4)]! 530 | 531 | # and 532 | .plt:0000B0B4 ADR R12, 0xB0BC ; Load address 533 | .plt:0000B0B8 ADD R12, R12, #0x55000 ; Rd = Op1 + Op2 534 | .plt:0000B0BC LDR PC, [R12,#(XXXX_ptr - 0x600BC)]! ; Indirect Jump 535 | """ 536 | if not dis0 or not dis1: return 537 | 538 | if dis0.itype not in (idaapi.ARM_adrl, idaapi.ARM_adr) or dis0.Op1.type != idaapi.o_reg or dis0.Op2.type != idaapi.o_imm: 539 | return 540 | 541 | r = dis0.Op1.reg 542 | addr = dis0.Op2.value 543 | 544 | if dis1.itype == idaapi.ARM_add and rest_items and dis1.Op1.type == idaapi.o_reg \ 545 | and dis1.Op2.type == idaapi.o_reg and dis1.Op3.type == idaapi.o_imm and dis1.Op1.reg == r and dis1.Op2.reg == r: 546 | 547 | addr += dis1.Op3.value 548 | dis1 = decode_insn(rest_items[0]) # ldr should be the third instruction 549 | 550 | if dis1.itype != idaapi.ARM_ldrpc or dis1.Op1.type != idaapi.o_reg or dis1.Op2.type != idaapi.o_displ or \ 551 | dis1.Op2.reg != r or not dis1.Op2.addr: 552 | return 553 | 554 | addr = dis1.Op2.addr + addr 555 | seg = idaapi.getseg(addr) 556 | if not seg: return 557 | if idaapi.get_segm_name(seg) not in ('.got',): return 558 | 559 | offs = idaapi.get_dword(addr) 560 | if not offs: 561 | offs = idaapi.get_word(addr + idaapi.get_imagebase()) 562 | if not offs: return 563 | 564 | offs_seg = idaapi.getseg(offs) 565 | if not offs_seg: 566 | offs_seg = idaapi.getseg(offs + idaapi.get_imagebase()) 567 | if not offs_seg or idaapi.get_segm_name(offs_seg) not in ('extern',): 568 | return 569 | 570 | return offs 571 | 572 | def _preprocess_api_wrappers(self, fnqty): 573 | rv = defaultdict(dict) 574 | 575 | for i in xrange(fnqty): 576 | fn = idaapi.getn_func(i) 577 | items = list(FuncItems(self.start_ea_of(fn))) 578 | if not (0 < len(items) <= 4): 579 | continue 580 | 581 | dis0 = decode_insn(items[0]) 582 | if dis0 is None: 583 | continue 584 | addr = self._check_is_jmp_wrapper(dis0) 585 | 586 | if not addr and len(items) > 1: 587 | dis1 = decode_insn(items[1]) 588 | if dis1 is not None: 589 | addr = self._check_is_push_retn_wrapper(dis0, dis1) 590 | if not addr: 591 | addr = self._check_is_mipsl_jmp(dis0, dis1, items[2:]) 592 | if not addr: 593 | addr = self._check_is_armle_jmp(dis0, dis1, items[2:]) 594 | 595 | if not addr: 596 | continue 597 | 598 | name = idaapi.get_long_name(addr) 599 | name = name.replace(idaapi.FUNC_IMPORT_PREFIX, '') 600 | if not name or any(x in name for x in BLACKLIST): 601 | continue 602 | 603 | imp_stripped_name = name.lstrip('_') 604 | 605 | for tag, names in TAGS.items(): 606 | for tag_api in names: 607 | if tag in STRICT_TAG_NAME_CHECKING: 608 | match = tag_api in (name, imp_stripped_name) 609 | else: 610 | match = tag_api in name 611 | if not match: 612 | continue 613 | 614 | p = name.find(tag_api) 615 | if p == -1: 616 | continue 617 | after_name = name[p+len(tag_api):] 618 | if after_name and after_name[0].isalpha(): 619 | # not fully matching the api name, skip it 620 | continue 621 | pre_name = name[p-1:p] if p > 0 else None 622 | if pre_name and pre_name[0].isalpha(): 623 | # not fully matching the api name, skip it 624 | continue 625 | 626 | refs = list(CodeRefsTo(self.start_ea_of(fn), 1)) 627 | 628 | for ref in refs: 629 | ref_fn = idaapi.get_func(ref) 630 | if not ref_fn: 631 | # idaapi.msg('AutoRE: there is no func for ref: %08x for api: %s' % (ref, name)) 632 | continue 633 | if tag not in rv[self.start_ea_of(ref_fn)]: 634 | rv[self.start_ea_of(ref_fn)][tag] = list() 635 | if name not in rv[self.start_ea_of(ref_fn)][tag]: 636 | rv[self.start_ea_of(ref_fn)][tag].append(name) 637 | return dict(rv) 638 | 639 | def run(self, arg): 640 | if RDEBUG and RDEBUG_EGG: 641 | if not os.path.isfile(RDEBUG_EGG): 642 | idaapi.msg('AutoRE: Remote debug is enabled, but I cannot find the debug egg: %s' % RDEBUG_EGG) 643 | else: 644 | import sys 645 | 646 | if RDEBUG_EGG not in sys.path: 647 | sys.path.append(RDEBUG_EGG) 648 | 649 | import pydevd 650 | pydevd.settrace(RDEBUG_HOST, port=RDEBUG_PORT, stdoutToServer=True, stderrToServer=True) 651 | 652 | 653 | try: 654 | self._data = dict() 655 | count = idaapi.get_func_qty() 656 | 657 | # pre-process of api wrapper functions 658 | known_refs_tags = self._preprocess_api_wrappers(count) 659 | 660 | for i in xrange(count): 661 | fn = idaapi.getn_func(i) 662 | fn_an = self.analyze_func(fn) 663 | 664 | # if fn_an['math']: 665 | # print('fn: %#08x has math' % self.start_ea_of(fn)) 666 | 667 | if idaapi.has_dummy_name(self.get_flags_at(self.start_ea_of(fn))): 668 | self._handle_calls(fn, fn_an) 669 | 670 | known_refs = known_refs_tags.get(self.start_ea_of(fn)) 671 | self._handle_tags(fn, fn_an, known_refs) 672 | 673 | if self.view: 674 | self.view.Close(AutoREView.OPT_FORM_NO_CONTEXT) 675 | self.view = AutoREView(self._data) 676 | self.view.Show() 677 | except: 678 | idaapi.msg('AutoRE: error: %s\n' % traceback.format_exc()) 679 | 680 | def term(self): 681 | self._data = None 682 | 683 | @classmethod 684 | def disasm_func(cls, fn): 685 | rv = list() 686 | items = list(FuncItems(cls.start_ea_of(fn))) 687 | for item_ea in items: 688 | obj = {'ea': item_ea, 'fn_ea': cls.start_ea_of(fn), 'dis': None} 689 | insn = decode_insn(item_ea) 690 | if insn is not None: 691 | obj['dis'] = insn 692 | rv.append(obj) 693 | return rv 694 | 695 | @classmethod 696 | def get_callee_netnode(cls): 697 | node_name = cls._CALLEE_NODE_NAMES.get(idaapi.ph.id, cls._DEFAULT_CALLEE_NODE_NAME) 698 | n = idaapi.netnode(node_name) 699 | return n 700 | 701 | @classmethod 702 | def get_callee(cls, ea): 703 | n = cls.get_callee_netnode() 704 | v = n.altval(ea) 705 | v -= 1 706 | if v == idaapi.BADNODE: 707 | return 708 | return v 709 | 710 | @classmethod 711 | def _analysis_handle_call_insn(cls, dis, rv): 712 | rv['calls'].append(dis) 713 | if dis.Op1.type != o_mem or not dis.Op1.addr: 714 | callee = cls.get_callee(dis.ip) 715 | if not callee: 716 | return 717 | else: 718 | callee = dis.Op1.addr 719 | 720 | cls._apply_tag_on_callee(callee, rv, is_call=True) 721 | 722 | @classmethod 723 | def _apply_tag_on_callee(cls, callee_ea, rv, is_call=False): 724 | name = idaapi.get_ea_name(callee_ea) 725 | name = name.replace(idaapi.FUNC_IMPORT_PREFIX, '') 726 | 727 | if '@' in name: 728 | name = name.split('@')[0] 729 | 730 | if not name: 731 | return 732 | 733 | if name in IGNORE_CALL_LIST: 734 | if is_call: 735 | rv['calls'].pop() 736 | return 737 | 738 | if name in TAGS_IGNORE_LIST: 739 | return 740 | 741 | for tag, names in TAGS.items(): 742 | for tag_api in names: 743 | if tag in STRICT_TAG_NAME_CHECKING: 744 | match = tag_api in (name, name.lstrip('_')) 745 | else: 746 | match = tag_api in name 747 | if not match or name in rv['tags'][tag]: 748 | continue 749 | 750 | # print('%#08x: %s, tag: %s' % (dis.ea, name, tag)) 751 | rv['tags'][tag].append(name) 752 | break 753 | 754 | @classmethod 755 | def __is_ptr_val(cls, flags): 756 | if idaapi.IDA_SDK_VERSION >= 700: 757 | return (idaapi.is_qword if inf_is_64bit() else idaapi.is_dword)(flags) 758 | return (idaapi.isQwrd if inf_is_64bit() else idaapi.isDwrd)(flags) 759 | 760 | @classmethod 761 | def __get_ptr_val(cls, ea): 762 | if inf_is_64bit(): 763 | return idaapi.get_qword(ea) 764 | 765 | return (idaapi.get_dword if idaapi.IDA_SDK_VERSION >= 700 else idaapi.get_long)(ea) 766 | 767 | @classmethod 768 | def start_ea_of(cls, o): 769 | return getattr(o, 'start_ea' if idaapi.IDA_SDK_VERSION >= 700 else 'startEA') 770 | 771 | @classmethod 772 | def end_ea_of(cls, o): 773 | return getattr(o, 'end_ea' if idaapi.IDA_SDK_VERSION >= 700 else 'endEA') 774 | 775 | @classmethod 776 | def get_flags_at(cls, ea): 777 | return getattr(idaapi, 'get_flags' if idaapi.IDA_SDK_VERSION >= 700 else 'getFlags')(ea) 778 | 779 | @classmethod 780 | def is_data(cls, flags): 781 | return getattr(idaapi, 'is_data' if idaapi.IDA_SDK_VERSION >= 700 else 'isData')(flags) 782 | 783 | @classmethod 784 | def analyze_func(cls, fn): 785 | rv = { 786 | 'fn': fn, 787 | 'calls': [], 788 | 'math': [], 789 | 'has_bads': False, 790 | 'strange_flow': False, 791 | 'tags': defaultdict(list) 792 | } 793 | items = cls.disasm_func(fn) 794 | items_set = set(map(lambda x: x['ea'], items)) 795 | 796 | for item in items: 797 | dis = item['dis'] 798 | if dis is None: 799 | rv['has_bads'] = True 800 | continue 801 | 802 | if dis.itype in (idaapi.NN_call, idaapi.NN_callfi, idaapi.NN_callni): 803 | cls._analysis_handle_call_insn(dis, rv) 804 | elif dis.itype == idaapi.NN_xor: 805 | if dis.Op1.type == o_reg and dis.Op2.type == o_reg and dis.Op1.reg == dis.Op2.reg: 806 | continue 807 | rv['math'].append(dis) 808 | elif dis.itype in (idaapi.NN_shr, idaapi.NN_shl, idaapi.NN_sal, idaapi.NN_sar, idaapi.NN_ror, 809 | idaapi.NN_rol, idaapi.NN_rcl, idaapi.NN_rcl): 810 | # TODO 811 | rv['math'].append(dis) 812 | elif dis.itype in cls._JMP_TYPES: 813 | if dis.Op1.type not in (o_far, o_near, o_mem, o_displ): 814 | continue 815 | 816 | if dis.Op1.type == o_displ: 817 | rv['strange_flow'] = True 818 | continue 819 | 820 | ea = dis.Op1.value 821 | if not ea and dis.Op1.addr: 822 | ea = dis.Op1.addr 823 | if ea not in items_set: 824 | rv['strange_flow'] = True 825 | 826 | # flags = self.get_flags_at(ea) 827 | # if dis.itype == idaapi.NN_jmpni and dis.Op1.type == o_mem and ea and self.is_data(flags): 828 | # if cls.__is_ptr_val(flags): 829 | # val = cls.__get_ptr_val(ea) 830 | # if val: 831 | # cls._apply_tag_on_callee(val, rv, is_call=False) 832 | 833 | return rv 834 | 835 | @classmethod 836 | def normalize_name(cls, n): 837 | for repl in REPLACEMENTS: 838 | n = n.replace(*repl) 839 | if '@' in n: 840 | n = n.split('@')[0] 841 | if len(n) < 3: 842 | return '' 843 | if not n.startswith(cls._PREFIX_NAME): 844 | n = cls._PREFIX_NAME + n 845 | return n 846 | 847 | # @classmethod 848 | # def mark_position(cls, ea, name, slot=[0]): 849 | # curloc = idaapi.curloc() 850 | # curloc.ea = ea 851 | # curloc.lnnum = 0 852 | # curloc.x = 0 853 | # curloc.y = 0 854 | # slot[0] += 1 855 | # curloc.mark(slot[0], name, name) 856 | 857 | 858 | # noinspection PyPep8Naming 859 | def PLUGIN_ENTRY(): 860 | return auto_re_t() 861 | --------------------------------------------------------------------------------