We have recently published an article discussing the basics of certificate pinning – TLS Certificate Pinning 101. We recommend that you read that first. In this article, we will be looking into how we can leverage the lack of strong binary protections (as well as complete lack of runtime protections) to bypass, with relative ease, the certificate pinning mechanism in Snapchat’s latest release for Android – version 10.19.5.0 at the time of writing.
Let’s assume the common scenario where we want to intercept the HTTP(S) traffic of an Android application – in this case Snapchat. We’ll be running an HTTP proxy on our test machine – in this instance we will be using Burp Suite – and setup the proxy settings on our test device with Snapchat installed. Naturally, we ensure that the relevant root CA certificate is installed on our device to perform HTTPS interception.
If we opened Snapchat and tried to login, we would see the following error message:
As we can see, some sort of error occurred when the application tried to connect to Snapchat’s server. If we look into our Burp Suite’s Alerts tab we can find a little more detail about the problem we are facing:
Assuming we want to capture user credentials, of particular interest is the highlighted alert, where the TLS negotiation fails for auth.snapchat.com:443 – Snapchat’s authentication server.
By now, there is very good indication that certificate pinning is in place. Let us have a deeper look at what is happening just to be sure. For this, we’ll be firing up Wireshark:
As we can see in the screenshot above, the application opens a TCP connection with our HTTP proxy, and then starts the TLS handshake with a Client Hello message. Our proxy responds with a Server Hello message. The screenshot above highlights that the certificate shown is for auth.snapchat.com and that it has been signed by PortSwigger CA – Burp Suite’s CA.
As seen in the screenshot above, the application halts the TLS handshake as soon as it receives the Server Hello message, and then closes the TCP connection.
We now have more than enough evidence to infer that certificate pinning is in place. Our next steps will be to reverse engineer the application, find and understand the pinning mechanism, and – obviously – bypass it.
For this part of the exercise we will do some very basic and standard Android application reverse engineering.
- Extract the APK from the device
- adb pull /data/app/com.snapchat.android-1/base.apk
- Unzip the APK
- unzip -d snapchat base.apk
- Convert the DEX files to JAR files
- dex2jar snapchat/classes*.dex
- Open the JAR files in a Java disassembler – in this instance we will be using JD-GUI.
Now it is time to find where/how certificate pinning has been implemented in Snapchat for Android. You will note that some “gentle” obfuscation has been applied to the source code, but certainly not enough subvert our efforts! Some useful strings to look/search/grep for when looking for certificate pinning related code are:
If you’ve read our previous post on the basics of certificate pinning – TLS Certificate Pinning 101 – you should already be familiar with the most common ways of implementing certificate pinning on Android, one of which is to:
- Create an empty KeyStore;
- Add the relevant certificates to it;
- Initialise a TrustManagerFactory with this KeyStore;
- Initialise the SSLContext with this TrustManagerFactory.
With this in mind, should we search for “TrustManagerFactory” it is very likely we will find the code shown below. In this instance the class name is “hpx.class” however, this may change with different Snapchat releases due to code obfuscation – or at least we would hope so!
As we can see in the screenshot above, there is a method – “a()” – in the “hpx.class” that returns a SSLSocketFactory. Looking closer at that method, we can see that (1) it initialises a KeyStore (we’ll look into that next), (2) initialises a TrustManagerFactory with that KeyStore, and finally (3) initialises an SSLContext with that TrustManagerFactory. This is the exact behaviour we described earlier as one of the most common ways to implement certificate pinning in Android.
Out of curiosity, let’s look at how that KeyStore gets initialised – “npu.a()”.
As we can see from the screenshot above, JD failed to decompile the method we’re interested in. However, we can still gauge a lot of useful information from the bytecode. For example, we can see the setCertificateEntry method being invoked – which is the method used to add a certificate to the KeyStore. Furthermore, outside the method, we can see a static variable – b – which looks like a string of certificates. If we parse that string we’ll have something that looks like this:
Figure 7. Certificates embedded in the app.
As we can see, that variable holds what looks to be several certificates. This all seems to fit in line with the certificate pinning strategy we’ve been describing.
Now that we know how certificate pinning has been implemented, and understand what application is doing, it is time to figure out how we can bypass this security control.
There are many ways we could go about bypassing this control. For now, let’s look at the TrustManagerFactory’s init(KeyStore) method.
As highlighted in the screenshot above, the init() method can receive a KeyStore or null. Unfortunately, the documentation does not clarify what happens when null is received. However, should you test this yourself (it should be fairly simple to write your own app and verify), you would find that when the KeyStore passed as an argument to init() is null, then the TrustManagerFactory is initialised with the system’s default key store – i.e., the certificates and CA’s that the system trusts. Since we have already installed PortSwigger CA on out test device, if somehow we managed to change the method’s argument to null, the connection should go through!
A simple, and very valid, way to achieve this would be to patch the smali code so that the TrustManagerFactory is initialised with null, repackage the APK, re-sign it, and re-install it. That should do the trick. But let us try a more… dynamic approach.
Introducing the reader to runtime hooking and the ways of Frida would be separate blog posts themselves. Nonetheless, the following Frida script should be fairly self-explanatory now that we know exactly what we are trying to achieve:
- Overload the TrustManagerFactory’s init method;
- Log its invocation to standard output;
- Call the original method with null for its argument.
If we now run the Frida script, open Snapchat, and try to login we should be able to capture those tasty credentials!