Skip to content

Q3 iter-1 — spike listener replays real iPhone bytes at CDS

Status: verified — completed 2026-04-18

iter-1 wired the iPhone-side bytes captured by Q2 (live rsd-info round-trip) into the spike listener as the application-layer replies to CDS's DATA frames. Verdict: naive upfront replay is invalid — it violates HTTP/2 stream ownership rules. CDS responds with a clean PROTOCOL_ERROR GOAWAY carrying the diagnostic text "request HEADERS: invalid stream_id". Full writeup: findings.md.

iter-2 (event-driven dispatch keyed on client frames) is the next implementation pass.

What iter-1 does

  1. Extract iPhone→client HTTP/2 frames from /tmp/iosmux-q2-utun-capture.pcap as raw bytes. Redact device-identifying substrings before committing.
  2. Ship those bytes as a Python module the listener imports.
  3. Modify iosmux-spike-listener.py to consume the module: after the h2c handshake, send the iPhone frames in order with a small per-frame delay matching wall-clock from the pcap, then observe what CDS sends next.
  4. Deploy the updated listener to havoc, restart tunneld in SPIKE mode, trigger CDS, collect the new session artifacts.
  5. Decode CDS's next request (post-replay) and document it here.

Files

  • iphone_replay_bytes.py — the extracted iPhone frames (10 frames total, 14236 bytes). Personal identifiers inside the Handshake DATA payload are zeroed in place with same-length replacement so bplist16 length prefixes remain valid. See the module's REDACTED_AT_SOURCE constant for exact byte ranges.
  • Future: cds-reply-session-NN.md — what CDS sent after the replay.

Frame inventory

Taken from the Q2 pcap, chronological:

idx type stream length purpose
0 SETTINGS 0 12 server initial SETTINGS
1 WINDOW_UPDATE 0 4 server connection-level window bump
2 SETTINGS 0 0 SETTINGS-ACK
3 HEADERS 1 0 server response headers on ROOT_CHANNEL
4 DATA 1 44 empty-dict XpcWrapper (handshake mirror)
5 DATA 1 24 24-byte sync wrapper, flags 0x0201
6 HEADERS 3 0 server response headers on REPLY_CHANNEL
7 DATA 3 24 INIT_HANDSHAKE on reply channel
8 DATA 1 14124 Handshake dict — MessageType/MPV/Services/Properties/UUID
9 RST_STREAM 1 4 error_code=5 (STREAM_CLOSED), close ROOT_CHANNEL

Frame 8 is the one CDS is waiting on: it carries MessageType="Handshake" with the full Services directory inline (62 services) and the Properties dictionary (46 entries).

Redactions applied to frame 8

All six device-identifying fields in the Handshake payload are zeroed with length-preserving in-place replacement. STRING fields are filled with ASCII 0, binary fields with NUL.

offset length field
0x31b4 17 EthernetMacAddress (ASCII colon-form)
0x3364 10 SerialNumber (ASCII)
0x34bc 25 UniqueDeviceID / UDID (ASCII hex + dash)
0x34ec 8 UniqueChipID (uint64 little-endian)
0x3594 16 BootSessionUUID (binary)
0x371c 16 top-level UUID (session-bound, binary)

No VM hostname literal nor ASCII ULA substring was observed inside any DATA payload in the Q2 capture — those live only at the transport layer. The extractor scans for them defensively anyway.

How reproducible

Generator script: intentionally not committed (one-shot, kept on /tmp). But the inputs and the pipeline are: /tmp/iosmux-q2-utun-capture.pcap (held local on dev host) → scapy TCP reassembly → HTTP/2 frame walk → pymobiledevice3 XpcWrapper.parse for field locations → same-length redaction → iphone_replay_bytes.py. See Q3-iter-00-pcap-dispatch.md for the protocol reference the extraction was verified against.