├── .gitignore ├── LICENSE ├── README.md ├── continuum ├── __init__.py ├── analyze.py ├── client.py ├── index.py ├── plugin.py ├── project.py ├── proto.py ├── server.py ├── ui.py └── ui │ ├── ProjectCreationDialog.ui │ ├── ProjectExplorer.ui │ ├── arrow_refresh.png │ └── page_gear.png ├── continuum_ldr.py └── media ├── project-creation.png └── project-explorer.png /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/python 3 | 4 | ### Python ### 5 | # Byte-compiled / optimized / DLL files 6 | __pycache__/ 7 | *.py[cod] 8 | *$py.class 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # IPython Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | venv/ 87 | ENV/ 88 | 89 | # Spyder project settings 90 | .spyderproject 91 | 92 | # Rope project settings 93 | .ropeproject 94 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Joel Hoener 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | continuum 2 | ========= 3 | 4 | continuum is an IDA Pro plugin adding multi-binary project support, 5 | allowing fast navigation in applications involving many shared libraries. 6 | 7 | **This project is still work in progress and not suitable for production use.** 8 | 9 | ## Features 10 | - Quick navigation between many IDA instances 11 | - Project explorer widget in IDA's "sidebar" 12 | - Pressing `SHIFT + F` on an `extrn` symbol navigates to the instance where the symbol is defined 13 | - If required, new IDA instances are automatically spawned for IDBs with no instance open 14 | - Type information is synchronized between all IDBs in a project (beta) 15 | 16 | ## Screenshots 17 | ![Project creation](https://raw.githubusercontent.com/zyantific/continuum/master/media/project-creation.png) 18 | ![Project explorer](https://raw.githubusercontent.com/zyantific/continuum/master/media/project-explorer.png) 19 | 20 | ## Requirements 21 | - IDA >= 6.9 22 | - IDAPython (ships with IDA) 23 | 24 | All operating systems supported by IDA are also supported by this plugin. 25 | Lacking licenses for Linux and OSX, it hasn't been tested on these platforms, yet. 26 | 27 | ## Installation 28 | Place the `continuum` directory and `continuum_ldr.py` into the `plugins` directory of your IDA installation. 29 | -------------------------------------------------------------------------------- /continuum/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import absolute_import, print_function, division 26 | 27 | import sys 28 | import random 29 | import socket 30 | import asyncore 31 | import idaapi 32 | import subprocess 33 | from idautils import * 34 | from idc import * 35 | from PyQt5.QtCore import QTimer, QObject, pyqtSignal 36 | 37 | from .server import Server 38 | from .client import Client 39 | from .project import Project 40 | 41 | 42 | def launch_ida_gui_instance(idb_path): 43 | """Launches a fresh IDA instance, opening the given IDB.""" 44 | return subprocess.Popen([sys.executable, idb_path]) 45 | 46 | 47 | class Continuum(QObject): 48 | """ 49 | Plugin core class, providing functionality required for both, the 50 | analysis stub and the full GUI instance version. 51 | """ 52 | project_opened = pyqtSignal([Project]) 53 | project_closing = pyqtSignal() 54 | client_created = pyqtSignal([Client]) 55 | 56 | def __init__(self): 57 | super(Continuum, self).__init__() 58 | 59 | self.project = None 60 | self.client = None 61 | self.server = None 62 | self._timer = None 63 | 64 | # Sign up for events. 65 | idaapi.notify_when(idaapi.NW_OPENIDB, self.handle_open_idb) 66 | idaapi.notify_when(idaapi.NW_CLOSEIDB, self.handle_close_idb) 67 | 68 | def create_server_if_none(self): 69 | """Creates a localhost server if none is alive, yet.""" 70 | # Server alive? 71 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 72 | server_port = self.read_or_generate_server_port() 73 | try: 74 | sock.connect(('127.0.0.1', server_port)) 75 | except socket.error: 76 | # Nope, create one. 77 | print("[continuum] Creating server.") 78 | self.server = Server(server_port, self) 79 | finally: 80 | sock.close() 81 | 82 | def create_client(self): 83 | """Creates a client connecting to the localhost server.""" 84 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 85 | server_port = self.read_or_generate_server_port() 86 | try: 87 | sock.connect(('127.0.0.1', server_port)) 88 | self.client = Client(sock, self) 89 | self.client_created.emit(self.client) 90 | except socket.error: 91 | sock.close() 92 | raise Exception("No server found") 93 | 94 | def enable_asyncore_loop(self): 95 | """Hooks our asyncore loop into Qt's event queue.""" 96 | def beat(): 97 | asyncore.loop(count=1, timeout=0) 98 | 99 | # Yep, this isn't especially real-time IO, but it's fine for what we do. 100 | timer = QTimer() 101 | timer.timeout.connect(beat) 102 | timer.setSingleShot(False) 103 | timer.setInterval(15) 104 | timer.start() 105 | 106 | self._timer = timer 107 | 108 | def disable_asyncore_loop(self): 109 | """Removes our asyncore loop from Qt's event queue.""" 110 | self._timer = None 111 | 112 | def open_project(self, project): 113 | """Performs operations required when a project is opened.""" 114 | print("[continuum] Opening project.") 115 | 116 | self.project = project 117 | self.create_server_if_none() 118 | self.create_client() 119 | self.enable_asyncore_loop() 120 | 121 | self.project_opened.emit(project) 122 | 123 | def close_project(self): 124 | """Performs clean-up work when a project is closed.""" 125 | print("[continuum] Closing project.") 126 | 127 | self.project_closing.emit() 128 | self.disable_asyncore_loop() 129 | 130 | # Are we server? Initiate host migration. 131 | if self.server: 132 | self.server.migrate_host_and_shutdown() 133 | self.server = None 134 | 135 | self.client.close() 136 | self.client = None 137 | self.project = None 138 | 139 | def handle_open_idb(self, _, is_old_database): 140 | """Performs start-up tasks when a new IDB is loaded.""" 141 | # Is IDB part of a continuum project? Open it. 142 | proj_dir = Project.find_project_dir(GetIdbDir()) 143 | if proj_dir: 144 | project = Project() 145 | project.open(proj_dir) 146 | self.open_project(project) 147 | project.index.sync_types_into_idb() 148 | 149 | def handle_close_idb(self, _): 150 | """Handles the situation a user closes the current IDB.""" 151 | if self.client: 152 | self.close_project() 153 | 154 | def read_or_generate_server_port(self, force_fresh=False): 155 | """ 156 | Obtains the localhost server port. If the port isn't yet defined, 157 | a random one is chosen and written to disk for other instances to read. 158 | """ 159 | server_port_file = os.path.join(self.project.meta_dir, 'server_port') 160 | if not force_fresh and os.path.exists(server_port_file): 161 | with open(server_port_file) as f: 162 | return int(f.read()) 163 | else: 164 | server_port = int(random.uniform(10000, 65535)) 165 | with open(server_port_file, 'w') as f: 166 | f.write(str(server_port)) 167 | return server_port 168 | 169 | def follow_extern(self): 170 | """Follows the extern symbol under the cursor, if possible.""" 171 | ea = ScreenEA() 172 | if GetSegmentAttr(ea, SEGATTR_TYPE) != SEG_XTRN: 173 | return 174 | 175 | name = Name(ea) 176 | if name.startswith('__imp_'): 177 | name = name[6:] 178 | 179 | self.client.send_focus_symbol(name) 180 | 181 | 182 | def PLUGIN_ENTRY(): 183 | """Entry point.""" 184 | from .plugin import Plugin 185 | return Plugin() 186 | -------------------------------------------------------------------------------- /continuum/analyze.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | # NOTE: This is not a regular project file, this is invoked as batch script by IDA! 26 | 27 | from __future__ import absolute_import, print_function, division 28 | 29 | import sys 30 | import os 31 | from idc import * 32 | from idautils import * 33 | 34 | sys.path.append( 35 | os.path.join( 36 | os.path.dirname(os.path.realpath(__file__)), 37 | '..', 38 | ) 39 | ) 40 | 41 | from continuum import Continuum 42 | from continuum.project import Project 43 | 44 | # Connect to server instance. 45 | proj = Project() 46 | cont = Continuum() 47 | proj.open(Project.find_project_dir(GetIdbDir()), skip_analysis=True) 48 | cont.open_project(proj) 49 | 50 | # Wait for auto-analysis to complete. 51 | SetShortPrm(INF_AF2, GetShortPrm(INF_AF2) | AF2_DODATA) 52 | print("Analyzing input file ...") 53 | cont.client.send_analysis_state('auto-analysis') 54 | Wait() 55 | 56 | # Index types. 57 | print("Indexing types ...") 58 | cont.client.send_analysis_state('indexing-types') 59 | proj.index.index_types_for_this_idb() 60 | 61 | # Index symbols. 62 | print("Indexing symbols ...") 63 | cont.client.send_analysis_state('indexing-symbols') 64 | proj.index.index_symbols_for_this_idb() 65 | cont.client.send_sync_types(purge_non_indexed=False) 66 | 67 | # Prevent UI from popping up. 68 | cont.client.send_analysis_state('done') 69 | print("All good, exiting.") 70 | Exit(0) 71 | -------------------------------------------------------------------------------- /continuum/client.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import absolute_import, print_function, division 26 | 27 | import sys 28 | import asyncore 29 | from idc import * 30 | from idautils import * 31 | from .proto import ProtoMixin 32 | from PyQt5.QtCore import QObject, pyqtSignal 33 | 34 | 35 | class Client(QObject, ProtoMixin, asyncore.dispatcher_with_send): 36 | """Client class for the localhost network.""" 37 | client_analysis_state_updated = pyqtSignal([str, str]) # idb_path, state 38 | sync_types = pyqtSignal([bool]) # purge_non_indexed 39 | 40 | def __init__(self, sock, core): 41 | asyncore.dispatcher_with_send.__init__(self, sock=sock) 42 | ProtoMixin.__init__(self) 43 | QObject.__init__(self) 44 | self.core = core 45 | self.idb_path = GetIdbPath() 46 | 47 | self.send_packet({ 48 | 'kind': 'new_client', 49 | 'input_file': GetInputFile(), 50 | 'idb_path': GetIdbPath(), 51 | 'pid': os.getpid(), 52 | }) 53 | 54 | print("[continuum] Connected.") 55 | 56 | def handle_close(self): 57 | asyncore.dispatcher_with_send.handle_close(self) 58 | print("[continuum] Connection lost, reconnecting.") 59 | self.core.create_client() 60 | 61 | def handle_msg_focus_symbol(self, symbol, **_): 62 | for i in xrange(GetEntryPointQty()): 63 | ordinal = GetEntryOrdinal(i) 64 | if GetEntryName(ordinal) == symbol: 65 | # `Jump` also focuses the instance. 66 | Jump(GetEntryPoint(ordinal)) 67 | break 68 | 69 | def handle_msg_focus_instance(self, **_): 70 | Jump(ScreenEA()) 71 | 72 | def handle_msg_become_host(self, **_): 73 | print("[continuum] We were elected as host.") 74 | self.core.create_server_if_none() 75 | 76 | def handle_msg_analysis_state_updated(self, client, state, **_): 77 | self.client_analysis_state_updated.emit(client, state) 78 | 79 | def handle_msg_sync_types(self, purge_non_indexed, **_): 80 | self.sync_types.emit(purge_non_indexed) 81 | 82 | @staticmethod 83 | def _allow_others_focusing(): 84 | if sys.platform == 'win32': 85 | # On Windows, there's a security mechanism preventing other applications 86 | # from putting themselves into the foreground unless explicitly permitted. 87 | import ctypes 88 | ctypes.windll.user32.AllowSetForegroundWindow(-1) 89 | 90 | def send_focus_symbol(self, symbol): 91 | self._allow_others_focusing() 92 | self.send_packet({ 93 | 'kind': 'focus_symbol', 94 | 'symbol': symbol, 95 | }) 96 | 97 | def send_focus_instance(self, idb_path): 98 | self._allow_others_focusing() 99 | self.send_packet({ 100 | 'kind': 'focus_instance', 101 | 'idb_path': idb_path, 102 | }) 103 | 104 | def send_analysis_state(self, state): 105 | self.send_packet({ 106 | 'kind': 'update_analysis_state', 107 | 'state': state, 108 | }) 109 | 110 | def send_sync_types(self, purge_non_indexed): 111 | self.send_packet({ 112 | 'kind': 'sync_types', 113 | 'purge_non_indexed': purge_non_indexed, 114 | }) 115 | -------------------------------------------------------------------------------- /continuum/index.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import absolute_import, print_function, division 26 | 27 | import sqlite3 28 | import idaapi 29 | from idc import * 30 | 31 | 32 | class LocalTypesIter(object): 33 | """Iterator for local types.""" 34 | def __init__(self, flags=idaapi.NTF_TYPE | idaapi.NTF_SYMM): 35 | self.flags = flags 36 | self.cur = idaapi.first_named_type(idaapi.cvar.idati, flags) 37 | 38 | def __iter__(self): 39 | return self 40 | 41 | def next(self): 42 | if self.cur is None: 43 | raise StopIteration 44 | 45 | val = self.cur 46 | self.cur = idaapi.next_named_type(idaapi.cvar.idati, self.cur, self.flags) 47 | return val 48 | 49 | 50 | # noinspection SqlNoDataSourceInspection 51 | class Index(object): 52 | """Central SQLite based index for project-level data.""" 53 | INDEX_DB_NAME = 'index.db' 54 | 55 | def __init__(self, project): 56 | self.db = sqlite3.connect(os.path.join(project.meta_dir, self.INDEX_DB_NAME)) 57 | self.db.row_factory = sqlite3.Row 58 | self.project = project 59 | self.create_schema() 60 | 61 | def create_schema(self): 62 | """Create DB schema if none exists, yet.""" 63 | cursor = self.db.cursor() 64 | cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='binary'") 65 | 66 | # Schema already good? Skip. 67 | if cursor.fetchone(): 68 | return 69 | 70 | # Nope, create schema now. 71 | cursor.executescript(""" 72 | CREATE TABLE binary ( 73 | id INTEGER PRIMARY KEY, 74 | idb_path TEXT NOT NULL UNIQUE, 75 | input_file TEXT NOT NULL 76 | ); 77 | 78 | CREATE TABLE export ( 79 | id INTEGER PRIMARY KEY, 80 | binary_id INTEGER NOT NULL, 81 | name TEXT NOT NULL, 82 | FOREIGN KEY(binary_id) REFERENCES binary(id) 83 | ON UPDATE CASCADE 84 | ON DELETE CASCADE 85 | ); 86 | 87 | CREATE INDEX idx_export_name ON export(name); 88 | 89 | /*CREATE TABLE xrefs ( 90 | id INTEGER PRIMARY KEY, 91 | export_id INTEGER NOT NULL, 92 | binary_id INTEGER NOT NULL, 93 | FOREIGN KEY(export_id) REFERENCES export(id) 94 | ON UPDATE CASCADE 95 | ON DELETE CASCADE, 96 | FOREIGN KEY(binary_id) REFERENCES binary(id) 97 | ON UPDATE CASCADE 98 | ON DELETE CASCADE 99 | );*/ 100 | 101 | CREATE TABLE types ( 102 | id INTEGER PRIMARY KEY, 103 | name TEXT NOT NULL UNIQUE, 104 | is_fwd_decl INTEGER NOT NULL, 105 | c_type TEXT NOT NULL 106 | ); 107 | """) 108 | self.db.commit() 109 | 110 | def is_idb_indexed(self, idb_path): 111 | """Determines whether the IDB is indexed yet.""" 112 | cursor = self.db.cursor() 113 | cursor.execute("SELECT id FROM binary WHERE idb_path=?", [idb_path]) 114 | return cursor.fetchone() is not None 115 | 116 | def index_symbols_for_this_idb(self): 117 | idb_path = GetIdbPath() 118 | if self.is_idb_indexed(idb_path): 119 | raise Exception("Cache for this IDB is already built.") 120 | 121 | # Create binary record. 122 | cursor = self.db.cursor() 123 | cursor.execute( 124 | "INSERT INTO binary (idb_path, input_file) VALUES (?, ?)", 125 | [idb_path, GetInputFile()], 126 | ) 127 | binary_id = cursor.lastrowid 128 | 129 | # Populate index. 130 | for i in xrange(GetEntryPointQty()): 131 | ordinal = GetEntryOrdinal(i) 132 | name = GetEntryName(ordinal) 133 | 134 | # For of now, we only support names exported by-name. 135 | if name is None: 136 | continue 137 | 138 | cursor.execute( 139 | "INSERT INTO export (binary_id, name) VALUES (?, ?)", 140 | [binary_id, name], 141 | ) 142 | 143 | # All good, flush. 144 | self.db.commit() 145 | 146 | def index_types_for_this_idb(self, purge_locally_deleted=False): 147 | """Indexes local types from this IDB into the DB.""" 148 | cursor = self.db.cursor() 149 | 150 | # Create or update types. 151 | local_types = set() 152 | for cur_named_type in LocalTypesIter(): 153 | local_types.add(cur_named_type) 154 | code, type_str, fields_str, cmt, field_cmts, sclass, value = idaapi.get_named_type64( 155 | idaapi.cvar.idati, 156 | cur_named_type, 157 | idaapi.NTF_TYPE | idaapi.NTF_SYMM, 158 | ) 159 | 160 | ti = idaapi.tinfo_t() 161 | ti.deserialize(idaapi.cvar.idati, type_str, fields_str) 162 | c_type = ti._print( 163 | cur_named_type, 164 | idaapi.PRTYPE_1LINE | idaapi.PRTYPE_TYPE | idaapi.PRTYPE_SEMI, 165 | 0, 0, None, cmt, 166 | ) 167 | 168 | # TODO: prefer more concrete type rather than stupidly replacing. 169 | cursor.execute( 170 | "INSERT OR REPLACE INTO types (name, is_fwd_decl, c_type) VALUES (?, ?, ?)", 171 | [cur_named_type, ti.is_forward_decl(), c_type], 172 | ) 173 | 174 | # If requested, remove locally deleted types from index. 175 | if purge_locally_deleted: 176 | cursor.execute("SELECT id, name FROM types") 177 | deleted_types = [x['id'] for x in cursor.fetchall() if x['name'] not in local_types] 178 | if deleted_types: 179 | print("[continuum] Deleting {} types".format(len(deleted_types))) 180 | query_fmt = "DELETE FROM types WHERE id IN ({})" 181 | cursor.execute(query_fmt.format(','.join('?' * len(deleted_types))), deleted_types) 182 | 183 | self.db.commit() 184 | 185 | def sync_types_into_idb(self, purge_non_indexed=False): 186 | """Loads types from the index into this IDB and deletes all orphaned types.""" 187 | cursor = self.db.cursor() 188 | self.project.ignore_changes = True 189 | 190 | try: 191 | cursor.execute("SELECT name, c_type FROM types") 192 | indexed_types = set() 193 | for cur_row in cursor.fetchall(): 194 | idaapi.parse_decls(idaapi.cvar.idati, str(cur_row['c_type']), None, 0) 195 | indexed_types.add(cur_row['name']) 196 | 197 | if purge_non_indexed: 198 | orphaned_types = [x for x in LocalTypesIter() if x not in indexed_types] 199 | for cur_orphan in orphaned_types: 200 | idaapi.del_named_type( 201 | idaapi.cvar.idati, cur_orphan, idaapi.NTF_TYPE | idaapi.NTF_SYMM 202 | ) 203 | finally: 204 | self.project.ignore_changes = False 205 | 206 | def find_export(self, symbol): 207 | """ 208 | Finds an exported symbol, returning either the desired info or `None` 209 | if the symbol wasn't found in the index. 210 | """ 211 | cursor = self.db.cursor() 212 | cursor.execute(""" 213 | SELECT e.id, b.idb_path FROM export e 214 | JOIN binary b ON e.binary_id = b.id 215 | WHERE e.name = ? 216 | """, [symbol]) 217 | row = cursor.fetchone() 218 | return None if row is None else dict(row) 219 | -------------------------------------------------------------------------------- /continuum/plugin.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import absolute_import, print_function, division 26 | 27 | import idaapi 28 | from . import Continuum 29 | from idautils import * 30 | from idc import * 31 | from PyQt5.QtWidgets import QDialog 32 | 33 | from .ui import ProjectExplorerWidget, ProjectCreationDialog 34 | from .project import Project 35 | 36 | 37 | class Plugin(idaapi.plugin_t): 38 | """Core class for the plugin, registered into IDA.""" 39 | flags = idaapi.PLUGIN_FIX 40 | 41 | comment = "Plugin adding multi-binary project support" 42 | help = comment 43 | wanted_name = "continuum" 44 | wanted_hotkey = None # We don't need a hotkey. 45 | 46 | def __init__(self): 47 | super(Plugin, self).__init__() 48 | self.core = None 49 | self.project_explorer = None 50 | self.idb_hook = None 51 | self.ui_hook = None 52 | 53 | def init(self): 54 | """init callback, invoked by IDA when the plugin is loaded.""" 55 | self.core = Continuum() 56 | zelf = self 57 | 58 | # Place UI hook so we know when to create our UI stuff. 59 | class UiHooks(idaapi.UI_Hooks): 60 | def ready_to_run(self, *_): 61 | zelf.ui_init() 62 | zelf.ui_hook.unhook() 63 | 64 | self.ui_hook = UiHooks() 65 | self.ui_hook.hook() 66 | 67 | # Setup IDP hook for type changes. 68 | class IdbHooks(idaapi.IDB_Hooks): 69 | def local_types_changed(self, *args): 70 | if zelf.core.client and not zelf.core.project.ignore_changes: 71 | zelf.core.project.index.index_types_for_this_idb(purge_locally_deleted=True) 72 | zelf.core.client.send_sync_types(purge_non_indexed=True) 73 | return 0 74 | 75 | self.idb_hook = IdbHooks() 76 | self.idb_hook.hook() 77 | 78 | # Hack ref to plugin core object into idaapi for easy debugging. 79 | idaapi.continuum = self.core 80 | 81 | print("[continuum] v0.0.0 by athre0z (zyantific.com) loaded!") 82 | return idaapi.PLUGIN_KEEP 83 | 84 | def run(self, arg): 85 | """run callback, invoked by IDA when the user clicks the plugin menu entry.""" 86 | print("[continuum] No fancy action hidden here, yet!") 87 | 88 | def term(self): 89 | """term callback, invoked by IDA when the plugin is unloaded.""" 90 | if self.core.client: 91 | self.core.close_project() 92 | 93 | self.idb_hook.unhook() 94 | self.core.disable_asyncore_loop() 95 | print("[continuum] Plugin unloaded.") 96 | 97 | def ui_init(self): 98 | """Initializes the plugins interface extensions.""" 99 | # Register menu entry. 100 | # @HR: I really preferred the pre-6.5 mechanic. 101 | zelf = self 102 | class MenuEntry(idaapi.action_handler_t): 103 | def activate(self, ctx): 104 | zelf.open_proj_creation_dialog() 105 | return 1 106 | 107 | def update(self, ctx): 108 | return idaapi.AST_ENABLE_ALWAYS 109 | 110 | action = idaapi.action_desc_t( 111 | 'continuum_new_project', 112 | "New continuum project...", 113 | MenuEntry(), 114 | ) 115 | idaapi.register_action(action) 116 | idaapi.attach_action_to_menu("File/Open...", 'continuum_new_project', 0) 117 | 118 | # Alright, is an IDB loaded? Pretend IDB open event as we miss the callback 119 | # when it was loaded before our plugin was staged. 120 | if GetIdbPath(): 121 | self.core.handle_open_idb(None, None) 122 | 123 | # Register hotkeys. 124 | idaapi.add_hotkey('Shift+F', self.core.follow_extern) 125 | 126 | # Sign up for events. 127 | self.core.project_opened.connect(self.create_proj_explorer) 128 | self.core.project_closing.connect(self.close_proj_explorer) 129 | self.core.client_created.connect(self.subscribe_client_events) 130 | 131 | # Project / client already open? Fake events. 132 | if self.core.project: 133 | self.create_proj_explorer(self.core.project) 134 | if self.core.client: 135 | self.subscribe_client_events(self.core.client) 136 | 137 | def create_proj_explorer(self, project): 138 | """Creates the project explorer "sidebar" widget.""" 139 | self.project_explorer = ProjectExplorerWidget(project) 140 | self.project_explorer.Show("continuum project") 141 | self.project_explorer.refresh_project_clicked.connect(self.refresh_project) 142 | self.project_explorer.focus_instance_clicked.connect( 143 | lambda idb_path: self.core.client.send_focus_instance(idb_path) 144 | ) 145 | idaapi.set_dock_pos("continuum project", "Functions window", idaapi.DP_BOTTOM) 146 | 147 | def close_proj_explorer(self): 148 | """Removes the project explorer widget.""" 149 | self.project_explorer.Close(0) 150 | self.project_explorer = None 151 | 152 | def subscribe_client_events(self, client): 153 | """Subscribe to events of the `Client` instance.""" 154 | client.sync_types.connect(self.core.project.index.sync_types_into_idb) 155 | 156 | def open_proj_creation_dialog(self): 157 | """Performs sanity checks and pops up a project creation dialog, if applicable.""" 158 | if self.core.client: 159 | print("[continuum] A project is already opened.") 160 | return 161 | 162 | if not GetIdbPath(): 163 | print("[continuum] Please load an IDB related to the project first.") 164 | return 165 | 166 | # Check if auto-analysis is through prior allowing project creation here. 167 | # This probably isn't intended to be done by plugins, but there there seems to be no 168 | # official API to check for this that also works when auto-analysis has temporarily 169 | # been disabled due to an UI action (specifically here: opening menus). 170 | # I found this netnode by reversing IDA. 171 | if not idaapi.exist(idaapi.netnode("$ Auto ready")): 172 | print("[continuum] Please allow auto-analysis to finish first.") 173 | return 174 | 175 | dialog = ProjectCreationDialog(GetIdbDir()) 176 | chosen_action = dialog.exec_() 177 | 178 | if chosen_action == QDialog.Accepted: 179 | project = Project.create(dialog.project_path, dialog.file_patterns) 180 | self.core.open_project(project) 181 | 182 | def refresh_project(self, *_): 183 | """Refreshes the project, scanning for new files etc..""" 184 | if not self.project_explorer: 185 | return 186 | 187 | self.project_explorer.update() 188 | -------------------------------------------------------------------------------- /continuum/project.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | from __future__ import absolute_import, print_function, division 26 | 27 | import sys 28 | import subprocess 29 | import itertools 30 | import ConfigParser 31 | import fnmatch 32 | from PyQt5.QtCore import QObject, pyqtSignal 33 | from idc import * 34 | 35 | from .index import Index 36 | 37 | 38 | class Project(QObject): 39 | """Represents a continuum project and it's properties.""" 40 | META_DIR_NAME = '.continuum' 41 | CFG_FILE_NAME = 'project.conf' 42 | 43 | def __init__(self): 44 | super(Project, self).__init__() 45 | self.conf = None 46 | self.index = None 47 | self.proj_dir = None 48 | self.meta_dir = None 49 | self.ignore_changes = False 50 | self.files = [] 51 | 52 | def open(self, root, skip_analysis=False): 53 | """Opens an existing project by its root path.""" 54 | if self.proj_dir: 55 | raise Exception("A project is already opened") 56 | 57 | # Find meta directory and read config. 58 | meta_dir = os.path.join(root, self.META_DIR_NAME) 59 | conf = ConfigParser.SafeConfigParser() 60 | if not conf.read(os.path.join(meta_dir, self.CFG_FILE_NAME)): 61 | raise Exception("Project is lacking its config file") 62 | 63 | # Determine project files. 64 | file_patterns = conf.get('project', 'file_patterns') 65 | if file_patterns is None: 66 | raise Exception("Project configuration lacks `file_patterns` directive") 67 | files = list(self.find_project_files(root, file_patterns)) 68 | 69 | # Everything fine, put info into `self`. 70 | self.conf = conf 71 | self.proj_dir = root 72 | self.meta_dir = meta_dir 73 | self.files = files 74 | self.index = Index(self) 75 | 76 | if not skip_analysis: 77 | # Is index for *this* IDB built? If not, do so. 78 | if not self.index.is_idb_indexed(GetIdbPath()): 79 | self.index.index_types_for_this_idb() 80 | self.index.index_symbols_for_this_idb() 81 | 82 | # Analyze other files, if required. 83 | self._analyze_project_files() 84 | 85 | def _analyze_project_files(self): 86 | """Launches background analysis instances for binaries that aren't indexed yet.""" 87 | plugin_root = os.path.dirname(os.path.realpath(__file__)) 88 | procs = [] 89 | 90 | for cur_file in self.files: 91 | # IDB already indexed? Skip. 92 | if self.index.is_idb_indexed(self.file_to_idb(cur_file)): 93 | continue 94 | 95 | procs.append(subprocess.Popen([ 96 | sys.executable, 97 | '-A', 98 | '-S"{}"'.format(os.path.join(plugin_root, 'analyze.py')), 99 | '-L{}.log'.format(cur_file), 100 | cur_file, 101 | ])) 102 | 103 | return procs 104 | 105 | @staticmethod 106 | def file_to_idb(file): 107 | """Obtains the IDB path for a binary.""" 108 | return os.path.splitext(file)[0] + '.idb' 109 | 110 | @classmethod 111 | def find_project_dir(cls, start_path): 112 | """ 113 | Traverses up the directory tree, searching for a project root. 114 | If one is found, returns the path, else `None`. 115 | """ 116 | tail = object() 117 | head = start_path 118 | while tail: 119 | head, tail = os.path.split(head) 120 | cur_meta_path = os.path.join(head, tail, cls.META_DIR_NAME) 121 | if os.path.exists(cur_meta_path): 122 | return os.path.join(head, tail) 123 | 124 | @staticmethod 125 | def find_project_files(root, file_patterns): 126 | """Locates all binaries that are part of a project.""" 127 | file_patterns = [x.strip() for x in file_patterns.split(';')] 128 | for dirpath, _, filenames in os.walk(root): 129 | relevant_files = itertools.chain.from_iterable( 130 | fnmatch.filter(filenames, x) for x in file_patterns 131 | ) 132 | 133 | # Py2 Y U NO SUPPORT "yield from"? :( 134 | for cur_file in relevant_files: 135 | yield os.path.join(dirpath, cur_file) 136 | 137 | @classmethod 138 | def create(cls, root, file_patterns): 139 | """Creates a new project.""" 140 | # Create meta directory. 141 | cont_dir = os.path.join(root, cls.META_DIR_NAME) 142 | if os.path.exists(cont_dir): 143 | raise Exception("Directory is already a continuum project") 144 | os.mkdir(cont_dir) 145 | 146 | # Create config file. 147 | config = ConfigParser.SafeConfigParser() 148 | config.add_section('project') 149 | config.set('project', 'file_patterns', file_patterns) 150 | with open(os.path.join(cont_dir, cls.CFG_FILE_NAME), 'w') as f: 151 | config.write(f) 152 | 153 | # Create initial index. 154 | project = Project() 155 | project.open(root) 156 | 157 | return project 158 | -------------------------------------------------------------------------------- /continuum/proto.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from __future__ import absolute_import, print_function, division 25 | 26 | import struct 27 | import json 28 | 29 | 30 | class ProtoMixin(object): 31 | """Mixin implementing a simple length-prefixed packet JSON protocol.""" 32 | 33 | NET_HDR_FORMAT = '>I' 34 | NET_HDR_LEN = struct.calcsize(NET_HDR_FORMAT) 35 | 36 | def __init__(self): 37 | self.recv_buf = bytearray() 38 | 39 | def handle_packet(self, packet): 40 | """ 41 | Handles complete messages, invoking the corresponding `handle_msg_*` handler 42 | based on the `kind` field in the packet. 43 | """ 44 | handler = getattr(self, 'handle_msg_' + packet['kind'], None) 45 | if handler is None: 46 | print("Received packet of unknown kind '{}'".format(packet['kind'])) 47 | return 48 | 49 | print("[continuum] {} RECVED: {!r}".format(self.__class__.__name__, packet)) 50 | if type(packet) != dict or any(type(x) != unicode for x in packet.keys()): 51 | print("Received malformed packet.") 52 | return 53 | 54 | try: 55 | handler(**packet) 56 | except TypeError as exc: 57 | print("Received invalid arguments for packet: " + str(exc)) 58 | return 59 | 60 | def handle_read(self): 61 | """Receives fresh data, performing TCP reassembly and JSON decoding.""" 62 | self.recv_buf += self.recv(1500) 63 | if len(self.recv_buf) < self.NET_HDR_LEN: 64 | return 65 | 66 | packet_len, = struct.unpack( 67 | self.NET_HDR_FORMAT, 68 | self.recv_buf[:self.NET_HDR_LEN], 69 | ) 70 | if len(self.recv_buf) < packet_len: 71 | return 72 | 73 | packet = self.recv_buf[self.NET_HDR_LEN:packet_len + self.NET_HDR_LEN] 74 | packet = json.loads(packet.decode('utf8')) 75 | self.handle_packet(packet) 76 | self.recv_buf = self.recv_buf[packet_len + self.NET_HDR_LEN:] 77 | 78 | def send_packet(self, packet): 79 | """Serializes data to JSON and sends it.""" 80 | packet = json.dumps(packet).encode('utf8') 81 | self.send(struct.pack(self.NET_HDR_FORMAT, len(packet))) 82 | self.send(packet) 83 | -------------------------------------------------------------------------------- /continuum/server.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from __future__ import absolute_import, print_function, division 25 | 26 | import asyncore 27 | import socket 28 | from idc import * 29 | from idautils import * 30 | from collections import defaultdict 31 | from .proto import ProtoMixin 32 | from PyQt5.QtCore import QObject, pyqtSignal 33 | 34 | 35 | class ClientConnection(ProtoMixin, asyncore.dispatcher_with_send): 36 | """Represents a client that is connected to the localhost server.""" 37 | def __init__(self, sock, server): 38 | # We need to use old-style init calls here because asyncore 39 | # consists of old-style classes :( 40 | asyncore.dispatcher_with_send.__init__(self, sock=sock) 41 | ProtoMixin.__init__(self) 42 | 43 | self.input_file = None 44 | self.idb_path = None 45 | self.server = server 46 | self.project = server.core.project 47 | self.server.clients.add(self) 48 | 49 | def handle_close(self): 50 | self.server.clients.remove(self) 51 | self.server.update_idb_client_map() 52 | print("[continuum] A client disconnected.") 53 | asyncore.dispatcher_with_send.handle_close(self) 54 | 55 | def send_or_delay_packet(self, receiver_idb_path, packet): 56 | # Is a client for this IDB alive? Just send message. 57 | client = self.server.idb_client_map.get(receiver_idb_path) 58 | if client: 59 | client.send_packet(packet) 60 | # Nope, put message into backlog and launch a fresh idaq. 61 | else: 62 | self.server.queue_delayed_packet(receiver_idb_path, packet) 63 | from . import launch_ida_gui_instance 64 | launch_ida_gui_instance(receiver_idb_path) 65 | 66 | def broadcast_packet(self, packet): 67 | for cur_client in self.server.clients: 68 | if cur_client == self: 69 | continue 70 | cur_client.send_packet(packet) 71 | 72 | def handle_msg_new_client(self, input_file, idb_path, **_): 73 | self.input_file = input_file 74 | self.idb_path = idb_path 75 | self.server.update_idb_client_map() 76 | 77 | # Client start-up sequence is completed, deliver delayed messages. 78 | self.server.process_delayed_packets(self) 79 | 80 | def handle_msg_focus_symbol(self, symbol, **_): 81 | export = self.project.index.find_export(symbol) 82 | if export is None: 83 | print("[continuum] Symbol '{}' not found.".format(symbol)) 84 | return 85 | 86 | self.send_or_delay_packet(export['idb_path'], { 87 | 'kind': 'focus_symbol', 88 | 'symbol': symbol, 89 | }) 90 | 91 | def handle_msg_focus_instance(self, idb_path, **_): 92 | self.send_or_delay_packet(idb_path, {'kind': 'focus_instance'}) 93 | 94 | def handle_msg_update_analysis_state(self, state, **_): 95 | self.broadcast_packet({ 96 | 'kind': 'analysis_state_updated', 97 | 'client': self.idb_path, 98 | 'state': state, 99 | }) 100 | 101 | def handle_msg_sync_types(self, purge_non_indexed, **_): 102 | self.broadcast_packet({ 103 | 'kind': 'sync_types', 104 | 'purge_non_indexed': purge_non_indexed, 105 | }) 106 | 107 | 108 | class Server(asyncore.dispatcher): 109 | """ 110 | The server for the localhost network, spawning and tracking 111 | `ClientConnection` instances for all connected clients. 112 | """ 113 | def __init__(self, port, core): 114 | asyncore.dispatcher.__init__(self) 115 | 116 | self.core = core 117 | self.clients = set() 118 | self.idb_client_map = dict() 119 | self._delayed_packets = defaultdict(list) 120 | 121 | self.create_socket(socket.AF_INET, socket.SOCK_STREAM) 122 | self.set_reuse_addr() 123 | self.bind(('127.0.0.1', port)) 124 | self.listen(5) 125 | 126 | def handle_accept(self): 127 | pair = self.accept() 128 | if pair is not None: 129 | sock, addr = pair 130 | print("[continuum] Connection from {!r}".format(addr)) 131 | ClientConnection(sock, self) 132 | 133 | def update_idb_client_map(self): 134 | """Updates the IDB-Path -> Client map.""" 135 | self.idb_client_map = { 136 | x.idb_path: x for x in self.clients if x.idb_path is not None 137 | } 138 | 139 | def queue_delayed_packet(self, idb_path, packet): 140 | """Queues a delayed packet to be delivered to a client that isn't online, yet.""" 141 | self._delayed_packets[idb_path].append(packet) 142 | 143 | def process_delayed_packets(self, client): 144 | """Delivers pending delayed packets to a client that freshly became ready.""" 145 | assert client.idb_path 146 | for cur_packet in self._delayed_packets[client.idb_path]: 147 | client.send_packet(cur_packet) 148 | 149 | def migrate_host_and_shutdown(self): 150 | """ 151 | Orders another client to take over the host position in the network and shuts down. 152 | As sockets tend to take several seconds before they fully close and free the used 153 | port again, we select a fresh one before getting to work. 154 | """ 155 | # Any other client online? Migrate host. 156 | host_candidates = [x for x in self.clients if x.idb_path != self.core.client.idb_path] 157 | if host_candidates: 158 | self.core.read_or_generate_server_port(force_fresh=True) 159 | elected_client = next(iter(host_candidates)) 160 | elected_client.send_packet({'kind': 'become_host'}) 161 | 162 | # Close server socket. 163 | self.close() 164 | 165 | # Disconnect clients. 166 | for cur_client in self.clients: 167 | cur_client.close() 168 | -------------------------------------------------------------------------------- /continuum/ui.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | from __future__ import absolute_import, print_function, division 25 | 26 | import os 27 | import sys 28 | import sip 29 | from PyQt5 import uic 30 | from PyQt5.QtCore import Qt, pyqtSignal, QObject, QFileInfo 31 | from PyQt5.QtGui import QIcon 32 | from PyQt5.QtWidgets import QFileDialog, QListWidgetItem, QTreeWidgetItem, QFileIconProvider 33 | from idaapi import PluginForm 34 | 35 | from .project import Project 36 | 37 | 38 | ui_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'ui') 39 | Ui_ProjectCreationDialog, ProjectCreationDialogBase = uic.loadUiType( 40 | os.path.join(ui_dir, 'ProjectCreationDialog.ui') 41 | ) 42 | Ui_ProjectExplorerWidget, ProjectExplorerWidgetBase = uic.loadUiType( 43 | os.path.join(ui_dir, 'ProjectExplorer.ui') 44 | ) 45 | 46 | 47 | class ProjectCreationDialog(ProjectCreationDialogBase): 48 | """Dialog allowing convenient project creation.""" 49 | def __init__(self, initial_path=None): 50 | super(ProjectCreationDialog, self).__init__() 51 | 52 | self._ui = Ui_ProjectCreationDialog() 53 | self._ui.setupUi(self) 54 | 55 | if initial_path: 56 | self._ui.project_path.setText(os.path.realpath(initial_path)) 57 | self.update_binary_list() 58 | 59 | self._ui.browse_project_path.clicked.connect(self._browse_project_path) 60 | self._ui.project_path.textChanged.connect(self.update_binary_list) 61 | self._ui.file_patterns.textChanged.connect(self.update_binary_list) 62 | 63 | def _browse_project_path(self): 64 | path = QFileDialog.getExistingDirectory() 65 | path = os.path.realpath(path) 66 | self._ui.project_path.setText(path) 67 | 68 | def update_binary_list(self, *_): 69 | binaries = Project.find_project_files( 70 | self._ui.project_path.text(), 71 | self._ui.file_patterns.text(), 72 | ) 73 | 74 | self._ui.binary_list.clear() 75 | for cur_binary in binaries: 76 | item = QListWidgetItem(cur_binary) 77 | self._ui.binary_list.addItem(item) 78 | 79 | @property 80 | def project_path(self): 81 | return self._ui.project_path.text() 82 | 83 | @property 84 | def file_patterns(self): 85 | return self._ui.file_patterns.text() 86 | 87 | 88 | class ProjectExplorerWidget(QObject, PluginForm): 89 | """Project explorer widget, usually mounted to IDA's "sidebar".""" 90 | focus_instance_clicked = pyqtSignal([str]) # idb_path: str 91 | refresh_project_clicked = pyqtSignal() 92 | open_project_settings_clicked = pyqtSignal() 93 | 94 | def __init__(self, project): 95 | super(ProjectExplorerWidget, self).__init__() 96 | self.project = project 97 | self._tform = None 98 | self._qwidget = None 99 | self._ui = None 100 | 101 | def OnCreate(self, form): 102 | self._tform = form 103 | self._qwidget = self.FormToPyQtWidget(form, sys.modules[__name__]) 104 | 105 | # Setup UI. 106 | self._ui = Ui_ProjectExplorerWidget() 107 | self._ui.setupUi(self._qwidget) 108 | 109 | # Load icons. 110 | self._ui.open_project_settings.setIcon( 111 | QIcon(os.path.join(ui_dir, 'page_gear.png')) 112 | ) 113 | self._ui.refresh_project_files.setIcon( 114 | QIcon(os.path.join(ui_dir, 'arrow_refresh.png')) 115 | ) 116 | 117 | # Subscribe events. 118 | self._ui.open_project_settings.clicked.connect( 119 | lambda _: self.open_project_settings_clicked.emit() 120 | ) 121 | self._ui.refresh_project_files.clicked.connect( 122 | lambda _: self.refresh_project_clicked.emit() 123 | ) 124 | self._ui.project_tree.itemDoubleClicked.connect( 125 | lambda item, _: self.focus_instance_clicked.emit(item.data(0, Qt.UserRole)) 126 | ) 127 | 128 | self.update() 129 | 130 | def update(self): 131 | # Update files. 132 | self._ui.project_tree.clear() 133 | items = [] 134 | icon_provider = QFileIconProvider() 135 | 136 | for cur_file in self.project.files: 137 | file_info = QFileInfo(cur_file) 138 | item = QTreeWidgetItem(None, [ 139 | os.path.relpath(cur_file, self.project.proj_dir), 140 | "N/A", 141 | ]) 142 | item.setData(0, Qt.UserRole, Project.file_to_idb(cur_file)) 143 | item.setIcon(0, icon_provider.icon(file_info)) 144 | items.append(item) 145 | 146 | self._ui.project_tree.insertTopLevelItems(0, items) 147 | 148 | # Update other stuff. 149 | self._ui.project_path.setText(self.project.proj_dir) 150 | -------------------------------------------------------------------------------- /continuum/ui/ProjectCreationDialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Dialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 462 10 | 328 11 | 12 | 13 | 14 | Create continuum project ... 15 | 16 | 17 | 18 | 19 | 20 | Pathes 21 | 22 | 23 | 24 | 25 | 26 | Project path 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | File patterns 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 0 45 | 0 46 | 47 | 48 | 49 | Browse 50 | 51 | 52 | false 53 | 54 | 55 | 56 | 57 | 58 | 59 | *.dll; *.exe; *.dylib; *.so 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | Binaries 70 | 71 | 72 | 73 | 74 | 75 | NOTE: The list below is generated as preview resulting from the settings above. 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Qt::Horizontal 89 | 90 | 91 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | buttonBox 101 | accepted() 102 | Dialog 103 | accept() 104 | 105 | 106 | 248 107 | 254 108 | 109 | 110 | 157 111 | 274 112 | 113 | 114 | 115 | 116 | buttonBox 117 | rejected() 118 | Dialog 119 | reject() 120 | 121 | 122 | 316 123 | 260 124 | 125 | 126 | 286 127 | 274 128 | 129 | 130 | 131 | 132 | 133 | -------------------------------------------------------------------------------- /continuum/ui/ProjectExplorer.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | ProjectExplorer 4 | 5 | 6 | 7 | 0 8 | 0 9 | 217 10 | 430 11 | 12 | 13 | 14 | continuum project 15 | 16 | 17 | 18 | 0 19 | 20 | 21 | 22 | 23 | 24 | 25 | No project loaded 26 | 27 | 28 | 29 | 30 | 31 | 32 | Qt::Horizontal 33 | 34 | 35 | 36 | 40 37 | 20 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | Refresh project files 46 | 47 | 48 | ... 49 | 50 | 51 | 52 | 53 | 54 | 55 | Edit project settings 56 | 57 | 58 | ... 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | File 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /continuum/ui/arrow_refresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyantific/continuum/ba7128ce3b8f1e7cda2790cf4f37d156d908d191/continuum/ui/arrow_refresh.png -------------------------------------------------------------------------------- /continuum/ui/page_gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyantific/continuum/ba7128ce3b8f1e7cda2790cf4f37d156d908d191/continuum/ui/page_gear.png -------------------------------------------------------------------------------- /continuum_ldr.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file is part of the continuum IDA PRO plugin (see zyantific.com). 3 | 4 | The MIT License (MIT) 5 | 6 | Copyright (c) 2016 Joel Hoener 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | # This file acts as a proxy, allowing the plugin to live in an extra directory. 26 | 27 | import sys 28 | import os 29 | 30 | sys.path.append( 31 | os.path.join( 32 | os.path.dirname(os.path.realpath(__file__)), 33 | 'continuum', 34 | ) 35 | ) 36 | 37 | import continuum 38 | 39 | def PLUGIN_ENTRY(): 40 | return continuum.PLUGIN_ENTRY() 41 | -------------------------------------------------------------------------------- /media/project-creation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyantific/continuum/ba7128ce3b8f1e7cda2790cf4f37d156d908d191/media/project-creation.png -------------------------------------------------------------------------------- /media/project-explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zyantific/continuum/ba7128ce3b8f1e7cda2790cf4f37d156d908d191/media/project-explorer.png --------------------------------------------------------------------------------