Skip to main content

Decrypting iOS TLS traffic

Let's say I want to intercept an iPhone's network traffic, both the traffic from apps and from system components, in order to research the protocols (HTTP or otherwise). Most interesting traffic will be encrypted with TLS, so I'll have to decrypt it.

Proxies

A popular way to do this is to use a proxy, like mitmproxy. I run mitmproxy on my computer, set my device's proxy settings to point at it, and configure the proxy's CA certificate as trusted. Then I can see the intercepted HTTP requests in mitmproxy's UI.

One problem with this approach is that any application using certificate pinning for security reasons won't accept the proxy's CA certificate, and will reject connections. On a jailbroken device, there are tweaks I can inject into applications to disable pinning, but there are many ways to implement cert pinning and no tweak seems to reliably neutralize them all.

Another problem is that tools like mitmproxy only support HTTP, not other protocols that may also be using TLS. This is annoying since, as I'll show in later blog posts, I'm especially interested in looking at the push notification service, which uses a proprietary "APNS" protocol with TLS encryption. There is a man-in-the-middle proxy for APNS called pushproxy, but it doesn't work for current iOS versions (and I would still need to deal with the certificate pinning issue).

Wireshark

Another option is to passively capture the network traffic and analyze it with Wireshark, which can decrypt TLS if we give it the session keys.

To intercept the raw network traffic, I can run a software Wi-Fi access point on my computer, make my phone connect to it, and run Wireshark on the Wi-Fi interface. On a Mac there are other ways to get an iOS packet trace.

But how do we get TLS session keys to decrypt the traffic? We'll need a jailbroken iPhone, but that's something we would have needed to bypass certificate pinning anyway, if we used the MitM proxy method. (Maybe it can be done without jailbreaking for App Store apps, but for system software there's no way around it.)

Getting keys with Frida

I found a blog post by Andy Davies about Capturing and Decrypting HTTPS Traffic From iOS Apps Using Frida. "Frida injects a JavaScript VM into applications and we can write JS code that runs in that VM and can inspect memory and processes, intercept functions, create our own native functions etc."

BoringSSL (based on OpenSSL, used by Apple on iOS nowadays) already has code to dump session keys in the format expected by Wireshark. In Andy's blog post, he injects a Frida script into the application that sets the BoringSSL "keylog callback" to a custom function, which then outputs the keys to a file.

This Frida script uses a single hardcoded offset to find the keylog callback function pointer inside the SSL_CTX structure, which means it will only work on certain iOS versions. The SSL_CTX_set_keylog_callback function is how you'd usually set it, but it's not present in any Apple libboringssl.dylib version I looked at.

I haven't tried Frida yet (although it looks useful and I really should try it). But it seems a disadvantage here is that you have to inject Frida into the specific process whose traffic you want to watch, and it wouldn't be easy to inject it into everything.

Getting keys with a tweak

As an alternative, I wrote a MobileSubstrate tweak to get TLS session keys from iOS processes. You can get it from the sslkeylog GitLab repository, or my Cydia repo at
https://data.nicolas17.xyz/cydia/.

It has several advantages over the Frida script:

  • It's injected into every process in the system, instead of only the one process where you attach Frida.
  • It works on all 64-bit iOS versions between 12.0 and 14.4, by including a list of offsets for the BoringSSL keylog callback for all those versions.
  • It supports not only BoringSSL but also the older "SecureTransport" Apple framework. Notably, the push notification daemon still uses SecureTransport in iOS 12.

My first version just dumped the keys to the syslog, but it was annoying to parse them out of the syslog messages. Then I added a command-line tool to get the keys more easily. The way it works is that the injected tweak extracts the keys, and sends them to an sslkeylogd daemon over XPC. At any time you can run the sslkeylog command line tool (eg. over ssh), and it will connect to the daemon and start printing out the keys that the tweak extracts.

You can either get the keys in real time over ssh, or redirect them into a file and copy the file off the device later.

Tutorial

Here is how to capture the iPhone's network traffic on Linux and decrypt the network traffic. Requires a Linux computer with Wi-Fi (I use an external USB dongle) and a jailbroken iPhone. (Of course this should work on iPad and iPod touch too, I just say "iPhone" for simplicity).

First, install sslkeylog on iOS. You can add https://data.nicolas17.xyz/cydia/ as a Cydia repository source, or get the .deb file from the Releases page. Make sure the mobilesubstrate dependency gets installed too.

If you want to get keys from system daemons, you should now reboot the iPhone. This is the best way to ensure every system daemon restarts and loads the tweak. But it's not necessary if you're only interested in a particular app.

As a test, you can now connect to the phone via SSH and try dumping keys. If you used checkra1n to jailbreak, you may have to proxy it over USB, using iproxy or inetcat from the libimobiledevice package.

ssh -o 'ProxyCommand=inetcat 44' mobile@iPhone sslkeylog

It will print "subscribed" at first. Make some TLS traffic (eg. open Safari and load a website). You should see TLS session keys like this:

CLIENT_HANDSHAKE_TRAFFIC_SECRET 99cf2fabcc272914ae2c93a3603864a505e76092a6511176d7ae2f7f9c6058f7 37bed74ebb5c40dd525ae480adfe02d23e3d0a4c9083d8767f35cbbf23852da4
SERVER_HANDSHAKE_TRAFFIC_SECRET 99cf2fabcc272914ae2c93a3603864a505e76092a6511176d7ae2f7f9c6058f7 1cd72af732da492b02568ae75e9d67ed394d50061fe6a56dfe5d847633e6a549
CLIENT_TRAFFIC_SECRET_0 99cf2fabcc272914ae2c93a3603864a505e76092a6511176d7ae2f7f9c6058f7 63164e02b24232e34a5cf50d7f3ee92e82700393f4a838e8b561770dc835990d
SERVER_TRAFFIC_SECRET_0 99cf2fabcc272914ae2c93a3603864a505e76092a6511176d7ae2f7f9c6058f7 452f5a90a24b5a857bbe38ab682790e379cfd6fcd1fcf599837662e3f3ffe798
EXPORTER_SECRET 99cf2fabcc272914ae2c93a3603864a505e76092a6511176d7ae2f7f9c6058f7 845b6799e8dde45012a69d6805bf950503d17a5f5ce64d77b918b1d9aa5e2eaa
CLIENT_RANDOM 6088F6D60B7034F82E9D20CD833D2DFC2F34A7A91D618348BE1C07F668B4DB4F A2F9AF6082BB09FD37C504845F73094966B44ACED72A96D2C3BD5FA2CB475A4F59031298A9BD517344ECDD0B73764786

Simply use Ctrl-C to stop it.

Let's do some actual packet capturing now. We will run a Wi-Fi access point on the Linux computer and bridge it to the Internet connection, so we can then let the iPhone connect to it and capture packets from the Wi-Fi interface.

On your Linux system, install hostapd. Create a config file hostapd.conf:

# the interface used by the AP, replace as needed
interface=wlan0
bridge=br0

# the name of the AP
ssid=iPhoneCapture
# Driver interface type (hostap/wired/none/nl80211/bsd)
driver=nl80211
# Country code (ISO/IEC 3166-1)
country_code=AR

# 1=wpa, 2=wep, 3=both
auth_algs=1
# 1=WPA, 2=WPA2, 3=both
wpa=2

# Set of accepted cipher suites; disabling insecure TKIP
wpa_pairwise=CCMP
# Set of accepted key management algorithms
wpa_key_mgmt=WPA-PSK
# Network password
wpa_passphrase=helloworld

Ensure IP forwarding is enabled, and that the firewall allows forwarding. Forgetting this step caused me big troubleshooting headaches...

sudo sysctl net.ipv4.ip_forward=1
sudo iptables -P FORWARD ACCEPT

Create the network bridge, and add your normal Internet connection network interface to it (replace enp1s0 if needed):

sudo ip link add name br0 type bridge
sudo ip link set br0 up
sudo ip link set enp1s0 master br0

Finally, start the access point:

sudo hostapd hostapd.conf

It will automatically add the Wi-Fi network interface to the bridge too. When hostapd prints AP-ENABLED, your iPhone should be able to see the network in Wi-Fi settings; but it's better to start capturing packets before connecting the phone to the network to ensure you get the traffic of all connections from the beginning, so don't connect it yet.

To start dumping TLS keys, run the ssh command mentioned above, but sending the output to a file:

ssh -o 'ProxyCommand=inetcat 44' mobile@iPhone sslkeylog | tee /tmp/ssl.log

Now open Wireshark. Go to Edit → Preferences → Protocols → TLS (may be called "SSL" in older Wireshark versions). Set the "(Pre)-Master-Secret log filename" to /tmp/ssl.log and save preferences.

Wireshark TLS preferences

Make Wireshark start capturing from the Wi-Fi interface, and then finally connect the iPhone to the AP. You should see keys start appearing in your ssh sskleylog | tee terminal session, and all new TLS connections in Wireshark should be getting decrypted.

Wireshark showing TLS-encrypted HTTP2 requests from iOS

When you're done, you can kill hostapd with Ctrl-C, and you can delete the bridge with sudo ip link delete br0. Killing hostapd sometimes makes the Internet connection on my computer stop working (I haven't figured out the exact conditions), deleting the bridge fixes that.