Bypassing Certificate Pinning on Android for fun and profit

Introduction

Charles Proxy is a great tool that helps you debug network traffic by sniffing your requests. One super useful feature is that it allows you to also monitor SSL traffic by generating self signed certificates and acting as Man in the Middle. For iOS and Android devices, that doesnt work well and you have to install its certificate instead. This is a known workaround and has helped us many times in the past in order to debug API requests, for example.

Another security best practice that has become more popular in the recent years is using Certificate Pinning, which helps prevent man-in-the-middle attacks. Since that is exactly the strategy used by Charles, it doesn’t work and cannot show the plain text contents of those SSL requests since its “fake” certificate is rejected by the application.

This article will guide you through one of the possible workarounds to that limitation, by decompiling the target application, disabling the certificate pinning and repackaging it.


Disclaimer

The sole purpose of this article is educational and for testing of your own applications. This is not intended for piracy or any other non-legal use.


Steps

Download Charles proxy. Enable SSL Proxying and add the desired hostnames, eg.: *.example.com. You’ll also need to install the Charles SSL Certificate into your test device or emulator as explained here.

Download the APK from your device or emulator:

adb shell pm list packages
adb shell pm path com.example.android
adb pull /data/app/com.example.android/base.apk

Download APKTool.

Decompile your APK (The one you pulled from the device above):

apktool -d example.apk

This should create a directory with the entire package contents, including resources. Look for the smali directory. Now it’s your turn to look through the code/classes/packages and find where the certificate pinning initialization logic is performed.

This is an example of how it might look like:

.method provideCertificatePinner()Lokhttp3/CertificatePinner;
.locals 5
.annotation runtime Ljavax/inject/Singleton;
.end annotation
.prologue
.line 26
new-instance v0, Lokhttp3/CertificatePinner$Builder;
invoke-direct {v0}, Lokhttp3/CertificatePinner$Builder;-><init>()V
const-string v1, “api.example.com”
const/4 v2, 0x1
new-array v2, v2, [Ljava/lang/String;
const/4 v3, 0x0
const-string v4, “sha1/XXXXXXXXXXXXXXX=”
aput-object v4, v2, v3
.line 27
invoke-virtual {v0, v1, v2}, Lokhttp3/CertificatePinner$Builder;->add(Ljava/lang/String;[Ljava/lang/String;)Lokhttp3/CertificatePinner$Builder;
move-result-object v0
.line 68
invoke-virtual {v0}, Lokhttp3/CertificatePinner$Builder;->build()Lokhttp3/CertificatePinner;
move-result-object v0
return-object v0
.end method

In the example above you can clearly see how ”sha1/XXXXXXXXXXXXXXX=” is being pinned to api.example.com. If you just delete the lines associated to the call to Builder.add(), you could potentially build a version of this application that doesn’t do certificate pinning anymore. This is exactly what we’re gonna do. The resulting smali code would look like this:

.method provideCertificatePinner()Lokhttp3/CertificatePinner;
.locals 5
.annotation runtime Ljavax/inject/Singleton;
.end annotation
.prologue
.line 26
new-instance v0, Lokhttp3/CertificatePinner$Builder;
invoke-direct {v0}, Lokhttp3/CertificatePinner$Builder;-><init>()V
.line 68
invoke-virtual {v0}, Lokhttp3/CertificatePinner$Builder;->build()Lokhttp3/CertificatePinner;
move-result-object v0
return-object v0
.end method

(Optional) You can also use the JADX UI to decompile the dex file(s), which makes it much easier to read the decompiled Java code instead of Smali. The code above would look like this inside JADX:

@Singleton
CertificatePinner provideCertificatePinner() {
return new Builder().add(“api.example.com”, “sha1/XXXXXXXXXXXXXXX=”).build();
}

Once you have finished modifying the code, go ahead and use apktool again, now to repackage the apk:

./apktool b example/ -o example.unaligned.apk

Sign the apk with your debug key:

jarsigner -verbose -sigalg MD5withRSA -digestalg SHA1 -keystore ~/.android/debug.keystore -storepass android example.unaligned.apk androiddebugkey

Zipalign it:

zipalign -v 4 example.unaligned.apk example.smali.apk

Uninstall the original apk and install our modified one:

adb uninstall com.example.android
adb install example.smali.apk

(Optional) Use pidcat to monitor logcat, see what’s going on, in case it crashes or behaves weirdly:

pidcat com.example.android

Open Charles and open your app. You should see plain text SSL traffic now.


Conclusion

This article explained one possible solution to work around analyzing SSL traffic of Android applications that are using certificate pinning by repackaging it. This is not a bullet proof solution and might not work in all cases, depending on how the application performs the pinning or if it performs any kind of runtime signing checks.

Other solutions have been described in this presentation.

Additional Resources

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.