Skip to content

Phase D.6.5 control findings — lockdown back-connect hypothesis FALSIFIED; CDS bails upstream of Services dict

Status: verified — 2026-04-24

Control experiment against D.6.5's implementation-sketch assumption. Re-ran the iter-09 wide-filter pcap methodology on the SPIKE backend, but with devicectl device info details as the trigger (the one that iter-10 showed causes lockdown traffic against the real iPhone). Expected: SYN to [::1]:50367 with RST, confirming CDS consults the advertised Services dict. Actual: exactly one SYN — the client→our-backend [::1]:51152 → [::1]:34719 — identical shape to iter-09. Zero SYNs to any advertised service port. Zero RSTs. CDS does NOT attempt the lockdown back-connect in SPIKE mode, regardless of trigger choice. The "implement lockdown handler" plan from D.6.5 is necessary but not sufficient — CDS bails upstream of consulting Services.

TL;DR

The SPIKE Handshake is accepted at the wire level (no timeout since D.6.1 UUID patch, no RST_STREAM) — but something in CDS's application-level evaluation after Handshake reads our device as "unpaired / untrusted / not a real device" and aborts before ever opening a lockdown service connection. The advertised com.apple.mobile.lockdown.remote.trusted at :50367 is never reached.

D.6.6 is no longer "implement lockdown handler → CDS connects". It needs to be preceded by D.6.5-ish research: find the pair/trust signaling field in our Handshake (or tunneld advertisement) that CDS uses to gate the lockdown decision.

The capture

  • Trigger: devicectl device info details --device <devicectl-uuid> (not manage pair like iter-09 — this is the key change)
  • Backend: ffe7c29 (unchanged since iter-10/D.6.1-B, with IOSMUX_HANDSHAKE_UUID=random + IOSMUX_BACKEND_VERBOSE=1)
  • tcpdump filter: tcp and not port 49151 (same as iter-09 wide-filter, excludes only tunneld HTTP API noise)

devicectl output (abridged):

hardwareModel: iPhone14,6
pairingState: unpaired
tunnelState: unavailable
WARNING: Unable to retrieve complete information for this device.
Error: An error occurred while communicating with a remote process.

Identical to iter-10's devicectl output against the real iPhone — "unpaired / unavailable" + Mercury-class error.

SYN destination table (the whole result)

src dst count
[::1]:51152 [::1]:34719 1
[::1]:34719 [::1]:51152 1 SYN-ACK (response)

Unique destination ports: only 34719. Total SYNs: 1. Total RSTs: 0. Pcap has 42 packets, all belonging to the one H2 session between CDS and our backend.

Comparison across control points

iter trigger backend observed SYN count (unique dsts beyond 34719)
iter-09 devicectl manage pair SPIKE 0
iter-12 (this) devicectl device info details SPIKE 0
iter-10 pymd3 lockdown info + devicectl device info details stock 1 (:50367 from pymd3 side)

The stock-tunneld path produced a :50367 back-connect (iter-10), the SPIKE-tunneld path produces none regardless of trigger (iter-09, iter-12). The missing variable is upstream of the trigger choice.

What this changes in the D.6.x causal model

Previous model (D.6.5):

SPIKE Handshake OK
  → CDS reads Services dict
  → CDS tries to SYN :50367 (lockdown.remote.trusted)
  → ECONNREFUSED (SPIKE has no listener)
  → Mercury 1000 error

Revised model (D.6.5-control):

SPIKE Handshake OK at wire level
  → CDS evaluates some pair/trust signal
    [signal reads "unpaired / untrusted" in SPIKE mode]
  → CDS bails without consulting Services dict
  → devicectl surfaces "pairingState: unpaired" +
    Mercury 1000 error

The difference matters because D.6.6's implementation order flips. Implementing the lockdown handler at :50367 first — which was D.6.5's recommendation — would not move the needle alone. CDS will not connect to it.

Candidate pair/trust signals (hypotheses, not verified)

Where can the trust signal live? Three places:

H-Adv: tunneld advertisement

GET / on tunneld:49151 returns JSON with tunnel-address + tunnel-port. In stock mode, tunnel-address is a real iPhone ULA (e.g. fd45:3648:8491::1). In SPIKE mode it's ::1. CDS may inspect this and refuse to proceed on a loopback-style ULA (looks suspicious to it). Testable: MITM the tunneld response to swap ::1 for a fake ULA and see if CDS then opens lockdown.

H-HS: Handshake Properties dict

Our iter-01 fixture's 46-entry Properties dict contains fields like HostAttached, TrustedHostAttached, PairRecords, ActivationState, etc. One of these may carry the trust signal. Since our fixture is a verbatim copy of a real iPhone's Handshake Properties (just zeroed identity fields), the dict shape is correct. The question is whether any one zeroed field (UDID, SerialNumber, MAC) is what's flipping CDS's decision. D.6.1 fixed the UUID; maybe one of the other zeroed fields (UDID/Serial) is the next gate.

H-Tx: some per-connection TCP-level signal

Less likely but worth naming: source IP / source-port pattern on lo0 vs utun might affect CDS's decision.

iter-10 re-reading

Important caveat: iter-10's 50367 flow was opened by pymobiledevice3's lockdown info command, not by devicectl. pymd3 and devicectl have different logic — pymd3 connects directly to the advertised port without a trust gate; devicectl (via CDS) enforces the trust gate first. That's why even in iter-10 devicectl reported pairingState: unpaired while pymd3 succeeded in opening 50367.

So the actual split:

  • pymd3 behaviour: reads Services dict, connects directly to lockdown, works regardless of trust state
  • CDS / devicectl behaviour: evaluates trust first, opens lockdown only if trusted

In SPIKE mode we have no pymd3 trying to talk to us — only CDS via devicectl. And CDS is the trust-gating one.

Revised D.6.6 plan (priority order)

  1. D.6.6-research-a: byte-diff our SPIKE Handshake Properties dict against the real iPhone's Handshake Properties dict (both available in iter-10 pcap + our fixture). Identify every field that differs. Most will be zeroed-at-source identity fields. At least one is probably the trust gate.
  2. D.6.6-research-b: patch one field at a time (similar to D.6.1 UUID patching — env-gated, per-session rewrite of the fixture bytes before emit). Re-run SPIKE + devicectl device info details wide pcap. If a SYN to :50367 appears — that field was the gate.
  3. D.6.6-impl (conditional on a/b finding a gate): implement the lockdown handler at :50367 serving the 3-verb protocol. After step 2 confirms CDS will connect.

Estimated cost: (a) is 1-2 h of pure pcap-analysis (same as iter-11 methodology). (b) is ~1 day per field, bounded by how many candidate fields remain after UDID/Serial/MAC/UUID are already patched. © is the original ~1 day D.6.6 estimate, unblocked by (a)+(b).

Total to meaningfully move past handshake into service traffic: ~2-4 days instead of the original ~1 day. Not a disaster, just a refinement.

Operational note for future scripts

macOS does not have GNU timeout(1) as a standard command. The trigger script in this iter initially used timeout 30 and had to be rewritten to use a &-backgrounded command + poll-loop + explicit kill. Future scripts running on havoc that need a time-bounded command should use this pattern from the start, documented here once so other iter scripts don't hit the same snag.

Artifacts

Under iter-12-spike-control/:

Pcap (42 packets, ~2 KB — trivial) and verbose log stay at /tmp/iosmux-d12-* — small enough to re-generate from the scripts if needed, not worth committing.

Status

  • Step A (trigger + wide pcap): delivered
  • Step B (SYN destination analysis): delivered
  • Step C (document revised causal model + revised D.6.6 plan): this file

iter-12 closes the D.6.5 control question. D.6.6 splits: research-a (Properties dict diff) + research-b (field-bisection patching) precede the implementation step.