feat: basic replay functionality

This commit is contained in:
2025-01-16 04:11:20 +01:00
parent cd03668490
commit 299f6ef3aa
12 changed files with 296 additions and 66 deletions

17
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,17 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "reSIMulate: record",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/resimulate/resimulate.py",
"console": "integratedTerminal",
"args": ["record", "-o test.pkl"],
"justMyCode": false
}
]
}

68
poetry.lock generated
View File

@@ -558,14 +558,14 @@ windows-terminal = ["colorama (>=0.4.6)"]
[[package]]
name = "pyosmocom"
version = "0.0.7"
version = "0.0.8"
description = "Python implementation of core osmocom utilities / protocols"
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pyosmocom-0.0.7-py3-none-any.whl", hash = "sha256:6abcd35b7ecca8e8c5edd1d3f5a508c8692a2084a27aca488a4ff487fad7e317"},
{file = "pyosmocom-0.0.7.tar.gz", hash = "sha256:d6fcab6234fb5007af5dd4bc676ff29397dc40a53756997e64493e3e335502b4"},
{file = "pyosmocom-0.0.8-py3-none-any.whl", hash = "sha256:eaa09524bb031b0408ce69dba4ce0f60525c016b8033e92aaa5475483ad5a472"},
{file = "pyosmocom-0.0.8.tar.gz", hash = "sha256:d008d64d6423dd79980b1e8df4ab751eb68acb48a3e18c976e293d71b50d36f0"},
]
[package.dependencies]
@@ -601,29 +601,30 @@ dev = ["build", "flake8", "mypy", "pytest", "twine"]
[[package]]
name = "pyscard"
version = "2.2.0"
version = "2.2.1"
description = "Smartcard module for Python."
optional = false
python-versions = ">=3.9"
groups = ["main"]
files = [
{file = "pyscard-2.2.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05cfa9840b3f4b08769487e4d84b1432d9d913035a3726856329c2648444a6ba"},
{file = "pyscard-2.2.0-cp310-cp310-win32.whl", hash = "sha256:c363a36a803cd3e6334546d661c0b1375c16ab42600bc4be18ade3ed70eae1a6"},
{file = "pyscard-2.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:001e760f42d2f9b7b6aba6b83fca67314e925d6df1ded75689c5ef5377f6e701"},
{file = "pyscard-2.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:51758a04e70d07b233a5d7ed58492007bbbebcb3f47130a638edbe021b82df86"},
{file = "pyscard-2.2.0-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a138707d8ef5c53da4e7a86d1e604e2cba42fe92249099a67374624503825326"},
{file = "pyscard-2.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:45ec2a19cbc40dc56c9831f9979c81617a3a89265b1430708e1c137a82ec5c83"},
{file = "pyscard-2.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9a3d47a6799efbc8e6124638730a86b705ddecfba92874aec6b79348e764fe8a"},
{file = "pyscard-2.2.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:fe8625b7cb1be2af0fe5d90d6daabb26a65aeb7d601455446c6a06341a2f0814"},
{file = "pyscard-2.2.0-cp312-cp312-win32.whl", hash = "sha256:b0b476691652bb641b175d7d345bb27639615c6a5bf140e63a057750aefc1bd9"},
{file = "pyscard-2.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:9ce06b7007147f7346114274dd20edd0399e9aedbdadcc9cf4d0c867bec1cae1"},
{file = "pyscard-2.2.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:5296c705d2f5b6947f6929a63d55521a380125c0160679b8069a11107c8b94ec"},
{file = "pyscard-2.2.0-cp313-cp313-win32.whl", hash = "sha256:6cdb99c1d51f625fe246df57b65fad6d834a094fa5efd36d0c40356916c19431"},
{file = "pyscard-2.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:ff6f2b8ef00b48fa5f1448f0c7d812f677ce98a9994e917500ecf00e3f31dc89"},
{file = "pyscard-2.2.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0f23309f4f98ae41c836090c3449d8fb7470c19d57adda2592803e1df668bece"},
{file = "pyscard-2.2.0-cp39-cp39-win32.whl", hash = "sha256:5857d60b44bbb074c9b174111fe94af4179b882942e53292de16f0c798f50c5d"},
{file = "pyscard-2.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:e4e664b1d3b87956104b87094f81a98bdd6ac40986b52d0c40451de1fc98ca49"},
{file = "pyscard-2.2.0.tar.gz", hash = "sha256:6aa194d4bb295e78a97056dd1d32273cc69ddbe3c852aad60a8578f04017a1bf"},
{file = "pyscard-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e0e85b8a0a316690be7d5b2e85302651f0443db271a8ca77cb09f8a6fdd6ebfa"},
{file = "pyscard-2.2.1-cp310-cp310-win32.whl", hash = "sha256:fcac209666236bd08d876834f8233046713139490b46a37baa19e4eee959d003"},
{file = "pyscard-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:4a638bc33d6c6b4a95ac4e7b5eb4426b3e1d0c5b74e018a650294473456b43c3"},
{file = "pyscard-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:21b458aef2baaf97a3dc719b7d6d94d9333d5cff703536688ee9881993607328"},
{file = "pyscard-2.2.1-cp311-cp311-win32.whl", hash = "sha256:4bec90a07e25220a6eeaa2a6d6e45c60ce40b5e0f0c7059f03f16357c01e66a8"},
{file = "pyscard-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:aa886dda9d6dfc9acb58902dca1db6da7cf22a6dd284ded08ad6bf1ac7b207fc"},
{file = "pyscard-2.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc2157faa3e75bfb81f393215c2fb980757a5f466015acb6cd221769ba5fc52d"},
{file = "pyscard-2.2.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:8a682ecb2ac56d88f265f2bc0afe032f8ffc2ec544f384d7a0cdea2d08cae35c"},
{file = "pyscard-2.2.1-cp312-cp312-win32.whl", hash = "sha256:0a96499b4e910ad4a8244d5406dc6afb4c91812e1124b992832b48e9e4d93b7e"},
{file = "pyscard-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7acb878270ee58379474646fa6be17dd101b8d870eeadb004f3c2cad736b0b09"},
{file = "pyscard-2.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:803b52f04389456a011ae8b3c02bcdc232e69b32c137b8b5c8695430bb2006af"},
{file = "pyscard-2.2.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:dc926ba396c1fa6e4db1007b9e62b2f330c79c943c3bbbb893908f589071fc86"},
{file = "pyscard-2.2.1-cp313-cp313-win32.whl", hash = "sha256:2aca599294cd5178d359627b9b58a8e636ea3ebd73b70da79a95c684be985c83"},
{file = "pyscard-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:9c33a6cd75479f3f2e1f812399d228e371f030d6f0630b69c34a02519bbf6dca"},
{file = "pyscard-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:affafc324ed4a8e735c392e7576f8b55c26a7f2c4f529e3ccb2adf88531d85a4"},
{file = "pyscard-2.2.1-cp39-cp39-win32.whl", hash = "sha256:b209b6f4c996f9c4a7e04fb3e3e8210f4175986b2bc36ef5d735667d53071381"},
{file = "pyscard-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:4b7615ff1c6b620136ce554e528affe93bdd9f705601ac23453c2c5d56bf979b"},
{file = "pyscard-2.2.1.tar.gz", hash = "sha256:920e688a5108224cb19b915c3fd7ea7cf3d1aa379587ffd087973e84c13f8d94"},
]
[package.extras]
@@ -670,7 +671,7 @@ optional = false
python-versions = "*"
groups = ["main"]
files = []
develop = false
develop = true
[package.dependencies]
bidict = "*"
@@ -689,10 +690,8 @@ pyyaml = ">=5.1"
termcolor = "*"
[package.source]
type = "git"
url = "https://github.com/osmocom/pySim.git"
reference = "HEAD"
resolved_reference = "712946eddb9eedce30b44276d7bd75f40c9a69b9"
type = "directory"
url = "../pysim"
[[package]]
name = "pytlv"
@@ -787,6 +786,21 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "rich-argparse"
version = "1.6.0"
description = "Rich help formatters for argparse and optparse"
optional = false
python-versions = ">=3.8"
groups = ["main"]
files = [
{file = "rich_argparse-1.6.0-py3-none-any.whl", hash = "sha256:fbe70a1d821b3f2fa8958cddf0cae131870a6e9faa04ab52b409cb1eda809bd7"},
{file = "rich_argparse-1.6.0.tar.gz", hash = "sha256:092083c30da186f25bcdff8b1d47fdfb571288510fb051e0488a72cc3128de13"},
]
[package.dependencies]
rich = ">=11.0.0"
[[package]]
name = "six"
version = "1.17.0"
@@ -845,4 +859,4 @@ files = [
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
content-hash = "d787a3424fd3e8689e3e2b99f4e6225e7f78400d0fe0eb11117972c96d9ce99d"
content-hash = "0f33c17a00302b58f79d1748001e128c11b8f27149fd66a329fd2c8c1990cf3d"

View File

@@ -9,9 +9,10 @@ readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"rich (>=13.9.4,<14.0.0)",
"pysim @ git+https://github.com/osmocom/pySim.git",
"pyshark (>=0.6,<0.7)",
"argcomplete (>=3.5.3,<4.0.0)",
"rich-argparse (>=1.6.0,<2.0.0)",
"pysim @ file:///home/niklas/Documents/documents/uni/master_thesis/pysim",
]
@@ -21,3 +22,6 @@ build-backend = "poetry.core.masonry.api"
[tool.poetry.group.dev.dependencies]
black = "^24.10.0"
[tool.poetry.dependencies]
pysim = {develop = true}

0
resimulate/__init__.py Normal file
View File

View File

@@ -1,24 +1,18 @@
import pickle
from queue import Empty, Queue
import signal
from queue import Empty, Queue, ShutDown
from threading import Thread
import time
from pySim.apdu import Apdu, ApduCommand
from pySim.apdu_source.gsmtap import ApduSource
from rich.align import Align
from rich.console import Group
from rich.live import Live
from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn
from rich.text import Text
from util.logger import log
from util.tracer import Tracer
from rich.console import Group
from rich.panel import Panel
from rich.progress import (
BarColumn,
Progress,
SpinnerColumn,
TextColumn,
TimeElapsedColumn,
)
class Recorder:
def __init__(self, source: ApduSource):
@@ -30,6 +24,10 @@ class Recorder:
self.tracer_thread = Thread(
target=self.tracer.main, args=(self.package_queue,), daemon=True
)
signal.signal(signal.SIGINT, self.__signal_handler)
def __signal_handler(self, sig, frame):
self.package_queue.shutdown(immediate=True)
def record(self, output_path: str, timeout: int):
capture_progress = Progress(
@@ -47,8 +45,12 @@ class Recorder:
)
main_group = Group(
Panel(capture_progress),
# Panel(capture_progress, title="APDU Packets captured", expand=False),
overall_progress,
Align.left(
Text.assemble("Press ", ("Ctrl+C", "bold red"), " to stop capturing."),
vertical="bottom",
),
)
overall_task_id = overall_progress.add_task(
@@ -57,7 +59,7 @@ class Recorder:
total=None,
)
with Live(main_group):
with Live(main_group) as live:
self.tracer_thread.start()
while self.tracer_thread.is_alive():
@@ -69,23 +71,28 @@ class Recorder:
log.debug("No more APDU packets to capture.")
break
capture_task_id = capture_progress.add_task(
log.info("Captured %s %s", apdu_command._name, apdu)
""" capture_task_id = capture_progress.add_task(
"",
completed=len(self.captured_apdus),
packet_type=str(apdu_command._name) or "",
packet_description=str(apdu_command.path_str),
packet_code=str(apdu_command.col_sw),
)
) """
self.captured_apdus.append(apdu)
capture_progress.stop_task(capture_task_id)
""" capture_progress.stop_task(capture_task_id) """
except TimeoutError:
log.debug("Timeout reached, stopping capture.")
break
except Empty:
log.debug("No more APDU packets to capture.")
break
except ShutDown:
log.debug("Shutting down capture.")
break
except UnboundLocalError as e:
log.debug("Error capturing APDU packets: %s", e)
break
@@ -95,8 +102,6 @@ class Recorder:
description=f"[bold green]{len(self.captured_apdus)} packet(s) captured!",
)
capture_progress.stop_task(capture_task_id)
overall_progress.update(
overall_task_id,
description="[bold yellow]Saving captured APDU commands...",
@@ -106,7 +111,6 @@ class Recorder:
with open(output_path, "wb") as f:
pickle.dump(self.captured_apdus, f)
time.sleep(3)
overall_progress.update(
overall_task_id,

View File

@@ -1 +1,53 @@
# https://github.com/Textualize/rich/blob/master/examples/dynamic_progress.py
import pickle
from osmocom.utils import b2h
from pySim.apdu import Apdu
from pySim.app import init_card
from pySim.card_handler import CardHandler
from rich.align import Align
from rich.console import Group
from rich.live import Live
from rich.progress import BarColumn, Progress, TextColumn, TimeElapsedColumn
from rich.text import Text
from util.logger import log
from util.pcsc_link import PcscLink
class Replayer:
def __init__(self, device: int):
self.pcsc_link = PcscLink(device=device)
self.card_handler = CardHandler(self.pcsc_link)
self.runtime_state, self.card = init_card(self.card_handler)
def replay(self, input_path: str):
progress = Progress(
TimeElapsedColumn(),
BarColumn(),
TextColumn("{task.description}"),
)
main_group = Group(
progress,
Align.left(
Text.assemble(
"Press ", ("Ctrl+C", "bold red"), " to stop replaying APDUs."
),
vertical="bottom",
),
)
progress_id = progress.add_task(
f"[bold red]Loading APDUs from {input_path}...", start=True, total=None
)
with Live(main_group):
with open(input_path, "rb") as f:
apdus: list[Apdu] = pickle.load(f)
with self.pcsc_link as link:
for id, apdu in enumerate(apdus):
progress.update(
progress_id, description=f"Replaying APDU {id} / {len(apdus)}"
)
data, sw = link.send_tpdu(b2h(apdu))
log.debug("APDU: %s, SW: %s", b2h(apdu), sw)

2
resimulate/exceptions.py Normal file
View File

@@ -0,0 +1,2 @@
class PcscError(Exception):
pass

View File

@@ -4,13 +4,14 @@
import argparse
import argcomplete
from pySim.apdu_source.gsmtap import GsmtapApduSource
from commands.record import Recorder
from commands.replay import Replayer
from pySim.apdu_source.gsmtap import GsmtapApduSource
from rich_argparse import RichHelpFormatter
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=argparse.ArgumentDefaultsHelpFormatter,
formatter_class=RichHelpFormatter,
)
subparsers = parser.add_subparsers(
@@ -65,11 +66,11 @@ replay_parser.add_argument(
help="File containing APDU commands to replay (e.g., 'commands.apdu').",
)
replay_parser.add_argument(
"-d",
"--device",
type=str,
default="default_device",
help="Target simtrace device to send APDU commands (default: 'default_device').",
"-p",
"--pcsc-device",
type=int,
default=0,
help="Target simtrace device to send APDU commands (default: 0).",
)
if __name__ == "__main__":
@@ -83,7 +84,8 @@ if __name__ == "__main__":
recorder.record(args.output, args.timeout)
elif args.command == "replay":
pass
replayer = Replayer(args.pcsc_device)
replayer.replay(args.input.name)
else:
raise ValueError(f"Unsupported command: {args.command}")

View File

@@ -0,0 +1,26 @@
from rich.highlighter import RegexHighlighter
class ApduHighlighter(RegexHighlighter):
base_style = "apdu."
highlights = [
# Class name (e.g., Apdu)
r"(?P<class_name>\w+)\(",
# CLA (2 hex digits)
r"(?P<cla>[0-9A-F]{2})\s",
# INS (2 hex digits)
r"(?P<ins>[0-9A-F]{2})\s",
# P1 (2 hex digits)
r"(?P<p1>[0-9A-F]{2})\s",
# P2 (2 hex digits)
r"(?P<p2>[0-9A-F]{2})\s",
# P3 (2 hex digits)
r"(?P<p3>[0-9A-F]{2})\s",
# Command Data (hex bytes separated by spaces)
r"(?P<cmd_data>(?:[0-9A-F]{2}\s?)+)",
# Response Data (hex bytes separated by spaces)
r"(?P<rsp_data>(?:[0-9A-F]{2}\s?)+)",
# SW (4 hex digits)
r"(?P<sw>[0-9A-F]{4})\)",
]

View File

@@ -1,6 +1,8 @@
import logging
from rich.logging import RichHandler
from rich.console import Console
from rich.logging import RichHandler
from util.apdu_highlighter import ApduHighlighter
class RichLogger:
@@ -22,7 +24,9 @@ class RichLogger:
self.logger = logging.getLogger("rich")
def get_logger(self, console: Console = None) -> logging.Logger:
if console:
if not console:
console = Console(highlighter=ApduHighlighter())
self._initialize(console)
return self.logger

View File

@@ -0,0 +1,98 @@
from exceptions import PcscError
from osmocom.utils import Hexstr, h2i, i2h
from pySim.transport import LinkBaseTpdu
from pySim.utils import ResTuple
from smartcard import System
from smartcard.CardConnection import CardConnection
from smartcard.CardRequest import CardRequest
from smartcard.Exceptions import (
CardConnectionException,
CardRequestTimeoutException,
NoCardException,
)
from smartcard.ExclusiveConnectCardConnection import ExclusiveConnectCardConnection
from util.logger import log
class PcscLink(LinkBaseTpdu):
protocol = CardConnection.T0_protocol
def __init__(self, device: int, **kwargs):
super().__init__(**kwargs)
readers = System.readers()
if device > len(readers):
raise PcscError(f"Device with index {device} not found.")
self.pcsc_device = readers[device]
self.card_connection = ExclusiveConnectCardConnection(
self.pcsc_device.createConnection()
)
def __str__(self) -> str:
return "PCSC[%s]" % (self._reader)
def __del__(self):
try:
self.card_connection.disconnect()
except:
pass
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_value, traceback):
self.card_connection.disconnect()
log.debug("Disconnected from device %s", self.pcsc_device)
def connect(self):
try:
self.card_connection.disconnect()
self.card_connection.connect()
supported_protocols = self.card_connection.getSupportedProtocols()
self.card_connection.disconnect()
if supported_protocols & CardConnection.T0_protocol:
protocol = CardConnection.T0_protocol
elif supported_protocols & CardConnection.T1_protocol:
protocol = CardConnection.T1_protocol
else:
raise PcscError("No supported protocol found.")
log.debug(
"Connecting to device %s using protocol %s", self.pcsc_device, protocol
)
self.card_connection.connect(protocol=protocol)
except (CardConnectionException, NoCardException) as e:
raise PcscError from e
log.debug("Connected to device %s", self.pcsc_device)
def disconnect(self):
self.card_connection.disconnect()
log.debug("Disconnected from device %s", self.pcsc_device)
def _reset_card(self):
self.disconnect()
self.connect()
def wait_for_card(self, timeout: int | None = None, newcardonly: bool = False):
card_request = CardRequest(
readers=[self.pcsc_device], timeout=timeout, newcardonly=newcardonly
)
try:
log.debug("Waiting for card on device %s", self.pcsc_device)
card_request.waitforcard()
except CardRequestTimeoutException as e:
raise PcscError from e
self.__connect()
def send_tpdu(self, tpdu: Hexstr) -> ResTuple:
try:
data, sw1, sw2 = self.card_connection.transmit(h2i(tpdu))
return i2h(data) + i2h([sw1, sw2])
except CardConnectionException as e:
raise PcscError from e

View File

@@ -1,22 +1,27 @@
from queue import Queue
from pySim.apdu import ApduDecoder, CardReset
from pySim.apdu.global_platform import ApduCommands as GlobalPlatformCommands
from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
from pySim.apdu.ts_102_221 import UiccSelect, UiccStatus
from pySim.apdu.ts_102_222 import ApduCommands as ManageApduCommands
from pySim.apdu_source import ApduSource
from pySim.ara_m import CardApplicationARAM
from pySim.cards import UiccCardBase
from pySim.commands import SimCardCommands
from pySim.euicc import CardApplicationECASD, CardApplicationISDR
from pySim.global_platform import CardApplicationISD
from pySim.runtime import RuntimeState
from pySim.ts_31_102 import CardApplicationUSIM
from pySim.ts_31_103 import CardApplicationISIM
from pySim.ts_102_221 import CardProfileUICC
from util.dummy_sim_link import DummySimLink
from util.logger import log
APDU_COMMANDS = UiccApduCommands + UsimApduCommands
APDU_COMMANDS = (
UiccApduCommands + UsimApduCommands + ManageApduCommands + GlobalPlatformCommands
)
# Taken from the pySim project and modified for the ReSIMulate project
@@ -29,6 +34,8 @@ class Tracer:
profile.add_application(CardApplicationISIM())
profile.add_application(CardApplicationISDR())
profile.add_application(CardApplicationECASD())
profile.add_application(CardApplicationARAM())
profile.add_application(CardApplicationISD())
scc = SimCardCommands(transport=DummySimLink())
card = UiccCardBase(scc)
@@ -53,7 +60,7 @@ class Tracer:
package_queue.task_done()
return
except Exception as e:
log.error("Error reading APDU: %s", e)
log.error("Error reading APDU (%s): %s", apdu, e)
continue
if apdu is None:
@@ -71,10 +78,10 @@ class Tracer:
try:
apdu_command.process(self.runtime_state)
except ValueError as e:
log.error("Error processing APDU: %s", e)
log.error("Error reading APDU (%s): %s", apdu, e)
continue
except AttributeError as e:
log.error("Error processing APDU: %s", e)
log.error("Error processing APDU (%s): %s", apdu, e)
return
# Avoid cluttering the log with too much verbosity