App flagged with bypassed SSL Pinning during Mobile App Penetration Test

Recently our app went through a series of Mobile App Penetration Test (MAPT), and was flagged with bypassed SSL Pinning (https://cwe.mitre.org/data/definitions/693.html).

The tester is using Frida and is able to attach to SSL_CTX_set_custom_verify() from libboringssl.dylib, as shown in this script (https://codeshare.frida.re/@federicodotta/ios13-pinning-bypass/).

As per my research, though I'm not absolutely sure, I see that boringSSL was added since iOS 11 (https://developer.apple.com/forums/thread/88387) and (https://github.com/firebase/firebase-ios-sdk/issues/314).

I would like to check if there is anyway around this, as I am using TrustKit (https://cocoapods.org/pods/TrustKit), and I realised many other pods also tag on SSL_CTX_set_custom_verify() for SSL Pinning.

As our app requires SSL Pinning, and a resolution to this issue, I would like to ask if there is any solution, whether it being a recommended pod/library, or a native solution (preferred) to do SSL Certificate Pinning.

Thank you.

Answered by DTS Engineer in 794937022

So, your goal is to increase security. Thus, you have one simple obvious path forward, after which things get more complex.

If you’re using an API that’s subject to App Transport security — that means URLSession and everything layered on top of it, most notably the web views — then you can apply pinning declaratively using the NSPinnedDomains property.

If that doesn’t work for you, post more details about what API you’re using and I can offer more specific advice.


Coming back to SSL_CTX_set_custom_verify, you are correct that Apple’s TLS implementation is currently based on an internal version of BoringSSL. However, that is an implementation detail; it’s not exposed as API. Rather, Apple APIs have their own mechanism for customising TLS server trust evaluation. For example, URLSession uses the authentication challenge mechanism.

Given that, you should not be linking to SSL_CTX_set_custom_verify yourself. And that speaks to the mechanism used by this security tool:

  • If the tool is based on static analysis, you should look at your code to see why it’s referencing SSL_CTX_set_custom_verify.

  • If the tool is based on dynamic analysis, then it’ll have to be smarter about how it treats SSL_CTX_set_custom_verify. For example, the following code, which doesn’t override TLS server trust evaluation, triggers a call to that routine on my Mac:

import Foundation
import Network

func main() {
    print("connection will start")
    let connection = NWConnection(to: .hostPort(host: "example.com", port: 443), using: .tls)
    connection.stateUpdateHandler = { newState in
        print("connection did change state, new: \(newState)")
    }
    connection.start(queue: .main)
    print("connection did start")
    withExtendedLifetime(connection) {
        dispatchMain()
    }
}

main()

That’s because, internally, Network framework uses Apple’s BoringSSL and it always calls SSL_CTX_set_custom_verify in order to apply Apple’s security policies.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

As our app requires SSL Pinning

Why is that? All Apple APIs implement a default TLS server trust evaluation, and many APIs implement additional security on top of that. Thus, your app gets good server trust evaluation by default. Are you trying to:

  • Increase security by adding to this default server trust evaluation?

  • Decrease security by disabling this default server trust evaluation?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

SSL Pinning is a requirement by our client's security department. The purpose is to ensure we are communicating with the intended server with the right SSL Certificate. This prevents any man-in-the-middle attacks.

We need to map each domains to a fixed set of SSL Certificates, and prevent any unknown server's SSL Certificate. The app would not proceed to communicate with a server if the domain doesnt match the SSL Certificate specified by the app.

This is to increase security, locking a domain to only a specified SSL Certificate, not allowing any other SSL Certificate, regardless if it is a valid/verified certificate.

So, your goal is to increase security. Thus, you have one simple obvious path forward, after which things get more complex.

If you’re using an API that’s subject to App Transport security — that means URLSession and everything layered on top of it, most notably the web views — then you can apply pinning declaratively using the NSPinnedDomains property.

If that doesn’t work for you, post more details about what API you’re using and I can offer more specific advice.


Coming back to SSL_CTX_set_custom_verify, you are correct that Apple’s TLS implementation is currently based on an internal version of BoringSSL. However, that is an implementation detail; it’s not exposed as API. Rather, Apple APIs have their own mechanism for customising TLS server trust evaluation. For example, URLSession uses the authentication challenge mechanism.

Given that, you should not be linking to SSL_CTX_set_custom_verify yourself. And that speaks to the mechanism used by this security tool:

  • If the tool is based on static analysis, you should look at your code to see why it’s referencing SSL_CTX_set_custom_verify.

  • If the tool is based on dynamic analysis, then it’ll have to be smarter about how it treats SSL_CTX_set_custom_verify. For example, the following code, which doesn’t override TLS server trust evaluation, triggers a call to that routine on my Mac:

import Foundation
import Network

func main() {
    print("connection will start")
    let connection = NWConnection(to: .hostPort(host: "example.com", port: 443), using: .tls)
    connection.stateUpdateHandler = { newState in
        print("connection did change state, new: \(newState)")
    }
    connection.start(queue: .main)
    print("connection did start")
    withExtendedLifetime(connection) {
        dispatchMain()
    }
}

main()

That’s because, internally, Network framework uses Apple’s BoringSSL and it always calls SSL_CTX_set_custom_verify in order to apply Apple’s security policies.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

The main issue would be that the Tester was able to use Frida to hook on to SSL_CTX_set_custom_verify(). Is there a way to do pinning without it going through SSL_CTX_set_custom_verify?

Does NSPinnedDomains goes through SSL_CTX_set_custom_verify eventually? As the Frida mainly hook on the SSL_CTX_set_custom_verify and returns SSL_verify_none, which just skips through the pinning checks, making any attempts to pin the SSL Certificate redundant.


As for the implementation, we use Trustkit (https://github.com/datatheorem/TrustKit), as linked in the original post. As the README.md file indicates we just implemented it this way:

    NSDictionary *trustKitConfig = @{
    kTSKSwizzleNetworkDelegates: @NO,
    kTSKPinnedDomains : @{
            @"domain1.com" : @{
                    kTSKExpirationDate: @"2017-12-01",
                    kTSKPublicKeyHashes : @[
                            @"HXXQgxueCIU5TTLHob/bPbwcKOKw6DkfsTWYHbxbqTY=",
                            @"0SDf3cRToyZJaMsoS17oF72VMavLxj/N7WBNasNuiR8="
                            ],
                    kTSKEnforcePinning : @NO,
                    },
            @"domain2.com" : @{
                    kTSKPublicKeyHashes : @[
                            @"TQEtdMbmwFgYUifM4LDF+xgEtd0z69mPGmkp014d6ZY=",
                            @"rFjc3wG7lTZe43zeYTvPq8k4xdDEutCmIhI5dn4oCeE=",
                            ],
                    kTSKIncludeSubdomains : @YES
                    }
            }};
    
    [TrustKit initSharedInstanceWithConfiguration:trustKitConfig];

In summary, our concern is more of that Frida is able to intercept SSL_CTX_set_custom_verify(). As such, we are unable to pass our Penetration Test for the mobile app.

It is a requirement to Pin the SSL Certificate of the respective domains, but also be able to not be intercepted by hackers and scripts to bypass this SSL pinning checks.

We are looking for solutions to pin our SSL certificate that does not go through SSL_CTX_set_custom_verify(), so that Frida would not be able to hook onto that method to hijack our SSL pinning checks.

Is there a way to do pinning without it going through SSL_CTX_set_custom_verify?

What pinning? In the example from my previous post, there’s no pinning at all. Rather, the program is just calling standard Apple APIs.

Does NSPinnedDomains goes through SSL_CTX_set_custom_verify eventually?

As I demonstrate above, all TLS trust evaluation on our system goes through that call.

It is a requirement … to not be intercepted by hackers and scripts to bypass this SSL pinning checks.

I see a lot of this and IMO this issue here isn’t your code but your security requirements. If someone attacking your app is able to intercept the code path between your app calling URLSession and iOS ultimately managing its trust evaluation using its own internal copy of BoringSSL, then all bets are off.

Imagine that you added TrustKit to your product to do your own pinning [1]. It’s implemented in terms of trust objects, ultimately calling down to SecTrustEvaluate (or one of its more modern variants). If someone can hook iOS’s SSL_CTX_set_custom_verify, they can just as easily hook SecTrustEvaluate. So, all the work you did to integrate this third-party library is pointless.

I recommend that you work with your security team to come up with more sensible requirements. For example, you would be better of spending your time integrating App Attest than worrying about this issue.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] This is not a suggestion that you do this, just a though experiment.

App flagged with bypassed SSL Pinning during Mobile App Penetration Test
 
 
Q