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>(notmanage pairlike iter-09 — this is the key change) - Backend:
ffe7c29(unchanged since iter-10/D.6.1-B, withIOSMUX_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)¶
- 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.
- 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 detailswide pcap. If a SYN to :50367 appears — that field was the gate. - 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/:
findings.md— this documentindex.md— summary
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.