diff --git a/CODEOWNERS b/CODEOWNERS index e10987a..1c24bae 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,5 +2,5 @@ # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in the repo. -* @gerardofn @winden-g @escipion +* @gerardofn @escipion @guspascual diff --git a/README.md b/README.md index 9376958..5129167 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # VirusTotal Plugin for IDA Pro -This is the official VirusTotal plugin for Hex-Rays IDA Pro, version **1.06**. It seamlessly integrates VirusTotal's powerful analysis capabilities directly into your reverse engineering workflow. +This is the official VirusTotal plugin for Hex-Rays IDA Pro. It seamlessly integrates VirusTotal's powerful analysis capabilities directly into your reverse engineering workflow. The plugin offers two core functionalities: 1. **Code Similarity Search**: Perform advanced searches for code, bytes, and strings across VirusTotal's massive dataset directly from IDA's disassembly and strings views. @@ -116,11 +116,12 @@ While other architectures may work, they have not been officially tested. Raw by Check IDA Pro's output window for any message that may need your attention. ## Changelog -- v1.06 : Updated plugin metadata to support HCLI Plugin Manager ecosystem -- v1.05 : Fixes crash when Code Insight returns an invalid response +- v1.07 : Improved error handling, now CodeInsight works with other CPU architectures identified by IDA Pro. +- v1.06 : Updated plugin metadata to support HCLI Plugin Manager ecosystem. +- v1.05 : Fixes crash when Code Insight returns an invalid response. - v1.04 : Fixes issue that left IDA hanging while a query was being performed. -- v1.03 : BUG fixed (wrongly showing an invalid api key msg) +- v1.03 : BUG fixed (wrongly showing an invalid api key msg). - v1.02 : Added support for IDA Pro 9.2 -- v1.00 : Added Code Insight panel +- v1.00 : Added Code Insight panel. - v0.11 : Added support for IDA Pro 8.x -- v0.10 : Initial release \ No newline at end of file +- v0.10 : Initial release. \ No newline at end of file diff --git a/VERSION b/VERSION index 3515b60..a2a169c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.06 \ No newline at end of file +1.07 \ No newline at end of file diff --git a/ida-plugin.json b/ida-plugin.json index 5fc51dc..c4fcecb 100644 --- a/ida-plugin.json +++ b/ida-plugin.json @@ -3,7 +3,7 @@ "plugin": { "name": "vt-ida-plugin", "entryPoint": "plugin/vt.py", - "version": "1.0.6", + "version": "1.0.7", "idaVersions": ">=8", "description": "Integrates VirusTotal's powerful analysis capabilities directly into your reverse engineering workflow.", "license": "Apache 2.0", diff --git a/plugin/virustotal/ci_notebook.py b/plugin/virustotal/ci_notebook.py index 62b29af..c901d8f 100644 --- a/plugin/virustotal/ci_notebook.py +++ b/plugin/virustotal/ci_notebook.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2025 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -90,7 +90,11 @@ def encode_response(summary, description): "summary": summary, "description": description } - response_str = json.dumps(response) + try: + response_str = json.dumps(response) + except TypeError: + logging.error('[VT Plugin] ERROR encoding CI response (not serializable).') + response_str = "{}" encoded_response = base64.b64encode(response_str.encode('utf-8')) return encoded_response.decode('ascii') diff --git a/plugin/virustotal/codeinsight.py b/plugin/virustotal/codeinsight.py index 47f4f6b..468de91 100644 --- a/plugin/virustotal/codeinsight.py +++ b/plugin/virustotal/codeinsight.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2025 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -14,6 +14,7 @@ __author__ = 'gerardofn@virustotal.com' import logging +import binascii import requests from virustotal import config from virustotal.vt_ida.disassembler import Disassembler @@ -133,7 +134,7 @@ def _process_output(self, response): try: decoded_str = base64.urlsafe_b64decode(answer) - except: + except (binascii.Error, ValueError): logging.debug('[VT Plugin] ERROR decoding Code Insight response: %s', response) return None @@ -195,7 +196,7 @@ def run(self): try: response = requests.post(f'{API_URL}/{endpoint}', json = {'data': payload}, headers=headers_apiv3) - except: + except requests.RequestException: logging.debug('[VT Plugin] ERROR: unable to connect to Code Insight') self._error_msg = 'ERROR: unable to connect to Code Insight' return @@ -375,10 +376,9 @@ def askCI(self, *args, **kwargs): if json_str: try: - return_msg = json.loads(json_str) - except: - logging.debug('[CodeInsight] Error processing the returned json file.') - return return_msg + return json.loads(json_str) + except json.JSONDecodeError: + logging.debug('[CodeInsight] Error processing the returned json file.') else: self.error_msg = ci.get_error_msg() @@ -463,7 +463,10 @@ def askCI(self, *args, **kwargs): self.encoded_src = ci.get_encoded_src() if json_str: - return json.loads(json_str) + try: + return json.loads(json_str) + except json.JSONDecodeError: + logging.debug('[CodeInsight] Error processing the returned json file.') else: self.error_msg = ci.get_error_msg() diff --git a/plugin/virustotal/vt_ida/disassembler.py b/plugin/virustotal/vt_ida/disassembler.py index 72f785e..3bef7eb 100644 --- a/plugin/virustotal/vt_ida/disassembler.py +++ b/plugin/virustotal/vt_ida/disassembler.py @@ -1,4 +1,4 @@ -# Copyright 2020 Google Inc. All Rights Reserved. +# Copyright 2019 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/plugin/virustotal/vt_ida/plugin_loader.py b/plugin/virustotal/vt_ida/plugin_loader.py index 6a53495..65cde13 100644 --- a/plugin/virustotal/vt_ida/plugin_loader.py +++ b/plugin/virustotal/vt_ida/plugin_loader.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2019 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -22,6 +22,7 @@ import logging import os import requests +import pathlib import threading from virustotal import config from virustotal import vtgrep @@ -37,7 +38,7 @@ except ImportError: import configparser -VT_IDA_PLUGIN_VERSION = '1.06' +VT_IDA_PLUGIN_VERSION = '1.07' widget_panel = VTPanel() if config.DEBUG: @@ -60,18 +61,24 @@ def calculate_hash(input_file): """Return hash if the file hash has been properly calculated.""" file_hash = None + + try: + path_obj = pathlib.Path(input_file) + except TypeError: + logging.debug('[VT Plugin] Invalid path format: %s', input_file) + path_obj = None - if os.path.isfile(input_file): + if path_obj and path_obj.is_file(): hash_f = hashlib.sha256() logging.debug('[VT Plugin] Input file available.') - with open(input_file, 'rb') as file_r: - try: + try: + with path_obj.open('rb') as file_r: for file_buffer in iter(lambda: file_r.read(8192), b''): hash_f.update(file_buffer) - file_hash = hash_f.hexdigest() - logging.debug('[VT Plugin] Input file hash been calculated.') - except: - logging.debug('[VT Plugin] Can\'t load the input file.') + file_hash = hash_f.hexdigest() + logging.debug('[VT Plugin] Input file hash been calculated.') + except OSError: + logging.debug('[VT Plugin] Can\'t load the input file.') else: logging.debug('[VT Plugin] Input file not available.') tmp_hash = idautils.GetInputFileMD5() @@ -462,7 +469,7 @@ def check_file_missing_in_VT(self): logging.debug('[VT Plugin] Checking hash: %s', self.file_hash) try: response = requests.get(url, headers=headers) - except: + except requests.RequestException: logging.error('[VT Plugin] Unable to connect to VirusTotal.com') return False @@ -496,8 +503,9 @@ def upload_file_to_VT(self): try: response = requests.post(url, files=files, headers=headers) - except: + except requests.RequestException: logging.error('[VT Plugin] Unable to connect to VirusTotal.com') + return if response.ok: logging.debug('[VT Plugin] Uploaded successfully.') @@ -548,7 +556,7 @@ def read_config(self): else: self.auto_upload = False return True - except: + except configparser.Error: logging.error('[VT Plugin] Error reading the user config file.') return False @@ -564,7 +572,7 @@ def write_config(self): parser.set('General', 'auto_upload', str(self.auto_upload)) parser.write(config_file) config_file.close() - except: + except (OSError, configparser.Error): logging.error('[VT Plugin] Error while creating the user config file.') return False return True @@ -601,7 +609,7 @@ def check_version(self): try: response = requests.get(url, headers=headers) - except: + except requests.RequestException: logging.error('[VT Plugin] Unable to check for updates.') return False @@ -676,6 +684,13 @@ class VTplugin(idaapi.plugin_t): vtpanel = None vtsetup = None + def _safe_register_action(self, action_cls, label): + """Helper to safely register an action, logging any failures without crashing.""" + try: + action_cls.register(self, label) + except Exception: + logging.exception('[VT Plugin] Failed to register action: %s', label) + def init(self): """Set up menu hooks and implements search methods.""" @@ -716,48 +731,57 @@ def init(self): arch_info = idaapi.get_inf_structure() proc_name = get_procname(arch_info) - try: - logging.debug('[VT Plugin] Processor detected by IDA: %s', proc_name) - if (proc_name in self.SEARCH_STRICT_SUPPORTED) | (proc_name in self.SEARCH_CODE_SUPPORTED): - VTGrepWildcards.register(self, 'Search for similar code') - VTGrepWildCardsFunction.register(self, 'Search for similar functions') - if len(config.API_KEY) > 0: - CodeInsightASM.register(self, 'Ask Code Insight') - CodeInsightDecompiled.register(self, 'Ask Code Insight') - - ### Register menu entry - current_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '.')) - file_icon = os.path.join(current_path, - 'ui', - 'resources', - 'vt_icon.png') - vticon_data = open(file_icon, 'rb').read() - vtmenu = idaapi.load_custom_icon(data=vticon_data) - action_desc = idaapi.action_desc_t( - 'my:vtpanel', - 'VirusTotal', - MenuVTPanel(), - '', - 'Show VirusTotal panel with information about the current file', - vtmenu) - - idaapi.register_action(action_desc) - idaapi.attach_action_to_menu( - 'View/Open subviews/', - 'my:vtpanel', - idaapi.SETMENU_APP) - - if proc_name in self.SEARCH_STRICT_SUPPORTED: - VTGrepWildCardsStrict.register(self, 'Search for similar code (strict)') + logging.debug('[VT Plugin] Processor detected by IDA: %s', proc_name) - else: - logging.info('\n - Processor detected: %s', get_procname(arch_info)) - logging.info(' - Searching for similar code is not available.') + if len(config.API_KEY) > 0: + self._safe_register_action(CodeInsightASM, 'Ask Code Insight') + self._safe_register_action(CodeInsightDecompiled, 'Ask Code Insight') + + ### Register VirusTotal menu entry + vticon_data = None + current_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '.')) + file_icon = os.path.join(current_path, + 'ui', + 'resources', + 'vt_icon.png') + try: + vticon_data = open(file_icon, 'rb').read() + except OSError: + logging.error('[VT Plugin] Failed to load icon file: %s', file_icon) + + if vticon_data: + try: + vtmenu = idaapi.load_custom_icon(data=vticon_data) + action_desc = idaapi.action_desc_t( + 'my:vtpanel', + 'VirusTotal', + MenuVTPanel(), + '', + 'Show VirusTotal panel with information about the current file', + vtmenu) + + idaapi.register_action(action_desc) + idaapi.attach_action_to_menu( + 'View/Open subviews/', + 'my:vtpanel', + idaapi.SETMENU_APP) + except Exception: + logging.exception('[VT Plugin] Failed to register VirusTotal menu icon/action.') + + if (proc_name in self.SEARCH_STRICT_SUPPORTED) | (proc_name in self.SEARCH_CODE_SUPPORTED): + self._safe_register_action(VTGrepWildcards, 'Search for similar code') + self._safe_register_action(VTGrepWildCardsFunction, 'Search for similar functions') - VTGrepBytes.register(self, 'Search for bytes') - VTGrepStrings.register(self, 'Search for string') - except: - logging.error('[VT Plugin] Unable to register popups actions.') + if proc_name in self.SEARCH_STRICT_SUPPORTED: + self._safe_register_action(VTGrepWildCardsStrict, 'Search for similar code (strict)') + + else: + logging.info(' - Processor detected: %s', proc_name) + logging.info(' - Searching for similar code is not available.') + + self._safe_register_action(VTGrepBytes, 'Search for bytes') + self._safe_register_action(VTGrepStrings, 'Search for string') + else: logging.info('[VT Plugin] Plugin disabled, restart IDA to proceed. ') ida_kernwin.warning('Plugin disabled, restart IDA to proceed.') diff --git a/plugin/virustotal/vt_ida/ui/qt5logo.py b/plugin/virustotal/vt_ida/ui/qt5logo.py index 4b5c501..4332746 100644 --- a/plugin/virustotal/vt_ida/ui/qt5logo.py +++ b/plugin/virustotal/vt_ida/ui/qt5logo.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Copyright 2025 Google LLC. All Rights Reserved. # Resource object code # diff --git a/plugin/virustotal/vt_ida/ui/qt5panel.py b/plugin/virustotal/vt_ida/ui/qt5panel.py index b32bdb0..688b8f8 100644 --- a/plugin/virustotal/vt_ida/ui/qt5panel.py +++ b/plugin/virustotal/vt_ida/ui/qt5panel.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Copyright 2025 Google LLC. All Rights Reserved. # Form implementation generated from reading ui file 'vtpanel.ui' # diff --git a/plugin/virustotal/vt_ida/ui/qt6logo.py b/plugin/virustotal/vt_ida/ui/qt6logo.py index 7d6798b..b36936c 100644 --- a/plugin/virustotal/vt_ida/ui/qt6logo.py +++ b/plugin/virustotal/vt_ida/ui/qt6logo.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +# Copyright 2025 Google LLC. All Rights Reserved. # Resource object code # diff --git a/plugin/virustotal/vt_ida/ui/qt6panel.py b/plugin/virustotal/vt_ida/ui/qt6panel.py index 14725c3..ccb6cf7 100644 --- a/plugin/virustotal/vt_ida/ui/qt6panel.py +++ b/plugin/virustotal/vt_ida/ui/qt6panel.py @@ -1,3 +1,4 @@ +# Copyright 2025 Google LLC. All Rights Reserved. # Form implementation generated from reading ui file 'vtpanel.ui' # # Created by: PyQt6 UI code generator 6.9.1 diff --git a/plugin/virustotal/vt_ida/vtpanel.py b/plugin/virustotal/vt_ida/vtpanel.py index 4803e33..9a129b7 100644 --- a/plugin/virustotal/vt_ida/vtpanel.py +++ b/plugin/virustotal/vt_ida/vtpanel.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2025 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -118,8 +118,9 @@ def _load(self): logging.debug('[VT Plugin] Loading CodeInsight Notebook file: %s', filename) with open(filename, 'r', encoding='utf-8') as f: imported_json = json.load(f) - except: - logging.error('[VT Plugin] ERROR importing file: %s', filename) + except (OSError, json.JSONDecodeError): + logging.error('[VT Plugin] ERROR importing file: %s', filename) + return ci_notebook.import_data(imported_json) self._read_notebook() @@ -145,7 +146,7 @@ def _export(self): logging.debug('[VT Plugin] Exporting CodeInsight Notebook to file: %s', filename) with open(filename, 'w', encoding='utf-8') as f: json.dump(ci_notebook.show_pages(), f) - except: + except (OSError, TypeError): logging.error('[VT Plugin] ERROR saving file: %s', filename) @@ -199,7 +200,7 @@ def _askCI_Disassembled(self, code_src): try: self.summary = self.ci_report['summary'] self.description = self.ci_report['description'] - except: + except (KeyError, TypeError): logging.error('[VT Plugin] Invalid answer received from Code Insight') return False @@ -221,7 +222,7 @@ def _askCI_Decompiled(self, code_src): try: self.summary = self.ci_report['summary'] self.description = self.ci_report['description'] - except: + except (KeyError, TypeError): logging.error('[VT Plugin] Invalid answer received from Code Insight') return False diff --git a/plugin/virustotal/vt_ida/vtwidgets.py b/plugin/virustotal/vt_ida/vtwidgets.py index ebb24f9..b9ddd92 100644 --- a/plugin/virustotal/vt_ida/vtwidgets.py +++ b/plugin/virustotal/vt_ida/vtwidgets.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2025 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at diff --git a/plugin/virustotal/vtgrep.py b/plugin/virustotal/vtgrep.py index c53e6a8..021e2a7 100644 --- a/plugin/virustotal/vtgrep.py +++ b/plugin/virustotal/vtgrep.py @@ -1,4 +1,4 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2019 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -347,6 +347,6 @@ def search(self, wildcards=False, strict=False): try: webbrowser.open_new(url) - except: + except webbrowser.Error: logging.error('[VTGREP] Error while opening the web browser.') VTWidgets.show_warning('Error while opening the web browser.') diff --git a/plugin/vt.py b/plugin/vt.py index fd47982..d590fa2 100644 --- a/plugin/vt.py +++ b/plugin/vt.py @@ -1,5 +1,5 @@ -# Copyright 2025 Google Inc. All Rights Reserved. +# Copyright 2019 Google LLC. All Rights Reserved. # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at