From cd4f8a34a6c81403b2e26d17e21271e5d696aee6 Mon Sep 17 00:00:00 2001 From: aaron Date: Fri, 24 Jan 2025 13:46:19 -0500 Subject: [PATCH 1/9] Added a fix for ida timeout issue --- dragodis/ida/disassembler.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index 4f77719..35082a4 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -184,7 +184,7 @@ class IDARemoteDisassembler(IDADisassembler): "sync_request_timeout": 60 } - def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, processor=None, detach=False, **unused): + def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, processor=None, detach=False, should_autoanalyze=True, **unused): """ Initializes IDA disassembler. @@ -210,7 +210,7 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, proc self._script_path = ida_server.__file__ if timeout is not None: self._rpyc_config = dict(self._rpyc_config) - self._rpyc_config["sync_request_timeout"] = timeout + self._rpyc_config["sync_request_timeout"] = None # Determine if 64 bit. if is_64_bit is None: @@ -240,6 +240,7 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, proc self._running = False self._detach = detach and sys.platform != "win32" + self._should_autoanalyze = should_autoanalyze self._socket_path = None self._process = None self._bridge = None @@ -348,7 +349,7 @@ def _initialize_bridge(self): self._ida_intel: ida_intel = self._bridge.root.getmodule("ida_intel") self._ida_helpers: ida_helpers = self._bridge.root.getmodule("ida_helpers") - def unix_connect(self, socket_path, retry=10) -> rpyc.Connection: + def unix_connect(self, socket_path, retry=20) -> rpyc.Connection: """ Connects to bridge using unix socket. """ @@ -366,7 +367,7 @@ def unix_connect(self, socket_path, retry=10) -> rpyc.Connection: raise DragodisError(f"Could not connect to {socket_path} after {retry} tries.") - def win_connect(self, pipe_name, retry=10) -> rpyc.Connection: + def win_connect(self, pipe_name, retry=20) -> rpyc.Connection: """ Connects to bridge using Windows named pipe. """ @@ -409,6 +410,7 @@ def start(self): command = [ self._ida_exe, "-P", + "-a", "-A", f'-S""{script_path}" "{pipe_name or socket_path}""', f'-L"{self.input_path}_ida.log"', @@ -443,7 +445,12 @@ def start(self): # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. self._root = self._bridge.root self._running = True - self._idc.auto_wait() + if self._should_autoanalyze: + logger.debug('Running autoanalysis.') + self._idc.set_flag(self._idc.INF_GENFLAGS, self._idc.INFFL_AUTO, 1) + self._idc.auto_wait() + else: + logger.debug('Skipping autoanalysis.') logger.debug("IDA Disassembler ready!") From 2f6af5ed1e4685be653de6749a3f9bf025ef202e Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 27 Jan 2025 10:52:15 -0500 Subject: [PATCH 2/9] Moved auto analysis to analyze() function --- dragodis/ida/disassembler.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index 35082a4..bd76123 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -184,7 +184,7 @@ class IDARemoteDisassembler(IDADisassembler): "sync_request_timeout": 60 } - def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, processor=None, detach=False, should_autoanalyze=True, **unused): + def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, processor=None, detach=False, analyze=True, **unused): """ Initializes IDA disassembler. @@ -199,6 +199,7 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, proc :param detach: Detach the IDA subprocess from the parent process group. This will cause signals to no longer propagate. (Linux only) + :param analyze: Determines whether autoanalysis should be conducted on the input file at IDA startup. """ super().__init__(input_path, processor=processor) self._ida_path = ida_path or os.environ.get("IDA_INSTALL_DIR", os.environ.get("IDA_DIR")) @@ -240,7 +241,7 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, proc self._running = False self._detach = detach and sys.platform != "win32" - self._should_autoanalyze = should_autoanalyze + self._analyze = analyze self._socket_path = None self._process = None self._bridge = None @@ -445,10 +446,8 @@ def start(self): # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. self._root = self._bridge.root self._running = True - if self._should_autoanalyze: - logger.debug('Running autoanalysis.') - self._idc.set_flag(self._idc.INF_GENFLAGS, self._idc.INFFL_AUTO, 1) - self._idc.auto_wait() + if self._analyze: + self.analyze() else: logger.debug('Skipping autoanalysis.') @@ -513,6 +512,14 @@ def _async(self, proxy_func) -> Callable[_, rpyc.AsyncResult]: Good for functions that we don't care to get results back from. """ return rpyc.async_(proxy_func) + + def analyze(self) -> None: + """ + Instruct IDA to initiate and wait for auto analysis completion. + """ + logger.debug('Running autoanalysis.') + self._idc.set_flag(self._idc.INF_GENFLAGS, self._idc.INFFL_AUTO, 1) + self._idc.auto_wait() def teleport(self, func: Callable) -> Callable: def wrapper(*args, **kwargs): From 5da963427179bf689649f636ab54d7ab63e503f3 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 27 Jan 2025 11:45:22 -0500 Subject: [PATCH 3/9] attempted to allow timeout parameter --- dragodis/ida/disassembler.py | 30 ++++++++++++++++++++++++++---- dragodis/ida/ida_server.py | 11 +++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index bd76123..c31908f 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -211,7 +211,7 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, proc self._script_path = ida_server.__file__ if timeout is not None: self._rpyc_config = dict(self._rpyc_config) - self._rpyc_config["sync_request_timeout"] = None + self._rpyc_config["sync_request_timeout"] = timeout # Determine if 64 bit. if is_64_bit is None: @@ -432,8 +432,11 @@ def start(self): atexit.register(self._process.kill) finally: os.chdir(orig_cwd) - - logger.debug(f"Initializing IDA Bridge connection...") + + original_timeout = self._rpyc_config['sync_request_timeout'] + #For the initial connection, set rpyc_timeout to None so that auto_wait() is allowed to complete. + self._rpyc_config['sync_request_timeout'] = None + logger.debug(f"Initializing autoanalysis IDA Bridge connection...") if socket_path: self._bridge = self.unix_connect(socket_path) # Remember socket path so we can close it later. @@ -442,15 +445,33 @@ def start(self): self._bridge = self.win_connect(pipe_name) else: raise RuntimeError("Unexpected error. Failed to setup socket or pipe.") + self._initialize_bridge() # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. self._root = self._bridge.root - self._running = True + self._rpyc_config['sync_request_timeout'] = original_timeout + if self._analyze: self.analyze() else: logger.debug('Skipping autoanalysis.') + #Close the bridge once auto analysis is complete. Re initialize the bridge later with a proper timeout. + self._bridge.close() + logger.debug(f"Initializing IDA Bridge connection...") + if socket_path: + self._bridge = self.unix_connect(socket_path) + # Remember socket path so we can close it later. + self._socket_path = socket_path + elif pipe_name: + self._bridge = self.win_connect(pipe_name) + else: + raise RuntimeError("Unexpected error. Failed to setup socket or pipe.") + + self._initialize_bridge() + # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. + self._root = self._bridge.root + self._running = True logger.debug("IDA Disassembler ready!") def stop(self, *exc_info): @@ -516,6 +537,7 @@ def _async(self, proxy_func) -> Callable[_, rpyc.AsyncResult]: def analyze(self) -> None: """ Instruct IDA to initiate and wait for auto analysis completion. + NOTE: This function is likely subject to the timeout value provided in the IDARemoteDisassembler constructor. """ logger.debug('Running autoanalysis.') self._idc.set_flag(self._idc.INF_GENFLAGS, self._idc.INFFL_AUTO, 1) diff --git a/dragodis/ida/ida_server.py b/dragodis/ida/ida_server.py index 1c40276..82d3151 100644 --- a/dragodis/ida/ida_server.py +++ b/dragodis/ida/ida_server.py @@ -51,6 +51,17 @@ def main(): # Don't compress to improve speed. rpyc.core.channel.Channel.COMPRESSION_LEVEL = 0 + if sys.platform == "win32": + pipe_name = idc.ARGV[1] + stream = NamedPipeStream.create_server(pipe_name) + with rpyc.classic.connect_stream(stream) as srv: + srv.serve_all() + else: + socket_path = idc.ARGV[1] + server = OneShotServer(SlaveService, socket_path=socket_path, auto_register=False) + server.start() + #The first connection is specifically for auto analysis. This allows setting a timeout of None to allow for very long autoanalysis waittime. + #This connection can then have a reasonable timeout value to prevent issues if IDA crashes. if sys.platform == "win32": pipe_name = idc.ARGV[1] stream = NamedPipeStream.create_server(pipe_name) From 405b565644739378a49741dbaf7c5a36882e0804 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 27 Jan 2025 13:13:48 -0500 Subject: [PATCH 4/9] Made timeout parameter allow None --- dragodis/ida/disassembler.py | 27 ++++++--------------------- dragodis/ida/ida_server.py | 11 ----------- 2 files changed, 6 insertions(+), 32 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index c31908f..6a68824 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -184,7 +184,7 @@ class IDARemoteDisassembler(IDADisassembler): "sync_request_timeout": 60 } - def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, processor=None, detach=False, analyze=True, **unused): + def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=60, processor=None, detach=False, analyze=True, **unused): """ Initializes IDA disassembler. @@ -209,8 +209,11 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=None, proc "Please provide it during instantiation or set the IDA_INSTALL_DIR environment variable." ) self._script_path = ida_server.__file__ - if timeout is not None: - self._rpyc_config = dict(self._rpyc_config) + self._rpyc_config = dict(self._rpyc_config) + if timeout <= 0: + #Treat zero timeouts or -1 timeouts as infinite. + self._rpyc_config["sync_request_timeout"] = None + else: self._rpyc_config["sync_request_timeout"] = timeout # Determine if 64 bit. @@ -433,9 +436,7 @@ def start(self): finally: os.chdir(orig_cwd) - original_timeout = self._rpyc_config['sync_request_timeout'] #For the initial connection, set rpyc_timeout to None so that auto_wait() is allowed to complete. - self._rpyc_config['sync_request_timeout'] = None logger.debug(f"Initializing autoanalysis IDA Bridge connection...") if socket_path: self._bridge = self.unix_connect(socket_path) @@ -449,26 +450,10 @@ def start(self): self._initialize_bridge() # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. self._root = self._bridge.root - self._rpyc_config['sync_request_timeout'] = original_timeout - if self._analyze: self.analyze() else: logger.debug('Skipping autoanalysis.') - #Close the bridge once auto analysis is complete. Re initialize the bridge later with a proper timeout. - self._bridge.close() - - logger.debug(f"Initializing IDA Bridge connection...") - if socket_path: - self._bridge = self.unix_connect(socket_path) - # Remember socket path so we can close it later. - self._socket_path = socket_path - elif pipe_name: - self._bridge = self.win_connect(pipe_name) - else: - raise RuntimeError("Unexpected error. Failed to setup socket or pipe.") - - self._initialize_bridge() # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. self._root = self._bridge.root self._running = True diff --git a/dragodis/ida/ida_server.py b/dragodis/ida/ida_server.py index 82d3151..1c40276 100644 --- a/dragodis/ida/ida_server.py +++ b/dragodis/ida/ida_server.py @@ -51,17 +51,6 @@ def main(): # Don't compress to improve speed. rpyc.core.channel.Channel.COMPRESSION_LEVEL = 0 - if sys.platform == "win32": - pipe_name = idc.ARGV[1] - stream = NamedPipeStream.create_server(pipe_name) - with rpyc.classic.connect_stream(stream) as srv: - srv.serve_all() - else: - socket_path = idc.ARGV[1] - server = OneShotServer(SlaveService, socket_path=socket_path, auto_register=False) - server.start() - #The first connection is specifically for auto analysis. This allows setting a timeout of None to allow for very long autoanalysis waittime. - #This connection can then have a reasonable timeout value to prevent issues if IDA crashes. if sys.platform == "win32": pipe_name = idc.ARGV[1] stream = NamedPipeStream.create_server(pipe_name) From 2ec9c1f016fdcfe0e94f538d5eb1b9bc555013cc Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 27 Jan 2025 13:14:31 -0500 Subject: [PATCH 5/9] Removed old comments --- dragodis/ida/disassembler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index 6a68824..5799d62 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -436,8 +436,7 @@ def start(self): finally: os.chdir(orig_cwd) - #For the initial connection, set rpyc_timeout to None so that auto_wait() is allowed to complete. - logger.debug(f"Initializing autoanalysis IDA Bridge connection...") + logger.debug(f"Initializing IDA Bridge connection...") if socket_path: self._bridge = self.unix_connect(socket_path) # Remember socket path so we can close it later. From 36fb788e05763993aa654c18a7c431954b4acaf9 Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 27 Jan 2025 13:32:53 -0500 Subject: [PATCH 6/9] Removed unneeded lines --- dragodis/ida/disassembler.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index 5799d62..a78c01d 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -453,8 +453,6 @@ def start(self): self.analyze() else: logger.debug('Skipping autoanalysis.') - # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. - self._root = self._bridge.root self._running = True logger.debug("IDA Disassembler ready!") From e2cb298990f8ad6ffb992bae0c548b798c4e0eec Mon Sep 17 00:00:00 2001 From: aaron Date: Mon, 27 Jan 2025 15:40:59 -0500 Subject: [PATCH 7/9] Code cleanups & bugfixes --- dragodis/ida/disassembler.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index a78c01d..36173b7 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -210,11 +210,10 @@ def __init__(self, input_path, is_64_bit=None, ida_path=None, timeout=60, proces ) self._script_path = ida_server.__file__ self._rpyc_config = dict(self._rpyc_config) - if timeout <= 0: + if timeout is None or timeout <= 0: #Treat zero timeouts or -1 timeouts as infinite. - self._rpyc_config["sync_request_timeout"] = None - else: - self._rpyc_config["sync_request_timeout"] = timeout + timeout = None + self._rpyc_config["sync_request_timeout"] = timeout # Determine if 64 bit. if is_64_bit is None: From 614201c9a2e75ed9b2ceae3d2b3846ae4b0eda1a Mon Sep 17 00:00:00 2001 From: aaron Date: Tue, 28 Jan 2025 12:23:15 -0500 Subject: [PATCH 8/9] Changed idc.auto_wait() to async() --- dragodis/ida/disassembler.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index 36173b7..21fba0c 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -518,11 +518,11 @@ def _async(self, proxy_func) -> Callable[_, rpyc.AsyncResult]: def analyze(self) -> None: """ Instruct IDA to initiate and wait for auto analysis completion. - NOTE: This function is likely subject to the timeout value provided in the IDARemoteDisassembler constructor. """ logger.debug('Running autoanalysis.') self._idc.set_flag(self._idc.INF_GENFLAGS, self._idc.INFFL_AUTO, 1) - self._idc.auto_wait() + #To avoid timeouts, run it as async but block until complete here. + self._async(self._idc.auto_wait)().wait() def teleport(self, func: Callable) -> Callable: def wrapper(*args, **kwargs): From ffcf662bcefdc441cf76df881fbc68a3424df642 Mon Sep 17 00:00:00 2001 From: aaron Date: Fri, 7 Feb 2025 15:46:31 -0500 Subject: [PATCH 9/9] moved _running flag to before analyze --- dragodis/ida/disassembler.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dragodis/ida/disassembler.py b/dragodis/ida/disassembler.py index 21fba0c..62ff959 100644 --- a/dragodis/ida/disassembler.py +++ b/dragodis/ida/disassembler.py @@ -448,11 +448,11 @@ def start(self): self._initialize_bridge() # Keep a hold of the root remote object to prevent rpyc from prematurely closing on us. self._root = self._bridge.root + self._running = True if self._analyze: self.analyze() else: logger.debug('Skipping autoanalysis.') - self._running = True logger.debug("IDA Disassembler ready!") def stop(self, *exc_info): @@ -519,6 +519,9 @@ def analyze(self) -> None: """ Instruct IDA to initiate and wait for auto analysis completion. """ + if not self._running: + logger.error('Cannot run IDARemoteDisassembler.analyze() without a connected IDA instance.') + return logger.debug('Running autoanalysis.') self._idc.set_flag(self._idc.INF_GENFLAGS, self._idc.INFFL_AUTO, 1) #To avoid timeouts, run it as async but block until complete here.