Investigating and disabling hard-coded certificate pinning in an Android application
mitmproxy is an open source interactive HTTPS proxy, which makes it easy to intercept HTTPS for reverse engineering, including an Android clients. It does this by installing its own CA certificate on the client device.1
Recently, I was attempting to reverse engineer the HTTPS API used by one Android application. Using mitmproxy, it should be simple to add the mitmproxy CA certificate and intercept the traffic.
However, when using mitmproxy, opening the app results in the message ‘This app certificate is out of date. Please update the FooBar app in the Play store.’ It appears that there is some sort of certificate pinning at play, which is preventing the app from working with our mitmproxy CA certificate.
We turn, then, to decompiling the source code of the Android app to investigate further. Using jadx, we find:
public void onResourcesFailedToLoad(final ErrorHandler.Error error) {
// ...
switch (error) {
// ...
case CERTIFICATE_PINNING_VALIDATION_FAILURE:
str = "This app certificate is out of date. Please update the FooBar app in the Play store.";
// ...
Looks good! Searching for references to CERTIFICATE_PINNING_VALIDATION_FAILURE, we find:
public WebResult doInBackground(WebRequest... webRequestArr) {
// ...
try {
// ...
validatePinning(this.mTrustManagerExt, (HttpsURLConnection) this.mConnection);
// ...
} catch (SSLPeerUnverifiedException e7) {
AppAnalytics.logException(e7);
e7.printStackTrace();
webResult.error = ErrorHandler.Error.CERTIFICATE_PINNING_VALIDATION_FAILURE;
// ...
So it appears, from an initial reading of this code, that the validatePinning method is called to validate the certificate used in the HTTPS connection. If the certificate is valid, the method returns normally, but if the certificate is invalid, an SSLPeerUnverifiedException exception is raised, which is caught and later results in the error message.
Inspecting the validatePinning method itself, we find our suspicions are confirmed:
private void validatePinning(X509TrustManagerExtensions x509TrustManagerExtensions, HttpsURLConnection httpsURLConnection) throws SSLException {
String[] strArr = NetworkConstants.mValidSSHCertPins;
// ...
for (String str : strArr) {
if (...) { // Compare observed certificate with pinned certificates in strArr
return;
}
}
throw new SSLPeerUnverifiedException("Certificate pinning failure");
// ...
}
We now have the information necessary to patch this function and disable the certificate pinning check. Just as when we investigated Google Cast authentication, we can use the (Ed)Xposed Framework to hook validatePinning and always force it to return normally:
public class FooBarNoPinning implements IXposedHookLoadPackage {
public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lpparam) throws Throwable {
if (!lpparam.packageName.equals("com.example.foobar")) {
return;
}
findAndHookMethod("com.example.foobar.ServerHandler", lpparam.classLoader, "validatePinning", "android.net.http.X509TrustManagerExtensions", "javax.net.ssl.HttpsURLConnection", new XC_MethodHook() {
protected void afterHookedMethod(XC_MethodHook.MethodHookParam param) throws Throwable {
// Force success
param.setThrowable(null);
}
});
}
}
With this module compiled and enabled, we now find that the app functions correctly, and we can begin analysing the encrypted traffic using mitmproxy.
Footnotes
-
CA certificates on Android can be installed into the user-managed CA store. However, apps can instruct Android not to accept user-installed CA certificates, and accept only ones in the system CA store, which is not user-modifiable on stock systems. On rooted systems with Magisk, MagiskTrustUserCerts can be used to move user-installed CA certificates into the system CA store. ↩