├── Decyx ├── decyx │ ├── __init__.py │ ├── dialogs │ │ ├── __init__.py │ │ ├── model_selection_dialog.py │ │ ├── prompt_review_dialog.py │ │ ├── action_selection_dialog.py │ │ ├── caller_selection_dialog.py │ │ └── suggestion_dialog.py │ ├── gui.py │ ├── config.py │ ├── api.py │ ├── decompiler.py │ └── utils.py └── Decyx.py ├── imgs └── showcase.gif ├── LICENSE ├── .gitignore └── README.md /Decyx/decyx/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Decyx/decyx/dialogs/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /imgs/showcase.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/philsajdak/decyx/HEAD/imgs/showcase.gif -------------------------------------------------------------------------------- /Decyx/decyx/gui.py: -------------------------------------------------------------------------------- 1 | # gui.py 2 | 3 | from decyx.dialogs.suggestion_dialog import show_suggestion_dialog 4 | from decyx.dialogs.caller_selection_dialog import show_caller_selection_dialog 5 | from decyx.dialogs.prompt_review_dialog import show_prompt_review_dialog 6 | from decyx.dialogs.model_selection_dialog import show_model_select_dialog 7 | from decyx.dialogs.action_selection_dialog import show_action_select_dialog 8 | 9 | __all__ = [ 10 | 'show_suggestion_dialog', 11 | 'show_caller_selection_dialog', 12 | 'show_prompt_review_dialog', 13 | 'show_model_select_dialog', 14 | 'show_action_select_dialog' 15 | ] 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Phil Sajdak 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Decyx/decyx/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | 3 | CLAUDE_API_URL = "https://api.anthropic.com/v1/messages" 4 | CLAUDE_MODELS = ["claude-sonnet-4-20250514"] 5 | 6 | # Set to True to enable fast selection and skip prompt confirmation windows 7 | SKIP_PROMPT_CONFIRMATION = False 8 | 9 | # Default window dimensions 10 | DEFAULT_WINDOW_WIDTH = 750 11 | DEFAULT_WINDOW_HEIGHT = 500 12 | 13 | # Prompt Templates 14 | PROMPTS = { 15 | "rename_retype": ( 16 | "Analyze the following decompiled C function code and its variables. Provide the following:\n" 17 | "1. A suggested concise and descriptive name for the function.\n" 18 | "2. Suggested new names and data types for each variable, including globals if applicable.\n\n" 19 | "Respond with a JSON object containing 'function_name' and 'variables' fields. The 'variables' field should be an array of objects, each containing 'old_name', 'new_name', and 'new_type'.\n\n" 20 | ), 21 | "explanation": ( 22 | "Provide a brief detailed explanation of the following decompiled C function code and its variables. " 23 | "The explanation should be in-depth but concise, incorporating any meaningful names where applicable.\n\n" 24 | "Respond with a plain text explanation, without any formatting.\n\n" 25 | ), 26 | "line_comments": ( 27 | "Analyze the following decompiled C function code annotated with addresses. Provide concise, meaningful comments " 28 | "**only** for important lines or sections of the code. Focus on explaining the purpose or significance of each " 29 | "important operation.\n\n" 30 | "Respond with a JSON object where each key is the address (as a string) and the value is the suggested " 31 | "comment for that line. Only include addresses that need comments.\n\n" 32 | "Example format:\n" 33 | "{\n" 34 | " \"0x401000\": \"Initialize the device object\",\n" 35 | " \"0x401010\": \"Check OS version for compatibility\",\n" 36 | " \"0x401020\": \"Create symbolic link for the device\"\n" 37 | "}\n\n" 38 | ) 39 | } 40 | 41 | # Global variable patterns 42 | GLOBAL_VARIABLE_PATTERNS = [ 43 | r'\bDAT_[0-9a-fA-F]+\b', # Default Ghidra pattern 44 | r'\bg_\w+\b' # Most likely renamed by our script 45 | ] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Decyx 2 | 3 | Decyx is an __extension for Ghidra that leverages AI to assist in reverse engineering and binary analysis__. 4 | 5 | It uses Anthropic's Claude API to provide intelligent suggestions for function and variable renaming, type inference, and code explanations. Decyx prioritizes customized automation by allowing the user to edit each suggestion at each step of the way, without having the model forcefully modify the Ghidra project. This ensures that users maintain control over their reverse engineering workflows, tailoring suggestions to their specific needs and preferences. 6 | 7 | ![Decyx in action](imgs/showcase.gif) 8 | 9 | ## Features 10 | 11 | - **AI-assisted function renaming**: Provides meaningful names for unidentified functions based on context. 12 | - **Variable renaming & type inference**: Suggests new names and infers variable types to enhance code clarity. Invalid types are highlighted to the user before retyping. 13 | - **Function code explanations**: Generates detailed explanations for decompiled functions. 14 | - **Caller context analysis**: Incorporates the decompiled code of calling functions to provide additional context, improving the accuracy of AI-driven suggestions. 15 | - **Line-by-line comments**: Adds insightful comments to critical sections of the code for better understanding. 16 | - **User-friendly interface**: Interactive GUI for reviewing AI-generated suggestions and customizing actions. 17 | 18 | ## Prerequisites 19 | 20 | - __Ghidra `>= 11.1.2`__. You can download the latest version of Ghidra [here](https://ghidra-sre.org). 21 | - __Claude API key__. You can obtain one [here](https://www.anthropic.com/api). 22 | 23 | > [!NOTE] 24 | > Ghidra ships with a Python 2.7.3 extension system based on Jython, so Decyx is written with this in mind. 25 | 26 | ## Installation 27 | 28 | 1. Clone this repository. 29 | 30 | 2. Add the main `Decyx` directory in Ghidra: 31 | - Go to `Window` > `Script Manager` 32 | - In the `Script Manager` window, go to `Manage Script Directories` 33 | - In the `Bundle Manager` window, add the `Decyx` directory 34 | - The script will be populated inside the `Script Manager` window. Enable it by checking the `In Tool` box. 35 | 36 | 3. Add your Claude API key to Ghidra's preferences: 37 | - When you run the Decyx extension for the first time, it will prompt you to enter your Claude API key. Once entered, the key will be stored in Ghidra's preferences for future use. 38 | 39 | ## Usage 40 | 41 | 1. Open your binary in Ghidra and analyze it. 42 | 43 | 2. Navigate to the function you want to analyze. 44 | 45 | 3. Press `SHIFT + R` (You can modify this hotkey in `Decyx.py`) 46 | 47 | 4. Follow the on-screen prompts to: 48 | - Select the Claude model to use 49 | - Choose the actions to perform (rename/retype, explain, add comments) 50 | - Review and edit the generated prompt (if enabled) 51 | - Select caller functions to include for additional context 52 | - Review and apply the AI-generated suggestions 53 | 54 | > [!IMPORTANT] 55 | > AI is a tool to assist and augment human analysis, not to replace it entirely. Users should always critically evaluate the suggestions. 56 | 57 | ## Configuration 58 | 59 | You can modify the `config.py` file to customize various aspects of Decyx: 60 | 61 | - `CLAUDE_MODELS`: List of available Claude models. By default, it uses `claude-sonnet-4-20250514` and will skip prompting for model selection if there is only one model in this list. This project was developed with the older model `claude-3-5-sonnet-latest` in mind, so if there are any issues, please use that model instead. 62 | - `SKIP_PROMPT_CONFIRMATION`: Set to `True` to skip the prompt review step. By default, it is set to `False` so users can modify each prompt to their preferences. 63 | - `PROMPTS`: Customize the default prompts sent to the Claude API for each action. 64 | 65 | ## References 66 | 67 | Decyx accesses the [Swing library](https://docs.oracle.com/javase/8/docs/api/javax/swing/package-summary.html) for its GUI, and utilizes the [Ghidra API](https://ghidra.re/ghidra_docs/api/) for Ghidra-specific functionality. 68 | 69 | ## Contributing 70 | 71 | Contributions are welcome! Please feel free to submit a Pull Request. Decyx was made with Claude in mind but aims to integrate other AI APIs in the future. 72 | -------------------------------------------------------------------------------- /Decyx/decyx/dialogs/model_selection_dialog.py: -------------------------------------------------------------------------------- 1 | # model_selection_dialog.py 2 | 3 | import threading 4 | import javax.swing as swing 5 | from javax.swing import JFrame, JPanel, JButton, JScrollPane, BoxLayout, JLabel, JComboBox 6 | from java.awt import BorderLayout 7 | 8 | class ModelSelectionDialog(JFrame): 9 | """ 10 | Dialog for selecting the Claude model to use. 11 | """ 12 | def __init__(self, models, default_index, on_selection_complete): 13 | super(ModelSelectionDialog, self).__init__("Select Claude Model") 14 | self.models = models 15 | self.selected_model = None 16 | self.default_index = default_index 17 | self.on_selection_complete = on_selection_complete 18 | self.init_ui() 19 | 20 | def init_ui(self): 21 | try: 22 | panel = JPanel() 23 | panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS)) 24 | 25 | self.add_instruction_label(panel) 26 | self.add_model_combo_box(panel) 27 | self.add_buttons(panel) 28 | 29 | self.getContentPane().add(JScrollPane(panel), BorderLayout.CENTER) 30 | self.setSize(300, 150) 31 | self.setLocationRelativeTo(None) 32 | self.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) 33 | self.setVisible(True) 34 | except Exception as e: 35 | print("Error initializing ModelSelectionDialog UI: {}".format(e)) 36 | 37 | def add_instruction_label(self, panel): 38 | try: 39 | instruction_label = JLabel("Select the Claude model to use:") 40 | instruction_label.setToolTipText("Choose the desired Claude model") 41 | panel.add(instruction_label) 42 | except Exception as e: 43 | print("Error adding instruction label: {}".format(e)) 44 | 45 | def add_model_combo_box(self, panel): 46 | try: 47 | self.model_combo_box = JComboBox(self.models) 48 | self.model_combo_box.setSelectedIndex(self.default_index) 49 | self.model_combo_box.setToolTipText("Select a Claude model from the dropdown") 50 | panel.add(self.model_combo_box) 51 | except Exception as e: 52 | print("Error adding model combo box: {}".format(e)) 53 | 54 | def add_buttons(self, panel): 55 | try: 56 | button_panel = JPanel() 57 | ok_button = JButton("OK") 58 | ok_button.addActionListener(lambda e: self.ok()) 59 | ok_button.setToolTipText("Confirm model selection") 60 | cancel_button = JButton("Cancel") 61 | cancel_button.addActionListener(lambda e: self.cancel()) 62 | cancel_button.setToolTipText("Cancel model selection") 63 | button_panel.add(ok_button) 64 | button_panel.add(cancel_button) 65 | self.getContentPane().add(button_panel, BorderLayout.SOUTH) 66 | except Exception as e: 67 | print("Error adding buttons to ModelSelectionDialog: {}".format(e)) 68 | 69 | def ok(self): 70 | try: 71 | self.selected_model = self.model_combo_box.getSelectedItem() 72 | self.on_selection_complete(self.selected_model) 73 | self.dispose() 74 | except Exception as e: 75 | print("Error in OK action of ModelSelectionDialog: {}".format(e)) 76 | 77 | def cancel(self): 78 | try: 79 | self.selected_model = None 80 | self.on_selection_complete(self.selected_model) 81 | self.dispose() 82 | except Exception as e: 83 | print("Error in Cancel action of ModelSelectionDialog: {}".format(e)) 84 | 85 | def show_model_select_dialog(models): 86 | """ 87 | Displays the ModelSelectionDialog and waits for user interaction. 88 | 89 | Args: 90 | models (list): List of available Claude models. 91 | 92 | Returns: 93 | str or None: The selected model or None if cancelled. 94 | """ 95 | selected_model = [] 96 | dialog_complete = threading.Event() 97 | 98 | def on_selection(selected): 99 | selected_model.append(selected) 100 | dialog_complete.set() 101 | 102 | def create_dialog(): 103 | ModelSelectionDialog(models, 0, on_selection) 104 | 105 | swing.SwingUtilities.invokeLater(create_dialog) 106 | dialog_complete.wait() 107 | return selected_model[0] 108 | -------------------------------------------------------------------------------- /Decyx/decyx/api.py: -------------------------------------------------------------------------------- 1 | # api.py 2 | 3 | # Note: Decyx doesn't use Anthropic's official python API as it is intended for Python 3+ 4 | 5 | import json 6 | import urllib2 7 | from config import CLAUDE_API_URL 8 | 9 | def send_request(url, headers, data): 10 | """Send a POST request to the specified URL with the given headers and data. 11 | 12 | Args: 13 | url (str): The URL to send the request to. 14 | headers (dict): A dictionary of HTTP headers to include in the request. 15 | data (dict): The data to send in the body of the request. 16 | 17 | Returns: 18 | urllib2.Response or urllib2.HTTPError: The response object returned by the server. 19 | """ 20 | req = urllib2.Request(url, json.dumps(data), headers) 21 | try: 22 | response = urllib2.urlopen(req) 23 | return response 24 | except urllib2.HTTPError as e: 25 | return e 26 | except urllib2.URLError as e: 27 | print "Failed to reach server: {}".format(e.reason) 28 | return None 29 | 30 | def read_response(response): 31 | """Read the response from the response object. 32 | 33 | Args: 34 | response (urllib2.Response or urllib2.HTTPError): The response object returned by send_request. 35 | 36 | Returns: 37 | str: The content of the response as a string, or None if an error occurred. 38 | """ 39 | if response is None: 40 | return None 41 | elif isinstance(response, urllib2.HTTPError): 42 | error_content = response.read() 43 | print "Error: HTTP response code {}".format(response.code) 44 | print "Error message: {}".format(error_content) 45 | return None 46 | else: 47 | content = response.read() 48 | return content 49 | 50 | def parse_json_response(content): 51 | """Parse the JSON response from Claude API. 52 | 53 | Args: 54 | content (str): The response content as a string. 55 | 56 | Returns: 57 | dict: The parsed JSON object, or None if parsing failed. 58 | """ 59 | json_start = content.find('{') 60 | json_end = content.rfind('}') + 1 61 | if json_start != -1 and json_end != -1: 62 | json_str = content[json_start:json_end] 63 | try: 64 | return json.loads(json_str) 65 | except ValueError as e: 66 | print "Failed to parse JSON from Claude's response: {}".format(str(e)) 67 | else: 68 | print "No JSON object found in Claude's response" 69 | return None 70 | 71 | def get_response_from_claude(prompt, api_key, model, monitor, is_explanation=False): 72 | """Get a response from the Claude API. 73 | 74 | Args: 75 | prompt (str): The prompt to send to the Claude API. 76 | api_key (str): The API key for authentication. 77 | model (str): The model name to use. 78 | monitor (object): An object with a setMessage method to display status messages. 79 | is_explanation (bool, optional): Flag indicating if the response is an explanation. Defaults to False. 80 | 81 | Returns: 82 | dict or str: The parsed JSON response, or the content string if is_explanation is True. 83 | """ 84 | try: 85 | monitor.setMessage("Sending request to Claude API...") 86 | headers = { 87 | "Content-Type": "application/json", 88 | "x-api-key": api_key, 89 | "anthropic-version": "2023-06-01" 90 | } 91 | data = { 92 | "model": model, 93 | "messages": [{"role": "user", "content": prompt}], 94 | "max_tokens": 2000, 95 | "temperature": 0.2, 96 | "top_p": 1.0, 97 | "top_k": 30 98 | } 99 | 100 | print "Sending request to Claude API..." 101 | response = send_request(CLAUDE_API_URL, headers, data) 102 | 103 | monitor.setMessage("Waiting for response from Claude API...") 104 | content = read_response(response) 105 | 106 | if content: 107 | print "Received response from Claude API." 108 | response_json = json.loads(content) 109 | content_text = response_json['content'][0]['text'] 110 | 111 | if is_explanation: 112 | return content_text.strip() 113 | else: 114 | return parse_json_response(content_text) 115 | 116 | return None 117 | 118 | except Exception as e: 119 | print "Exception in get_response_from_claude: {}".format(e) 120 | return None 121 | finally: 122 | monitor.setMessage("") 123 | -------------------------------------------------------------------------------- /Decyx/decyx/dialogs/prompt_review_dialog.py: -------------------------------------------------------------------------------- 1 | # prompt_review_dialog.py 2 | 3 | import threading 4 | import javax.swing as swing 5 | from javax.swing import JFrame, JPanel, JButton, JScrollPane, BoxLayout, JLabel, JTextArea 6 | from java.awt import BorderLayout 7 | 8 | from decyx.config import SKIP_PROMPT_CONFIRMATION 9 | 10 | # Dialog Class 11 | class PromptReviewDialog(JFrame): 12 | """ 13 | Dialog for reviewing and editing the prompt before sending it to the Claude API. 14 | """ 15 | def __init__(self, prompt, title, on_submit, on_cancel): 16 | super(PromptReviewDialog, self).__init__(title) 17 | self.prompt = prompt 18 | self.on_submit = on_submit 19 | self.on_cancel = on_cancel 20 | self.final_prompt = None 21 | self.init_ui() 22 | 23 | def init_ui(self): 24 | try: 25 | panel = JPanel() 26 | panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS)) 27 | 28 | self.add_instruction_label(panel) 29 | self.add_prompt_text_area(panel) 30 | self.add_buttons(panel) 31 | 32 | self.getContentPane().add(JScrollPane(panel), BorderLayout.CENTER) 33 | self.setSize(700, 500) 34 | self.setLocationRelativeTo(None) 35 | self.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) 36 | self.setVisible(True) 37 | except Exception as e: 38 | print "Error initializing PromptReviewDialog UI: {}".format(e) 39 | 40 | def add_instruction_label(self, panel): 41 | try: 42 | instruction_label = JLabel("Review and edit the final prompt before sending to Claude API:") 43 | instruction_label.setToolTipText("You can modify the prompt as needed") 44 | panel.add(instruction_label) 45 | except Exception as e: 46 | print "Error adding instruction label: {}".format(e) 47 | 48 | def add_prompt_text_area(self, panel): 49 | try: 50 | self.prompt_text_area = JTextArea(self.prompt, 20, 60) 51 | self.prompt_text_area.setLineWrap(True) 52 | self.prompt_text_area.setWrapStyleWord(True) 53 | self.prompt_text_area.setToolTipText("Edit the prompt here") 54 | panel.add(JScrollPane(self.prompt_text_area)) 55 | except Exception as e: 56 | print "Error adding prompt text area: {}".format(e) 57 | 58 | def add_buttons(self, panel): 59 | try: 60 | button_panel = JPanel() 61 | send_button = JButton("Send to Claude API") 62 | send_button.addActionListener(lambda e: self.send()) 63 | send_button.setToolTipText("Send the prompt to the Claude API") 64 | cancel_button = JButton("Cancel") 65 | cancel_button.addActionListener(lambda e: self.cancel()) 66 | cancel_button.setToolTipText("Cancel and discard changes") 67 | button_panel.add(send_button) 68 | button_panel.add(cancel_button) 69 | self.getContentPane().add(button_panel, BorderLayout.SOUTH) 70 | except Exception as e: 71 | print "Error adding buttons to PromptReviewDialog: {}".format(e) 72 | 73 | def send(self): 74 | try: 75 | self.final_prompt = self.prompt_text_area.getText() 76 | self.on_submit(self.final_prompt) 77 | self.dispose() 78 | except Exception as e: 79 | print "Error sending prompt: {}".format(e) 80 | 81 | def cancel(self): 82 | try: 83 | self.final_prompt = None 84 | self.on_cancel() 85 | self.dispose() 86 | except Exception as e: 87 | print "Error cancelling PromptReviewDialog: {}".format(e) 88 | 89 | # Helper Functions 90 | def show_prompt_review_dialog(prompt, title): 91 | """ 92 | Displays the PromptReviewDialog and waits for user interaction. 93 | 94 | Args: 95 | prompt (str): The prompt text to review. 96 | title (str): The title of the dialog window. 97 | 98 | Returns: 99 | str or None: The final prompt after user edits or None if cancelled. 100 | """ 101 | if SKIP_PROMPT_CONFIRMATION: 102 | return prompt 103 | 104 | final_prompt = [] 105 | dialog_complete = threading.Event() 106 | 107 | def on_submit(final_p): 108 | final_prompt.append(final_p) 109 | dialog_complete.set() 110 | 111 | def on_cancel(): 112 | final_prompt.append(None) 113 | dialog_complete.set() 114 | 115 | def create_dialog(): 116 | PromptReviewDialog(prompt, title, on_submit, on_cancel) 117 | 118 | swing.SwingUtilities.invokeLater(create_dialog) 119 | dialog_complete.wait() 120 | return final_prompt[0] 121 | -------------------------------------------------------------------------------- /Decyx/decyx/dialogs/action_selection_dialog.py: -------------------------------------------------------------------------------- 1 | # action_selection_dialog.py 2 | 3 | import threading 4 | import javax.swing as swing 5 | from javax.swing import JFrame, JPanel, JCheckBox, JButton, BoxLayout, JLabel 6 | from java.awt import BorderLayout 7 | 8 | # Dialog Class 9 | class ActionSelectionDialog(JFrame): 10 | """ 11 | Dialog for selecting the actions to perform. 12 | """ 13 | def __init__(self, on_selection_complete): 14 | super(ActionSelectionDialog, self).__init__("Select Actions to Perform") 15 | self.selected_actions = [] 16 | self.on_selection_complete = on_selection_complete 17 | self.init_ui() 18 | 19 | def init_ui(self): 20 | try: 21 | panel = JPanel() 22 | panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS)) 23 | 24 | self.add_instruction_label(panel) 25 | self.add_checkboxes(panel) 26 | self.add_buttons(panel) 27 | 28 | self.getContentPane().add(panel, BorderLayout.CENTER) 29 | self.setSize(300, 200) 30 | self.setLocationRelativeTo(None) 31 | self.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) 32 | self.setVisible(True) 33 | except Exception as e: 34 | print "Error initializing ActionSelectionDialog UI: {}".format(e) 35 | 36 | def add_instruction_label(self, panel): 37 | try: 38 | instruction_label = JLabel("Select the actions you want to perform:") 39 | instruction_label.setToolTipText("Choose the desired actions") 40 | panel.add(instruction_label) 41 | except Exception as e: 42 | print "Error adding instruction label: {}".format(e) 43 | 44 | def add_checkboxes(self, panel): 45 | try: 46 | self.rename_retype_checkbox = JCheckBox("Rename and Retype Variables") 47 | self.rename_retype_checkbox.setSelected(True) 48 | self.rename_retype_checkbox.setToolTipText("Rename variables and update their types") 49 | panel.add(self.rename_retype_checkbox) 50 | 51 | self.explanation_checkbox = JCheckBox("Get Function Explanation") 52 | self.explanation_checkbox.setSelected(False) 53 | self.explanation_checkbox.setToolTipText("Obtain explanations for functions") 54 | panel.add(self.explanation_checkbox) 55 | 56 | self.line_comments_checkbox = JCheckBox("Add Line Comments") 57 | self.line_comments_checkbox.setSelected(False) 58 | self.line_comments_checkbox.setToolTipText("Add comments to lines of code") 59 | panel.add(self.line_comments_checkbox) 60 | except Exception as e: 61 | print "Error adding checkboxes: {}".format(e) 62 | 63 | def add_buttons(self, panel): 64 | try: 65 | button_panel = JPanel() 66 | ok_button = JButton("OK") 67 | ok_button.addActionListener(lambda e: self.ok()) 68 | ok_button.setToolTipText("Confirm action selection") 69 | cancel_button = JButton("Cancel") 70 | cancel_button.addActionListener(lambda e: self.cancel()) 71 | cancel_button.setToolTipText("Cancel action selection") 72 | button_panel.add(ok_button) 73 | button_panel.add(cancel_button) 74 | self.getContentPane().add(button_panel, BorderLayout.SOUTH) 75 | except Exception as e: 76 | print "Error adding buttons to ActionSelectionDialog: {}".format(e) 77 | 78 | def ok(self): 79 | try: 80 | self.selected_actions = [] 81 | if self.rename_retype_checkbox.isSelected(): 82 | self.selected_actions.append('rename_retype') 83 | if self.explanation_checkbox.isSelected(): 84 | self.selected_actions.append('explanation') 85 | if self.line_comments_checkbox.isSelected(): 86 | self.selected_actions.append('line_comments') 87 | self.on_selection_complete(self.selected_actions) 88 | self.dispose() 89 | except Exception as e: 90 | print "Error in OK action of ActionSelectionDialog: {}".format(e) 91 | 92 | def cancel(self): 93 | try: 94 | self.selected_actions = [] 95 | self.on_selection_complete(self.selected_actions) 96 | self.dispose() 97 | except Exception as e: 98 | print "Error in Cancel action of ActionSelectionDialog: {}".format(e) 99 | 100 | # Helper Functions 101 | def show_action_select_dialog(): 102 | """ 103 | Displays the ActionSelectionDialog and waits for user interaction. 104 | 105 | Returns: 106 | list: The list of selected actions or an empty list if none selected. 107 | """ 108 | selected_actions = [] 109 | dialog_complete = threading.Event() 110 | 111 | def on_selection(selected): 112 | selected_actions.extend(selected) 113 | dialog_complete.set() 114 | 115 | def create_dialog(): 116 | ActionSelectionDialog(on_selection) 117 | 118 | swing.SwingUtilities.invokeLater(create_dialog) 119 | dialog_complete.wait() 120 | return selected_actions 121 | -------------------------------------------------------------------------------- /Decyx/Decyx.py: -------------------------------------------------------------------------------- 1 | # Python extension that leverages Anthropic's Claude to assist in reverse engineering and binary analysis. 2 | # @author Phil Sajdak 3 | # @category AI Analysis 4 | # @keybinding Shift R 5 | # @menupath 6 | # @toolbar 7 | 8 | from ghidra.framework.preferences import Preferences 9 | from decyx.config import CLAUDE_MODELS, SKIP_PROMPT_CONFIRMATION 10 | from decyx.api import get_response_from_claude 11 | from decyx.decompiler import decompile_function, decompile_callers 12 | from decyx.utils import ( 13 | apply_selected_suggestions, apply_line_comments, apply_explanation, 14 | prepare_prompt 15 | ) 16 | from decyx.gui import * 17 | 18 | def get_api_key(preferences): 19 | """ 20 | Retrieve the API key from Ghidra's preferences. If the key does not exist, 21 | prompt the user to input the Anthropic Claude API key and store it in the preferences. 22 | """ 23 | api_key = preferences.getProperty("ANTHROPIC_API_KEY") 24 | if not api_key: 25 | api_key = askString("API Key", "Enter your Anthropic Claude API key:", "") 26 | if api_key: 27 | preferences.setProperty("ANTHROPIC_API_KEY", api_key) 28 | preferences.store() 29 | print "Anthropic API Key stored in {}.".format(preferences.getFilename()) 30 | return api_key 31 | 32 | def get_callers_code(func, current_program, monitor): 33 | """ 34 | Get the decompiled code of the functions that call the current function. 35 | This is useful for additional context in the analysis. 36 | """ 37 | callers = func.getCallingFunctions(monitor) 38 | if not callers: 39 | return None 40 | 41 | print "Found {} caller(s) for the current function.".format(len(callers)) 42 | selected_callers = show_caller_selection_dialog(list(callers), current_program, monitor) 43 | return decompile_callers(selected_callers, current_program, monitor) if selected_callers else None 44 | 45 | def process_action(action, func, current_program, monitor, api_key, model, callers_code): 46 | """ 47 | Process a specific action on the decompiled function, sending the data to the Claude API and applying the response. 48 | Actions can include renaming, retyping variables, adding explanations, and inserting line comments. 49 | """ 50 | decompiled_code, variables = decompile_function(func, current_program, monitor, annotate_addresses=(action == 'line_comments')) 51 | if not decompiled_code or not variables: 52 | print "Failed to obtain decompiled code or variable information for {}.".format(action) 53 | return False 54 | 55 | prompt = prepare_prompt(decompiled_code, variables, action=action, callers_code=callers_code) 56 | final_prompt = prompt if SKIP_PROMPT_CONFIRMATION else show_prompt_review_dialog(prompt, "Review and Edit Prompt ({})".format(action.replace('_', ' ').title())) 57 | if not final_prompt: 58 | print "Prompt review cancelled by user." 59 | return False 60 | 61 | is_explanation = action == 'explanation' 62 | response = get_response_from_claude(final_prompt, api_key, model, monitor, is_explanation=is_explanation) 63 | if not response: 64 | print "Failed to get {} from Claude API.".format(action.replace('_', ' ')) 65 | return False 66 | 67 | if action == 'rename_retype': 68 | selected_suggestions = show_suggestion_dialog(response, variables, state.getTool()) 69 | if selected_suggestions: 70 | apply_selected_suggestions(func, response, selected_suggestions, state.getTool()) 71 | else: 72 | print "Operation cancelled by user after receiving suggestions." 73 | return False 74 | elif action == 'explanation': 75 | apply_explanation(func, response) 76 | elif action == 'line_comments': 77 | apply_line_comments(func, response) 78 | 79 | return True 80 | 81 | def main(): 82 | """ 83 | The main entry point of the script. Responsible for gathering API keys, 84 | selecting models and actions, and processing the actions on the current function. 85 | """ 86 | api_key = get_api_key(Preferences) 87 | if not api_key: 88 | print "API key is required to proceed." 89 | return 90 | 91 | if len(CLAUDE_MODELS) == 1: 92 | model = CLAUDE_MODELS[0] 93 | print "Using the only available model: {}".format(model) 94 | else: 95 | model = show_model_select_dialog(CLAUDE_MODELS) 96 | if not model: 97 | print "Model selection cancelled by user." 98 | return 99 | 100 | selected_actions = show_action_select_dialog() 101 | if not selected_actions: 102 | print "No actions selected. Exiting." 103 | return 104 | 105 | func = getFunctionContaining(currentAddress) 106 | if not func or not currentProgram or not monitor: 107 | print "Required context is missing." 108 | return 109 | 110 | callers_code = get_callers_code(func, currentProgram, monitor) 111 | print "Callers' code {}.".format("included for additional context" if callers_code else "not included") 112 | 113 | for action in selected_actions: 114 | print "Processing action: {}".format(action) 115 | if not process_action(action, func, currentProgram, monitor, api_key, model, callers_code): 116 | print "Failed to process action: {}".format(action) 117 | return 118 | 119 | print "Decyx operations completed successfully." 120 | 121 | if __name__ == "__main__": 122 | main() -------------------------------------------------------------------------------- /Decyx/decyx/dialogs/caller_selection_dialog.py: -------------------------------------------------------------------------------- 1 | # caller_selection_dialog.py 2 | 3 | import threading 4 | import javax.swing as swing 5 | from javax.swing import JFrame, JPanel, JCheckBox, JButton, JScrollPane, BoxLayout, JLabel, JTextArea 6 | from java.awt import BorderLayout, Dimension 7 | 8 | from decyx.decompiler import decompile_function 9 | 10 | # Dialog Class 11 | class CallerSelectionDialog(JFrame): 12 | """ 13 | Dialog for selecting which caller functions' code to include for additional context. 14 | """ 15 | def __init__(self, callers, current_program, monitor, on_selection_complete): 16 | super(CallerSelectionDialog, self).__init__("Select Callers to Include") 17 | self.callers = callers 18 | self.current_program = current_program 19 | self.monitor = monitor 20 | self.on_selection_complete = on_selection_complete 21 | self.selected_callers = [] 22 | self.init_ui() 23 | 24 | def init_ui(self): 25 | try: 26 | panel = JPanel(BorderLayout()) 27 | 28 | self.add_callers_panel(panel) 29 | self.add_preview_panel(panel) 30 | self.add_buttons_panel(panel) 31 | 32 | self.getContentPane().add(panel) 33 | self.setSize(800, 400) 34 | self.setLocationRelativeTo(None) 35 | self.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) 36 | self.setVisible(True) 37 | except Exception as e: 38 | print "Error initializing CallerSelectionDialog UI: {}".format(e) 39 | 40 | def add_callers_panel(self, panel): 41 | try: 42 | callers_panel = JPanel() 43 | callers_panel.setLayout(BoxLayout(callers_panel, BoxLayout.Y_AXIS)) 44 | self.caller_checkboxes = [] 45 | for caller in self.callers: 46 | checkbox = JCheckBox(caller.getName()) 47 | checkbox.setSelected(False) 48 | checkbox.setToolTipText("Include code from caller: {}".format(caller.getName())) 49 | # Bind the caller and checkbox to the action listener using lambda with default arguments 50 | checkbox.addActionListener(lambda e, c=caller: self.update_preview(c, e)) 51 | self.caller_checkboxes.append(checkbox) 52 | callers_panel.add(checkbox) 53 | 54 | scroll_callers = JScrollPane(callers_panel) 55 | scroll_callers.setPreferredSize(Dimension(200, 300)) 56 | panel.add(scroll_callers, BorderLayout.WEST) 57 | except Exception as e: 58 | print "Error adding callers panel: {}".format(e) 59 | 60 | def update_preview(self, caller, event): 61 | try: 62 | if event.getSource().isSelected(): 63 | decompiled_code, _ = decompile_function(caller, self.current_program, self.monitor) 64 | if decompiled_code: 65 | code_length = len(decompiled_code) 66 | self.update_preview_label(code_length) 67 | self.set_preview_text(decompiled_code) 68 | else: 69 | self.set_preview_text("Decompilation failed or no code available.") 70 | self.update_preview_label(0) 71 | else: 72 | self.set_preview_text("Select a caller to see its decompiled code preview.") 73 | self.update_preview_label(0) 74 | except Exception as e: 75 | print "Error updating preview: {}".format(e) 76 | 77 | def add_preview_panel(self, panel): 78 | try: 79 | preview_panel = JPanel(BorderLayout()) 80 | self.preview_label = JLabel("Caller Function Preview:") 81 | preview_panel.add(self.preview_label, BorderLayout.NORTH) 82 | 83 | self.preview_text_area = JTextArea() 84 | self.preview_text_area.setEditable(False) 85 | self.preview_text_area.setLineWrap(True) 86 | self.preview_text_area.setWrapStyleWord(True) 87 | self.preview_text_area.setText("Select a caller to see its decompiled code preview.") 88 | self.preview_text_area.setToolTipText("Decompiled code preview of the selected caller") 89 | self.preview_text_area.setCaretPosition(0) 90 | scroll_preview = JScrollPane(self.preview_text_area) 91 | preview_panel.add(scroll_preview, BorderLayout.CENTER) 92 | 93 | panel.add(preview_panel, BorderLayout.CENTER) 94 | except Exception as e: 95 | print "Error adding preview panel: {}".format(e) 96 | 97 | def add_buttons_panel(self, panel): 98 | try: 99 | buttons_panel = JPanel() 100 | buttons_panel.setLayout(BoxLayout(buttons_panel, BoxLayout.Y_AXIS)) 101 | 102 | select_buttons = JPanel() 103 | select_all_button = JButton("Select All Callers") 104 | select_all_button.addActionListener(lambda e: self.select_all(True)) 105 | select_all_button.setToolTipText("Select all caller checkboxes") 106 | unselect_all_button = JButton("Unselect All Callers") 107 | unselect_all_button.addActionListener(lambda e: self.select_all(False)) 108 | unselect_all_button.setToolTipText("Unselect all caller checkboxes") 109 | select_buttons.add(select_all_button) 110 | select_buttons.add(unselect_all_button) 111 | 112 | action_buttons = JPanel() 113 | ok_button = JButton("OK") 114 | ok_button.addActionListener(lambda e: self.ok()) 115 | ok_button.setToolTipText("Confirm selection and proceed") 116 | cancel_button = JButton("Cancel") 117 | cancel_button.addActionListener(lambda e: self.cancel()) 118 | cancel_button.setToolTipText("Cancel selection") 119 | action_buttons.add(ok_button) 120 | action_buttons.add(cancel_button) 121 | 122 | buttons_panel.add(select_buttons) 123 | buttons_panel.add(action_buttons) 124 | panel.add(buttons_panel, BorderLayout.SOUTH) 125 | except Exception as e: 126 | print "Error adding buttons panel: {}".format(e) 127 | 128 | def set_preview_text(self, text): 129 | try: 130 | self.preview_text_area.setText(text) 131 | self.preview_text_area.setCaretPosition(0) 132 | except Exception as e: 133 | print "Error setting preview text: {}".format(e) 134 | 135 | def update_preview_label(self, length): 136 | try: 137 | self.preview_label.setText("Caller Function Preview (Length: {} characters):".format(length)) 138 | except Exception as e: 139 | print "Error updating preview label: {}".format(e) 140 | 141 | def select_all(self, select): 142 | try: 143 | for checkbox in self.caller_checkboxes: 144 | checkbox.setSelected(select) 145 | except Exception as e: 146 | print "Error selecting all callers: {}".format(e) 147 | 148 | def ok(self): 149 | try: 150 | self.selected_callers = [ 151 | caller for checkbox, caller in zip(self.caller_checkboxes, self.callers) 152 | if checkbox.isSelected() 153 | ] 154 | self.on_selection_complete(self.selected_callers) 155 | self.dispose() 156 | except Exception as e: 157 | print "Error in OK action: {}".format(e) 158 | 159 | def cancel(self): 160 | try: 161 | self.selected_callers = [] 162 | self.on_selection_complete(self.selected_callers) 163 | self.dispose() 164 | except Exception as e: 165 | print "Error in Cancel action: {}".format(e) 166 | 167 | # Helper Functions 168 | def show_caller_selection_dialog(callers, current_program, monitor): 169 | """ 170 | Displays the CallerSelectionDialog and waits for user interaction. 171 | 172 | Args: 173 | callers (list): List of caller Function objects. 174 | current_program (Program): The current program context. 175 | monitor (TaskMonitor): The monitor object for progress tracking. 176 | 177 | Returns: 178 | list: The list of selected caller Function objects. 179 | """ 180 | selected_callers = [] 181 | dialog_complete = threading.Event() 182 | 183 | def on_selection(selected): 184 | selected_callers.extend(selected) 185 | dialog_complete.set() 186 | 187 | def create_dialog(): 188 | CallerSelectionDialog(callers, current_program, monitor, on_selection) 189 | 190 | swing.SwingUtilities.invokeLater(create_dialog) 191 | dialog_complete.wait() 192 | return selected_callers 193 | -------------------------------------------------------------------------------- /Decyx/decyx/decompiler.py: -------------------------------------------------------------------------------- 1 | # decompiler.py 2 | 3 | import re 4 | from ghidra.app.decompiler import DecompInterface, DecompileOptions 5 | from ghidra.program.model.symbol import SourceType, SymbolType 6 | from ghidra.program.model.pcode import HighFunctionDBUtil 7 | from ghidra.app.decompiler import ClangStatement 8 | from config import GLOBAL_VARIABLE_PATTERNS 9 | 10 | def initialize_decompiler(): 11 | """ 12 | Initializes and configures the Ghidra Decompiler interface. 13 | 14 | Returns: 15 | DecompInterface: An initialized and configured DecompInterface object. 16 | """ 17 | decomp_interface = DecompInterface() 18 | options = DecompileOptions() 19 | decomp_interface.setOptions(options) 20 | decomp_interface.toggleCCode(True) 21 | decomp_interface.toggleSyntaxTree(True) 22 | return decomp_interface 23 | 24 | def traverse_clang_node(node, callback): 25 | """ 26 | Recursively traverses a Clang AST node, applying a callback to each node. 27 | 28 | Args: 29 | node (ClangNode): The current Clang AST node. 30 | callback (function): The function to apply to each node. 31 | """ 32 | callback(node) 33 | for i in range(node.numChildren()): 34 | child = node.Child(i) 35 | traverse_clang_node(child, callback) 36 | 37 | def annotate_code_with_addresses(code_markup): 38 | """ 39 | Annotates the decompiled code with addresses from the Clang AST. 40 | 41 | Args: 42 | code_markup (ClangNode): The root node of the Clang AST. 43 | 44 | Returns: 45 | str: The annotated decompiled code as a single string. 46 | """ 47 | annotated_lines = [] 48 | 49 | def collect_lines(node): 50 | if isinstance(node, ClangStatement): 51 | address = node.getMinAddress() 52 | code_line = node.toString() 53 | if address: 54 | annotated_line = "// Address: {}\n{}".format(address, code_line) 55 | else: 56 | annotated_line = code_line 57 | annotated_lines.append(annotated_line) 58 | 59 | traverse_clang_node(code_markup, collect_lines) 60 | return '\n'.join(annotated_lines) 61 | 62 | def find_global_variables(decompiled_code): 63 | """ 64 | Identifies global variables in the decompiled code based on naming patterns defined in config.py. 65 | Args: 66 | decompiled_code (str): The decompiled C code of the function. 67 | Returns: 68 | set: A set of global variable names found in the code. 69 | """ 70 | global_variables = set() 71 | for pattern in GLOBAL_VARIABLE_PATTERNS: 72 | global_variables.update(re.findall(pattern, decompiled_code)) 73 | return global_variables 74 | 75 | def extract_global_variables(global_vars, currentProgram): 76 | """ 77 | Extracts information about global variables. 78 | Args: 79 | global_vars (set): Set of global variable names. 80 | currentProgram (Program): The current Ghidra program. 81 | Returns: 82 | list: A list of dictionaries, each representing a global variable with its details. 83 | """ 84 | global_var_info = [] 85 | symbolTable = currentProgram.getSymbolTable() 86 | 87 | for global_var in global_vars: 88 | symbols = symbolTable.getGlobalSymbols(global_var) 89 | if symbols and len(symbols) > 0: 90 | symbol = symbols[0] # Get the first symbol if multiple exist 91 | if symbol.getSymbolType() == SymbolType.LABEL: 92 | addr = symbol.getAddress() 93 | data = currentProgram.getListing().getDataAt(addr) 94 | data_type = str(data.getDataType()) if data else "unknown" 95 | else: 96 | data_type = str(symbol.getObject().getDataType()) 97 | else: 98 | data_type = "unknown" 99 | 100 | global_var_info.append({ 101 | "old_name": global_var, 102 | "old_type": data_type, 103 | "storage": "global" 104 | }) 105 | 106 | return global_var_info 107 | 108 | def extract_variables(func, decompiled_code, currentProgram): 109 | """ 110 | Extracts all relevant variables from the function, including parameters, local variables, and globals. 111 | Args: 112 | func (Function): The function object from Ghidra. 113 | decompiled_code (str): The decompiled C code of the function. 114 | currentProgram (Program): The current Ghidra program. 115 | Returns: 116 | list: A list of dictionaries, each representing a variable with its details. 117 | """ 118 | all_vars = [] 119 | 120 | # Extract parameters 121 | for param in func.getParameters(): 122 | all_vars.append({ 123 | "old_name": param.getName(), 124 | "old_type": str(param.getDataType()), 125 | "storage": str(param.getVariableStorage()) 126 | }) 127 | 128 | # Extract local variables 129 | for local_var in func.getLocalVariables(): 130 | all_vars.append({ 131 | "old_name": local_var.getName(), 132 | "old_type": str(local_var.getDataType()), 133 | "storage": str(local_var.getVariableStorage()) 134 | }) 135 | 136 | # Identify global variables referenced in the decompiled code 137 | global_vars = find_global_variables(decompiled_code) 138 | 139 | # Extract global variables 140 | all_vars.extend(extract_global_variables(global_vars, currentProgram)) 141 | 142 | # Filter out variables with 'HASH' in their storage to exclude irrelevant entries 143 | return [var for var in all_vars if 'HASH' not in var['storage']] 144 | 145 | def decompile_function(func, current_program, monitor, annotate_addresses=False): 146 | """ 147 | Decompiles a given function to retrieve its C code and variable information. 148 | 149 | Args: 150 | func (Function): The function to decompile. 151 | current_program (Program): The current program context in Ghidra. 152 | monitor (TaskMonitor): The monitor object for progress tracking. 153 | annotate_addresses (bool): Whether to annotate the decompiled code with addresses. 154 | 155 | Returns: 156 | tuple: A tuple containing the decompiled code as a string and a list of variables. 157 | Returns (None, None) if decompilation fails. 158 | """ 159 | decomp_interface = initialize_decompiler() 160 | decomp_interface.openProgram(current_program) 161 | 162 | try: 163 | results = decomp_interface.decompileFunction(func, 60, monitor) 164 | if not results.decompileCompleted(): 165 | print "Decompilation failed for function at {}".format(func.getEntryPoint()) 166 | return None, None 167 | 168 | print "Decompilation completed successfully." 169 | decompiled_function = results.getDecompiledFunction() 170 | high_func = results.getHighFunction() 171 | code_markup = results.getCCodeMarkup() 172 | 173 | # Commit local names to database within a transaction 174 | if high_func is not None: 175 | transaction = current_program.startTransaction("Commit Local Names") 176 | try: 177 | HighFunctionDBUtil.commitLocalNamesToDatabase(high_func, SourceType.USER_DEFINED) 178 | except Exception as e: 179 | print "Warning: Could not commit local names to database: {}".format(e) 180 | finally: 181 | current_program.endTransaction(transaction, True) 182 | 183 | if annotate_addresses: 184 | decompiled_code_str = annotate_code_with_addresses(code_markup) 185 | else: 186 | decompiled_code_str = decompiled_function.getC() 187 | 188 | variables = extract_variables(func, decompiled_code_str, current_program) 189 | return decompiled_code_str, variables 190 | 191 | except Exception as e: 192 | print "Exception during decompilation: {}".format(e) 193 | return None, None 194 | finally: 195 | decomp_interface.dispose() 196 | 197 | def decompile_callers(callers, current_program, monitor): 198 | """ 199 | Decompiles a list of caller functions to provide additional context. 200 | 201 | Args: 202 | callers (list): A list of caller Function objects. 203 | current_program (Program): The current program context in Ghidra. 204 | monitor (TaskMonitor): The monitor object for progress tracking. 205 | 206 | Returns: 207 | dict: A dictionary mapping caller names to their decompiled code or error messages. 208 | """ 209 | decomp_interface = initialize_decompiler() 210 | decomp_interface.openProgram(current_program) 211 | 212 | callers_code = {} 213 | total_callers = len(callers) 214 | 215 | try: 216 | for index, caller in enumerate(callers): 217 | if monitor.isCancelled(): 218 | print "Decompilation cancelled by user." 219 | break 220 | progress_percentage = int(((index + 1) / float(total_callers)) * 100) 221 | monitor.setProgress(progress_percentage) 222 | try: 223 | results = decomp_interface.decompileFunction(caller, 60, monitor) 224 | if results.decompileCompleted(): 225 | decompiled_code = results.getDecompiledFunction().getC() 226 | callers_code[caller.getName()] = decompiled_code 227 | print "Decompiled caller '{}' successfully.".format(caller.getName()) 228 | else: 229 | callers_code[caller.getName()] = "Decompilation failed." 230 | print "Decompilation failed for caller '{}'.".format(caller.getName()) 231 | except Exception as e: 232 | callers_code[caller.getName()] = "Exception during decompilation: {}".format(e) 233 | print "Exception during decompilation of caller '{}': {}".format(caller.getName(), e) 234 | finally: 235 | decomp_interface.dispose() 236 | 237 | return callers_code -------------------------------------------------------------------------------- /Decyx/decyx/utils.py: -------------------------------------------------------------------------------- 1 | # utils.py 2 | 3 | import re 4 | import json 5 | from ghidra.program.model.symbol import SourceType 6 | from ghidra.program.model.listing import VariableSizeException 7 | from ghidra.app.services import DataTypeManagerService 8 | from ghidra.program.model.listing import CodeUnit 9 | from ghidra.app.script import GhidraScript 10 | from config import PROMPTS 11 | 12 | def find_data_type_by_name(name, tool): 13 | """ 14 | Finds a data type by its name from the data type manager. 15 | 16 | Args: 17 | name (str): The name of the data type to search for. 18 | tool (Tool): The tool context to retrieve the data type manager. 19 | 20 | Returns: 21 | DataType: The matching data type if found, else None. 22 | """ 23 | service = tool.getService(DataTypeManagerService) 24 | data_type_managers = service.getDataTypeManagers() 25 | 26 | for manager in data_type_managers: 27 | # Try with and without leading slash 28 | data_type = manager.getDataType("/" + name) 29 | if data_type is None: 30 | data_type = manager.getDataType(name) 31 | 32 | if data_type is not None: 33 | return data_type 34 | 35 | # If not found, search through all categories 36 | all_data_types = manager.getAllDataTypes() 37 | for dt in all_data_types: 38 | if dt.getName().lower() == name.lower(): 39 | return dt 40 | 41 | return None 42 | 43 | def retype_variable(variable, new_type_name, tool): 44 | """ 45 | Changes the data type of a variable to a new specified type. 46 | 47 | Args: 48 | variable (Variable): The variable to be retyped. 49 | new_type_name (str): The name of the new data type. 50 | tool (Tool): The tool context to find the new data type. 51 | 52 | Returns: 53 | bool: True if successful, False otherwise. 54 | """ 55 | new_data_type = find_data_type_by_name(new_type_name, tool) 56 | 57 | if new_data_type is None: 58 | return False 59 | 60 | try: 61 | variable.setDataType(new_data_type, SourceType.USER_DEFINED) 62 | print "Successfully retyped variable '{}' to '{}'".format(variable.getName(), new_type_name) 63 | return True 64 | except VariableSizeException as e: 65 | print "Error: Variable size conflict when retyping '{}' to '{}'. Details: {}".format( 66 | variable.getName(), new_type_name, str(e)) 67 | return False 68 | except Exception as e: 69 | print "Error retyping variable '{}' to '{}': {}".format( 70 | variable.getName(), new_type_name, str(e)) 71 | return False 72 | 73 | def retype_global_variable(listing, symbol, new_data_type): 74 | """Retype a global variable.""" 75 | addr = symbol.getAddress() 76 | try: 77 | # Clear existing data 78 | listing.clearCodeUnits(addr, addr.add(new_data_type.getLength() - 1), False) 79 | # Try to create new data 80 | data = listing.createData(addr, new_data_type) 81 | if data: 82 | print "Retyped global variable '{}' to '{}'".format(symbol.getName(), new_data_type.getName()) 83 | else: 84 | # If creation fails, try to modify existing data 85 | existing_data = listing.getDataAt(addr) 86 | if existing_data: 87 | existing_data.setDataType(new_data_type, SourceType.USER_DEFINED) 88 | print "Modified existing data type for global variable '{}' to '{}'".format(symbol.getName(), new_data_type.getName()) 89 | else: 90 | print "Error: Failed to create or modify data for global variable '{}' with type '{}'".format(symbol.getName(), new_data_type.getName()) 91 | except Exception as e: 92 | print "Error retyping global variable '{}' to '{}': {}".format(symbol.getName(), new_data_type.getName(), str(e)) 93 | 94 | def rename_function(func, new_name): 95 | """Rename the given function.""" 96 | func.setName(new_name, SourceType.USER_DEFINED) 97 | print "Renamed function to '{}'".format(new_name) 98 | 99 | def rename_symbol(symbol, new_name): 100 | """Rename the given symbol.""" 101 | old_name = symbol.getName() 102 | symbol.setName(new_name, SourceType.USER_DEFINED) 103 | print "Renamed variable '{}' to '{}'".format(old_name, new_name) 104 | 105 | def process_global_variable(symbol_table, listing, old_name, new_name, new_type_name, tool): 106 | """Process a global variable for renaming and retyping.""" 107 | if old_name.startswith('_'): 108 | old_name = old_name[1:] 109 | 110 | symbols = symbol_table.getSymbols(old_name) 111 | symbol = next(symbols, None) 112 | if symbol: 113 | if new_name: 114 | rename_symbol(symbol, new_name) 115 | 116 | if new_type_name: 117 | new_data_type = find_data_type_by_name(new_type_name, tool) 118 | if new_data_type: 119 | retype_global_variable(listing, symbol, new_data_type) 120 | else: 121 | print "Data type '{}' not found for global variable '{}'".format(new_type_name, symbol.getName()) 122 | else: 123 | print "Global variable '{}' not found".format(old_name) 124 | 125 | def process_local_variable(var_obj, new_name, new_type_name, tool): 126 | """Process a local variable for renaming and retyping.""" 127 | if new_name: 128 | rename_symbol(var_obj, new_name) 129 | if new_type_name: 130 | success = retype_variable(var_obj, new_type_name, tool) 131 | if not success: 132 | print "Warning: Failed to retype variable '{}' to '{}'. Skipping and continuing with other variables.".format(var_obj.getName(), new_type_name) 133 | 134 | def apply_selected_suggestions(func, suggestions, selected, tool): 135 | """ 136 | Applies the selected suggestions for renaming and retyping of variables and functions. 137 | Args: 138 | func (Function): The function object being modified. 139 | suggestions (dict): The original suggestions for renaming/retyping. 140 | selected (dict): The selected suggestions to apply. 141 | tool (Tool): The tool context for data type operations. 142 | Returns: 143 | None 144 | """ 145 | 146 | # Get the program, listing, and symbol table from the function's context 147 | program = func.getProgram() 148 | listing = program.getListing() 149 | symbol_table = program.getSymbolTable() 150 | 151 | # If a new function name is selected, apply the renaming 152 | if selected['function_name']: 153 | rename_function(func, selected['function_name']) 154 | 155 | # Gather all parameters and local variables of the function for processing 156 | all_vars = list(func.getParameters()) + list(func.getLocalVariables()) 157 | 158 | # Loop through the selected variable suggestions and apply changes 159 | for i, var_suggestion in enumerate(selected['variables']): 160 | if var_suggestion: 161 | old_name = suggestions['variables'][i]['old_name'] 162 | new_name = var_suggestion.get('new_name', None) 163 | new_type_name = var_suggestion.get('new_type', None) 164 | 165 | # Check if it's a global variable that hasn't been renamed already (indicated by "DAT") 166 | if "DAT" in old_name: 167 | process_global_variable(symbol_table, listing, old_name, new_name, new_type_name, tool) 168 | else: 169 | # Find the local variable object using the old name 170 | var_obj = next((v for v in all_vars if v.getName() == old_name), None) 171 | if var_obj: 172 | process_local_variable(var_obj, new_name, new_type_name, tool) 173 | else: 174 | print "Variable '{}' not found in function".format(old_name) 175 | 176 | def apply_line_comments(func, comments): 177 | """ 178 | Applies line comments to the assembly and decompiler views. 179 | 180 | Args: 181 | func (Function): The function to which comments are being applied. 182 | comments (dict): A dictionary of address-to-comment mappings. 183 | 184 | Returns: 185 | None 186 | """ 187 | program = func.getProgram() 188 | listing = program.getListing() 189 | 190 | # Apply comments to both the assembly listing and the decompiler 191 | for address_str, comment in comments.items(): 192 | address = program.getAddressFactory().getAddress(address_str) 193 | if address is None: 194 | print "Warning: Invalid address {}".format(address_str) 195 | continue 196 | 197 | code_unit = listing.getCodeUnitAt(address) 198 | if code_unit: 199 | # Set PRE comment (appears above the instruction, visible in decompiler) 200 | code_unit.setComment(CodeUnit.PRE_COMMENT, comment) 201 | print "Added PRE comment at address {}: {}".format(address_str, comment) 202 | else: 203 | print "Warning: No code unit found at address {}".format(address_str) 204 | 205 | print "Line comments have been applied to both assembly listing and decompiled function." 206 | 207 | def apply_explanation(func, explanation): 208 | # Apply explanation as comment 209 | func.setComment(explanation) 210 | print "Added explanation as comment to the function." 211 | 212 | def prepare_prompt(code, variables, action='rename_retype', callers_code=None): 213 | """ 214 | Prepares a prompt with the given code and variables for AI interaction. 215 | 216 | Args: 217 | code (str): The code being analyzed or modified. 218 | variables (list): A list of variables involved in the analysis. 219 | action (str): The type of action, e.g., 'rename_retype', 'line_comments'. 220 | callers_code (dict, optional): Code of functions calling the analyzed function. 221 | 222 | Returns: 223 | str: The generated prompt. 224 | """ 225 | # Fetch the prompt template based on the action 226 | prompt_template = PROMPTS.get(action) 227 | 228 | if not prompt_template: 229 | return None # Invalid action 230 | 231 | prompt = prompt_template 232 | 233 | if callers_code: 234 | prompt += "### Additional Context: Callers' Code\n" 235 | for caller_name, caller_code in callers_code.items(): 236 | prompt += "#### Caller: {}\n\n{}\n\n\n".format(caller_name, caller_code) 237 | 238 | prompt += "### Code:\n\n{}\n\n".format(code) 239 | 240 | # Include variables only if action is not 'line_comments' 241 | if action != 'line_comments': 242 | prompt += "### Variables:\n\n{}\n\n".format(json.dumps(variables, indent=2)) 243 | 244 | return prompt 245 | 246 | def format_new_type(type_str): 247 | """ 248 | Fixes the formatting of pointer types by ensuring there is a space before each '*' character. 249 | 250 | Args: 251 | type_str (str): The original type string, potentially containing pointers. 252 | 253 | Returns: 254 | str: The formatted type string with spaces before '*' characters. 255 | """ 256 | # Use regex to find '*' characters not preceded by a space and add a space before them 257 | fixed_type = re.sub(r'(? "char * *") 260 | # This handles cases like "char**", "char ***", etc. 261 | fixed_type = re.sub(r'\*\*+', lambda m: ' ' + ' *' * len(m.group()), fixed_type) 262 | 263 | # Remove any redundant spaces that may have been introduced 264 | fixed_type = re.sub(r'\s+', ' ', fixed_type).strip() 265 | 266 | return fixed_type -------------------------------------------------------------------------------- /Decyx/decyx/dialogs/suggestion_dialog.py: -------------------------------------------------------------------------------- 1 | # suggestion_dialog.py 2 | 3 | import threading 4 | import javax.swing as swing 5 | from javax.swing import ( 6 | JFrame, JPanel, JCheckBox, JButton, JScrollPane, BoxLayout, JLabel, 7 | JTextField, JTextArea, JTable, DefaultCellEditor 8 | ) 9 | from javax.swing.table import DefaultTableCellRenderer, DefaultTableModel 10 | from java.awt import Color, Dimension, Font 11 | 12 | from decyx.utils import find_data_type_by_name, format_new_type 13 | from decyx.config import DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT 14 | 15 | # Renderer Classes 16 | class BoldRenderer(DefaultTableCellRenderer): 17 | """ 18 | Custom renderer to display text in bold font. 19 | """ 20 | def __init__(self): 21 | super(BoldRenderer, self).__init__() 22 | self.bold_font = None 23 | 24 | def getTableCellRendererComponent(self, table, value, is_selected, has_focus, row, column): 25 | component = super(BoldRenderer, self).getTableCellRendererComponent( 26 | table, value, is_selected, has_focus, row, column 27 | ) 28 | if self.bold_font is None: 29 | font = component.getFont() 30 | self.bold_font = font.deriveFont(Font.BOLD) 31 | component.setFont(self.bold_font) 32 | return component 33 | 34 | class NewTypeCellRenderer(DefaultTableCellRenderer): 35 | """ 36 | Custom renderer to display new type cells in bold and color-coded based on validity. 37 | """ 38 | def __init__(self, type_validity): 39 | super(NewTypeCellRenderer, self).__init__() 40 | self.bold_font = None 41 | self.type_validity = type_validity 42 | 43 | def getTableCellRendererComponent(self, table, value, is_selected, has_focus, row, column): 44 | component = super(NewTypeCellRenderer, self).getTableCellRendererComponent( 45 | table, value, is_selected, has_focus, row, column 46 | ) 47 | if self.bold_font is None: 48 | font = component.getFont() 49 | self.bold_font = font.deriveFont(Font.BOLD) 50 | component.setFont(self.bold_font) 51 | if not self.type_validity[row]: 52 | component.setForeground(Color.RED) 53 | else: 54 | component.setForeground(table.getForeground()) 55 | return component 56 | 57 | class CheckboxRenderer(JCheckBox, swing.table.TableCellRenderer): 58 | """ 59 | Custom renderer for checkbox cells in a table. 60 | """ 61 | def __init__(self): 62 | super(CheckboxRenderer, self).__init__() 63 | self.setHorizontalAlignment(JLabel.CENTER) 64 | self.setOpaque(True) 65 | 66 | def getTableCellRendererComponent(self, table, value, is_selected, has_focus, row, column): 67 | if isinstance(value, bool): 68 | self.setSelected(value) 69 | elif isinstance(value, str): 70 | self.setSelected(value.lower() == 'true') 71 | else: 72 | self.setSelected(False) 73 | 74 | if is_selected: 75 | self.setForeground(table.getSelectionForeground()) 76 | self.setBackground(table.getSelectionBackground()) 77 | else: 78 | self.setForeground(table.getForeground()) 79 | self.setBackground(table.getBackground()) 80 | 81 | return self 82 | 83 | # Dialog Classes 84 | class SuggestionDialog(JFrame): 85 | """ 86 | Dialog for displaying and applying suggestions from the Claude API. 87 | Handles both Apply and Cancel actions by invoking appropriate callbacks. 88 | """ 89 | def __init__(self, suggestions, variables_with_old_types, tool, on_apply, on_cancel): 90 | super(SuggestionDialog, self).__init__("Claude Suggestions") 91 | self.suggestions = suggestions 92 | self.variables_with_old_types = variables_with_old_types 93 | self.tool = tool 94 | self.on_apply = on_apply 95 | self.on_cancel = on_cancel 96 | self.selected_suggestions = { 97 | 'function_name': None, 98 | 'variables': [{} for _ in self.suggestions['variables']], 99 | 'explanation': None 100 | } 101 | self.type_validity = [] 102 | self.init_ui() 103 | 104 | def init_ui(self): 105 | try: 106 | panel = JPanel() 107 | panel.setLayout(BoxLayout(panel, BoxLayout.Y_AXIS)) 108 | 109 | self.add_function_name_panel(panel) 110 | self.add_variable_table(panel) 111 | self.add_summary_label(panel) 112 | self.add_button_panel(panel) 113 | self.add_explanation_area(panel) 114 | self.add_apply_cancel_buttons(panel) 115 | 116 | self.getContentPane().add(panel) 117 | self.setSize(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT) 118 | self.setLocationRelativeTo(None) 119 | self.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE) 120 | self.setVisible(True) 121 | except Exception as e: 122 | print "Error initializing SuggestionDialog UI: {}".format(e) 123 | 124 | def add_function_name_panel(self, panel): 125 | func_panel = JPanel() 126 | func_panel.setLayout(BoxLayout(func_panel, BoxLayout.X_AXIS)) 127 | self.func_checkbox = JCheckBox("Rename function to:") 128 | self.func_checkbox.setSelected(True) 129 | self.func_checkbox.setToolTipText("Check to rename the function") 130 | self.func_name_field = JTextField(self.suggestions.get('function_name', ""), 20) 131 | self.func_name_field.setMaximumSize(Dimension(200, 25)) 132 | self.func_name_field.setToolTipText("Enter the new function name here") 133 | func_panel.add(self.func_checkbox) 134 | func_panel.add(self.func_name_field) 135 | panel.add(func_panel) 136 | 137 | def add_variable_table(self, panel): 138 | column_names = ["Old Name", "New Name", "Old Type", "New Type", "Rename", "Retype"] 139 | self.table_model = DefaultTableModel([], column_names) 140 | self.variable_table = JTable(self.table_model) 141 | self.populate_variable_table() 142 | self.set_table_column_widths() 143 | self.set_table_renderers() 144 | table_scroll = JScrollPane(self.variable_table) 145 | table_scroll.setPreferredSize(Dimension(700, 300)) 146 | panel.add(table_scroll) 147 | 148 | def populate_variable_table(self): 149 | old_name_to_type = {var['old_name']: var['old_type'] for var in self.variables_with_old_types} 150 | for var in self.suggestions['variables']: 151 | old_name = var['old_name'] 152 | new_name = var.get('new_name', "") 153 | old_type = old_name_to_type.get(old_name, 'unknown') 154 | new_type = format_new_type(var.get('new_type', "")) 155 | type_valid = find_data_type_by_name(new_type, self.tool) is not None 156 | self.type_validity.append(type_valid) 157 | self.table_model.addRow([old_name, new_name, old_type, new_type, True, type_valid]) 158 | 159 | def set_table_column_widths(self): 160 | column_widths = [100, 100, 100, 100, 60, 60] 161 | for i, width in enumerate(column_widths): 162 | self.variable_table.getColumnModel().getColumn(i).setPreferredWidth(width) 163 | 164 | def set_table_renderers(self): 165 | try: 166 | bold_renderer = BoldRenderer() 167 | new_type_renderer = NewTypeCellRenderer(self.type_validity) 168 | checkbox_renderer = CheckboxRenderer() 169 | 170 | self.variable_table.getColumnModel().getColumn(1).setCellRenderer(bold_renderer) 171 | self.variable_table.getColumnModel().getColumn(3).setCellRenderer(new_type_renderer) 172 | checkbox_columns = [4, 5] 173 | for col in checkbox_columns: 174 | column = self.variable_table.getColumnModel().getColumn(col) 175 | column.setCellRenderer(checkbox_renderer) 176 | column.setCellEditor(DefaultCellEditor(JCheckBox())) 177 | except Exception as e: 178 | print "Error setting table renderers: {}".format(e) 179 | 180 | def add_summary_label(self, panel): 181 | try: 182 | total_vars = len(self.variables_with_old_types) 183 | num_suggested_renames = sum( 184 | var['old_name'] != var.get('new_name', var['old_name']) 185 | for var in self.suggestions['variables'] 186 | ) 187 | num_suggested_retypes_valid = sum(1 for valid in self.type_validity if valid) 188 | num_suggested_retypes_invalid = len(self.type_validity) - num_suggested_retypes_valid 189 | 190 | summary_html = ( 191 | "Summary:
" 192 | "Rename suggestions: {1}/{0} total variables
" 193 | "Retype suggestions: {2}/{4} valid, {3}/{4} invalid" 194 | ).format(total_vars, num_suggested_renames, num_suggested_retypes_valid, num_suggested_retypes_invalid, num_suggested_retypes_valid + num_suggested_retypes_invalid) 195 | 196 | summary_label = JLabel(summary_html) 197 | panel.add(summary_label) 198 | except Exception as e: 199 | print "Error adding summary label: {}".format(e) 200 | 201 | def add_button_panel(self, panel): 202 | try: 203 | button_panel = JPanel() 204 | buttons = [ 205 | ("Select All Renames", lambda e: self.select_all(4, True)), 206 | ("Unselect All Renames", lambda e: self.select_all(4, False)), 207 | ("Select All Retypes", lambda e: self.select_all(5, True)), 208 | ("Unselect All Retypes", lambda e: self.select_all(5, False)) 209 | ] 210 | for text, action in buttons: 211 | button = JButton(text) 212 | button.addActionListener(action) 213 | button.setToolTipText("{}".format(text)) 214 | button_panel.add(button) 215 | panel.add(button_panel) 216 | except Exception as e: 217 | print "Error adding button panel: {}".format(e) 218 | 219 | def select_all(self, column, value): 220 | try: 221 | for row in range(self.table_model.getRowCount()): 222 | self.table_model.setValueAt(value, row, column) 223 | except Exception as e: 224 | print "Error selecting all in column {}: {}".format(column, e) 225 | 226 | def add_explanation_area(self, panel): 227 | try: 228 | if 'explanation' in self.suggestions and self.suggestions['explanation']: 229 | panel.add(JLabel("Explanation:")) 230 | self.explanation_area = JTextArea(self.suggestions['explanation'], 5, 30) 231 | self.explanation_area.setEditable(False) 232 | self.explanation_area.setLineWrap(True) 233 | self.explanation_area.setWrapStyleWord(True) 234 | self.explanation_area.setToolTipText("Explanation provided by the API") 235 | panel.add(JScrollPane(self.explanation_area)) 236 | except Exception as e: 237 | print "Error adding explanation area: {}".format(e) 238 | 239 | def add_apply_cancel_buttons(self, panel): 240 | try: 241 | bottom_button_panel = JPanel() 242 | apply_button = JButton("Apply Selected") 243 | apply_button.addActionListener(lambda e: self.apply_changes()) 244 | apply_button.setToolTipText("Apply the selected suggestions") 245 | cancel_button = JButton("Cancel") 246 | cancel_button.addActionListener(lambda e: self.cancel()) 247 | cancel_button.setToolTipText("Cancel without applying changes") 248 | bottom_button_panel.add(apply_button) 249 | bottom_button_panel.add(cancel_button) 250 | panel.add(bottom_button_panel) 251 | except Exception as e: 252 | print "Error adding apply and cancel buttons: {}".format(e) 253 | 254 | def apply_changes(self): 255 | try: 256 | self.process_function_name() 257 | self.process_variables() 258 | self.process_explanation() 259 | self.on_apply(self.selected_suggestions) 260 | self.dispose() 261 | except Exception as e: 262 | print "Error applying changes: {}".format(e) 263 | 264 | def process_function_name(self): 265 | if self.func_checkbox.isSelected(): 266 | self.selected_suggestions['function_name'] = self.func_name_field.getText() 267 | else: 268 | self.selected_suggestions['function_name'] = None 269 | 270 | def process_variables(self): 271 | self.selected_suggestions['variables'] = [] 272 | for row in range(self.table_model.getRowCount()): 273 | old_name = self.table_model.getValueAt(row, 0) 274 | new_name = self.table_model.getValueAt(row, 1) 275 | new_type = self.table_model.getValueAt(row, 3) 276 | rename = self.table_model.getValueAt(row, 4) 277 | retype = self.table_model.getValueAt(row, 5) 278 | 279 | var_suggestion = {'old_name': old_name} 280 | if rename: 281 | var_suggestion['new_name'] = new_name 282 | if retype: 283 | var_suggestion['new_type'] = new_type 284 | 285 | if rename or retype: 286 | self.selected_suggestions['variables'].append(var_suggestion) 287 | else: 288 | self.selected_suggestions['variables'].append(None) 289 | 290 | def process_explanation(self): 291 | if hasattr(self, 'explanation_area'): 292 | self.selected_suggestions['explanation'] = self.explanation_area.getText() 293 | else: 294 | self.selected_suggestions['explanation'] = None 295 | 296 | def cancel(self): 297 | try: 298 | self.on_cancel() 299 | self.dispose() 300 | except Exception as e: 301 | print "Error during cancel: {}".format(e) 302 | 303 | # Helper Functions 304 | def show_suggestion_dialog(suggestions, variables_with_old_types, tool): 305 | """ 306 | Displays the SuggestionDialog and waits for user interaction. 307 | 308 | Args: 309 | suggestions (dict): Suggestions returned from the Claude API. 310 | variables_with_old_types (list): List of variables with their old types. 311 | tool (object): The Ghidra tool instance. 312 | 313 | Returns: 314 | dict or None: The selected suggestions or None if cancelled. 315 | """ 316 | selected_suggestions = [] 317 | dialog_complete = threading.Event() 318 | 319 | def on_apply(selected): 320 | selected_suggestions.append(selected) 321 | dialog_complete.set() 322 | 323 | def on_cancel(): 324 | selected_suggestions.append(None) 325 | dialog_complete.set() 326 | 327 | def create_dialog(): 328 | SuggestionDialog(suggestions, variables_with_old_types, tool, on_apply, on_cancel) 329 | 330 | swing.SwingUtilities.invokeLater(create_dialog) 331 | dialog_complete.wait() 332 | return selected_suggestions[0] 333 | --------------------------------------------------------------------------------