aboutsummaryrefslogtreecommitdiffstats
path: root/pySim/apdu_source
diff options
context:
space:
mode:
authorHarald Welte <laforge@osmocom.org>2022-07-16 14:06:46 +0200
committerHarald Welte <laforge@osmocom.org>2022-07-23 12:18:57 +0200
commit21caf32e3d145a0655d9252ba7c9be1eb6928000 (patch)
tree98858a088d2e8a46f8a46b5b8261c7806a3cba29 /pySim/apdu_source
parentcfa3015bcf1b55e44267c1086725501579aace46 (diff)
Introduce APDU/TPDU trace decoder
This introduces a new pySim.apdu module hierarchy, which contains classes that represent TPDU/APDUs as exchanged between SIM/UICC/USIM/ISIM card and UE. It contains instruction level decoders for SELECT, READ BINARY and friends, and then uses the pySim.filesystem.Runtime{Lchan,State} classes to keep track of the currently selected EF/DF/ADF for each logical channel, and uses the file-specific decoder classes of pySim to decode the actual file content that is being read or written. This provides a much more meaningful decode of protocol traces than wireshark will ever be able to give us. Furthermore, there's the new pySim.apdu_source set of classes which provides "input plugins" for obtaining APDU traces in a variety of formats. So far, GSMTAP UDP live capture and pyshark based RSPRO live and pcap file reading are imlpemented. Change-Id: I862d93163d495a294364168f7818641e47b18c0a Closes: OS#5126
Diffstat (limited to 'pySim/apdu_source')
-rw-r--r--pySim/apdu_source/__init__.py35
-rw-r--r--pySim/apdu_source/gsmtap.py57
-rw-r--r--pySim/apdu_source/pyshark_rspro.py159
3 files changed, 251 insertions, 0 deletions
diff --git a/pySim/apdu_source/__init__.py b/pySim/apdu_source/__init__.py
new file mode 100644
index 0000000..098d3ca
--- /dev/null
+++ b/pySim/apdu_source/__init__.py
@@ -0,0 +1,35 @@
+import abc
+import logging
+from typing import Union
+from pySim.apdu import Apdu, Tpdu, CardReset, TpduFilter
+
+PacketType = Union[Apdu, Tpdu, CardReset]
+
+logger = logging.getLogger(__name__)
+
+class ApduSource(abc.ABC):
+ def __init__(self):
+ self.apdu_filter = TpduFilter(None)
+
+ @abc.abstractmethod
+ def read_packet(self) -> PacketType:
+ """Read one packet from the source."""
+ pass
+
+ def read(self) -> Union[Apdu, CardReset]:
+ """Main function to call by the user: Blocking read, returns Apdu or CardReset."""
+ apdu = None
+ # loop until we actually have an APDU to return
+ while not apdu:
+ r = self.read_packet()
+ if not r:
+ continue
+ if isinstance(r, Tpdu):
+ apdu = self.apdu_filter.input_tpdu(r)
+ elif isinstance(r, Apdu):
+ apdu = r
+ elif isinstance(r, CardReset):
+ apdu = r
+ else:
+ ValueError('Unknown read_packet() return %s' % r)
+ return apdu
diff --git a/pySim/apdu_source/gsmtap.py b/pySim/apdu_source/gsmtap.py
new file mode 100644
index 0000000..fe450e2
--- /dev/null
+++ b/pySim/apdu_source/gsmtap.py
@@ -0,0 +1,57 @@
+# coding=utf-8
+
+# (C) 2022 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+from pySim.gsmtap import GsmtapMessage, GsmtapSource
+from . import ApduSource, PacketType, CardReset
+
+from pySim.apdu.ts_102_221 import ApduCommands as UiccApduCommands
+from pySim.apdu.ts_31_102 import ApduCommands as UsimApduCommands
+from pySim.apdu.global_platform import ApduCommands as GpApduCommands
+ApduCommands = UiccApduCommands + UsimApduCommands + GpApduCommands
+
+class GsmtapApduSource(ApduSource):
+ """ApduSource for handling GSMTAP-SIM messages received via UDP, such as
+ those generated by simtrace2-sniff. Note that *if* you use IP loopback
+ and localhost addresses (which is the default), you will need to start
+ this source before starting simtrace2-sniff, as otherwise the latter will
+ claim the GSMTAP UDP port.
+ """
+ def __init__(self, bind_ip:str='127.0.0.1', bind_port:int=4729):
+ """Create a UDP socket for receiving GSMTAP-SIM messages.
+ Args:
+ bind_ip: IP address to which the socket should be bound (default: 127.0.0.1)
+ bind_port: UDP port number to which the socket should be bound (default: 4729)
+ """
+ super().__init__()
+ self.gsmtap = GsmtapSource(bind_ip, bind_port)
+
+ def read_packet(self) -> PacketType:
+ gsmtap_msg, addr = self.gsmtap.read_packet()
+ if gsmtap_msg['type'] != 'sim':
+ raise ValueError('Unsupported GSMTAP type %s' % gsmtap_msg['type'])
+ sub_type = gsmtap_msg['sub_type']
+ if sub_type == 'apdu':
+ return ApduCommands.parse_cmd_bytes(gsmtap_msg['body'])
+ elif sub_type == 'atr':
+ # card has been reset
+ return CardReset()
+ elif sub_type in ['pps_req', 'pps_rsp']:
+ # simply ignore for now
+ pass
+ else:
+ raise ValueError('Unsupported GSMTAP-SIM sub-type %s' % sub_type)
diff --git a/pySim/apdu_source/pyshark_rspro.py b/pySim/apdu_source/pyshark_rspro.py
new file mode 100644
index 0000000..499e9ff
--- /dev/null
+++ b/pySim/apdu_source/pyshark_rspro.py
@@ -0,0 +1,159 @@
+# coding=utf-8
+
+# (C) 2022 by Harald Welte <laforge@osmocom.org>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+
+import sys
+import logging
+from pprint import pprint as pp
+from typing import Tuple
+import pyshark
+
+from pySim.utils import h2b, b2h
+from pySim.apdu import Tpdu
+from . import ApduSource, PacketType, CardReset
+
+logger = logging.getLogger(__name__)
+
+class _PysharkRspro(ApduSource):
+ """APDU Source [provider] base class for reading RSPRO (osmo-remsim) via tshark."""
+
+ def __init__(self, pyshark_inst):
+ self.pyshark = pyshark_inst
+ self.bank_id = None
+ self.bank_slot = None
+ self.cmd_tpdu = None
+ super().__init__()
+
+ @staticmethod
+ def get_bank_slot(bank_slot) -> Tuple[int, int]:
+ """Convert a 'bankSlot_element' field into a tuple of bank_id, slot_nr"""
+ bank_id = bank_slot.get_field('bankId')
+ slot_nr = bank_slot.get_field('slotNr')
+ return int(bank_id), int(slot_nr)
+
+ @staticmethod
+ def get_client_slot(client_slot) -> Tuple[int, int]:
+ """Convert a 'clientSlot_element' field into a tuple of client_id, slot_nr"""
+ client_id = client_slot.get_field('clientId')
+ slot_nr = client_slot.get_field('slotNr')
+ return int(client_id), int(slot_nr)
+
+ @staticmethod
+ def get_pstatus(pstatus) -> Tuple[int, int, int]:
+ """Convert a 'slotPhysStatus_element' field into a tuple of vcc, reset, clk"""
+ vccPresent = int(pstatus.get_field('vccPresent'))
+ resetActive = int(pstatus.get_field('resetActive'))
+ clkActive = int(pstatus.get_field('clkActive'))
+ return vccPresent, resetActive, clkActive
+
+ def read_packet(self) -> PacketType:
+ p = self.pyshark.next()
+ return self._parse_packet(p)
+
+ def _set_or_verify_bank_slot(self, bsl: Tuple[int, int]):
+ """Keep track of the bank:slot to make sure we don't mix traces of multiple cards"""
+ if not self.bank_id:
+ self.bank_id = bsl[0]
+ self.bank_slot = bsl[1]
+ else:
+ if self.bank_id != bsl[0] or self.bank_slot != bsl[1]:
+ raise ValueError('Received data for unexpected B(%u:%u)' % (bsl[0], bsl[1]))
+
+ def _parse_packet(self, p) -> PacketType:
+ rspro_layer = p['rspro']
+ #print("Layer: %s" % rspro_layer)
+ rspro_element = rspro_layer.get_field('RsproPDU_element')
+ #print("Element: %s" % rspro_element)
+ msg_type = rspro_element.get_field('msg')
+ rspro_msg = rspro_element.get_field('msg_tree')
+ if msg_type == '12': # tpduModemToCard
+ modem2card = rspro_msg.get_field('tpduModemToCard_element')
+ #print(modem2card)
+ client_slot = modem2card.get_field('fromClientSlot_element')
+ csl = self.get_client_slot(client_slot)
+ bank_slot = modem2card.get_field('toBankSlot_element')
+ bsl = self.get_bank_slot(bank_slot)
+ self._set_or_verify_bank_slot(bsl)
+ data = modem2card.get_field('data').replace(':','')
+ logger.debug("C(%u:%u) -> B(%u:%u): %s" % (csl[0], csl[1], bsl[0], bsl[1], data))
+ # store the CMD portion until the RSP portion arrives later
+ self.cmd_tpdu = h2b(data)
+ elif msg_type == '13': # tpduCardToModem
+ card2modem = rspro_msg.get_field('tpduCardToModem_element')
+ #print(card2modem)
+ client_slot = card2modem.get_field('toClientSlot_element')
+ csl = self.get_client_slot(client_slot)
+ bank_slot = card2modem.get_field('fromBankSlot_element')
+ bsl = self.get_bank_slot(bank_slot)
+ self._set_or_verify_bank_slot(bsl)
+ data = card2modem.get_field('data').replace(':','')
+ logger.debug("C(%u:%u) <- B(%u:%u): %s" % (csl[0], csl[1], bsl[0], bsl[1], data))
+ rsp_tpdu = h2b(data)
+ if self.cmd_tpdu:
+ # combine this R-TPDU with the C-TPDU we saw earlier
+ r = Tpdu(self.cmd_tpdu, rsp_tpdu)
+ self.cmd_tpdu = False
+ return r
+ elif msg_type == '14': # clientSlotStatus
+ cl_slotstatus = rspro_msg.get_field('clientSlotStatusInd_element')
+ #print(cl_slotstatus)
+ client_slot = cl_slotstatus.get_field('fromClientSlot_element')
+ bank_slot = cl_slotstatus.get_field('toBankSlot_element')
+ slot_pstatus = cl_slotstatus.get_field('slotPhysStatus_element')
+ vccPresent, resetActive, clkActive = self.get_pstatus(slot_pstatus)
+ if vccPresent and clkActive and not resetActive:
+ logger.debug("RESET")
+ return CardReset()
+ else:
+ print("Unhandled msg type %s: %s" % (msg_type, rspro_msg))
+
+
+class PysharkRsproPcap(_PysharkRspro):
+ """APDU Source [provider] class for reading RSPRO (osmo-remsim) from a PCAP
+ file via pyshark, which in turn uses tshark (part of wireshark).
+
+ In order to use this, you need a wireshark patched with RSPRO support,
+ such as can be found at https://gitea.osmocom.org/osmocom/wireshark/src/branch/laforge/rspro
+
+ A STANDARD UPSTREAM WIRESHARK *DOES NOT WORK*.
+ """
+ def __init__(self, pcap_filename):
+ """
+ Args:
+ pcap_filename: File name of the pcap file to be opened
+ """
+ pyshark_inst = pyshark.FileCapture(pcap_filename, display_filter='rspro', use_json=True, keep_packets=False)
+ super().__init__(pyshark_inst)
+
+class PysharkRsproLive(_PysharkRspro):
+ """APDU Source [provider] class for reading RSPRO (osmo-remsim) from a live capture
+ via pyshark, which in turn uses tshark (part of wireshark).
+
+ In order to use this, you need a wireshark patched with RSPRO support,
+ such as can be found at https://gitea.osmocom.org/osmocom/wireshark/src/branch/laforge/rspro
+
+ A STANDARD UPSTREAM WIRESHARK *DOES NOT WORK*.
+ """
+ def __init__(self, interface, bpf_filter='tcp port 9999 or tcp port 9998'):
+ """
+ Args:
+ interface: Network interface name to capture packets on (like "eth0")
+ bfp_filter: libpcap capture filter to use
+ """
+ pyshark_inst = pyshark.LiveCapture(interface=interface, display_filter='rspro', bpf_filter=bpf_filter,
+ use_json=True)
+ super().__init__(pyshark_inst)