SSL Pinning bypass in iOS application
Hi, my name is Andrey Batutin, I am a Senior iOS Developer in DataArt. In the previous article, we talked about how you can sniff the traffic of our mobile application using an HTTPS proxy. In this post we discuss how to bypass SSL pinning. Just in case, I recommend reading the first article, if you have not read it yet: it will be necessary to understand the text below.
Actually, in practice, SSL Pinning is used so that the described method of inspecting and modifying the traffic of a mobile application is not accessible to bad guys or a curious boss.
What is SSL Pinning
In the previous article, we installed a Charles Root Certificate on our mobile device, which allowed our Charles Proxy to receive, decrypt, show us traffic, encrypt it back and send it to Dropbox.
If, as a mobile application developer, I want my traffic to be inspected only by my server and no one else, even if this other one has installed its SSL certificate on the device, I can use SSL Pinning.
Its essence comes down to the fact that during the SSL handshake the client verifies the certificate received from the server.
This article discusses the easiest way to implement SSL Pinning using the allowed list of certificates sewn into the application (whitelisting).
Read more about SSL Pinning types here..
Implementing SSL Pinning at FoodSniffer
The complete project code is here . First we need to get two certificates in DER format for two hosts:
- www.dropbox.com ;
- uc9b17f7c7fce374f5e5efd0a422.dl.dropboxusercontent.com.
The second server stores JSON itself with a list of our purchases.
To get certificates in the right format, I used Mozila Firefox.
Open in dropbox.com browser.
Click on the lock symbol in the address bar.
Click More Information, select Security -> View Certificate.
Then select Details and find the final certificate in Certificate Hierarchy.
Click Export and save in DER format.
Repeat the same procedure for uc9b17f7c7fce374f5e5efd0a422.dl.dropboxusercontent.com.
Note
The Dropbox content server (* .dl.dropboxusercontent.com) uses a wildcard certificate. This means that the certificate that you extracted for the uc9b17f7c7fce374f5e5efd0a422 server will be suitable for any other * .dl.dropboxusercontent.com Dropbox servers.
As a result, I got two files with certificates:
dropboxcom.crt ,
dldropboxusercontentcom.crt ,
which I added to the project of the FoodSniffer iOS application.
Then I added an extention for the FoodListAPIConsumer-class, in which I check the certificate received from the server. To do this, I look for it in the list of allowed certificates, processing the Authentication Challenge delegate of the NSURLSessionDelegate protocol:
extensionFoodListAPIConsumer{
funcurlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guardlet trust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
let credential = URLCredential(trust: trust)
if (validateTrustCertificateList(trust)) {
completionHandler(.useCredential, credential)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
funcvalidateTrustCertificateList(_ trust:SecTrust) -> Bool{
for index in0..<SecTrustGetCertificateCount(trust) {
iflet certificate = SecTrustGetCertificateAtIndex(trust, index){
let serverCertificateData = SecCertificateCopyData(certificate) asDataif ( certificates.contains(serverCertificateData) ){
returntrue
}
}
}
returnfalse
}
}
In the certificates array , I have stored Data representing my allowed certificates.
Now, when Charles Proxy is running, the application will break the connection with it due to the fact that the Charles-certificate is not included in the list of allowed. The user will see the following error:
Hackers are down!
But now there is one small problem - how can I, the developer, monitor the HTTPS traffic of my application?
Frida
One option is to disable SSL Pinning using Frida's dynamic code injection framework .
The idea is that during the application development process, the validateTrustCertificateList method always returns true.
This, of course, can be achieved without dynamic code injection, for example, using the #if targetEnvironment (simulator) condition to disable SSL Pinning on the simulator, but this is too simple.
With the help of Frida, we will be able to write a script in JavaScript (deftly, right?), In which we substitute the implementation of validateTrustCertificateList with one that always returns true.
And this script will be injected into the application already at the execution stage.
How Frida works on iOS, you can read here..
Installing Frida (taken from here ).
sudo pip install frida-tools
Frida script
The immediate script for replacing the validateTrustCertificateList function looks like this:
// Are we debugging it?
DEBUG = true;
functionmain() {
// 1var ValidateTrustCertificateList_prt = Module.findExportByName(null, "_T016FoodSnifferFrida0A15ListAPIConsumerC024validateTrustCertificateD0SbSo03SecG0CF");
if (ValidateTrustCertificateList_prt == null) {
console.log("[!] FoodSniffer!validateTrustCertificateList(...) not found!");
return;
}
// 2var ValidateTrustCertificateList = new NativeFunction(ValidateTrustCertificateList_prt, "int", ["pointer"]);
// 3
Interceptor.replace(ValidateTrustCertificateList_prt, new NativeCallback(function(trust) {
if (DEBUG) console.log("[*] ValidateTrustCertificateList(...) hit!");
return1;
}, "int", ["pointer"]));
console.log("[*] ValidateTrustCertificateList(...) hooked. SSL pinnig is disabled.");
}
// Run the script
main();
- We find in the full name of the function a pointer to validateTrustCertificateList in the application binary.
- Wrap the pointer in a NativeFunction wrapper, indicating the type of the parameter and the output value of the function.
- We replace the implementation of the validateTrustCertificateList function such that it always returns 1 (that is, true).
The whole script is in {source_root} /fridascrpts/killCertPinnig.js .
One of the problems is how the full name of the function _T016FoodSnifferFrida0A15ListAPIConsumerC024validateTrustCertificateD0SbSo03SecG0CF was obtained .
For this, I used the following technique.
- Created in the application of an additional target FoodSnifferFrida .
- I connected to it the library FridaGadget.dylib , which I took here. More information about the process of connecting the library is described here.
- Launched on the simulator FoodSniffer application.
- Used this command to find the full name of the validateTrustCertificateList function :
frida-trace -R -f re.frida.Gadget -i "* validateTrust *" - Got it in the form:
And then used it in killCertPinnig.js .
Why such a “strange” name came to the function in the end, and what all these T016 and 0A15 mean, can be found here .
Kill SSL Pinning
Now finally launch FoodSniffer with Pinnig SSL turned off!
Run Charles Proxy.
Run the FoodSnifferFrida target in the Xcode project in the simulator. We should just see a white screen. The application waits for Frida to connect to it.
Run Frida to execute the killCertPinnig.js script:
frida -R -f re.frida.Gadget -l ./fridascrpts/killCertPinnig.js Let's wait for the
connection to the iOS application: Let's
continue the application using the% resume command:
Now we should see the list of food in application:
And JSON in Charles Proxy:
Profit!
Conclusion
Frida is like Wireshark for binaries. It works on iOS, Android, Linux, Windows-based platforms. This framework allows you to track calls to methods and functions - both system and user. And also to replace the values of parameters, return values and implementation of functions.
Bypassing SSL Pinning in the design process using Frida may seem a bit overkill . It attracts me because I don’t need to have specific logic in the application itself for debugging and developing the application. Such logic clutters the code and, if incorrectly implemented, can leak into the release version of the assembly (macros, hello to you!).
In addition, Frida is applicable for Android. Which gives me the opportunity to make life easier for all my team and ensure a smooth development process for the entire product line.
Frida positions itself as a black box process code injection tool. With it, it is possible, without changing the immediate code of the iOS application, to add to the runtime the logging of method calls, which can be indispensable when debugging complex and rare bugs.