Some iOS apps ship their own HTTP and TLS stack instead of relying on Apple’s NSURLSession
or the lower level frameworks it relies on. There are many reasons to do this, but the most common one I’ve encountered is apps that use a shared core, typically written in C++, which is used in applications on different platforms. This poses a problem for anyone trying to snoop on the apps network traffic. Recently, I was investigating an app like this and found myself having to intercept its HTTP traffic.
Apps that rely on system libraries will respect any HTTP(s) proxies configure on the device. This can be used with tools like mitmproxy, burp suite, or Charles to intercept or even modify network traffic. Apps that use their own HTTP and TLS stack typically don’t respect system proxies and their traffic is completely invisible to these tools. If you aren’t attentive you can even miss the traffic altogether as I almost did.
Finding the Traffic
My modus operandi is to start with mitmproxy or Burp Suite, but in this case doing so meant I missed all of the app’s traffic. Luckily for me, it was obvious from the app’s behaviour that it was doing some form of networking. To figure this out I reached for Wireshark to capture all the traffic on the device.
To capture traffic on a remote device we need to make it visible to Wireshark. Apple provides a utility called Remote Virtual Interface Tool, rvictl
for short, which does just this. With rvictl
we can create a virtual interface on a mac that will capture all packets sent by a mobile device attached via USB. To do this we first need to find the UUID of the device. It’s available in Xcode under Window > Device and Simulators
but can also be found with the ideviceinfo
utility from libimobiledevice
.
Now we can create a virtual interface for the device.
This will create a virtual network interface typically called rvi0
which we can configure Wireshark to use. Mission accomplished, right? Not quite. While we are capturing the traffic, all HTTPS traffic from a custom stack will be encrypted and opaque to us. With DNS queries in the clear we get a few more clues. TLS also reveals a tiny bit of information but the majority of the traffic is hidden behind encryption.
Cracking the Crypto
The specific app that I was looking at used BoringSSL for TLS. After some Googling, I found a blog post by Andy Davies which described a method to dump the TLS sessions keys from BoringSSL. Andy’s method relies on knowing a specific offset for the keylog callback function in the SSL_CTX
struct and hooking SSL_CTX_set_info_callback
to set this value.
I realised that all SSL_CTX
are created via the SSL_CTX_new
function and that there’s a dedicated function, SSL_CTX_set_keylog_callback
, for setting the keylog callback. With this knowledge we can hook SSL_CTX_new
and get a pointer to every SSL_CTX
from its return value. Then we can simply call SSL_CTX_set_keylog_callback
directly without needing to rely on known offsets, which are brittle. I’ve created a Frida Codeshare that does this. This method relies on us being able to find pointers to the two functions SSL_CTX_new
and SSL_CTX_set_keylog_callback
in the binary, luckily for me these were both external symbols of a dynamic framework.
With the above Frida snippet we can dump the TLS session keys the app generates, while running a TCP dump on rvi0
to capture the encrypted traffic.
Then in a different terminal.
Where inject.js
calls startTLSKeylogger
from the code share.
After capturing some traffic we can verify that the keys work with tshark
as follows.
If they do simply open Wireshark with the captured dump and the keylog.
Voila, decrypted HTTPS traffic.