diff options
author | Harald Welte <laforge@osmocom.org> | 2022-07-16 14:06:46 +0200 |
---|---|---|
committer | Harald Welte <laforge@osmocom.org> | 2022-07-23 12:18:57 +0200 |
commit | 21caf32e3d145a0655d9252ba7c9be1eb6928000 (patch) | |
tree | 98858a088d2e8a46f8a46b5b8261c7806a3cba29 /pySim/apdu_source | |
parent | cfa3015bcf1b55e44267c1086725501579aace46 (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__.py | 35 | ||||
-rw-r--r-- | pySim/apdu_source/gsmtap.py | 57 | ||||
-rw-r--r-- | pySim/apdu_source/pyshark_rspro.py | 159 |
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) |