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¶
- Extract iPhone→client HTTP/2 frames from
/tmp/iosmux-q2-utun-capture.pcapas raw bytes. Redact device-identifying substrings before committing. - Ship those bytes as a Python module the listener imports.
- Modify
iosmux-spike-listener.pyto 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. - Deploy the updated listener to havoc, restart tunneld in SPIKE mode, trigger CDS, collect the new session artifacts.
- 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'sREDACTED_AT_SOURCEconstant 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.