├── LICENSE ├── .gitignore ├── README.md └── plugin_loader.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Tamir Bahar 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 | -------------------------------------------------------------------------------- /.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 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # IDA Plugin Loader 2 | ## Why? 3 | 4 | IDA provides a single way to install plugins. Just stick them under `plugins` and you're good to go. 5 | 6 | While it works, the drawbacks are many. 7 | 8 | 1. You have to copy your plugins into the plugins directory. For Python plugins, this can get quite cumbersome; 9 | 2. Plugins often depend on multiple files. This has the tendency to clutter your `plugins` directory quite a bit; 10 | 3. The same plugins are loaded for all users and projects. This is not always desireable; 11 | 4. Adding a new plugin requires root access. 12 | 13 | The plugin-loader plugin allows you to circumvent all those drawbacks. 14 | 15 | ## How? 16 | 17 | Once installed (the old way) the `plugin_loader.py` plugin allows you to define plugin-lists. There are 3 types of lists - system-wide, user-specific, and project-specific. Listed plugins load automatically at the appropriate time. 18 | 19 | ### Plugin Lists 20 | 21 | All plugin lists are named `plugins-7.1.list` (`7.1` being the IDA version), and look something like this: 22 | 23 | ``` 24 | C:\Plugins\my_plugin.py 25 | 26 | # This is a comment. Comments are always entire lines. 27 | C:\OtherPlugins\another_plugin.py 28 | ``` 29 | 30 | When IDA starts, both `my_plugin.py` and `another_plugin.py` will be loaded. 31 | 32 | #### System-Wide 33 | 34 | The system-wide list resides under IDA’s `cfg` subdirectory. The path can be found using `idaapi.idadir(idaapi.CFG_SUBDIR)`. This list requires root access to modify as it is in IDA’s installation directory. 35 | 36 | #### User-Specific 37 | 38 | Located in IDA’s user-directory. `$HOME/.idapro` on Linux, `%APPDATA%/HexRays/IDA Pro` on Windows. The path can be found using `idaapi.get_user_idadir()`. Users can set their own plugins to load, thus eliminating the need for root access. 39 | 40 | #### Project-Specific 41 | 42 | Located in the same directory as the `.idb` you're loading. 43 | 44 | 45 | 46 | ## Usage 47 | 48 | To install your plugins, just add them to one of the lists. This allows you to easily update plugins as you go without ever needing to copy them. 49 | 50 | When IDA starts, the plugin lists the locations of plugin lists in the output window. 51 | 52 | ``` 53 | [PluginLoader] Loading plugins from: 54 | [PluginLoader] System-wide List: C:\Program Files\IDA 7.1\cfg\plugins-7.1.list 55 | [PluginLoader] User-specific List: C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins-7.1.list 56 | [PluginLoader] Failed creating system plugin list at C:\Program Files\IDA 7.1\cfg\plugins-7.1.list 57 | [PluginLoader] Created user plugin list at C:\Users\user\AppData\Roaming\Hex-Rays\IDA Pro\plugins-7.1.list 58 | ``` 59 | _Since IDA is not running as admin - it cannot create the system plugin list._ 60 | 61 | -------------------------------------------------------------------------------- /plugin_loader.py: -------------------------------------------------------------------------------- 1 | import glob 2 | import os 3 | import itertools 4 | import idaapi 5 | import idc 6 | 7 | PLUGINS_LIST = "plugins-{}.list".format(idaapi.get_kernel_version()) 8 | 9 | USER_PLUGIN_LIST_PATH = os.path.join(idaapi.get_user_idadir(), PLUGINS_LIST) 10 | SYS_PLUGIN_LIST_PATH = os.path.join(idaapi.idadir(idaapi.CFG_SUBDIR), PLUGINS_LIST) 11 | IDB_PATH = idaapi.get_path(idaapi.PATH_TYPE_IDB) 12 | if IDB_PATH: 13 | PROJECT_PLUGIN_LIST_PATH = os.path.join(os.path.dirname(IDB_PATH), PLUGINS_LIST) 14 | else: 15 | PROJECT_PLUGIN_LIST_PATH = None 16 | 17 | 18 | def message(*messages): 19 | for msg in messages: 20 | for line in msg.splitlines(): 21 | idaapi.msg("[PluginLoader] {}\n".format(line)) 22 | 23 | 24 | def iter_without_duplicates(*iterables): 25 | visited = set() 26 | chained_iterables = itertools.chain(*iterables) 27 | for item in chained_iterables: 28 | if item in visited: 29 | continue 30 | yield item 31 | visited.add(item) 32 | 33 | 34 | def iter_paths(filepath): 35 | if not filepath: 36 | return 37 | try: 38 | with open(filepath) as f: 39 | for line in f: 40 | # Use `#` for comments 41 | if line.startswith("#"): 42 | continue 43 | # Remove trailing spaces and newlines, then normalize to avoid duplicates. 44 | path = os.path.normpath(line.strip()) 45 | if path: 46 | # Allow for relative paths 47 | real_path = os.path.join(os.path.dirname(path), path) 48 | yield real_path 49 | except IOError: 50 | pass 51 | 52 | 53 | def iter_plugin_paths(): 54 | glob_patterns = iter_without_duplicates(iter_paths(SYS_PLUGIN_LIST_PATH), 55 | iter_paths(USER_PLUGIN_LIST_PATH), 56 | iter_paths(PROJECT_PLUGIN_LIST_PATH)) 57 | return iter_without_duplicates(*(glob.iglob(pattern) for pattern in glob_patterns)) 58 | 59 | 60 | class PluginLoader(idaapi.plugin_t): 61 | flags = idaapi.PLUGIN_FIX 62 | comment = "Plugin Loader" 63 | help = "Plugin Loader" 64 | wanted_name = "PluginLoader" 65 | wanted_hotkey = "" 66 | 67 | def init(self): 68 | # Show usage message. 69 | usage_message = ["Loading plugins from:", 70 | " System-wide List: {}".format(SYS_PLUGIN_LIST_PATH), 71 | " User-specific List: {}".format(USER_PLUGIN_LIST_PATH)] 72 | if PROJECT_PLUGIN_LIST_PATH: 73 | usage_message.append(" Project-specific List: {}".format(PROJECT_PLUGIN_LIST_PATH)) 74 | 75 | message(*usage_message) 76 | 77 | # Make sure the files exist. If not - create them. 78 | if not os.path.isfile(SYS_PLUGIN_LIST_PATH): 79 | try: 80 | with open(SYS_PLUGIN_LIST_PATH, "wb"): 81 | message("Created system plugin list at {}".format(SYS_PLUGIN_LIST_PATH)) 82 | except IOError: 83 | message("Failed creating system plugin list at {}".format(SYS_PLUGIN_LIST_PATH)) 84 | 85 | if not os.path.isfile(USER_PLUGIN_LIST_PATH): 86 | try: 87 | with open(USER_PLUGIN_LIST_PATH, "wb"): 88 | message("Created user plugin list at {}".format(USER_PLUGIN_LIST_PATH)) 89 | except IOError: 90 | message("Failed creating user plugin list at {}".format(USER_PLUGIN_LIST_PATH)) 91 | 92 | for path in iter_plugin_paths(): 93 | # This check is not needed, but saves us from the dreaded error message-box 94 | # that pops when a python plugin is not found. 95 | if not os.path.isfile(path): 96 | message("Plugin not found: {}".format(path)) 97 | continue 98 | idaapi.load_plugin(path) 99 | return idaapi.PLUGIN_SKIP 100 | 101 | def term(self): 102 | pass 103 | 104 | def run(self, arg): 105 | pass 106 | 107 | 108 | def PLUGIN_ENTRY(): 109 | return PluginLoader() 110 | --------------------------------------------------------------------------------