Skip to main content

Command Palette

Search for a command to run...

iOS: Protecting against TLS bypass attacks

Updated
5 min read
iOS: Protecting against TLS bypass attacks
D

I love technology and everything related, from gadgets to new professional techniques. I like thinking, researching, optimizing, inventing and developing. I have a strong background in software research and development, operating systems, Voice-over-IP, network security, wired and wireless network engineering, complemented with electronic engineering background.

My career goal is to always keep learning, to be challenged, and to work remotely so I can be present for my family.

Bug hacker and master troubleshooter, my strength is understanding a problem and getting to the root of it. I'm mostly a self-taught individual and a constant learner. I push my technical boundaries daily and search for ways to improve my skills every day. With over 20 years of experience writing software in various languages, creating or optimizing algorithms, the digital development world is my turf.

Sample challenges which I particularly enjoyed:

  • Created a GLSL based magnification tool for a client who was turned down by three other companies as "impossible to do on macOS".
  • Optimized several SQL queries to reduce load time of a particular web page from several seconds to sub 50ms.
  • Identified the root cause of stuttering animations in iOS mobile app and implemented mitigation strategy

Specialties: Swift, Objective-C and PHP Software Development; TCP/IP and Wireless Network Engineering

The cat and mouse game against hacker never ends. While we can never fully prevent against hackers modifying our runtime binaries and bypass our security measures, we sure can make it more difficult for them.

TLS Pinning is a method by which we setup a URLSession delegate, during the TLS handshake the delegate will compare the public certificate against an expected certificate.

Hackers have since developed a method to bypass this using a tool named Objection, distributed as part of Frida. Using this tool, they use the .dylib loading mechanism to intercept and redirect SSL library calls. They can then immediately accept all certificate requests, all with a single command.

When they do this, the two attacks they can perform is to either skip the app URLSession delegate method entirely, or change the response from the URLSession delegate from Rejected to Accepted.

So how can we protect against this?

In the solution below, when a secure communication is required, we create a brand new URLSession and its delegate. A query is made and verify the TLS certificate like we usually do for TLS Pinning. However, we also record whether or not the TLS certificate was validated and if it passed validation.

Upon receiving the data/response, we double-check with the delegate whether it received a certificate validation request and whether the validation was successful.

Assuming either of the current Objection attacks, this method would correctly detect a TLS validation bypass attack. If they skip the validation and return data, it will be detected. And if they override the rejection with an acceptation, since we are performing the extra step of double-checking with our custom used-once delegate, it will also detect it.

SecureURLSession

final class SecureURLSession {

    private final class Delegate: NSObject, URLSessionDelegate {
        private let expectedHost: String
        var validatedChallenge: () -> Void = { /* to be set later */ }
        private let allowedSignatures: [Data]

        init(host: String, allowedSignatures signatures: [Data]) {
            expectedHost = host
            allowedSignatures = signatures
            super.init()
        }

        func urlSession(
            _ session: URLSession,
            didReceive challenge: URLAuthenticationChallenge,
            completionHandler: @escaping @Sendable (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
        ) {
            guard expectedHost == challenge.protectionSpace.host,
                  let serverTrust = challenge.protectionSpace.serverTrust,
                  let signature = signatureFromTrust(serverTrust),
                  allowedSignatures.contains(signature)
            else {
                completionHandler(.rejectProtectionSpace, nil)
                return
            }

            validatedChallenge()
            completionHandler(.useCredential, challenge.proposedCredential)
        }

        private func signatureFromTrust(_ trust: SecTrust) -> Data? {
            guard SecTrustGetCertificateCount(trust) > 0,
                  let certificate = SecTrustGetCertificateAtIndex(trust, 0)
            else {
                return nil
            }

            let certificateData = SecCertificateCopyData(certificate) as Data
            let digest = SHA256.hash(data: certificateData)
            return Data(digest)
        }
    }

    init(configuration: URLSessionConfiguration = .default, host: String, allowedSignatures: [Data]) {
        let sessionDelegate = Delegate(host: host, allowedSignatures: allowedSignatures)
        session = URLSession(
            configuration: configuration,
            delegate: sessionDelegate,
            delegateQueue: nil)
        delegate = sessionDelegate
        delegate.validatedChallenge = { [weak self] in self?.continuation = { true } }
    }

    private let delegate: Delegate
    private let session: URLSession
    private var continuation: () -> Bool = { false }

    enum Errors: Error {
        case sslBypassDetected
    }

    func data(from url: URL) async throws -> (Data, URLResponse) {
        let (data, response) = try await session.data(from: url)
        guard continuation() else {
            throw Errors.sslBypassDetected
        }
        return (data, response)
    }
}

How to use

And here’s a sample code securely fetching an Emoji using TLS Pinning while preventing TLS bypass:

extension URLResponse {
    func isHttpWithStatusCode(_ statusCode: Int) -> Bool {
        guard let httpResponse = self as? HTTPURLResponse else {
            return false
        }
        return httpResponse.statusCode == statusCode
    }
}

struct FetchEmojiActivity: Sendable {

    enum Errors: Error {
        case unexpectedServerResponse
    }

    /// {
    ///   "name": "hugging face",
    ///   "category": "smileys and people",
    ///   "group": "face positive",
    ///   "htmlCode": ["🤗"],
    ///   "unicode": ["U+1F917"]
    /// }
    struct DTO: Decodable {
        let name: String
        let unicode: [String]
    }

    func begin() async throws -> String {
        let emojiHubTlsSignature = Data(base64Encoded: "88bp12uxOpEr+AukM0O/I/9Jt+/gYpN+u2FPiLeD8tI=")! <-- do not store your security hash in a string in a Production
        let secureUrlSession = SecureURLSession(
            host: "emojihub.yurace.pro",
            allowedSignatures: [emojiHubTlsSignature]
        )
        let randomEmojiUrl = URL(string: "https://emojihub.yurace.pro/api/random")!
        let (data, response) = try await secureUrlSession.data(from: randomEmojiUrl)
        guard response.isHttpWithStatusCode(200) else {
            throw Errors.unexpectedServerResponse
        }

        let dto = try JSONDecoder().decode(DTO.self, from: data)
        let emoji = dto.unicode
            .compactMap { (_ seq: String) -> String? in
                guard let int = Int(seq.replacingOccurrences(of: "U+", with: ""), radix: 16),
                      let scalar = UnicodeScalar(int)
                else {
                    return nil
                }
                return String(scalar)
            }
            .joined()
        return emoji
    }
}

CAVEAT: For sake of simplicity, the TLS certificate in the FetchEmojiActivity is included as a string. This should be avoided unless you have some kind of cryptographic signature validation elsewhere in your code to ensure the hacker has not replaced it with its own validation hash.

Limitations and Usage

We shouldn’t be re-using the above SecureURLSession instance for multiple queries. Reusing the SecureURLSession class above for multiple queries would allow the attacker to bypass the TLS validation after the first request is made.

This will have an impact on performance. Doing this for background URLSession is not recommended as it will trigger your app to be throttled by the system.

The mechanism described here should only be used when the utmost security is required, like when bootstrapping a public/private asymmetric key exchange with the server.

Remember to implement TLS certificate update mechanism in your application. Your TLS certificate will eventually expire or may have to be revoked; you will want your current app installations to continue working after you publish a new TLS certificate on your server. Consider using LaunchDarkly or other similar services to transmit an encrypted hash overriding the built-in hash.

If your application needs to do multiple parallel server queries or background transfers, consider using symmetric cryptography for your API queries & responses on top of the standard URLSession without TLS pinning; limit the use of SecureURLSession to transfer your encryption keys and certificates.

Conclusion

It seems possible to prevent typical TLS pinning bypass attacks by creating a custom URLSession and delegate when making a new request.

It’s impossible to fully protect against all attacks. If the attacker is able to do a memory dump of your process, use a debugger, modify your binary executable, there’s not much you can do. Clever attackers may even be able to acquire the master secret used to establish the TLS connection along with the exchanged keys then combined with Pcap gain access to all encrypted traffic. The best protection against these kinds of attack is Runtime Application Self-Protection (RASP).

However, as with anything, an extra layer of security means fewer hackers able to pull off the attack, or at the very least more efforts required which may be just enough to discourage an attacker.

Assuming RASP is able to detect runtime intrusion, the mechanism here should be able to properly protect your TLS communications.

Stay safe!

More from this blog

B

Blog on Mobile app security and reliability on iOS and macOS.

16 posts

I'm a passionate senior iOS & macOS developer with over 13 years in the Apple ecosystem and 25 years in the software industry. I have a special interest for data security and privacy compliance!