From 47d27140e3e225a8d96772add94f0315524d569b Mon Sep 17 00:00:00 2001 From: Fish Date: Wed, 5 Jun 2024 22:16:43 -0700 Subject: [PATCH 1/6] Support loading CaRT files. --- angrmanagement/data/jobs/loading.py | 110 +++++++++++++++++------ angrmanagement/logic/threads.py | 6 +- angrmanagement/ui/dialogs/load_binary.py | 88 +++++++++++++++--- 3 files changed, 160 insertions(+), 44 deletions(-) diff --git a/angrmanagement/data/jobs/loading.py b/angrmanagement/data/jobs/loading.py index 2f48d32c1..c7127a675 100644 --- a/angrmanagement/data/jobs/loading.py +++ b/angrmanagement/data/jobs/loading.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING +from typing import Any, TYPE_CHECKING import angr import archinfo @@ -11,6 +11,7 @@ from angrmanagement.logic.threads import gui_thread_schedule from angrmanagement.ui.dialogs import LoadBinary +from angrmanagement.ui.dialogs.set_encryption_key import SetEncryptionKeyDialog from .job import Job @@ -44,7 +45,7 @@ def _run(self, inst) -> None: apb = archr.arsenal.angrProjectBow(t, dsb) partial_ld = apb.fire(return_loader=True, perform_relocations=False, load_debug_info=False) self._progress_callback(50) - load_options = gui_thread_schedule(LoadBinary.run, (partial_ld,)) + load_options, _ = gui_thread_schedule(LoadBinary.run, (partial_ld,)) if load_options is None: return @@ -69,20 +70,94 @@ def __init__(self, fname, load_options=None, on_finish=None) -> None: def _run(self, inst) -> None: self._progress_callback(5) + retry = True + partial_ld = None + main_opts = {"ignore_missing_arch": True} + while partial_ld is None and retry: + retry, partial_ld, main_opts = self._load_binary_as_partial_loader(main_opts) + if main_opts: + if "ignore_missing_arch" in main_opts: + del main_opts["ignore_missing_arch"] + + self._progress_callback(50) + new_load_options, simos = gui_thread_schedule( + LoadBinary.run, + (partial_ld,), + kwargs={ + "suggested_backend": partial_ld.main_object.__class__, + "suggested_os_name": partial_ld.main_object.os, + "suggested_main_opts": main_opts, + "suggested_original_backend": ( + None if partial_ld.original_main_object is None else partial_ld.original_main_object.__class__ + ), + "suggested_main_filename": ( + None if partial_ld.original_main_object is None else partial_ld.original_main_object.unpacked_name + ), + }, + ) + if new_load_options is None: + return + + engine = None + if hasattr(new_load_options["arch"], "pcode_arch"): + engine = angr.engines.UberEnginePcode + + self.load_options.update(new_load_options) + + proj = angr.Project(self.fname, load_options=self.load_options, engine=engine, simos=simos) + self._progress_callback(95) + + def callback() -> None: + inst._reset_containers() + inst.project.am_obj = proj + inst.project.am_event() + + gui_thread_schedule(callback, ()) + + def _load_binary_as_partial_loader(self, main_opts: dict[str, Any]) -> tuple[bool, Any, dict[str, Any]]: load_as_blob = False load_with_libraries = True - partial_ld = None + + def _load_user_passphrase(backend_cls) -> bytes | None: + dialog = SetEncryptionKeyDialog( + prompt_msg=f"The encryption key does not work or does not exist for " + f"the CLE backend {backend_cls.__name__}." + ) + dialog.exec_() + return dialog.result + try: # Try automatic loading partial_ld = cle.Loader( - self.fname, perform_relocations=False, load_debug_info=False, main_opts={"ignore_missing_arch": True} + self.fname, + perform_relocations=False, + load_debug_info=False, + main_opts=main_opts, ) except archinfo.arch.ArchNotFound: _l.warning("Could not identify binary architecture.") partial_ld = None load_as_blob = True - except cle.CLECompatibilityError: + except cle.CLEInvalidEncryptionError as ex: + # it needs a password, but the default password does not work + if ex.backend is not None and ex.enckey_argname is not None: + # load a new password + enc_key: bytes | None = gui_thread_schedule(_load_user_passphrase, (ex.backend,)) + if enc_key: + # Update main opts + main_opts[ex.enckey_argname] = enc_key + return True, None, main_opts + + _l.warning( + "Failed to load binary with user-specified encryption key or the encryption key is missing. " + "Attempted to use backend %s (and failed). " + "Loading it as a blob instead.", + ex.backend, + ) + # go load it as a blob + load_as_blob = True + except (cle.CLEInvalidFileFormatError, cle.CLECompatibilityError): # Continue loading as blob load_as_blob = True except cle.CLEError: @@ -110,30 +185,9 @@ def _run(self, inst) -> None: except cle.CLECompatibilityError: # Failed to load executable, even as blob! gui_thread_schedule(LoadBinary.binary_loading_failed, (self.fname,)) - return - - self._progress_callback(50) - new_load_options, simos = gui_thread_schedule( - LoadBinary.run, (partial_ld, partial_ld.main_object.__class__, partial_ld.main_object.os) - ) - if new_load_options is None: - return - - engine = None - if hasattr(new_load_options["arch"], "pcode_arch"): - engine = angr.engines.UberEnginePcode - - self.load_options.update(new_load_options) - - proj = angr.Project(self.fname, load_options=self.load_options, engine=engine, simos=simos) - self._progress_callback(95) + return False, None, main_opts - def callback() -> None: - inst._reset_containers() - inst.project.am_obj = proj - inst.project.am_event() - - gui_thread_schedule(callback, ()) + return False, partial_ld, main_opts class LoadAngrDBJob(Job): diff --git a/angrmanagement/logic/threads.py b/angrmanagement/logic/threads.py index 927df8084..693576518 100644 --- a/angrmanagement/logic/threads.py +++ b/angrmanagement/logic/threads.py @@ -201,7 +201,7 @@ def is_gui_thread() -> bool: def gui_thread_schedule( - callable: Callable[..., T], args: tuple[Any] = None, timeout: int = None, kwargs: dict[str, Any] | None = None + callable: Callable[..., T], args: tuple = None, timeout: int = None, kwargs: dict[str, Any] | None = None ) -> T: """ Schedules the given callable to be executed on the GUI thread. If the current thread is the GUI thread, the callable @@ -241,9 +241,7 @@ def gui_thread_schedule( return event.result -def gui_thread_schedule_async( - callable: Callable[..., T], args: tuple[Any] = None, kwargs: dict[str, Any] = None -) -> None: +def gui_thread_schedule_async(callable: Callable[..., T], args: tuple = None, kwargs: dict[str, Any] = None) -> None: """ Schedules the given callable to be executed on the GUI thread. If the current thread is the GUI thread, the callable is executed immediately. Otherwise, the callable is executed as an event on the GUI thread. diff --git a/angrmanagement/ui/dialogs/load_binary.py b/angrmanagement/ui/dialogs/load_binary.py index 69f0a8701..fa8cb533d 100644 --- a/angrmanagement/ui/dialogs/load_binary.py +++ b/angrmanagement/ui/dialogs/load_binary.py @@ -67,17 +67,25 @@ def __init__( partial_ld, suggested_backend: cle.Backend | None = None, suggested_os_name: str | None = None, + suggested_main_opts: dict | None = None, + suggested_original_backend: cle.Backend | None = None, + suggested_main_filename: str | None = None, parent=None, ) -> None: super().__init__(parent) # initialization - self.file_path = partial_ld.main_object.binary + self.file_path: str | None = ( + partial_ld.main_object.binary if suggested_main_filename is None else suggested_main_filename + ) self.md5 = None self.sha256 = None self.option_widgets = {} self.suggested_backend = suggested_backend self.suggested_os_name = suggested_os_name + self.suggested_main_opts = suggested_main_opts or {} + self.suggested_original_backend = suggested_original_backend + self.suggested_main_filename = suggested_main_filename self.available_backends: dict[str, cle.Backend] = cle.ALL_BACKENDS self.available_simos = {} self.arch = partial_ld.main_object.arch @@ -121,7 +129,7 @@ def __init__( @property def filename(self): - return os.path.basename(self.file_path) + return os.path.basename(self.file_path) if self.file_path else "" # # Private methods @@ -184,7 +192,10 @@ def _init_widgets(self) -> None: filename_caption.setText("File name:") filename = QLabel(self) - filename.setText(self.filename) + if self.filename: + filename.setText(self.filename) + else: + filename.setText("") layout.addWidget(filename_caption, 0, 0, Qt.AlignRight) layout.addWidget(filename, 0, 1) @@ -232,9 +243,36 @@ def _init_central_tab(self, tab) -> None: self._init_load_options_tab(tab) def _init_load_options_tab(self, tab): + # + # Outer backend selection + # + + outer_backend_layout = QHBoxLayout() + outer_backend_caption = QLabel() + outer_backend_caption.setText("Outer backend:") + outer_backend_caption.setSizePolicy(QSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)) + outer_backend_layout.addWidget(outer_backend_caption) + + outer_backend_dropdown = QComboBox() + suggested_outer_backend_name = None + outer_backend_dropdown.addItem("") + for backend_name, backend in self.available_backends.items(): + if getattr(backend, "is_outer", False) is True: + outer_backend_dropdown.addItem(backend_name) + if backend is self.suggested_original_backend: + suggested_outer_backend_name = backend_name + if suggested_outer_backend_name is not None: + outer_backend_dropdown.setCurrentText(suggested_outer_backend_name) + else: + outer_backend_dropdown.setCurrentText("") + outer_backend_layout.addWidget(outer_backend_dropdown) + + self.option_widgets["outer_backend"] = outer_backend_dropdown + # # Backend selection # + backend_layout = QHBoxLayout() backend_caption = QLabel() backend_caption.setText("Backend:") @@ -244,9 +282,10 @@ def _init_load_options_tab(self, tab): backend_dropdown = QComboBox() suggested_backend_name = None for backend_name, backend in self.available_backends.items(): - backend_dropdown.addItem(backend_name) - if backend is self.suggested_backend: - suggested_backend_name = backend_name + if getattr(backend, "is_outer", False) is False: + backend_dropdown.addItem(backend_name) + if backend is self.suggested_backend: + suggested_backend_name = backend_name if suggested_backend_name is not None: backend_dropdown.setCurrentText(suggested_backend_name) backend_layout.addWidget(backend_dropdown) @@ -368,6 +407,7 @@ def _init_load_options_tab(self, tab): self.option_widgets["load_debug_info"] = load_debug_info layout = QVBoxLayout() + layout.addLayout(outer_backend_layout) layout.addLayout(backend_layout) layout.addLayout(os_layout) layout.addLayout(blob_layout) @@ -465,6 +505,14 @@ def _on_ok_clicked(self) -> None: self.load_options["auto_load_libs"] = self.option_widgets["auto_load_libs"].isChecked() self.load_options["load_debug_info"] = self.option_widgets["load_debug_info"].isChecked() + outer_backend_dropdown: QComboBox = self.option_widgets["outer_backend"] + outer_backend: str | None = outer_backend_dropdown.currentText() + if outer_backend == "": + outer_backend = None + elif not outer_backend or outer_backend not in self.available_backends: + QMessageBox.critical(None, "Incorrect backend selection", "Please select a backend before continue.") + return + backend_dropdown: QComboBox = self.option_widgets["backend"] backend: str = backend_dropdown.currentText() if not backend or backend not in self.available_backends: @@ -490,7 +538,7 @@ def _on_ok_clicked(self) -> None: arch = archinfo.ArchPcode(arch.id) self.load_options["arch"] = arch - self.load_options["main_opts"] = { + main_opts = { "backend": backend, } @@ -502,7 +550,7 @@ def _on_ok_clicked(self) -> None: except ValueError: QMessageBox.critical(None, "Incorrect base address", "Please input a valid base address.") return - self.load_options["main_opts"]["base_addr"] = base_addr + main_opts["base_addr"] = base_addr if self._entry_addr_checkbox.isChecked(): try: @@ -510,13 +558,23 @@ def _on_ok_clicked(self) -> None: except ValueError: QMessageBox.critical(None, "Incorrect entry point address", "Please input a valid entry point address.") return - self.load_options["main_opts"]["entry_point"] = entry_addr + main_opts["entry_point"] = entry_addr if force_load_libs: self.load_options["force_load_libs"] = force_load_libs if skip_libs: self.load_options["skip_libs"] = skip_libs + if outer_backend is not None: + self.load_options["main_opts"] = { + "backend": outer_backend, + } | self.suggested_main_opts + self.load_options["lib_opts"] = { + self.suggested_main_filename: main_opts, + } + else: + self.load_options["main_opts"] = main_opts | self.suggested_main_opts + self.close() def _on_cancel_clicked(self) -> None: @@ -524,21 +582,27 @@ def _on_cancel_clicked(self) -> None: @staticmethod def run( - partial_ld, suggested_backend=None, suggested_os_name: str | None = None - ) -> tuple[dict | None, dict | None, dict | None]: + partial_ld, + suggested_backend=None, + suggested_os_name: str | None = None, + suggested_main_opts: dict | None = None, + **kwargs, + ) -> tuple[dict | None, dict | None]: try: dialog = LoadBinary( partial_ld, suggested_backend=suggested_backend, suggested_os_name=suggested_os_name, + suggested_main_opts=suggested_main_opts, parent=GlobalInfo.main_window, + **kwargs, ) dialog.setModal(True) dialog.exec_() return dialog.load_options, dialog.simos except LoadBinaryError: pass - return None, None, None + return None, None @staticmethod def binary_arch_detect_failed(filename: str, archinfo_msg: str) -> None: From 1f14b3695554b84a207237ac178a56235d4cc776 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 6 Jun 2024 05:19:47 +0000 Subject: [PATCH 2/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- angrmanagement/data/jobs/loading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/angrmanagement/data/jobs/loading.py b/angrmanagement/data/jobs/loading.py index c7127a675..8309dea19 100644 --- a/angrmanagement/data/jobs/loading.py +++ b/angrmanagement/data/jobs/loading.py @@ -1,7 +1,7 @@ from __future__ import annotations import logging -from typing import Any, TYPE_CHECKING +from typing import TYPE_CHECKING, Any import angr import archinfo From fc9ddb33b7d9092bb29de897929cde98a1e3f3d4 Mon Sep 17 00:00:00 2001 From: Fish Date: Mon, 10 Jun 2024 18:19:36 -0700 Subject: [PATCH 3/6] lib_opts should not contain None. --- angrmanagement/ui/dialogs/load_binary.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/angrmanagement/ui/dialogs/load_binary.py b/angrmanagement/ui/dialogs/load_binary.py index fa8cb533d..c8da52311 100644 --- a/angrmanagement/ui/dialogs/load_binary.py +++ b/angrmanagement/ui/dialogs/load_binary.py @@ -569,9 +569,10 @@ def _on_ok_clicked(self) -> None: self.load_options["main_opts"] = { "backend": outer_backend, } | self.suggested_main_opts - self.load_options["lib_opts"] = { - self.suggested_main_filename: main_opts, - } + if self.suggested_main_filename is not None: + self.load_options["lib_opts"] = { + self.suggested_main_filename: main_opts, + } else: self.load_options["main_opts"] = main_opts | self.suggested_main_opts From 7c935841c0b01151329c35f4d8cb2a6b67aac854 Mon Sep 17 00:00:00 2001 From: Fish Date: Mon, 10 Jun 2024 18:20:01 -0700 Subject: [PATCH 4/6] Display a message box when job exceptions happen. --- angrmanagement/ui/workspace.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/angrmanagement/ui/workspace.py b/angrmanagement/ui/workspace.py index 719b882a5..054c47221 100644 --- a/angrmanagement/ui/workspace.py +++ b/angrmanagement/ui/workspace.py @@ -973,10 +973,20 @@ def _instance_project_initalization(self, **kwargs) -> None: # pylint:disable=u self.plugins.handle_project_initialization() - def _handle_job_exception(self, job: Job, e: Exception) -> None: + def _handle_job_exception(self, job: Job, ex: Exception) -> None: + + def _display_messagebox() -> None: + msg = "".join(traceback.format_exception(ex)) + QMessageBox.critical( + self.main_window, + f"Error during job {job.name}", + f"An exception occurred when running {job.name}:\n\n{msg}", + ) + self.log(f'Exception while running job "{job.name}":') - self.log(e) + self.log(ex) self.log("Type %debug to debug it") + gui_thread_schedule_async(_display_messagebox) def _update_simgr_debuggers(self, **kwargs) -> None: # pylint:disable=unused-argument sim_dbg = None From c9b1f49d8fea9cf7b267e3d91825bedd7c2bd585 Mon Sep 17 00:00:00 2001 From: Fish Date: Mon, 10 Jun 2024 22:11:29 -0700 Subject: [PATCH 5/6] Add the missing file --- .../ui/dialogs/set_encryption_key.py | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 angrmanagement/ui/dialogs/set_encryption_key.py diff --git a/angrmanagement/ui/dialogs/set_encryption_key.py b/angrmanagement/ui/dialogs/set_encryption_key.py new file mode 100644 index 000000000..a0603aeb4 --- /dev/null +++ b/angrmanagement/ui/dialogs/set_encryption_key.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import base64 +from enum import IntEnum + +from PySide6.QtWidgets import ( + QDialog, + QDialogButtonBox, + QHBoxLayout, + QLabel, + QLineEdit, + QPushButton, + QVBoxLayout, + QGroupBox, + QRadioButton, +) + + +class EncKeyFormat(IntEnum): + BASE64 = 0 + BYTESTRING = 1 + ASCII = 2 + + +class SetEncryptionKeyDialog(QDialog): + """ + A generic dialog box for loading an encryption key from the user. + """ + + def __init__( + self, + window_title: str = "Set an encryption key", + prompt_msg: str = "", + initial_text: str = "", + parent=None, + ) -> None: + super().__init__(parent) + self._prompt_msg: str = prompt_msg + self._initial_text: str = initial_text + self._enckey_box: QLineEdit = None + self._status_label: QLabel = None + self._key_preview: QLabel = None + self._key_preview_bytes: QLabel = None + self._auto_radio: QRadioButton = None + self._base64_radio: QRadioButton = None + self._bytestring_radio: QRadioButton = None + self._ascii_radio: QRadioButton = None + self._ok_button: QPushButton = None + self.main_layout: QVBoxLayout = QVBoxLayout() + self.result: bytes | None = None + self._init_widgets() + self.setLayout(self.main_layout) + self.setWindowTitle(window_title) + + # + # Private methods + # + + def _init_widgets(self) -> None: + prompt_label = QLabel(self) + prompt_label.setText(self._prompt_msg) + self.main_layout.addWidget(prompt_label) + + secondary_prompt_label = QLabel(self) + secondary_prompt_label.setText( + "Please provide an encryption key (in the form of a Base64-encoed string, a Python byte string, or an " + "ASCII string):" + ) + self.main_layout.addWidget(secondary_prompt_label) + + # key format + format_groupbox = QGroupBox("Key format") + self._auto_radio = QRadioButton("Auto-detect") + self._auto_radio.clicked.connect(self._on_enckey_changed) + self._base64_radio = QRadioButton("Base64") + self._base64_radio.clicked.connect(self._on_enckey_changed) + self._bytestring_radio = QRadioButton("Python byte string") + self._bytestring_radio.clicked.connect(self._on_enckey_changed) + self._ascii_radio = QRadioButton("ASCII") + self._ascii_radio.clicked.connect(self._on_enckey_changed) + format_layout = QHBoxLayout() + format_layout.addWidget(self._auto_radio) + self._auto_radio.setChecked(True) + format_layout.addWidget(self._base64_radio) + format_layout.addWidget(self._bytestring_radio) + format_layout.addWidget(self._ascii_radio) + format_groupbox.setLayout(format_layout) + self.main_layout.addWidget(format_groupbox) + + enckey_label = QLabel(self) + enckey_label.setText("Encryption key") + key_box = QLineEdit(self) + if self._initial_text: + key_box.setText(self._initial_text) + key_box.selectAll() + key_box.textChanged.connect(self._on_enckey_changed) + self._enckey_box = key_box + + label_layout = QHBoxLayout() + label_layout.addWidget(enckey_label) + label_layout.addWidget(key_box) + self.main_layout.addLayout(label_layout) + + status_label = QLabel() + self.main_layout.addWidget(status_label) + self._status_label = status_label + + self._key_preview = QLabel() + self.main_layout.addWidget(self._key_preview) + self._key_preview_bytes = QLabel() + self.main_layout.addWidget(self._key_preview_bytes) + + buttons = QDialogButtonBox(parent=self) + buttons.setStandardButtons(QDialogButtonBox.StandardButton.Cancel | QDialogButtonBox.StandardButton.Ok) + buttons.accepted.connect(self._on_ok_clicked) + buttons.rejected.connect(self.close) + self._ok_button = buttons.button(QDialogButtonBox.Ok) + self._ok_button.setEnabled(False) + self.main_layout.addWidget(buttons) + + def _get_enckey(self, txt: str) -> bytes | None: + if not txt: + return None + + format = None + if self._base64_radio.isChecked(): + format = EncKeyFormat.BASE64 + elif self._bytestring_radio.isChecked(): + format = EncKeyFormat.BYTESTRING + elif self._ascii_radio.isChecked(): + format = EncKeyFormat.ASCII + + # parse it as a base64 string + if format is None or format == EncKeyFormat.BASE64: + try: + key = base64.b64decode(txt, validate=True) + # it works! + return key + except (ValueError, TypeError): + # can't be parsed as a base64 string + pass + if format == EncKeyFormat.BASE64: + return None + + # parse it as a Python byte string + if format is None or format == EncKeyFormat.BYTESTRING: + if (txt.startswith('b"') or txt.startswith("b'")) and (txt.endswith('"') or txt.endswith("'")): + trimmed_txt = txt[2:-1] + else: + trimmed_txt = txt + try: + key = eval(f'b"{trimmed_txt}"') + if isinstance(key, bytes): + return key + except Exception: + pass + + if format == EncKeyFormat.BYTESTRING: + return None + + # encode it as a normal string + if format is None or format == EncKeyFormat.ASCII: + try: + return txt.encode("ascii") + except Exception: + pass + if format == EncKeyFormat.ASCII: + return None + + # everything has failed + return None + + # + # Event handlers + # + + def _on_enckey_changed(self, new_text) -> None: # pylint:disable=unused-argument + if self._enckey_box is None: + # initialization is not done yet + return + + enc_key = self._get_enckey(self._enckey_box.text()) + + if enc_key is None: + # the variable name is invalid + self._status_label.setText("Invalid") + self._status_label.setProperty("class", "status_invalid") + self._ok_button.setEnabled(False) + + self._key_preview.setText("Key in byte string: ") + self._key_preview_bytes.setText("Key in bytes: ") + else: + self._status_label.setText("Valid") + self._status_label.setProperty("class", "status_valid") + self._ok_button.setEnabled(True) + + self._key_preview.setText("Key in byte string: " + str(repr(enc_key))) + self._key_preview_bytes.setText("Key in bytes: " + " ".join(f"{x:02x}" for x in enc_key)) + + self._status_label.style().unpolish(self._status_label) + self._status_label.style().polish(self._status_label) + + def _on_ok_clicked(self) -> None: + enc_key = self._get_enckey(self._enckey_box.text()) + if enc_key is not None: + self.result = enc_key + self.close() From 5f837c174f2d6f5579e5d40c6975e8e49d53193f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 11 Jun 2024 05:11:47 +0000 Subject: [PATCH 6/6] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- angrmanagement/ui/dialogs/set_encryption_key.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/angrmanagement/ui/dialogs/set_encryption_key.py b/angrmanagement/ui/dialogs/set_encryption_key.py index a0603aeb4..f799de37a 100644 --- a/angrmanagement/ui/dialogs/set_encryption_key.py +++ b/angrmanagement/ui/dialogs/set_encryption_key.py @@ -6,13 +6,13 @@ from PySide6.QtWidgets import ( QDialog, QDialogButtonBox, + QGroupBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, - QVBoxLayout, - QGroupBox, QRadioButton, + QVBoxLayout, )