You Can’t Spell WebRTC without RCE - Part 3 — Margin Research
You Can’t Spell WebRTC without RCE - Part 3

You Can’t Spell WebRTC without RCE - Part 3

Ian Dupont
by Ian Dupont
Michael C
by Michael C
Aug 12, 2024

This is the third and final part of our blog post series on Signal and iOS exploitation via the insertion of synthetic vulnerabilities.

Part one explored Signal and WebRTC, detailing the injected vulnerabilities and the process of adding them. In part two we leverage these vulnerabilities to exfiltrate the Signal database from a victim's phone. For our final post, we’ll discuss indicators of compromise (IOCs) created by this attack, as well as IOCs that a real in-the-wild exploit might generate.

Part 3 - Post Mortem

Exploit Limitations and Considerations

Before commenting on IOCs, it is worth reviewing the exploit’s limitations. Overall, the goal of the project was to create a realistic instant messaging app exploit using injected bugs in order to learn more about Signal-iOS and WebRTC, practice iOS exploitation, and assess indicators of compromise. As a result, a number of decisions were made that don’t necessarily reflect what would be found in a complex operation.

As mentioned and reiterated in this series, the exploit relies upon powerful synthetic vulnerabilities. In a real-world use case, researchers must identify and chain smaller vulnerabilities into the read and write primitives leveraged for this research. That said, the primitives need not exist only in WebRTC; as shown through the leak chains in part 2, we could potentially use primitives found in other Signal attack surface to leak the shared cache slide, worker thread stack, global objects, etc. We could perhaps write to the worker thread stack during execution on a different thread. The key here is finding primitives that can achieve the capabilities leveraged, which is obviously the difference between synthetic research and real-world 0-day exploits.

Another limitation of this exploit is the outgoing DTLS packet size, which restricts each leak to ~2000 bytes. This is likely not sufficient to leak the entire database before the unanswered call times out. An operational solution may be to leak pieces at a time when the target user is known to not be active. Another option is to pivot the database leak to the data channel in the connection, which would allow sending much larger streams of information versus the small RTCP control packets.

It should also be noted that the exploit targets a debug Signal-iOS build that contains non-stripped RingRTC and WebRTC libraries. This is to facilitate looking up target symbol offsets when throwing the exploit. The exploit strategy should work when targeting a release build (with the synthetic vulnerabilities, of course) if the target offsets are calculated without symbols.

Moreover, the entire decision to use an Android virtual device to throw the exploit was made because it offered the quickest approach. This has downsides, as it depends on Signal not detecting and blocking registrations from virtual devices. It is also more resource intensive than a custom script which could enable more parallelized throwing.

There are surely more limitations based on the intended use for such an exploit chain, and we could spend days picking apart these constraints. Let’s instead spend the rest of this post reframing our perspective from an offensive to defensive lens to better understand the IOCs from the target’s point of view.

IOCs

User Interface IOCs

As the fake bugs are triggered over WebRTC, the attacker’s device must initiate a call with the target. As a result, it produces an obvious IOC in the form of a call in the call list, which we don’t attempt to clean up. This is obvious to the user and could be programmatically detected by forensics tools with an awareness of the Signal database, especially where specific caller identities are known and could be used as strong indicators.

During testing, we noticed one interesting quirk of Signal: if the application crashes while the call is in progress, it doesn’t appear on the calls list. We didn’t dive too far into this, but suspect it’s because the list is updated once the call window is closed. This means that crashing the app while the call is in progress results in no logged entry.

A more subtle indicator is that after exploitation the device is unable to make or receive calls due to an infinite loop in the worker thread. This will not affect the rest of the application, and will disappear once Signal is restarted. If this were a concern in an offensive product, it could be resolved in one of a few ways:

  1. Further research to avoid dependency on the infinite loop
  2. Functionality added to close Signal when backgrounded
  3. Tolerated as an acceptable risk

Depending on the user, this isn’t necessarily a strong indicator—it’s certainly unusual, but users often have Signal opened for extended periods of time without making or receiving a call, and those that use WebRTC functionality would likely disregard the behaviour as an innocuous software bug.

Process-level IOCs

The exploit sends a large number of Loss Notification packet requests to the target device, which isn’t out of the ordinary for benign Signal traffic.

Unique to exploitation, however, the target device hits the loss_notification->media_ssrc() != local_media_ssrc() condition in RTCPReceiver::TriggerCallbacksFromRtcpPacket and inevitably causes an infinite loop. This would be observable via tracing as this condition isn’t usually hit.

It is worth mentioning that this anomaly exists, however the practicality of detecting it is low unless the target suspects exploitation ahead of time.

Fig 1: IDA graph view of the `RTCPReceiver::TriggerCallbacksFromRtcpPacket` function's branching statement checking the `loss_notification->media_ssrc() != local_media_ssrc()` condition.
Fig. 1: IDA graph view of the RTCPReceiver::TriggerCallbacksFromRtcpPacket function's branching statement checking the loss_notification->media_ssrc() != local_media_ssrc() condition.

Network-level IOCs

The end goal of the exploit is to exfiltrate the Signal database, and we do this in the largest possible chunks supported by SRTP packets. This provides multiple indicators of compromise and would be relatively simple to notice if network monitoring was in place.

First, SRTP packets are around 32 bytes long on average, however we transmit the largest possible chunks of around 2004 bytes, making sure to leave some space for the packet header. This is highly unusual behaviour that is visible from the network level and could be used to identify exfiltration in progress.

Secondly, SRTP packets are meant to start with 0x80 or 0x81, however at the start of exfiltration we send an unencrypted packet with the beginning of the database (including the SQLite file magic) to the attacker.

                   0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F  0123456789ABCDEF
b4000075d3e70258  53 51 4c 69 74 65 20 66 f4 13 f7 83 15 37 02 ff  SQLite f.....7..
b4000075d3e70268  cc c4 29 b4 7f b1 88 94 7f 5d ef f5 62 bf ce 71  ..)......]..b..q
b4000075d3e70278  d8 47 83 42 59 bb 4b 03 c1 33 2e 9a 53 4b 42 d4  .G.BY.K..3..SKB.
b4000075d3e70288  39 99 c6 42 ac 82 06 3a c1 9e 84 1e 4d 9f 94 ef  9..B...:....M...
b4000075d3e70298  97 f6 dd 6a 21 cb e9 f2 70 76 b8 51 ab 01 c3 a2  ...j!...pv.Q....
b4000075d3e702a8  64 da 43 49 ae 5e 11 64 d5 68 57 9e ac 3d bb 94  d.CI.^.d.hW..=..

This is because the exploit jumps directly to the MediaChannelUtil::TransportForMediaChannels::SendRtcp function with the database data as the outgoing buffer. The packet data is encrypted by subfunction calls, however it assumes the buffer begins with an RTP/RTCP header, which is not encrypted. The result is a leak of eight bytes of database data in plaintext, though this is only noticeable in the first packet which leaks the SQLite f magic bytes.

An easy solution is to advance the buffer by 15 bytes to bypass the SQLite format 3 database header, leaving only (seemingly random) data bytes in the unencrypted header bytes. A slightly more tedious but technically correct solution would be adjusting the ROP chain to mimic the entire function call chain of creating an SDES packet and queuing the outgoing packet at the SRTP layer. This would result in valid SDES header bytes, reducing the exploit's footprint.

First and third-party applications

A lot of public documentation of in-the-wild 0-click chains feature iMessage exploits, which is understandable given the enormous amount of functionality and its ubiquity on iOS. It’s worth noting that third party messengers lack a lot of IOCs one might find with iMessage, owing to the fact they’re largely monolithic processes without extensive inter-process communication.

For example, logs from com.apple.madrid lookups of known NSO Apple IDs have been used in the past to confirm compromise, and the timing of on-demand daemons such as IMTransferAgent (used to handle attachments sent with iMessage) can narrow down the time window of an exploitation attempt and provide a signature via unusual sequencing of process starts/stops. Signal and other third party applications don’t have any equivalent to these IOCs, which might contribute to less observed in-the-wild exploitation.

The closest we have for this is the Signal database which can contain indicators, but it's also easier to clean than the disparate artefacts generated by many different daemons interacting with each other and subject to entitlement and sandboxing limitations. Assuming execution has been achieved within a third-party chat application, even a chain that subsequently fails to escape out of the app sandbox or escalate privileges is likely sufficiently entitled to scrub artefacts.

Generic indicators

Finally, lets consider some other generic indicators.

The DataUsage.sqlite and netusage.sqlite databases used for tracking which processes made network connections already contain an entry for Signal. In this case it’s likely of little use aside from recording that Signal sent an unusual amount of data while receiving comparatively little. However, it would be hard to tell this apart from a call or other behaviour, and would be a weak indicator of compromise.

A sophisticated actor might instead leverage a bug, like the ones we introduced, to launch their own payload either as an independent process or injected into a legitimate daemon. In both cases, an entry for this process might be a useful sign of compromise. An example of this can be found in the incomplete cleanup by Pegasus leading to leftover references to bh and BridgeHead.

Conclusion

We hope you enjoyed the wild ride of this blog series! We've covered a wide range of technical details, including some iOS internals, deployment and limitations of different technologies such as the simulator, and internals of RTP, WebRTC and other technologies that make up the Signal app. If you're still craving more technical detail, the final source code for all research presented here is available in our WebRCE GitHub repository.


Share this article:

arrow-up icon