From 4a12d1add4e69b971a3f1954dc739c0c92236287 Mon Sep 17 00:00:00 2001 From: Niklas Bittner Date: Thu, 13 Feb 2025 17:02:25 +0100 Subject: [PATCH] fix: replay issues --- .vscode/launch.json | 4 +-- pyproject.toml | 2 +- resimulate.py | 26 ++++++++++++---- resimulate/card.py | 19 ++++++------ resimulate/commands/record.py | 8 +++-- resimulate/commands/replay.py | 50 +++++++++++++++++++++---------- resimulate/models/euicc_info_2.py | 39 ------------------------ resimulate/recording.py | 19 ++++++++++-- resimulate/util/enums.py | 14 +++++---- 9 files changed, 98 insertions(+), 83 deletions(-) delete mode 100644 resimulate/models/euicc_info_2.py diff --git a/.vscode/launch.json b/.vscode/launch.json index a32e49d..6012774 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,7 @@ "program": "${workspaceFolder}/resimulate.py", "console": "integratedTerminal", "args": [ + "--verbose", "record", "-o", "${input:promptFilename}", @@ -26,11 +27,10 @@ "program": "${workspaceFolder}/resimulate.py", "console": "integratedTerminal", "args": [ + "--verbose", "replay", "-i", "${input:pickFile}", - "--src-isd-r", - "${input:pickISDR}", "--target-isd-r", "${input:pickISDR}" ], diff --git a/pyproject.toml b/pyproject.toml index e1433de..31d8ff8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,4 +26,4 @@ ruff = "^0.9.4" pyright = "^1.1.393" [tool.poetry.dependencies] -pysim = {develop = true} +pysim = { develop = true } diff --git a/resimulate.py b/resimulate.py index ccb284e..b07e600 100755 --- a/resimulate.py +++ b/resimulate.py @@ -2,6 +2,7 @@ # PYTHON_ARGCOMPLETE_OK import argparse +import importlib.metadata import logging import argcomplete @@ -16,6 +17,20 @@ parser = argparse.ArgumentParser( description="ReSIMulate is a terminal application built for eSIM and SIM-specific APDU analysis. It captures APDU commands, saves them, and replays them to facilitate differential testing, ensuring accurate validation and debugging of SIM interactions.", formatter_class=RichHelpFormatter, ) +parser.add_argument( + "--version", + action="version", + version=f"%(prog)s {importlib.metadata.version('resimulate')}", +) +parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable verbose output during replay.", + required=False, +) + +global_arg_parser = argparse.ArgumentParser(add_help=False) subparsers = parser.add_subparsers( title="Commands", @@ -23,14 +38,11 @@ subparsers = parser.add_subparsers( required=True, help="Available commands: record, replay", ) -parser.add_argument( - "-v", "--verbose", action="store_true", help="Enable verbose output during replay." -) -parser.add_argument("--version", action="version", version="%(prog)s 0.1") # Record command record_parser = subparsers.add_parser( "record", + parents=[global_arg_parser], help="Record APDU commands from a specified source. Uses the SIMtrace2 GSMTAP to capture APDUs via UDP.", ) record_parser.add_argument( @@ -60,13 +72,15 @@ record_parser.add_argument( "-t", "--timeout", type=int, - default=10, + default=15, help="Timeout in seconds for recording (default: 10).", ) # Replay command replay_parser = subparsers.add_parser( - "replay", help="Replay saved APDU commands to a target device." + "replay", + parents=[global_arg_parser], + help="Replay saved APDU commands to a target device.", ) replay_parser.add_argument( "-i", diff --git a/resimulate/card.py b/resimulate/card.py index 261e758..44f6d4a 100644 --- a/resimulate/card.py +++ b/resimulate/card.py @@ -1,15 +1,14 @@ -from pySim.transport import LinkBase -from pySim.commands import SimCardCommands -from pySim.filesystem import CardModel, CardApplication from pySim.cards import SimCardBase, UiccCardBase -from pySim.runtime import RuntimeState +from pySim.commands import SimCardCommands +from pySim.euicc import CardApplicationISDR, EuiccInfo2 +from pySim.exceptions import SwMatchError +from pySim.filesystem import CardApplication, CardModel from pySim.profile import CardProfile +from pySim.runtime import RuntimeState +from pySim.transport import LinkBase from pySim.ts_102_221 import CardProfileUICC from pySim.utils import all_subclasses -from pySim.exceptions import SwMatchError -from pySim.euicc import CardApplicationISDR, EuiccInfo2 -from resimulate.models.euicc_info_2 import EuiccInfo2Data from resimulate.util.enums import ISDR_AID from resimulate.util.logger import log @@ -19,6 +18,7 @@ class Card: card: SimCardBase | None profile: CardProfile | None generic_card: bool = False + euicc_info_2: dict | None = None def __init__(self, sim_link: LinkBase): self.sim_link = sim_link @@ -75,10 +75,9 @@ class Card: euicc_info_2 = CardApplicationISDR.store_data_tlv( self.sim_card_commands, EuiccInfo2(), EuiccInfo2 ) - self.euicc_info_2 = EuiccInfo2Data.from_list( - euicc_info_2.to_dict()["euicc_info2"] - ) + self.euicc_info_2 = euicc_info_2.to_dict()["euicc_info2"] except SwMatchError: + log.warning("Card has ISD-R but not a SGP.22/SGP.32 eUICC.") # has ISD-R but not a SGP.22/SGP.32 eUICC - maybe SGP.02? pass finally: diff --git a/resimulate/commands/record.py b/resimulate/commands/record.py index 270f08e..b17146b 100644 --- a/resimulate/commands/record.py +++ b/resimulate/commands/record.py @@ -26,7 +26,7 @@ class Recorder: self.tracer_thread = Thread( target=self.tracer.main, args=(self.package_queue,), daemon=True ) - self.recording = Recording() + self.recording = Recording(src_isd_r=isd_r_aid) signal.signal(signal.SIGINT, self.__signal_handler) def __signal_handler(self, sig, frame): @@ -45,7 +45,11 @@ class Recorder: # Panel(capture_progress, title="APDU Packets captured", expand=False), overall_progress, Align.left( - Text.assemble("Press ", ("Ctrl+C", "bold red"), " to stop capturing."), + Text.assemble( + "Press ", + ("Ctrl+C", "bold green"), + " to stop capturing and save recorded commands.", + ), vertical="bottom", ), ) diff --git a/resimulate/commands/replay.py b/resimulate/commands/replay.py index 971faca..bf06419 100644 --- a/resimulate/commands/replay.py +++ b/resimulate/commands/replay.py @@ -2,6 +2,7 @@ from osmocom.utils import b2h, h2b, h2i, i2h from pySim.apdu import Apdu from pySim.transport.pcsc import PcscSimLink from pySim.ts_102_221 import CardProfileUICC +from pySim.utils import ResTuple from rich.align import Align from rich.console import Group from rich.live import Live @@ -19,20 +20,28 @@ class Replayer: self.device = device self.target_isd_r_aid = ISDR_AID.get_aid(target_isd_r) - def __get_remaining_bytes(self, link: PcscSimLink, bytes_to_receive: int, cla: int): + def __get_remaining_bytes( + self, link: PcscSimLink, bytes_to_receive: int, cla: int + ) -> ResTuple: log.debug("Retrieving remaining bytes: %d", bytes_to_receive) apdu = Apdu(i2h([cla]) + "C00000" + i2h([bytes_to_receive])) return self.__send_apdu(link, apdu) - def __resend_with_modified_le(self, link: PcscSimLink, apdu: Apdu, le: int): + def __resend_with_modified_le( + self, link: PcscSimLink, apdu: Apdu, le: int + ) -> ResTuple: log.debug("Resending APDU with modified Le: %d", le) modified_apdu = Apdu(b2h(apdu.cmd)[:-2] + i2h([le])) return self.__send_apdu(link, modified_apdu) - def __send_apdu(self, link: PcscSimLink, apdu: Apdu): + def __send_apdu(self, link: PcscSimLink, apdu: Apdu) -> ResTuple: if self.recording.src_isd_r_aid and self.target_isd_r_aid: - if b2h(apdu.cmd_data) == self.recording.src_isd_r_aid.value: - apdu.cmd_data = h2b(self.target_isd_r_aid.value) + cmd_data = b2h(apdu.cmd_data) + if self.recording.src_isd_r_aid.value in cmd_data: + cmd_data = cmd_data.replace( + self.recording.src_isd_r_aid.value, self.target_isd_r_aid.value + ) + apdu.cmd_data = h2b(cmd_data) log.debug( "Sending APDU(%s) where CLA(%s), INS(%s), P1(%s), P2(%s), Lc(%s), DATA(%s), P3/Le(%s)", @@ -89,20 +98,21 @@ class Replayer: ) return + successful_replays = 0 try: with pcsc_link as link: log.debug("Replaying APDUs...") for idx, apdu in enumerate(self.recording.apdus, start=1): - progress.update( - progress_id, - total=len(self.recording.apdus), - completed=idx, - description=f"Replaying APDU {idx} / {len(self.recording.apdus)}", - ) - data, resp = self.__send_apdu(link, apdu) if resp == b2h(apdu.sw): + progress.update( + progress_id, + total=len(self.recording.apdus), + completed=idx, + description=f"Replaying APDU {idx} / {len(self.recording.apdus)}", + ) + successful_replays += 1 continue log.debug( @@ -145,7 +155,15 @@ class Replayer: ) else: log.debug("Replay finished.") - progress.update( - progress_id, - description=":white_check_mark: [bold green]Replay finished.", - ) + + if not progress.finished: + progress.update( + progress_id, + description=f":police_car_light: [bold yellow]Failed to replay all APDUs ({successful_replays}/{len(self.recording.apdus)}).", + ) + + else: + progress.update( + progress_id, + description=":white_check_mark: [bold green]Replay finished.", + ) diff --git a/resimulate/models/euicc_info_2.py b/resimulate/models/euicc_info_2.py deleted file mode 100644 index f9c9215..0000000 --- a/resimulate/models/euicc_info_2.py +++ /dev/null @@ -1,39 +0,0 @@ -from dataclasses import dataclass - - -@dataclass -class EuiccInfo2Data: - profile_version: str - svn: str - euicc_firmware_version: str - ext_card_resource: str - uicc_capability: str - ts102241_version: str - global_platform_version: str - rsp_capability: str - euicc_ci_pki_list_for_verification: list[dict[str, str]] - euicc_ci_pki_list_for_signing: list[dict[str, str]] - pp_version: str - sas_accreditation_number: str - - @staticmethod - def from_list(obj: list[dict[str, str]]) -> "EuiccInfo2Data": - return EuiccInfo2Data( - profile_version=obj[0]["profile_version"], - svn=obj[1]["svn"], - euicc_firmware_version=obj[2]["euicc_firmware_ver"], - ext_card_resource=obj[3]["ext_card_resource"], - uicc_capability=obj[4]["uicc_capability"], - ts102241_version=obj[5]["ts102241_version"], - global_platform_version=obj[6]["global_platform_version"], - rsp_capability=obj[7]["rsp_capability"], - euicc_ci_pki_list_for_verification=obj[8][ - "euicc_ci_pki_list_for_verification" - ], - euicc_ci_pki_list_for_signing=obj[9]["euicc_ci_pki_list_for_signing"], - pp_version=obj[11]["pp_version"], - sas_accreditation_number=obj[12]["ss_acreditation_number"], - ) - - def __repr_rich__(self) -> str: - return f"EuiccInfo2(profile_version={self.profile_version!r}, svn={self.svn!r}, euicc_firmware_version={self.euicc_firmware_version!r}, ext_card_resource={self.ext_card_resource!r}, uicc_capability={self.uicc_capability!r}, ts102241_version={self.ts102241_version!r}, global_platform_version={self.global_platform_version!r}, rsp_capability={self.rsp_capability!r}, euicc_ci_pki_list_for_verification={self.euicc_ci_pki_list_for_verification!r}, euicc_ci_pki_list_for_signing={self.euicc_ci_pki_list_for_signing!r}, pp_version={self.pp_version!r}, sas_accreditation_number={self.sas_accreditation_number!r})" diff --git a/resimulate/recording.py b/resimulate/recording.py index 06d415f..ab4f24b 100644 --- a/resimulate/recording.py +++ b/resimulate/recording.py @@ -1,3 +1,4 @@ +import importlib.metadata import pickle from os.path import exists, isfile @@ -10,9 +11,10 @@ from resimulate.util.logger import log class Recording: apdus: list[Apdu] src_isd_r_aid: ISDR_AID + version = importlib.metadata.version("resimulate") - def __init__(self, src_isd_r: str = "default"): - self.src_isd_r_aid = ISDR_AID.get_aid(src_isd_r) + def __init__(self, src_isd_r: ISDR_AID): + self.src_isd_r_aid = src_isd_r self.apdus = [] @staticmethod @@ -27,6 +29,19 @@ class Recording: raise TypeError(f"File {file_path} does not contain a Recording object.") log.debug("Loaded %d APDUs from %s", len(recording.apdus), file_path) + if recording.src_isd_r_aid != ISDR_AID._DEFAULT: + log.debug( + "Recording used ISD-R AID: %s (%s)", + recording.src_isd_r_aid.value, + recording.src_isd_r_aid.name, + ) + + if recording.version != importlib.metadata.version("resimulate"): + log.warning( + "File %s was created with a different version of ReSIMulate. " + "Please ensure compatibility.", + file_path, + ) return recording diff --git a/resimulate/util/enums.py b/resimulate/util/enums.py index 486ce9e..c555cc9 100644 --- a/resimulate/util/enums.py +++ b/resimulate/util/enums.py @@ -1,12 +1,16 @@ import enum -from typing import Union class ISDR_AID(str, enum.Enum): - _DEFAULT = "A0000005591010FFFFFFFF8900000100" - _5BER = "A0000005591010FFFFFFFF8900050500" + _DEFAULT = "a0000005591010ffffffff8900000100" + _5BER = "a0000005591010ffffffff8900050500" @staticmethod - def get_aid(aid_description: str) -> Union["ISDR_AID", None]: + def get_aid(aid_description: str) -> "ISDR_AID": mapping = {"default": ISDR_AID._DEFAULT, "5ber": ISDR_AID._5BER} - return mapping.get(aid_description) + isdr_aid = mapping.get(aid_description) + + if not isdr_aid: + raise ValueError(f"ISD-R AID {aid_description} is not supported!") + + return isdr_aid