SSL Kill Switch and Twitter iOS

Some time ago, SSL Kill Switch somehow stopped working on the Twitter iOS App, preventing the interception and decryption of the App’s HTTPS traffic for reverse-engineering purposes. Twitter was actually one of the first iOS Apps to implement SSL pinning, and I remember using it as a test App when I started working on the first version of SSL Kill Switch, a few years ago.

I finally took the time to investigate this issue and just released v0.10 of SSL Kill Switch, which will work on Twitter iOS again (and any CocoaSPDY App).

Why it stopped working

SSL Kill Switch broke when Twitter updated their iOS App to use the SPDY protocol to replace HTTPS when connecting to its server endpoints. This explains why this specific App was not affected by SSL Kill Switch, although the tool was able to disable SSL pinning in every other App on the Store and even Apple daemons and services.

Twitter open-sourced their SPDY implementation for iOS/OS X, CocoaSPDY, which helped me figure out what wasn’t working.

Act 1: CocoaSPDY and SSL Validation

At a very high level, SSL Kill Switch disables SSL validation and pinning by preventing Apps from being able to set the SecureTransport callback for validating the server’s certificate chain; it instead sets a callback that skips all SSL validation, making it easy to intercept and decrypt the App’s HTTPS traffic using a proxy like Burp or Charles.

However, CocoaSPDY does not rely on this callback mechanism, and instead always retrieves and validates the server certificate at the end of the TLS handshake, using the CFStream APIs:

- (void)_onTLSHandshakeSuccess
{
    [...]
        bool acceptTrust = YES;
        [...]
        // Get the server's certificate chain
        SecTrustRef trust = (SecTrustRef)CFReadStreamCopyProperty(_readStream, kCFStreamPropertySSLPeerTrust);
        // Validate the certificate chain
        acceptTrust = [_delegate socket:self securedWithTrust:trust];
    [...]
        if (!acceptTrust) {
            // Close the connection if the validation failed
            [self _closeWithError:SPDY_SOCKET_ERROR(SPDYSocketTLSVerificationFailed, @"TLS trust verification failed.")];
            return;
        }

This is why SSL Kill Switch did not affect CocoaSPDY (and Twitter iOS) at all and a new strategy was needed in order to disable SSL validation. Unfortunately, I did not find a way to disable SSL validation in a generic fashion, for code that validates the certificate by retrieving it with kCFStreamPropertySSLPeerTrust. Hence, I had to implement a CocaSPDY-specific workaround.

Within the library, certificate validation can be customized by creating a class that conforms to the SPDYTLSTrustEvaluator Objective-C protocol, and setting an instance of this class as SPDYProtocol’s trust evaluator, using the + setTLSTrustEvaluator: method. Then, all SPDY connections call into this trust evaluator when the server’s certificate chain needs to be validated.

To disable this mechanism, I simply had to update SSL Kill Switch override the + setTLSTrustEvaluator: method to force a nil trust evaluator (which CocoaSPDY sees as a trust-all evaluator):

// Will contain the original setTLSTrustEvaluator method
static void (*oldSetTLSTrustEvaluator)(id self, SEL _cmd, id evaluator);

// Our replacement method for setTLSTrustEvaluator
static void newSetTLSTrustEvaluator(id self, SEL _cmd, id evaluator)
{
    // Set a nil evaluator to disable SSL validation
    oldSetTLSTrustEvaluator(self, _cmd, nil);
}
[...]

// SSL Kill Switch's initialization function
__attribute__((constructor)) static void init(int argc, const char **argv)
{
    [...]
    // Is CocoaSPDY loaded in the App ? 
    Class spdyProtocolClass = NSClassFromString(@"SPDYProtocol");
    if (spdyProtocolClass)
    {
        // Disable trust evaluation
        MSHookMessageEx(object_getClass(spdyProtocolClass), NSSelectorFromString(@"setTLSTrustEvaluator:"), (IMP) &newSetTLSTrustEvaluator, (IMP *)&oldSetTLSTrustEvaluator);

This ensured that CocoaSPDY connections would not validate SSL certificates as soon as SSL Kill Switch was loaded in the App. However, I ran into another problem as I was still not able to intercept Twitter’s HTTPS traffic using the Burp proxy.

Act 2: SPDY Proxy-ing

After disabling SSL validation, I was still not able to see the traffic in Burp for a very simple reason: Burp does not support the SPDY protocol. Although I later realized that there are other proxy tools that do support SPDY (such as SSLSplit), I wanted to use Burp for what I needed to do.

Luckily, I noticed that CocoaSPDY is implemented in a way that makes it very easy to deploy SPDY within any App: it leverages the powerful NSURLProtocol API in order to transparently transform the App’s outgoing HTTPS requests into SPDY requests. Hence, all it takes to turn any iOS App into a SPDY App is a few lines of code:

// For NSURLConnection
[SPDYURLConnectionProtocol registerOrigin:@"https://api.twitter.com:443"];

// For NSURLSession
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
configuration.protocolClasses = @[[SPDYURLSessionProtocol class]];

This is very neat, and also gave me the idea to just disable these initialization routines in SSL Kill Switch in order to turn the App’s traffic back into HTTPS, which Burp obviously supports. For example, this is how SSL Kill Switch downgrades NSURLSession connections from SPDY to HTTPS:

static void (*oldSetprotocolClasses)(id self, SEL _cmd, NSArray <Class> *protocolClasses);
static void newSetprotocolClasses(id self, SEL _cmd, NSArray <Class> *protocolClasses)
{
    // Do not register protocol classes which is how CocoaSPDY works
    // This should force the App to downgrade from SPDY to HTTPS
}
[...]

// SSL Kill Switch's initialization function
__attribute__((constructor)) static void init(int argc, const char **argv)
{
    [...]                
    MSHookMessageEx(NSClassFromString(@"NSURLSessionConfiguration"), NSSelectorFromString(@"setprotocolClasses:"), (IMP) &newSetprotocolClasses, (IMP *)&oldSetprotocolClasses);
    [...]

After disabling CocoaSPDY’s initialization methods in SSL Kill Switch, I was finally able to use Burp to intercept Twitter iOS’ network traffic:

Try it out!

You can get the latest version of SSL Kill Switch on the project’s page.

February 21, 2016
ios, ssl

iOS Application Security at No Starch

I was the Technical Reviewer for David Thiel’s “iOS Application Security”.

The book is now available at No Starch Press and goes in great details about how to spot security issues affecting iOS Apps and how to avoid making these mistakes when building an App; it’s a great book!

Get it from No Starch’s website.

February 13, 2016
ios

SSLyze v0.13.3 Released

A new version of SSLyze is available. I made lots of changes and refactoring to SSLyze (and its OpenSSL wrapper)’s internals, which were required in order to be able to:

  • Add SSLyze to PyPi, at long last. This means that you can now install SSLyze by just running pip install sslyze on OS X, Linux and Windows. No more ZIP files to download!
  • Turn SSLyze into a Python library: you can now run the same scan commands as the command line tool and process the results directly from Python!

Usage a Python library

Using SSLyze as a library is a three step process:

  1. Configure the list of servers to scan and ensure that you can connect to them. This is implemented via the ServerConnectivityInfo class.
  2. Start SSLyze’s process pool (using the PluginsProcessPool class) and queue some scan commands for each server. The commands are the same as the ones available in the CLI; you can get a list by running the CLI with --help. Each scan command will be run concurrently within the process pool.
  3. Retrieve the result of each scan command and process it. Each result is a subclass of PluginResult with attributes that contain the actual result of the scan command run on the server (such as list of supported cipher suites for the --tlsv1 command). These attributes are specific to each plugin and command but are all documented (within each plugin’s module).

The following is a simple example to retrieve the list of SSL 3.0 cipher suites supported by smtp.gmail.com:587:

# Script to get the list of SSLv3 cipher suites supported by smtp.gmail.com
hostname = 'smtp.gmail.com'
try:
    # First we must ensure that the server is reachable
    server_info = ServerConnectivityInfo(hostname=hostname, port=587,
                                         tls_wrapped_protocol=TlsWrappedProtocolEnum.STARTTLS_SMTP)
    server_info.test_connectivity_to_server()
except ServerConnectivityError as e:
    raise RuntimeError('Error when connecting to {}: {}'.format(hostname, e.error_msg))

# Get the list of available plugins
sslyze_plugins = PluginsFinder()

# Create a process pool to run scanning commands concurrently
plugins_process_pool = PluginsProcessPool(sslyze_plugins)

# Queue a scan command to get the server's certificate
plugins_process_pool.queue_plugin_task(server_info, 'sslv3')

# Process the result and print the certificate CN
for server_info, plugin_command, plugin_result in plugins_process_pool.get_results():
    if plugin_result.plugin_command == 'sslv3':
        # Do something with the result
        print 'SSLV3 cipher suites'
        for cipher in plugin_result.accepted_cipher_list:
            print '    {}'.format(cipher.name)

More complex example are available in the project’s repository.

New plugins

Thanks to contributors davidgfnet and bcyrill, SSLyze has two new scan commands:

Full Changelog

Head to the project’s Releases tab for the full changelog.

As explained before, I will no longer upload ZIP files with the releases (except for the Windows py2exe build); PyPi or directly cloning the repo are now the right ways to install SSLyze.

February 01, 2016
ssl, sslyze

Burp and iOS 9 App Transport Security

When trying to intercept an iOS 9 App’s traffic using Burp (or any other proxy software), you might run into issues caused by App Transport Security, a new security feature introduced in iOS 9.

By default, App Transport Security will cause most Apps on iOS 9 to only allow network connections to servers with a strong SSL configuration:

  • TLS version 1.2
  • Perfect forward secrecy cipher suites
  • RSA 2048+ bits for the certificate chain

By default Burp does not support connections with these settings - TLS 1.2 is disabled by default - so most iOS 9 Apps will not be able to connect to Burp, when trying to proxy the App’s network traffic.

To address that, enable TLS 1.2 and the right cipher suites in Burp’s SSL settings:

Also, Burp’s default CA certificate is only 1024 bits, so you will have to generate a 2048 bits certificate and private key and import it into Burp (Proxy -> Options -> Import / Export CA certificate).

After updating Burp’s configuration and CA key pair, you should then be able to proxy iOS 9 Apps without any problem.

December 01, 2015
ios

SSL Kill Switch v0.8 Released

With the unexpectedly quick release of an iOS 9 jailbreak, users reported a boot loop issue when installing SSL Kill Switch. I think this was caused by underlying changes to the OS, rather than the tweak itself. Saurik subsequently updated Cydia Substrate and apparently made some changes to MobileLoader, the tweak injection mechanism, as SSL Kill Switch stopped working with following error in the logs:

MS:Error: extension does not have filter

As opposed to previous versions of Cydia Substrate, it seems like tweaks now must provide a plist filter or they won’t be loaded into any processes. Hence, I added the following filter for SSL Kill Switch:

Filter = {
  Bundles = (com.apple.UIKit);
};

This fixed the issue, with the side-effect that SSL Kill Switch will no longer be injected into processes that don’t link UIKit (ie. processes with no UI, such as system daemons like itunesd). If you need to use SSL Kill Switch in a daemon, you will need to modify the filter first and then recompile the tweak.

Go to the project’s Releases page to download the new version.

October 29, 2015
ios

TrustKit 1.2.0 for iOS 9 Released

I just released TrustKit 1.2.0, which adds support for iOS 9. As explained in a previous blog post, Apple implemented a behind-the-scene change in iOS 9 that broke TrustKit (and other tools).

Changelog

  • Complete re-write of the hooking strategy to automatically add SSL pinning to the App’s connections. TrustKit now swizzles NSURLSession and NSURLConnection delegates to add pinning validation to the delegate’s authentication handler methods; for developers who want to call into TrustKit manually, this behavior can be disabled using the TSKSwizzleNetworkDelegates setting. This change was made due to the previous hooking strategy (targeting SecureTransport) not working on iOS 9.
  • The pinning policy format has slightly changed, in order to add new global settings: TSKSwizzleNetworkDelegates, TSKIgnorePinningForUserDefinedTrustAnchors, TSKPinnedDomains. If you have an existing pinning policy for TrustKit 1.1.3, all you need to do is put it under the TSKPinnedDomains key.
  • Greatly simplified the TSKPinningValidator API to make it easy to write authentication handlers that enforce the App’s SSL pinning policy. Sample code describing how to do it is available in the documentation.
  • Updated Xcode project settings: stricter warnings, enabled bitcode, separate iOS and OS X build schemes.
  • Pinning failure reports now also send the IDFV in order to simplify the troubleshooting of errors, by being able to detect a single, malfunctioning device.

More information is available on the project’s github page.

Migrating from a previous version

If you were already using TrustKit in your App, here are a few things to take into account when updating to 1.2.0:

  • The new hooking technique based on swizzling NSURLSession and NSURLConnection delegates will only protect connections initiated via these APIs, while the previous implementation would also work on other APIs such as UIWebView and NSStream. The updated Getting Started guide has guidelines on how to leverage TrustKit for these network APIs.
  • As explained in the changelog, the policy format has slightly changed. For your existing policy to work on 1.2.0, you just need to put it under the TSKPinnedDomains key.
  • If you don’t want TrustKit to auto-magically try to enforce SSL pinning on your App’s connections, you can now disable this behavior by setting the TSKSwizzleNetworkDelegates to NO, and instead call into TrustKit manually within your App’s authentication handlers, using the TSKPinningValidator class.

TrustKit and PayPal

Unrelated to the 1.2.0 release but of particular interest is the fact that TrustKit was featured in a post about “Key Pinning in Mobile Applications” on PayPal’s engineering blog!

October 20, 2015
ios

TrustKit, iOS 9 And The Shared Cache

In August, Angela Chow, Eric Castro and myself released TrustKit at the Black Hat conference: an iOS & OS X library designed to make it very easy to deploy SSL pinning within an App.

When Apple released iOS 9 last month, it broke TrustKit; this post explains the behind-the-scene change that caused this and why it affected TrustKit.

TrustKit on iOS 8

As explained in our Black Hat talk, TrustKit worked by patching SecureTransport’s SSLHandshake() function at runtime. During the SSL handshake, this function gets called multiple times until the handshake is completed. Throughout these calls, TrustKit just forwarded them to the real SSLHandshake(), until it returned noErr, which means the handshake was succesful.

When that specific return value was detected and before returning it to the caller, TrustKit performed SSL pinning validation and changed the return value to an error if the validation had failed.

Here is how it looked like in TrustKit:

// Our replacement function for SSLHandshake()
static OSStatus replaced_SSLHandshake(SSLContextRef context)
{
    // First let's forward the call to the real SSLHandshake()
    OSStatus result = original_SSLHandshake(context);
    if (result == noErr)
    {
        // The handshake was sucessful
        // Let's perform SSL pinning validation
        
        // code starts here...
        // <code>
        // ...code ends here
    }
    return result;

Because every Apple Framework (NSURLConnection, NSURLSession, UIWebView, etc.) relies on SecureTransport for SSL, patching SSLHandshake() ensured that all of the App’s outgoing connections were automatically protected.

To do the actual hooking of SSLHandshake(), we used Facebook’s fishhook, which “provides functionality that is similar to using DYLD_INTERPOSE on OS X.”

What Changed in iOS 9

When iOS 9 was released, I received reports that TrustKit wasn’t working anymore, which was surprising to me as I had done some testing myself to ensure it worked fine.

I did some further investigation and discovered something very surprising: TrustKit would work fine on specific devices (such as the iPhone 4S or the iPad 3) but not on other devices (such as the iPhone 6), where TrustKit was successfully getting initialized but no pinning validation would actually be done.

This behavior implied that the hooking of SSLHandshake() was not actually working, although fishhook had been updated for iOS 9. After quite a lot of testing and googling, I luckily found the answer within a tweet from @comex from a few months back, talking about the beta of iOS 9:

To understand what this means, we first have look at how fishhook works. At a high level, fishhook modifies at runtime the process’ symbol table to have the symbol you want to hook point to your own code, instead of the original C function. Then, when any code wants to call that function, it looks up its symbol within the symbol table, which then points to your replacement function. Some more technical details about this symbol rebinding technique are available on the project’s page.

The iOS 9 change @comex described means that any code within the dyld shared cache will actually skip the symbol table and jump straight to the function itself, when calling another function in the cache. This was probably done as a performance optimization to skip the symbol lookup step.

Consequently, modifying the symbol table doesn’t affect calls happening within the shared cache (such as a method within NSURLConnection calling SecureTransport’s SSLHandshake(), as both frameworks live in the shared cache).

This explains why TrustKit stopped working on iOS 9:

  • Calls happening across Apple frameworks and libraries can no longer be hooked at all, meaning that TrustKit’s SSLHandshake() hook would only be triggered if the App itself directly calls this function.
  • This optimization can be enabled by Apple when compiling the shared cache, which is why TrustKit stopped working only on specific devices. It seems like the iOS 9 image for the iPhone 4S has the optimization disabled, while the one for the iPhone 6 has it enabled.

Unfortunately, this also means that some of the techniques we presented at the Black Hat talk for writing secure libraries through function hooking do not work on iOS 9. This also affects other Apps and products as I have been in contact with developers who faced the exact same issue.

What about Cydia Substrate and tweaks?

If you’re developing tweaks for jailbroken iOS and hooking C functions using Substrate, this change will not affect your code.

The reason for this is that, as explained in our talk, Substrate takes a different approach for hooking C functions: it actually modifies the instructions at the beginning of the function you want to hook, in order to insert some trampoline code to jump to your replacement function. Of course, this technique can only work on jailbroken devices (as it requires RWX memory pages). Therefore, Substrate’s hooking implementation will work regardless of who is calling the function (an Apple framework or the actual App).

What’s next for TrustKit ?

I will release TrustKit 1.2.0 for iOS 9 in the next few days, which has a completely different implementation for “hooking” into the App’s SSL connections to add pinning validation. I had to take the more conventional (but not as neat) approach of swizzling the App’s NSURLConnection and NSURLSession delegates.

In the release blog post which will come soon, I will provide more details about what has changed in TrustKit 1.2.0 and how to deploy it in your App; this version also brings some significant improvements over 1.1.3.

October 17, 2015
ios

Multipeer Connectivity on iOS 9

Apple just released iOS 9, which fixes a good number of security issues, and includes a mitigation for a man-in-the-middle attack on Multipeer Connectivity I presented last year at Black Hat US:

The issue was that an attacker on the network could downgrade the encryption level of a Multipeer Connectivity session configured with MCEncryptionOptional to MCEncryptionNone (ie. plaintext communication), even if authentication was enabled.

Apple did not fix the core issue with MCEncryptionOptional, as it would require significant changes to the implementation, in order to have each peer validate the security settings exchanged during the handshake after authentication is performed.

However they changed the default encryption level (used when the App does not explicitly specify one) from MCEncryptionOptional to MCEncryptionRequired, which is not vulnerable to the downgrade attack. Overall, this change should protect most Apps, but the ideal solution would have been to remove MCEncryptionOptional altogether.

September 19, 2015
ios

Apple Security Framework Wish List

While working on TrustKit, I ran into significant challenges when trying to implement SSL pinning the right way. The best practice is to pin the certificate’s Subject Public Key Info (ie. the public key and the key’s algorithm/type). However, the Security framework on iOS and OS X does not provide comprehensive APIs for parsing certificates or interacting with the OS’ trust store.

While I did manage to implement SPKI pinning in TrustKit, the workarounds I had to use are unsatisfying, and I hope that Apple can eventually extend the Security framework to make it easier to implement SSL pinning code.

Extracting the public key data from a certificate

On iOS, the only way to retrieve the public key bits from a certificate is convoluted:

  1. Create a SecTrustRef trust object with the certificate, and call SecTrustEvaluate() on it.
  2. Call SecTrustCopyPublicKey() on the trust object to retrieve a SecKeyRef of the public key.
  3. Add the public key to the device’s Keychain using SecItemAdd().
  4. Retrieve the public key from the Keychain and set kSecReturnData to YES in order to specify that the key’s data should be returned (instead of an opaque SecKeyRef).

Having to put data in Keychain just to retrieve the public key bits of a certificate seems really complex, slow and inefficient. On OS X, the key’s’ data can be retrieved by just calling SecCertificateCopyValues() on the certificate with kSecOIDX509V1SubjectPublicKey as the list of OIDs.

I had to implement both techniques in TrustKit within the pinning logic, which shows how different the code is depending on the platform.

See the bug on Open Radar.

Extracting the SPKI data from a certificate

On both iOS and OS X, there is no API to extract the Subject Public Key Info bits. On OS X, the SecCertificateCopyValues() function seemed like a good candidate in combination with the kSecOIDX509V1SubjectPublicKeyAlgorithm and kSecOIDX509V1SubjectPublicKeyAlgorithmParameters OIDs. However the function only returns a parsed output (such as the OID corresponding to the key algorithm) instead of the actual bytes of the SPKI.

Overall, the possible solutions here are far from ideal:

  • Parsing ASN1 manually, a non-starter
  • Embedding OpenSSL, which tends to quickly get dangerously outdated

In TrustKit, I took an unsatisfying but less dangerous approach: the developer has to specify the public key algorithm (RSA 2048, ECDSA, etc.) in the configuration. Then, TrustKit re-generates the SPKI by picking the bits corresponding to the algorithm from a hardcoded list, and combining it with the public key bits.

See the bug on Open Radar.

Detecting private trust anchors

When implementing SSL pinning, it is useful to not enforce pinning when a private trust anchor (ie. one that was manually added to the OS’ trust store) is detected in the server’s certificate chain. This is needed for allowing SSL connections through corporate proxies or firewalls and it is how Chrome operates in that scenario; stopping attackers with the ability to modify the OS (by adding certificates to the OS trust store, hooking SSL APIs, etc.) is not part of SSL pinning’s threat model.

There are no APIs on iOS to differentiate private trust anchors from system trust anchors (the ones shipped with the OS). Consequently, pinning can only be implemented in a way that will always block connections where pinning should not actually be enforced.

On OS X, the list of private trust anchors can be retrieved using SecTrustSettingsCopyCertificates() with the kSecTrustSettingsDomainUser and kSecTrustSettingsDomainAdmin domain settings. Then, this list can be used to detect a private trust anchor in the server’s certificate chain, as implemented in TrustKit.

See the bug on Open Radar.

August 11, 2015
ios

Introducing SSL Kill Switch 2

I recently started working on iOS SSL Kill Switch again, a tool which disables SSL validation and pinning in iOS Apps. I’ve implemented some significant changes including enabling support for OS X Apps, as well as adding the ability to disable TrustKit (the SSL pinning library I released at Black Hat).

To reflect all these changes, I’ve renamed the tool to “SSL Kill Switch 2” and moved the repository to a new GitHub page; the old repository will no longer be maintained.

August 08, 2015
ios, ssl