Andy Davies

Independent Web Performance Consultant

Capturing and Decrypting HTTPS Traffic From iOS Apps Using Frida

I often want to examine the web traffic generated by browsers, and other apps.

Sometimes I can use the tools built into browsers, other times proxies, but when I want to take a deeper look and particularly if I’m looking at how a browser is using HTTP/2, I rely on packet captures.

One challenge with analysing HTTP/2 traffic is that it’s encrypted and while Chrome and Firefox support logging TLS keys and tools like Wireshark can then decrypt the traffic.

Safari and iOS doesn’t have this feature natively, and proxies like Charles only communicate to the browser via HTTP/1.x so I needed to find another solution.

In this post I walk through how I capture iOS apptraffic using tcpdump, and how I use a Frida script to extract the TLS keys during the capture so that I can decrypt the traffic too.

Capturing iOS network traffic

Apple support capturing iOS device network traffic via a Remote Virtual Interface (RVI). This mirrors the network traffic from the device to a virtual interface in MacOS and from there the traffic can be captured using tools like tcpdump and Wireshark.

You can find more details in this article from Apple but the basic process is:

  • If they’re not already installed, install xcode’s command line tools:
1
xcode-select --install

If you’re unsure whether you have the command line tools installed, run the above command anyway and it will display an error message if they are already installed.

  • Attach an iOS device, and grab it’s UDID

I can’t remember where I got the command line below from but it extracts the device’s UDID from the system_profiler output

1
2
3
system_profiler SPUSBDataType | sed -n -e '/iPad/,/Serial/p;/iPhone/,/Serial/p;/iPod/,/Serial/p' | grep "Serial Number:" | awk -F ": " '{print $2}'

b0e8fe73db17d4993bd549418bfbdba70a4af2b1
  • Start the Remote Virtual Interface
1
2
3
rvictl -s b0e8fe73db17d4993bd549418bfbdba70a4af2b1

Starting device b0e8fe73db17d4993bd549418bfbdba70a4af2b1 [SUCCEEDED] with interface rvi0
  • Capture traffic using tcpdump (or Wireshark)
1
tcpdump -i rvi0 -w capture.pcap -P

The -P option is an Apple extension that captures the traffic in pcap-ng format, and includes metadata such as process name, pid etc. against each packet.

Unfortunately Wireshark can’t display or filter by this data yet but I’m hoping someone might implement support for it soon. Apple’s tcpdump can display it, see the -k option in man pages for more details.

  • Generate some network traffic

Open an app on the device e.g. Safari, and generate some network traffic to a site that uses HTTPS e.g. https://www.bbc.co.uk/news

When your done hit Ctrl-C in the terminal to stop tcpdump capturing.

  • Open the packet dump in Wireshark

If you don’t already have Wireshark installed, download and install it from https://www.wireshark.org, and then open the pcap:

1
open capture.pcap

You should see a screen something like this:

Wireshark showing an encrypted packet capture for bbc.co.uk/news

I’ve filtered the capture to just display the traffic to and from www.bbc.co.uk. But as the traffic is encrypted using TLS 1.2 we can’t see the contents of the packets.

To decrypt the packets we need the matching TLS keys, Chrome and Firefox will provide these when the SSLKEYLOGFILE environment variable is set but unfortunately there seems to be no equivalent for Safari.

Fortunately thanks to tools like Frida, we have the ability to implement it ourselves.

Extracting TLS Keys and Decrypting iOS Traffic

Frida describes itself as a “dynamic instrumentation toolkit”, it 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.

I’m testing with a Jailbroken iPhone 5S running iOS12.4.3, with Frida installed from Cydia. It’s possible to use a non-jailbroken device if you can include Frida’s libraries in the app - either via debugging an app you own, or repackaging someone else’s app and injecting the dylib.

As I want to decrypt Safari’s traffic I’m sticking with the Jailbroken phone.

  • Follow the Frida installation instructions for MacOS and iOS

MacOS CLI - https://frida.re/docs/installation/

iOS Device - https://frida.re/docs/ios

  • Check Frida is installed and working by listing the currently running apps
1
frida-ps -Ua

-U option instructs Frida to attach to a USB device

  • In one terminal window start the frida script:

The script for extracting the keys is hosted on Frida Code Share, I’ll walk through the process of how it works later in the post.

1
frida -U -n com.apple.WebKit.Networking --codeshare andydavies/ios-tls-keylogger -o bbc-news.keylog

The -o option writes the output of the script to a file, but it’s also mirrored to the console too so you can see the output as it happens too.

Safari’s networking happens in a separate process so the command above connects to that process rather than Safari itself.

Also the first time you use a script from codeshare you’ll be prompted whether to trust it.

If you want to inspect a different app frida-ps -Uai will list many of the apps available, and you can also select the app via Process ID too.

  • In a second window start the TCP capture
1
tcpdump -i rvi0 -w bbc-news.pcap -P
  • Open the app you want to decrypt the traffic from and generate some traffic

In this example I’m using Safari and https://www.bbc.co.uk/news

  • Terminate the tcpdump, and frida commands, and then use exit to quit the Frida REPL

You should have two files, in this example they will be bbc-news.pcap and bbc-news.keylog

  • Open the packet capture and provide the keylog as an option
1
wireshark -r bbc-news.pcap -o tls:keylog_file:bbc-news.keylog

You can also launch Wireshark, open the packet capture, and then specify the keylog in Preferences > Protocols > TLS > (Pre)-Master-Secret log filename

You should see a screen something like this:

Wireshark showing a decrypted packet capture for bbc.co.uk/news

I’ve filtered the capture to just display HTTP and HTTPS traffic and highlighted the start of one of the decrypted HTTP/2 connections.

How the Frida Script Works

I first came across Frida a few years when someone shared Tom Curran and Marat Nigmatullin’s paper on TLS Session Key Extraction from Memory on iOS Devices.

Tom and Marat used Frida to hook the CoreTLS function (tls_handshake_internal_prf) that generated key material and dump the relevant TLS keys.

Although I got a modified version of their code working well enough to inspect some apps, and talk about it at JSOxford I never quite managed to address extracting keys for resumed TLS sessions and some other cases where it didn’t work.

In iOS11, Apple migrated from OpenSSL to Google’s BoringSSL so Tom and Marat’s code stopped working but the ideas it introduced remained valid and compared to OpenSSL the BoringSSL code easier to understand.

And as Chrome supports TLS key logging so BoringSSL already contains code to log TLS keys.

Searching the code for the labels that are defined in key log format finds examples like this one in handshake.cc where the CLIENT_RANDOM values are being logged:

1
2
3
4
5
  // Log the master secret, if logging is enabled.
  if (!ssl_log_secret(ssl, "CLIENT_RANDOM", session->master_key,
                      session->master_key_length)) {
    return 0;
  }

ssl_log_secret is defined in ssl_lib.c – it checks whether a callback function for logging is defined and if it is, builds the log line and calls the callback with log line.

ssl_lib.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
int ssl_log_secret(const SSL *ssl, const char *label, const uint8_t *secret,
                   size_t secret_len) {

  if (ssl->ctx->keylog_callback == NULL) {
    return 1;
  }

  ScopedCBB cbb;
  uint8_t *out;
  size_t out_len;
  if (!CBB_init(cbb.get(), strlen(label) + 1 + SSL3_RANDOM_SIZE * 2 + 1 +
                          secret_len * 2 + 1) ||
      !CBB_add_bytes(cbb.get(), (const uint8_t *)label, strlen(label)) ||
      !CBB_add_bytes(cbb.get(), (const uint8_t *)" ", 1) ||
      !cbb_add_hex(cbb.get(), ssl->s3->client_random, SSL3_RANDOM_SIZE) ||
      !CBB_add_bytes(cbb.get(), (const uint8_t *)" ", 1) ||
      !cbb_add_hex(cbb.get(), secret, secret_len) ||
      !CBB_add_u8(cbb.get(), 0 /* NUL */) ||
      !CBB_finish(cbb.get(), &out, &out_len)) {

    return 0;
  }

  ssl->ctx->keylog_callback(ssl, (const char *)out);

  OPENSSL_free(out);

  return 1;
}

To enable TLS key logging, it appears we just need to be able to set a logging callback function.

The logging callback is set via SSL_CTX_set_keylog_callback but unfortunately this doesn’t seem to be included in Apple’s version of BoringSSL – to check, I extracted libboringssl.dylib from the shared dylib cache and disassembled it using Hopper but couldn’t find the function.

ssl_lib.c \
1
2
3
4
void SSL_CTX_set_keylog_callback(SSL_CTX *ctx,
                                 void (*cb)(const SSL *ssl, const char *line)) {
  ctx->keylog_callback = cb;
}

Reading the source code and tracing the execution of com.apple.WebKit.Networking using frida-trace, I came across SSL_CTX_set_info_callback

1
frida-trace -U com.apple.WebKit.Networking -I 'libboringssl.dylib'

SSL_CTX_set_info_callback appears to be called once per task and gets passed the address of the struct containing the pointer to logging callback function:

ssl_session.c
1
2
3
4
void SSL_CTX_set_info_callback(
    SSL_CTX *ctx, void (*cb)(const SSL *ssl, int type, int value)) {
  ctx->info_callback = cb;
}

If we create our own logging function, and wrap it in a native callback

1
2
3
4
5
6
7
// Logging function, reads null terminated string from address in line
function key_logger(ssl, line) {
   console.log(new NativePointer(line).readCString());
}

// Wrap key_logger JS function in NativeCallback
var key_log_callback = new NativeCallback(key_logger, 'void', ['pointer', 'pointer']);

We can then intercept calls to ‘SSL_CTX_set_info_callback` and write the address of the native callback created above into the relevant entry in the SSL struct:

1
2
3
4
5
6
7
8
9
10
11
12
var CALLBACK_OFFSET = 0x2A8;

var SSL_CTX_set_info_callback = Module.findExportByName("libboringssl.dylib", "SSL_CTX_set_info_callback");

Interceptor.attach(SSL_CTX_set_info_callback, {
   onEnter: function (args) {
       var ssl = new NativePointer(args[0]);
       var callback = new NativePointer(ssl).add(CALLBACK_OFFSET);

       callback.writePointer(key_log_callback);
   }
});

CALLBACK_OFFSET was determined by disassembling libboringssl.dylib, and like all magic numbers is fragile as it may change if the struct changes in future versions, or on different CPU architectures.

The completed code (TBH it’s more comments than code) is available from https://codeshare.frida.re/@andydavies/ios-tls-keylogger/ under an MIT License so feel free to build on it, incorporate it into other utilities etc.

Further Reading

The First Few Milliseconds of an HTTPS Connection

Technical Q&A QA1176 Getting a Packet Trace

NSS Key Log Format

TLS Session Key Extraction from Memory on iOS Devices

Inspecting iOS App Traffic with JavaScript - JSOxford - Jan 2018

BoringSSL

Frida

Hopper Disassembler

Comments