fix: recover the SMS-SUBMIT listings in RAM Explore (v3.6.4)

Explore returned partial data (no ELF/module entries, the installed package
missing) and then failed with cntr_low.  Two causes, both in the
actual-response SMS-SUBMIT path the card uses for big listings:

- `_find_sms_tpdu` read TLV lengths as a single byte; the FETCH carries
  `8B 81 97 ...` (BER long form) for the big pages, so it returned a
  corrupted, truncated TPDU.
- `_calc_ud_offset` treated the SMS-SUBMIT relative validity period (VPF=10)
  as 7 bytes instead of 1, shifting the UD offset: `_parse_sms_concat` then
  read a bogus UDH and reported no concatenation, so the segments never
  assembled and the PoR/data was dropped ("RAM RESPONSE-PACKET: empty").
  The step was marked failed, the counter did not advance, the same counter
  was retried and the card answered `cntr_low`, which also blocked P1=10
  (modules) where the installed package appears.

- the captured segments now always store the assembled UD (`submit_ud_hex`);
  `_sms_submit_por()` rebuilds the DELIVER-style packet (`02 71 00` + UD) for
  `_decode_por`.  Verified against the live capture: por_ok, remote SW 6310,
  438 hex chars of listing data (the exact bytes of the P1=20 page).
- `spPorAccepted` counts `actual_response_sms_submit` (0x0B) as accepted, so
  the counter advances when the data follows via SMS-SUBMIT.

Explore queries follow GP Card Spec v2.3.1 11.4.2.2: compact listings
(P2.b2=0) with the chained GET RESPONSE (`ramGetStatusApdu`, paging P2=00 ->
P2=01), the malformed P2=02 attempt is gone, and the ISD-only query (P1=80)
never pages with next-occurrence (the card shall reject it).

Tests: Python SmsSubmitCaptureTest with the live FETCH bytes (TPDU length,
UD offset, concat reassembly, 027100+UD decode); ram.test.js checks the APDU
builder and the missing P2=02 attempt; cards_counter covers 0x0B.

608 frontend / 492 Python green; version 3.6.4; sw simple-v277.
This commit is contained in:
2026-09-28 00:09:34 +03:00
parent 39c82f26f3
commit 9e85f522f6
8 changed files with 224 additions and 72 deletions
+68 -11
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.3'
VERSION = '3.6.4'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -883,9 +883,12 @@ def _send_envelope(tpdu_hex, scc, sm_sc='12345678912', submit_handler=None,
if len(matching) >= total:
sorted_segs = sorted(matching, key=lambda s: s[2])
assembled = b''.join(bytes.fromhex(s[3]) for s in sorted_segs)
submit_handler.submit_tpdu_hex = assembled.hex()
sys.stderr.write('SMS concat: assembled %d segments (ref=%s)\n' % (total, ref))
submit_handler.submit_ud_hex = assembled.hex()
submit_handler.submit_tpdu_hex = submit_handler.submit_tpdu_hex or tpdu_hex
sys.stderr.write('SMS concat: assembled %d segments (ref=%s, %d B)\n' % (
total, ref, len(assembled)))
else:
submit_handler.submit_ud_hex = payload.hex()
submit_handler.submit_tpdu_hex = tpdu_hex
_handle_proactive_chain(scc, sw, _capture_sms_tpdu)
data, sw = '', '9000'
@@ -1168,6 +1171,23 @@ def _ram_step_result(step_name, last_sw, por, por_hex, bytes_, segments):
return step, error
def _sms_submit_por(submit_handler):
"""Response packet carried by an actual-response SMS-SUBMIT, in the
DELIVER-style form `_decode_por` expects.
The submit UD has no RPI UDH (the RPI is a UDH IE there) and starts at
RPL/RHL/TAR/CNTR/PCNTR/STS, so the `02 71 00` RPI UDH is prepended. Returns
'' when no submit response was captured."""
ud_hex = (getattr(submit_handler, 'submit_ud_hex', None) or '').strip()
if not ud_hex:
return ''
try:
bytes.fromhex(ud_hex)
except ValueError:
return ''
return '027100' + ud_hex
def _decode_por(spi1, spi2, kic, kid, cntr_hex, kic_key_hex, kid_key_hex, response_hex):
from pySim.ota import OtaDialectSms, CompactRemoteResp
from osmocom.utils import h2b, b2h
@@ -1272,6 +1292,9 @@ class PoRSubmitHandler(ProactiveHandler):
def __init__(self):
super().__init__()
self.submit_tpdu_hex = None
# Assembled UD data of the actual-response SMS-SUBMIT(s), UDH stripped:
# the DELIVER-style response packet is `02 71 00` + this data.
self.submit_ud_hex = None
self.sms_segments = [] # [(ref, total, num, payload_hex), ...]
@@ -3685,6 +3708,19 @@ def _bip_flush_channel_events(scc):
_FLUSHING_CHANNEL_EVENTS = False
def _ber_len_at(raw, off):
"""Return (length, value_offset) of a BER length field at raw[off]."""
if off >= len(raw):
return 0, off
first = raw[off]
if first < 0x80:
return first, off + 1
n = first & 0x7F
if n < 1 or off + 1 + n > len(raw):
return 0, off + 1
return int.from_bytes(raw[off + 1:off + 1 + n], 'big'), off + 1 + n
def _skip_ber_len(raw, off):
if off >= len(raw):
return off
@@ -3852,13 +3888,22 @@ def _parse_proactive_header(raw):
def _find_sms_tpdu(raw):
if raw[0] == 0xD0:
"""Extract the SMS TPDU (tag 0x8B) from a FETCH response.
FETCH responses may use BER long-form lengths (`8B 81 97 ...` for the big
listings), so the TLV length must be parsed, not read as one byte: a
single-byte read silently truncates the TPDU and the PoR/data is lost."""
if raw and raw[0] == 0xD0:
off = _skip_ber_len(raw, 1)
while off < len(raw) - 1:
tag, tlen = raw[off], raw[off + 1]
val = raw[off + 2: off + 2 + tlen]; off += 2 + tlen
tag = raw[off]
tlen, val_off = _ber_len_at(raw, off + 1)
if tag == 0x8B and tlen >= 1:
return val.hex()
return raw[val_off:val_off + tlen].hex()
nxt = val_off + tlen
if nxt <= off: # no forward progress: stop
break
off = nxt
return None
@@ -3875,8 +3920,10 @@ def _calc_ud_offset(tpdu):
da_len_digits = tpdu[2]
da_data_bytes = (da_len_digits + 1) // 2
off = 1 + 1 + 1 + 1 + da_data_bytes + 1 + 1
if vpf in (0x01, 0x02): # relative or absolute
off += 7
# TP-VP: none (0), enhanced (7), relative (1), absolute (7) - reading
# the relative form as 7 bytes shifted the UD offset and made the
# concatenation UDH unparseable.
off += {0x00: 0, 0x01: 7, 0x02: 1, 0x03: 7}[vpf]
if off >= len(tpdu):
return None
return off + 1 # skip UDL byte
@@ -5935,7 +5982,12 @@ class PysimHandler(BaseHTTPRequestHandler):
'bytes': result['bytes'], 'segments': result['segments']}
por_src = 'envelope'
por_hex = resp['response_data']
if submit_handler and submit_handler.submit_tpdu_hex:
submit_hex = _sms_submit_por(submit_handler)
if submit_hex:
por_hex = submit_hex
por_src = 'sms-submit'
elif submit_handler and submit_handler.submit_tpdu_hex:
# no assembled UD: fall back to a raw RPI packet
tpdu_b = bytes.fromhex(submit_handler.submit_tpdu_hex)
idx = tpdu_b.find(b'\x02\x71\x00')
if idx >= 0:
@@ -6099,7 +6151,12 @@ class PysimHandler(BaseHTTPRequestHandler):
# Decode PoR
por_src = 'envelope'
por_hex = last_data
if submit_handler and submit_handler.submit_tpdu_hex:
submit_hex = _sms_submit_por(submit_handler)
if submit_hex:
por_hex = submit_hex
por_src = 'sms-submit'
elif submit_handler and submit_handler.submit_tpdu_hex:
# no assembled UD: fall back to a raw RPI packet
tpdu_b = bytes.fromhex(submit_handler.submit_tpdu_hex)
idx = tpdu_b.find(b'\x02\x71\x00')
if idx >= 0: