diff --git a/pySim-trace.py b/pySim-trace.py index 33cb75a3..65722f2b 100755 --- a/pySim-trace.py +++ b/pySim-trace.py @@ -27,10 +27,11 @@ logger = colorlog.getLogger() # merge all of the command sets into one global set. This will override instructions, # the one from the 'last' set in the addition below will prevail. +from pySim.apdu.ts_51_011 import ApduCommands as SimApduCommands 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 +ApduCommands = SimApduCommands + UiccApduCommands + UsimApduCommands #+ GpApduCommands class DummySimLink(LinkBase): diff --git a/pySim/apdu/ts_51_011.py b/pySim/apdu/ts_51_011.py new file mode 100644 index 00000000..615767ea --- /dev/null +++ b/pySim/apdu/ts_51_011.py @@ -0,0 +1,337 @@ +# coding=utf-8 +"""APDU definitions/decoders of 3GPP TS 51.011, the classic SIM spec. + +(C) 2022 by Harald Welte + +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 . +""" + +import logging +from pySim.construct import * +from pySim.filesystem import * +from pySim.apdu import ApduCommand, ApduCommandSet +from typing import Optional, Dict, Tuple + +logger = logging.getLogger(__name__) + +# TS 51.011 Section 9.2.1 +class SimSelect(ApduCommand, n='SELECT', ins=0xA4, cla=['A0']): + _apdu_case = 4 + + def process_on_lchan(self, lchan: RuntimeLchan): + path = [self.cmd_data[i:i+2] for i in range(0, len(self.cmd_data), 2)] + for file in path: + file_hex = b2h(file) + sels = lchan.selected_file.get_selectables(['FIDS']) + if file_hex in sels: + if self.successful: + #print("\tSELECT %s" % sels[file_hex]) + lchan.selected_file = sels[file_hex] + else: + #print("\tSELECT %s FAILED" % sels[file_hex]) + pass + continue + logger.warning('SELECT UNKNOWN FID %s (%s)' % (file_hex, '/'.join([b2h(x) for x in path]))) + if len(self.cmd_data) != 2: + raise ValueError('Expecting a 2-byte FID') + + # decode the SELECT response + if self.successful: + self.file = lchan.selected_file + if 'body' in self.rsp_dict: + # not every SELECT is asking for the FCP in response... + return lchan.selected_file.decode_select_response(self.rsp_dict['body']) + return None + + +# TS 51.011 Section 9.2.2 +class SimStatus(ApduCommand, n='STATUS', ins=0xF2, cla=['A0']): + _apdu_case = 2 + + def process_on_lchan(self, lchan): + if self.successful: + if 'body' in self.rsp_dict: + return lchan.selected_file.decode_select_response(self.rsp_dict['body']) + +def _decode_binary_p1p2(p1, p2) -> Dict: + ret = {} + if p1 & 0x80: + ret['file'] = 'sfi' + ret['sfi'] = p1 & 0x1f + ret['offset'] = p2 + else: + ret['file'] = 'currently_selected_ef' + ret['offset'] = ((p1 & 0x7f) << 8) & p2 + return ret + +# TS 51.011 Section 9.2.3 / 31.101 +class ReadBinary(ApduCommand, n='READ BINARY', ins=0xB0, cla=['A0']): + _apdu_case = 2 + def _decode_p1p2(self): + return _decode_binary_p1p2(self.p1, self.p2) + + def process_on_lchan(self, lchan): + self._determine_file(lchan) + if not isinstance(self.file, TransparentEF): + return b2h(self.rsp_data) + # our decoders don't work for non-zero offsets / short reads + if self.cmd_dict['offset'] != 0 or self.lr < self.file.size[0]: + return b2h(self.rsp_data) + method = getattr(self.file, 'decode_bin', None) + if self.successful and callable(method): + return method(self.rsp_data) + +# TS 51.011 Section 9.2.4 / 31.101 +class UpdateBinary(ApduCommand, n='UPDATE BINARY', ins=0xD6, cla=['A0']): + _apdu_case = 3 + def _decode_p1p2(self): + return _decode_binary_p1p2(self.p1, self.p2) + + def process_on_lchan(self, lchan): + self._determine_file(lchan) + if not isinstance(self.file, TransparentEF): + return b2h(self.rsp_data) + # our decoders don't work for non-zero offsets / short writes + if self.cmd_dict['offset'] != 0 or self.lc < self.file.size[0]: + return b2h(self.cmd_data) + method = getattr(self.file, 'decode_bin', None) + if self.successful and callable(method): + return method(self.cmd_data) + +def _decode_record_p1p2(p1, p2): + ret = {} + ret['record_number'] = p1 + if p2 >> 3 == 0: + ret['file'] = 'currently_selected_ef' + else: + ret['file'] = 'sfi' + ret['sfi'] = p2 >> 3 + mode = p2 & 0x7 + if mode == 2: + ret['mode'] = 'next_record' + elif mode == 3: + ret['mode'] = 'previous_record' + elif mode == 8: + ret['mode'] = 'absolute_current' + return ret + +# TS 51.011 Section 9.2.5 +class ReadRecord(ApduCommand, n='READ RECORD', ins=0xB2, cla=['A0']): + _apdu_case = 2 + def _decode_p1p2(self): + r = _decode_record_p1p2(self.p1, self.p2) + self.col_id = '%02u' % r['record_number'] + return r + + def process_on_lchan(self, lchan): + self._determine_file(lchan) + if not isinstance(self.file, LinFixedEF): + return b2h(self.rsp_data) + method = getattr(self.file, 'decode_record_bin', None) + if self.successful and callable(method): + return method(self.rsp_data) + +# TS 51.011 Section 9.2.6 +class UpdateRecord(ApduCommand, n='UPDATE RECORD', ins=0xDC, cla=['A0']): + _apdu_case = 3 + def _decode_p1p2(self): + r = _decode_record_p1p2(self.p1, self.p2) + self.col_id = '%02u' % r['record_number'] + return r + + def process_on_lchan(self, lchan): + self._determine_file(lchan) + if not isinstance(self.file, LinFixedEF): + return b2h(self.cmd_data) + method = getattr(self.file, 'decode_record_bin', None) + if self.successful and callable(method): + return method(self.cmd_data) + +# TS 51.011 Section 9.2.7 +class Seek(ApduCommand, n='SEEK', ins=0xA2, cla=['A0']): + _apdu_case = 4 + _construct_rsp = GreedyRange(Int8ub) + + def _decode_p1p2(self): + ret = {} + sfi = self.p2 >> 3 + if sfi == 0: + ret['file'] = 'currently_selected_ef' + else: + ret['file'] = 'sfi' + ret['sfi'] = sfi + mode = self.p2 & 0x7 + if mode in [0x4, 0x5]: + if mode == 0x4: + ret['mode'] = 'forward_search' + else: + ret['mode'] = 'backward_search' + ret['record_number'] = self.p1 + self.col_id = '%02u' % ret['record_number'] + elif mode == 6: + ret['mode'] = 'enhanced_search' + # TODO: further decode + elif mode == 7: + ret['mode'] = 'proprietary_search' + return ret + + def _decode_cmd(self): + ret = self._decode_p1p2() + if self.cmd_data: + if ret['mode'] == 'enhanced_search': + ret['search_indication'] = b2h(self.cmd_data[:2]) + ret['search_string'] = b2h(self.cmd_data[2:]) + else: + ret['search_string'] = b2h(self.cmd_data) + return ret + + def process_on_lchan(self, lchan): + self._determine_file(lchan) + return self.to_dict() + +# TS 51.011 Section 9.2.8 +class Increase(ApduCommand, n='INCREASE', ins=0x32, cla=['A0']): + _apdu_case = 4 + +PinConstructP2 = BitStruct('scope'/Enum(Flag, global_mf=0, specific_df_adf=1), + BitsInteger(2), 'reference_data_nr'/BitsInteger(5)) + +# TS 51.011 Section 9.2.9 +class VerifyChv(ApduCommand, n='VERIFY CHV', ins=0x20, cla=['A0']): + _apdu_case = 3 + _construct_p2 = PinConstructP2 + + @staticmethod + def _pin_process(apdu): + processed = { + 'scope': apdu.cmd_dict['p2']['scope'], + 'referenced_data_nr': apdu.cmd_dict['p2']['reference_data_nr'], + } + if apdu.lc == 0: + # this is just a question on the counters remaining + processed['mode'] = 'check_remaining_attempts' + else: + processed['pin'] = b2h(apdu.cmd_data) + if apdu.sw[0] == 0x63: + processed['remaining_attempts'] = apdu.sw[1] & 0xf + return processed + + @staticmethod + def _pin_is_success(sw): + if sw[0] == 0x63: + return True + else: + return False + + def process_on_lchan(self, lchan: RuntimeLchan): + return VerifyPin._pin_process(self) + + def _is_success(self): + return VerifyPin._pin_is_success(self.sw) + + +# TS 51.011 Section 9.2.10 +class ChangeChv(ApduCommand, n='CHANGE CHV', ins=0x24, cla=['A0']): + _apdu_case = 3 + _construct_p2 = PinConstructP2 + + def process_on_lchan(self, lchan: RuntimeLchan): + return VerifyPin._pin_process(self) + + def _is_success(self): + return VerifyPin._pin_is_success(self.sw) + + +# TS 51.011 Section 9.2.11 +class DisableChv(ApduCommand, n='DISABLE CHV', ins=0x26, cla=['A0']): + _apdu_case = 3 + _construct_p2 = PinConstructP2 + + def process_on_lchan(self, lchan: RuntimeLchan): + return VerifyPin._pin_process(self) + + def _is_success(self): + return VerifyPin._pin_is_success(self.sw) + + +# TS 51.011 Section 9.2.12 +class EnableChv(ApduCommand, n='ENABLE CHV', ins=0x28, cla=['A0']): + _apdu_case = 3 + _construct_p2 = PinConstructP2 + def process_on_lchan(self, lchan: RuntimeLchan): + return VerifyPin._pin_process(self) + + def _is_success(self): + return VerifyPin._pin_is_success(self.sw) + + +# TS 51.011 Section 9.2.13 +class UnblockChv(ApduCommand, n='UNBLOCK CHV', ins=0x2C, cla=['A0']): + _apdu_case = 3 + _construct_p2 = PinConstructP2 + + def process_on_lchan(self, lchan: RuntimeLchan): + return VerifyPin._pin_process(self) + + def _is_success(self): + return VerifyPin._pin_is_success(self.sw) + + +# TS 51.011 Section 9.2.14 +class Invalidate(ApduCommand, n='INVALIDATE', ins=0x04, cla=['A0']): + _apdu_case = 1 + _construct_p1 = BitStruct(BitsInteger(4), + 'select_mode'/Enum(BitsInteger(4), ef_by_file_id=0, + path_from_mf=8, path_from_current_df=9)) + +# TS 51.011 Section 9.2.15 +class Rehabilitate(ApduCommand, n='REHABILITATE', ins=0x44, cla=['A0']): + _apdu_case = 1 + _construct_p1 = Invalidate._construct_p1 + +# TS 51.011 Section 9.2.16 +class RunGsmAlgorithm(ApduCommand, n='RUN GSM ALGORITHM', ins=0x88, cla=['A0']): + _apdu_case = 4 + _construct = Struct('rand'/HexAdapter(Bytes(16))) + _construct_rsp = Struct('sres'/HexAdapter(Bytes(4)), 'kc'/HexAdapter(Bytes(8))) + +# TS 51.011 Section 9.2.17 +class Sleep(ApduCommand, n='SLEEP', ins=0xFA, cla=['A0']): + _apdu_case = 2 + +# TS 51.011 Section 9.2.18 +class GetResponse(ApduCommand, n='GET RESPONSE', ins=0xC0, cla=['A0']): + _apdu_case = 2 + +# TS 51.011 Section 9.2.19 +class TerminalProfile(ApduCommand, n='TERMINAL PROFILE', ins=0x10, cla=['A0']): + _apdu_case = 3 + +# TS 51.011 Section 9.2.20 +class Envelope(ApduCommand, n='ENVELOPE', ins=0xC2, cla=['A0']): + _apdu_case = 4 + +# TS 51.011 Section 9.2.21 +class Fetch(ApduCommand, n='FETCH', ins=0x12, cla=['A0']): + _apdu_case = 2 + +# TS 51.011 Section 9.2.22 +class TerminalResponse(ApduCommand, n='TERMINAL RESPONSE', ins=0x14, cla=['A0']): + _apdu_case = 3 + + +ApduCommands = ApduCommandSet('TS 51.011', cmds=[SimSelect, SimStatus, ReadBinary, UpdateBinary, ReadRecord, + UpdateRecord, Seek, Increase, VerifyChv, ChangeChv, DisableChv, + EnableChv, UnblockChv, Invalidate, Rehabilitate, RunGsmAlgorithm, + Sleep, GetResponse, TerminalProfile, Envelope, Fetch, TerminalResponse])