Scope: This is an independent research. I simply reviewed the iOS 18.3 and 18.3.1 binaries and verified dynamically the guard in iOS 26.6.1. My analysis may contain errors or oversights.
Timeline#
Apple released iOS and iPadOS 18.3.1 on 10 February 2025 but added the CVE-2025-43200 entry to that advisory on 11 June:
Impact: A logic issue existed when processing a maliciously crafted photo or video shared via an iCloud Link. Apple is aware of a report that this issue may have been exploited in an extremely sophisticated attack against specific targeted individuals.
Description: This issue was addressed with improved checks.
Citizen Lab’s Graphite report appeared on 12 June 2025. Its forensic case involved a device on iOS 18.2.1 and Citizen Lab reported that the zero-click vulnerability was mitigated as of iOS 18.3.1 and assigned CVE-2025-43200.
In the first version of this post, I found the new isFromMe check but described the surrounding path too speculatively. I wrote about a generic “resend request,” assumed that the attacker first needed to steal a GUID from the victim’s database, and wondered whether the media was decoded again outside BlastDoor. This updated version gives a more precise account.
Introduction#
Mallory sends Alice an iCloud Link of a iCloud Photos in an ordinary iMessage conversation. Mallory’s sending side already knows the message GUID. If Alice’s iOS 18.3 device later accepts an IDS (Identity Services) pair-decryption error for that same GUID, “Messages” app can retrieve Mallory’s inbound record and send it back out without checking that Alice authored it.
This is the security failure established by the patch, an attacker-authored object crosses from received-message state into sender-only processing. It is not, by itself, the memory-corruption or code-execution primitive used by Graphite.
In reconstructed pseudocode, the decisive change in iOS 18.3.1 is just:
if (![message isFromMe]) {
os_log("Being requested to re-send a message that wasn't sent by me");
return;
}The trigger is IDS command 120, reason 200: the receiver reports that it could not decrypt a message (pair decryption failure).
The vulnerable build checks that the referenced message exists, is recent, remains within the retry budget, belongs to a chat containing the requester, and passes a server-controlled client-compatibility gate. It does not check who authored the object before placing it on the outgoing send path.
The retry budget is a per-peer failure counter that limits repeated automatic retransmissions after decryption errors.
Versions, prerequisites and validation boundary#
| Role in this analysis | Version |
|---|---|
| Vulnerable build verified here | iOS 18.3, 22D63 |
| Fixed build verified here | iOS 18.3.1, 22D72 |
| Current fixed control | iOS 26.6.1, 23G83 |
For the reconstructed attack, Mallory must be a participant in the conversation, send the inbound message, retain its GUID, and cause command 120 / reason 200 to reach Alice within the retry window while the failure budget remains and the client-compatibility gate permits a resend. The production handler and its wire fields are present in the inspected binaries. I did not reproduce the exact peer-side behavior Paragon used to make IDS deliver that command.
The diagram below is a static control-flow reconstruction. The peer-side delivery of command 120 / reason 200 was not reproduced.
flowchart TD
A["Mallory sends an iCloud Photos Link
with message GUID 123"]
B["Alice decodes the inbound rich-link payload"]
C["Store payloadData and fileTransferGUIDs for 123"]
D["IDS command 120 / reason 200
references 123
(peer-side delivery not reproduced)"]
E["_reAttemptMessageDeliveryForGUID(123)"]
F{"Build behavior"}
G["No authorship check"]
H{"message.isFromMe?"}
I["Reject and return"]
J["Legitimate local retry"]
P{"Client-compatibility gate
permits resend?"}
Q["Reject: client version is out of date"]
K["Mark message as retrying"]
L["Sender-side rich-link processing"]
M["Resolve file-transfer GUIDs to local URLs"]
N["Recombine payload and attachments"]
O["Outbound delivery to the requesting participant"]
A --> B
B --> C
C --> D
D --> E
E --> F
F -->|"iOS 18.3"| G
F -->|"iOS 18.3.1 and 26.6.1"| H
H -->|"No"| I
H -->|"Yes"| J
G --> P
J --> P
P -->|"No"| Q
P -->|"Yes"| K
K --> L
L --> M
M --> N
N --> O
What changed in the Messages plug-in#
I compared the iMessage.imservice/iMessage Mach-O from the iPhone17,1 restore images for the vulnerable and hotfix builds.
| Build inspected | Retry method address |
|---|---|
| iOS 18.3, 22D63 | 0x8744 |
| iOS 18.3.1, 22D72 | 0x8678 |
| iOS 26.6.1, 23G83 | 0xc5fc |
The addresses are unslid and only apply to these binaries. The relevant Objective-C method is:
-[MessageServiceSession
_reAttemptMessageDeliveryForGUID:toIdentifier:fromIdentifier:
fromIDSID:isReflection:shouldShowError:cacheFlushError:imdAccount:]All three versions:
- find a message by GUID,
- find its chat,
- reject an old message,
- enforce a retry allowance,
- check that the requester is an acceptable participant,
- apply a client-compatibility gate.
iOS 18.3 then proceeds without asking who authored the message. In simplified pseudocode:
message = [messageStore messageWithGUID:guid];
chat = [chatRegistry chatForMessage:message];
if (messageIsTooOld(message))
return;
// Missing in iOS 18.3: is this actually our message?
if ([self _failuresForID:fromIdentifier] >= [self _maxFailuresAllowed])
return;
if (!participantIsAcceptable(fromIdentifier, chat))
return;
if (![self _shouldAdjustTimestampOfResentMessages])
return;
[message setIsBeingRetried:YES];
[messageSummary setHasBeenRetried:YES];
[message setRetryToParticipant:fromIdentifier];
[self sendMessage:message toChat:chat style:style];
[self _incrementDecryptionFailureForID:fromIdentifier];iOS 18.3.1 inserts the authorship check immediately after the age check and before the retry state is changed. In the iOS 18.3.1 binary, the essential instructions are around 0x89f8:
mov x0, x24
bl _objc_msgSend ; -[message isFromMe]
tbz w0, #0, rejectThe rejection block logs Being requested to re-send a message that wasn't sent by me and returns. It never reaches setIsBeingRetried:, setRetryToParticipant:, or sendMessage:toChat:style:.

Binary Ninja HLIL for iOS 18.3 (22D63) - after the age check, the vulnerable function proceeds directly to the retry quota; there is no authorship check.

The same region in iOS 18.3.1 (22D72) - the new isFromMe branch rejects the operation and logs that the requested message was not sent by the local user.
The trigger: IDS command 120, reason 200#
The vulnerable retry is reached through an ordinary iMessage peer-error mechanism. In +[MessagePushHandler addStandardCommandHandlersToRegistry:], Messages registers FTCommandIDErrorMessage as IDS command 120 (0x78).
Command 120 = a peer reports an error for a message
Reason 200 = the peer could not decrypt that messageThe command carries a compact dictionary. The fields most relevant to this vulnerability are:
| Field | Meaning | Why it matters |
|---|---|---|
fR | Error reason | The value 200 selects the pair-decryption-failure path |
fU | Failed message UUID/GUID | Identifies the local message that may be retried |
sP / tP | Sender and recipient identifiers | Provide peer and conversation context |
t | Peer token | Supplies additional IDS delivery context |
Other fields carry timestamps and storage or delivery context, but they do not determine whether the vulnerable retry occurs.
After parsing the dictionary, the dispatcher calls:
-[MessageServiceSession
handler:receivedError:forMessageID:toIdentifier:fromIdentifier:
fromToken:timeStamp:fromIDSID:needsDeliveryReceipt:deliveryContext:
storageContext:additionalInfo:shouldShowPeerErrors:]In iOS 18.3.1, this method begins at 0x92ac. It compares the received reason with 0xc8, or decimal 200. The matching branch identifies the condition as:
Pair decryption failure for messageID: ...
In iOS 18.3.1 (22D72), the peer-error handler compares the received reason with 0xc8 and branches to 0x9c0c.

At the 0x9c0c target, the same handler reaches the Pair decryption failure for messageID diagnostic. The replication check follows immediately below this block.
Unless the current session is a replication session, this path calls _reAttemptMessageDeliveryForGUID:... with the failed message GUID and the associated identity context.

In iOS 18.3.1 (22D72), the error-200 path prepares the GUID and identity arguments before calling the retry helper.
The normal purpose is straightforward. If Bob cannot decrypt a message originally sent by Alice, Bob reports the failure using command 120 with reason 200. Alice can then locate her original message by GUID and attempt to send it again.
The stock sender exists in the same binary. -[MessageDeliveryController sendMessageError:...] constructs the error dictionary, selects command 0x78, and sends it to the peer. This confirms that command 120 is a normal first-party iMessage mechanism.
One identity requires additional care. fromIDSID is not read from the compact command payload: it is supplied separately as the fromID value received by the IDS delegate. This makes it transport-provided metadata. Analysis of the Messages plug-in alone does not establish which authentication or authorization checks IDS applied before delivering it, so describing it as either fully attacker-controlled or cryptographically authenticated would overstate the available evidence that we have.
The security property that should hold is pretty simple:
A peer-decryption error may request retransmission only when the referenced message was originally authored by the local account.
In the vulnerable build, Messages checks that the GUID resolves to a message, that the message is recent enough, that the retry budget has not been exhausted, that the reporting peer belongs to the conversation, and that _shouldAdjustTimestampOfResentMessages permits the retry. It does not verify that the message was authored locally.
That missing check changes the direction of the operation. A peer can report a decryption failure for a message that the peer originally sent, causing the recipient to retrieve that incoming message and pass it into an outgoing delivery path.
Static analysis establishes that the production handler accepts this condition and that it can reach the vulnerable path. It does not establish exactly how Paragon caused the required IDS frame to be delivered.
The misleading fromIDSID check#
At first sight, the retry function appears to compare fromIDSID with the active device for fromIdentifier, but the control-flow graph changes that reading.
The function first evaluates failures < maxFailures. If budget remains, it goes directly to chat-participant validation and the resend. The active-device lookup and fromIDSID comparison are only reached on the budget-exhausted branch, where they help decide whether a remote-decryption-failure error should be displayed. They do not authorize the resend.
toIdentifier is also retained but does not participate in the resend decision. Before 18.3.1, the effective gates while budget remained were therefore:
- the message and chat exist;
- the message is recent enough;
- retry budget remains for
fromIdentifier; fromIdentifierbelongs to the chat;_shouldAdjustTimestampOfResentMessagespermits the resend.
For an attacker-authored one-to-one message, item 4 is naturally true: the attacker is the sender and a participant. The fifth check is a server-controlled client-compatibility policy: when it fails, the function logs that the client version is out of date and returns. It is a real reachability prerequisite, but it does not establish who authored the message. The new isFromMe guard is the missing object-ownership authorization check.
A 15-minute default retry window#
-[MessageServiceSession _messageRetryTimeout] tail-calls
_IMSharedHelperRetryTimeout. Both inspected builds embed the double 900.0, so the default retry window is 15 minutes. The helper also knows the feature override retryTimoutOverride (the typo is in the binary btw) and the configuration key allowed-app-retry-time-sec. These embedded configuration keys can override the default.
For DFIR, this default provides a useful initial window around the retry event.
The retry allowance is also versioned through a server-bag key. iOS 18.3 uses md-decryption-failure-retries-per-week; the 18.3.1 hotfix uses md-decryption-failure-retries-per-week-v2. Both inspected binaries retain the same _maxFailuresAllowed control flow and a default maximum of 10.
The GUID is not a secret the attacker must steal#
My original reconstruction assumed that the attacker first had to discover a message GUID inside the victim’s sms.db but that unnecessarily complicated the model.
The relevant message is attacker-authored. Mallory creates an iCloud Link and sends it to Alice, so Mallory’s sending side already knows the identifier assigned to that message. The vulnerable behavior begins when Alice’s device later accepts that same identifier in a peer-decryption error and mistakes an inbound record for something Alice is allowed to retransmit.
No second database-read vulnerability is required for that part of the chain. Likewise, I found no tangible evidence that Messages flips the stored row from is_from_me = 0 to is_from_me = 1, or creates a duplicate row with the same GUID. The code mutates the in-memory message with isBeingRetried and retryToParticipant.
What an iCloud Photos Link looks like inside Messages#
The missing authorship check is content-agnostic, so why does Apple’s advisory explicitly mention a photo or video shared through an iCloud Link?
The answer is not a function conveniently named something like processMaliciousICloudLink. It is the richer object graph that this particular message type carries through the boundary.
Three properties matter here:
1. It is a structured archive, not URL text
LinkPresentation represents the message with LPMessagesPayload, an NSSecureCoding object containing LPLinkMetadata, placeholder/fetch state, and out-of-line attachments stored separately from the archived metadata. Its API can decode the archive, substitute attachments, or encode the two separately:
+linkWithDataRepresentation:attachments:
+linkWithDataRepresentationWithoutSubstitutingAttachments:
-performSubstitutionWithAttachments:
-dataRepresentationWithOutOfLineAttachments:2. The decoder is deliberately bounded
It creates an NSKeyedUnarchiver, enables strict secure decoding, maps legacy attachment-substitute classes, and calls decodeTopLevelObjectOfClasses:forKey:error: with an allow-list containing LPMessagesPayload, LPLinkMetadata, and LPSharingMetadataWrapper. LPMessagesPayload also reports supportsSecureCoding = YES. This does not prove that every later semantic operation is safe, but it rules out the easy explanation of an unrestricted generic unarchive.
3. The object carries iCloud-specific state
LPiCloudSharingMetadata includes an application identity, title, preview material, and encoded tokens. Its provider specialization knows about CloudKit containers, share metadata, Quick Look thumbnails, and document URLs. The binaries also contain the expected URL markers and photo/video attachment substitutes:
icloud.com
share.icloud.com
photos_sharing
isiCloudPhotoShareURL:
isiCloudSharingURL:
LPImageAttachmentSubstitute
LPVideoAttachmentSubstitute
RichLinkImageAttachmentSubstitute
RichLinkVideoAttachmentSubstituteSo an iCloud Photos Link in Messages is not merely URL text. It can be a LinkPresentation archive tied to iCloud/CloudKit metadata, a preview, and out-of-line photo or video objects.
Following the same payload inbound and outbound#
The most useful way to understand the bug is to follow one message through the two directions. The screenshots below use 18.3.1 where its symbols and control flow are easiest to label, but the relevant entry points and outgoing sink are also present in 18.3:
| Stage | iOS 18.3, 22D63 | iOS 18.3.1, 22D72 |
|---|---|---|
| Inbound payload processor | 0x65bf4 | 0x65b94 |
| Retry helper | 0x8744 | 0x8678 |
| Outgoing delivery method | 0x3e644 | 0x3e5e4 |
| Rich-link recomposition call | 0x3f36c | 0x3f30c |
Btw, the 596-byte recomposition helper itself is byte-for-byte identical in the two releases. The material change is the new authorization check before the retry enters this existing pipeline.
1. Normal inbound processing#
Downloaded iMessage app payloads are handled by:
-[MessageAttachmentController
_processDownloadedPayload:forMessageGUID:balloonBundleID:
fromIdentifier:senderToken:withCompletionBlock:]In 22D72 its implementation starts at 0x65b94; the 22D63 equivalent starts at 0x65bf4. It finds the chat, constructs an IMSenderContext, obtains IMTranscodeController, and calls:
decodeiMessageAppPayload:
senderContext:
bundleID:
completionBlock:
blockUntilReply:YESIMTranscoding is an XPC client of com.apple.imtranscoding.IMTranscoderAgent. The dyld shared cache also contains _BlastDoorLPMessagesPayload, and BlastDoor contains consumers of linkWithDataRepresentation:attachments:.
This establishes an XPC-mediated, sender-context-aware decoding route for inbound rich-link data. It does not, by itself, establish the precise process or sandbox placement of every operation, so I do not draw a stronger process-boundary conclusion from the symbols alone.
The decode completion also closes the storage gap. sub_17f5c receives the decoded payload and attachment-URL array. For a non-empty attachment array, it calls IMDFileTransferCenter’s guidsForStoredAttachmentPayloadDataURLs:messageGUID: and gets back transfer GUIDs tied to the same message. It then calls setPayloadData:, conditionally calls setFileTransferGUIDs:, and finishes with didReceiveBalloonPayload:forChat:style:messageGUID:account:. Its diagnostic is Updating payload to length: %lu with attachments: %lu.

In iOS 18.3.1 (22D72), the inbound decoder completion hands its attachment-URL array to IMDFileTransferCenter, together with the same message GUID, and receives the stored transfer GUIDs. The same completion assigns decoded payloadData and the returned fileTransferGUIDs to the message, then calls didReceiveBalloonPayload for the chat/account context.
This matters for both exploit reasoning and DFIR because the outgoing retry does not simply invent arbitrary local URLs; it resolves the fileTransferGUIDs that the inbound completion attached to the message after decoding. An analyst should therefore attempt to pivot from the message row through message_attachment_join into the cached transfers that later feed rich-link recomposition. The exact schema and persistence of this relationship must still be validated on the acquired iOS version.
2. The vulnerable direction change#
After error 200, iOS 18.3 finds the same stored inbound message, marks it as being retried, sets retryToParticipant to the requesting participant, and invokes the normal send path. The missing isFromMe check is what permits this direction change.
The iMessage layer then calls common outgoing processing in IMDaemonCore. One relevant method in 22D72 is:
-[IMDServiceSession
processMessageForSending:toChat:style:allowWatchdog:account:
didReplaceMessageBlock:completionBlock:]at 0x1d990f1e4. Along this path, the code inspects balloonBundleID, body, and payloadData, calls photoShareURLFromPluginBundleID:contentString:payload:shouldAccept:, may call registerAndAcceptMomentShareForMessage:inChat:resetAssetTransfers:, and checks containsRichLink.
3. Rich-link attachment recombination#
During construction of the outgoing delivery dictionary, the code handles a message with non-empty payloadData, the RichLinks balloon identifier, and one or more fileTransferGUIDs. It logs:
Trying to recombine rich link payload from attachments for msg guid ...For each transfer GUID, it obtains the corresponding transfer from IMDFileTransferCenter, retrieves its localURL, and calls:
_IMSharedHelperCombinedPluginPayloadDictionaryDataWithAttachmentURLs(
message.payloadData,
localURLs
)
Binary Ninja HLIL for iOS 18.3.1 (22D72): the RichLinks branch resolves every fileTransferGUID through IMDFileTransferCenter, excludes moment-share transfers, and collects each remaining transfer's localURL.

In iOS 18.3.1 (22D72), the outgoing payload branch passes the stored payloadData and collected local URLs to the recomposition helper, gzips the replacement, and logs that the payload was updated for sending.
That helper reads the associated URLs with dataWithContentsOfURL:options:error: and serializes an outer object containing:
__payload__
__attachments__The helper therefore reads local files selected through the message’s existing file-transfer objects and serializes them for delivery. I found no evidence that the attacker can substitute an arbitrary filesystem path.
4. No second historical LPMessagesPayload decode#
The sender-side result is also useful for ruling out my earlier theory. The 18.3.1 iMessage Mach-O imports _OBJC_CLASS_$_LPMessagesPayload, but its only code reference is _fallbackMessageItemFromLinkMetadata:originalMessageItem: at 0x4a7d4. That function creates and encodes a new LPMessagesPayload; it does not decode one. The vulnerable 18.3 binary has the same single reference in the equivalent function at 0x4a834, and neither historical Mach-O contains the compiled selector linkWithDataRepresentation:attachments:.
A selector assembled entirely at runtime cannot be disproved from absence alone, but no such invocation appears in the retry, delivery, or rich-link construction paths. The strongest supported conclusion is therefore that the inbound path decodes the archive, while the vulnerable retry re-emits the stored representation without a direct second LPMessagesPayload decode in iMessage.
What the patch proves-and what it does not#
Taken together, the binaries establish an unauthorized inbound-to-outbound re-delivery of an attacker-authored Messages object. Under the prerequisites above, the remote peer can cause Alice’s Messages service to perform a sender-side operation on content Alice did not author.
| Claim | Status after this analysis |
|---|---|
| Error 200 is a pair-decryption-failure branch that reaches the retry helper | Directly observed in static binary analysis of 22D72; retained in 23G83 |
| iOS 18.3 lacks an authorship check before retrying | Directly observed in static binary analysis of 22D63 |
iOS 18.3.1 rejects a retry when isFromMe == NO | Directly observed in static binary analysis of 22D72 |
| Peer-side delivery of the required IDS frame to a retail vulnerable device | Not reproduced at runtime |
| The attacker knows the GUID of the message they authored | Inference from the attacker-authored-message model |
| Inbound decoded attachment URLs become stored transfer GUIDs on the same message | Directly observed in static analysis of the 22D72 decode completion; equivalent processor present in 22D63 |
| The retried object enters photo-share/rich-link processing | Directly observed in static analysis of the historical outgoing path |
| Associated local attachments are read and recombined with the payload | Directly observed in static binary analysis of 22D63 and 22D72 |
Historical retry directly decodes LPMessagesPayload again in iMessage | Static negative evidence: no decoder call or compiled selector reference in either historical plug-in |
| Transfer-GUID manipulation permits arbitrary local-file read | Not demonstrated |
| CVE-2025-43200 alone supplies Graphite’s memory corruption or code execution | Not demonstrated |
How I now interpret Graphite’s use of iCloud Link#
My best evidence-based interpretation is that iCloud Link was the content container whose trust context needed to be changed. It lets an attacker supply a complex message object with photo/video state, cached transfers, LinkPresentation metadata, and CloudKit-related handling. The retry bug takes that received object and places it into sender-side logic without local authorship.
That transition may have made a separate parser bug, attachment-state confusion, or CloudKit/photo-share behavior reachable under conditions useful to Graphite. The public record and the inspected binaries do not identify which one. It is also possible that the undisclosed chain depended on state not recoverable from a simple patch diff.
Forensic ideas#
If retained in a sysdiagnose or the unified log, these diagnostics are useful as ordered state markers rather than standalone indicators:
| Diagnostic substring | Earliest state it establishes | Important limitation |
|---|---|---|
Pair decryption failure for messageID | The reason-200 branch handled the referenced GUID | Pair-decryption failures can be legitimate |
Attempting to Burn a retry to send the message | Retry budget remained at that point | It is logged before the participant and client-compatibility gates, so it does not prove that a resend occurred |
Not resending message because client version is out of date | The client-compatibility gate rejected the retry | It is a negative-control event, not evidence of Graphite |
Being requested to re-send a message that wasn't sent by me | On a fixed build, the new isFromMe guard rejected an inbound object | It was introduced by the fix and does not identify what generated the peer error |
Trying to recombine rich link payload from attachments for msg guid | Outgoing rich-link recomposition was reached | Legitimate outgoing rich links use the same path |
The higher-value signal is a temporally coherent sequence correlated by message GUID, peer identity, database direction, and attachment state.
There is also a more specific database hypothesis. The retry code sets messageSummaryInfo[IMMessageSummaryInfoHasBeenRetried] = YES. In the persistence framework, message_summary_info is a message-table BLOB; its dictionary is serialized as a binary property list. The constant’s string value is hasBeenRetried.

In iOS 18.3.1 (22D72), after the participant and client-compatibility gates pass, Messages sets isBeingRetried, writes IMMessageSummaryInfoHasBeenRetried, records retryToParticipant, calls sendMessage:toChat:style:, and increments the decryption-failure counter.
That suggests a high-value hunt on an acquired database:
-- Conceptual predicate; message_summary_info must be decoded as a plist.
message.is_from_me = 0
AND message_summary_info["hasBeenRetried"] = trueAn ordinary retry should concern a locally authored row. An inbound row retaining hasBeenRetried = true would match the exact state confusion described by the patch. I have not yet executed the vulnerable path because I do not have a vulnerable test device, so persistence of this combination remains a testable hypothesis.
For message-store analysis, I would correlate such a candidate with:
- an inbound RichLinks/iCloud Photos message and its real
message.guid; balloon_bundle_id = com.apple.messages.URLBalloonProvider;- iCloud markers in the payload and attachments joined through
message_attachment_join; - nearby decryption-failure and retry events;
- transfer or message-state changes at the same time;
- the sender handle, account, chat membership, and the roughly 15-minute default retry window.
These are just investigation pivots. Without Citizen Lab’s underlying device records or a controlled reproduction, I do not know which of them survives long enough to be useful in a real acquisition.
Reproducing the two decisive iMessage checks#
The missing guard in 18.3, the new guard in 18.3.1, and the error-200 call into the retry helper can be reproduced directly from the extracted iMessage binaries.
iMessage binaries:
iOS 18.3 22D63 4628e8c9704e95e8ee95e251c7acd60ae5317b941580348a4373d95679e33719
iOS 18.3.1 22D72 d89febb6d7c43c847c8fd54c1b6fdb40026c0591c9a2f6a6271d315fa109d837
iOS 26.6.1 23G83 bc15e0c25adc02fd8d88f5bb5ef56f66b556f0d96ed6bbf580c161f1184e61c0Compare the same continuous retry region in the vulnerable and fixed builds:
# Vulnerable: the age check flows to _failuresForID without isFromMe.
ipsw macho disass \
extracted/18.3/22D63__iPhone17,1/System/Library/Messages/PlugIns/iMessage.imservice/iMessage \
--vaddr 0x8744 --count 340 \
| rg -B 28 -A 24 'isFromMe|_failuresForID|_shouldAdjustTimestampOfResentMessages|setIsBeingRetried'
# Fixed: isFromMe appears before _failuresForID and the later retry gates.
ipsw macho disass \
extracted/18.3.1/22D72__iPhone17,1/System/Library/Messages/PlugIns/iMessage.imservice/iMessage \
--vaddr 0x8678 --count 340 \
| rg -B 28 -A 24 'isFromMe|_failuresForID|_shouldAdjustTimestampOfResentMessages|setIsBeingRetried'The peer-error branch can be inspected separately:
ipsw macho disass \
extracted/18.3.1/22D72__iPhone17,1/System/Library/Messages/PlugIns/iMessage.imservice/iMessage \
--vaddr 0x92ac --count 1100 \
| rg -B 35 -A 40 'Pair decryption failure|0xc8|reAttemptMessageDelivery'Where I would go next#
The highest-value experiment remains a two-device test with owned accounts: send an iCloud Photos Link, cause the same pair-decryption error on a vulnerable build and a fixed build, then trace message-store and XPC behavior. Since I do not have a vulnerable lab device, I could not do it. If you have access to such a setup and want to go further, do not hesitate to contact me, I would be happy to work with you!
Conclusion#
CVE-2025-43200 is explained by a missing authorization check in iMessage’s retry handling for an IDS pair-decryption error. On the inspected iOS 18.3 build, the peer can reference a stored inbound message and the service can turn it into an outbound retry without confirming that the local user sent it. iOS 18.3.1 adds the exact invariant the path lacked: message.isFromMe must be true.
For an iCloud Photos Link, that failure carries a structured LPMessagesPayload and its photo/video attachments across the receive-to-send boundary. The outgoing path recognizes photo shares and rich links, resolves associated file transfers, reads their local URLs, and recombines the payload and attachments for delivery.
I still have not identified a second unsafe decode, an arbitrary-file primitive, or Graphite’s final memory-corruption step.