We're Hiring!

How to Protect Against Certificate Pinning Bypassing

Screenshot from the code editor with the Approov implementation in the mobile app and API server.

Editor's note: This post was originally published in October 2019 and has been revamped and updated for accuracy and comprehensiveness. The latest update was in November 2021.

In my previous article, we saw how to bypass certificate pinning within a device you control and in this article we will see how you can protect yourself against such an attack.

Below you will learn how to use a mobile app attestation service to protect your API server from accepting requests that come from a mobile app where certificate pinning has been bypassed. This means that even though the attacker has bypassed the certificate pinning, he will not be able to receive successful responses from the API server. Instead, the server will always return 401 responses, thus protecting your valuable data from getting into the wrong hands.

To demonstrate how to protect against certificate pinning bypassing we will use the same Currency Converter Demo mobile app and API server that was used in the previous article, and we will enhance the security of both the mobile app and the API server, by adding a Mobile App Attestation service to them.

Other ways to break Certificate Pinning

Before I go into details of how the Mobile App Attestation works, I would like to remind you that in the previous article I repackaged the mobile app to bypass certificate pinning. However, other tools exist, such as Frida or xPosed, which can be used to bypass certificate pinning during runtime, therefore not requiring repackaging the mobile app.

If you are interested in those alternative methods, you can see how it’s done with xPosed in this video:

 

If you prefer a step by step tutorial you can also follow the article How to Bypass Certificate Pinning with Frida on an Android App.

The Role of Mobile App Attestation

Before we dive into the role of a Mobile App Attestation service, we first need to understand the difference between what and who is accessing the API server. This is discussed in more detail in this article, where we can read:

The what is the thing making the request to the API server. Is it really a genuine instance of your mobile app, or is it a bot, an automated script or an attacker manually poking around your API server with a tool like Postman?
The who is the user of the mobile app that we can authenticate, authorize and identify in several ways, like using OpenID Connect or OAUTH2 flows.

The role of a Mobile App Attestation service is to authenticate what is sending the requests, thus only responding to requests coming from genuine mobile app instances and rejecting all other requests from unauthorized sources.

In order to know what is sending the requests to the API server, a Mobile App Attestation service, at run-time, will identify with high confidence that your mobile app is present, has not been tampered/repackaged, is not running in a rooted device, has not been hooked into by an instrumentation framework (Frida, xPosed, Cydia, etc.), and is not the object of a Man in the Middle Attack (MitM). This is achieved by running an SDK in the background that will communicate with a service running in the cloud to attest the integrity of the mobile app and device it is running on.

On a successful attestation of the mobile app integrity, a short time lived JWT token is issued and signed with a secret that only the API server and the Mobile App Attestation service in the cloud know. In the case that attestation fails the JWT token is signed with an incorrect secret. Since the secret used by the Mobile App Attestation service is not known by the mobile app, it is not possible to reverse engineer it at run-time even when the app has been tampered with, is running in a rooted device or communicating over a connection that is the target of a MitM attack.

The mobile app must send the JWT token in the header of every API request. This allows the API server to only serve requests when it can verify that the JWT token was signed with the shared secret and that it has not expired. All other requests will be refused. In other words a valid JWT token tells the API server that what is making the request is the genuine mobile app uploaded to the Google or Apple store, while an invalid or missing JWT token means that what is making the request is not authorized to do so, because it may be a bot, a repackaged app or an attacker making a MitM attack.

A great benefit of using a Mobile App Attestation service is its proactive and positive authentication model, which does not create false positives, and thus does not block legitimate users while it keeps the bad guys at bay.

The Mobile App Attestation service already exists as a SaaS solution at Approov . The solution supports SDKs for several platforms, including iOS, Android, React Native, Cordova, Ionic, NativeScript, Xamarin and Flutter. To deploy, a small check in the API server code to verify the JWT token issued by the Approov cloud service is needed. This check is how the API server authenticates what is making the request.

So let’s see how we can introduce Approov into the Currency Converter Demo mobile app in order that the API server knows which requests it should allow and those it should deny.

Implementing Approov

In order to implement Approov into the Currency Converter Demo mobile app and its API server, you need to clone the project from Github:

git clone --branch 0.5.1 https://github.com/approov/currency-converter-demo.git
cd currency-converter-demo/mobile-app/android

The Approov integration includes adding the Approov Service dependency into your mobile app, registering the mobile app with the Approov cloud service, and integrating a check for the Approov token in the API server. Optionally you can also tailor the configuration defaults used by the Approov cloud service to better match your needs, for example by changing the security policy that is used to determine when the Approov cloud service can issue a valid Approov token for the mobile app.

During the Approov implementation, we will need to use the Approov CLI tool which can be downloaded and installed by following these instructions, that will require you to signup for an Approov trial (no credit card needed).

Approov Integration on the API Server

Implementing Approov in the API server just requires the addition of a simple JWT token check, and we have several quickstarts covering the integration for different backend technologies.

For peace of mind, you can start to implement the Approov token check without blocking the requests on invalid tokens or in the absence of tokens, and once you are confident that you are only blocking unauthorized traffic, you can switch to block the invalid requests.

To implement Approov in the Currency Converter API, we created a Python Flask server approov_token_decorator.py, and then in each endpoint we want to protect with Approov we added the @approov_token_decorator.check_approov_token annotation. In this case, we used API versioning to add Approov, but this was purely for supporting the several demo stages in this series of blog posts. In a real-world API, you can implement Approov without the need to use API versioning, but you may want to use it, and if you do so we encourage you to deprecate the API endpoints not protected with Approov as fast as possible. If you don't, the attackers will continue to use them since they won't be able to get data from the new ones because of the Approov protection.

Approov Integration on the Mobile App

The Approov integration in your mobile app can be done in three simple steps:

  1. Approov SDK Integration.
  2. Approov SDK Usage.
  3. Mobile App Release.

To integrate Approov into the mobile app you just need follow some simple instructions from the Approov official quickstarts for your choosen HTTP stack. For example, these simple instructions will show you how to implement Approov in a mobile app using the Volley HTTP stack, the same one used in the Currency Converter Demo app.

Approov SDK Integration

We will start by grabbing the Approov config string from the onboarding email, the one highlighted by the red box:

Screenshot from the Approov onboarding email.

Then open your local.properties file and add to it:

approov.config="REPLACE_WITH_YOUR_APPROOV_CONFIG_STRING"

Now, to be able to use the Approov config add to the app level build.gradle file the lines in bold::

Properties properties = new Properties()
if (project.rootProject.file('local.properties').canRead()) {
properties.load(project.rootProject.file("local.properties").newDataInputStream())
}

android {
...
defaultConfig {
...
buildConfigField "String", "APPROOV_CONFIG_STRING", "${properties.getProperty('approov.config')}"
...
}
...
}

Next, you will be adding the Approov Volley Service dependency to the same app level build.gradle file:

dependencies {
...
implementation 'com.github.approov:approov-service-volley:2.7.0'
}

The Approov service dependency is a wrapper for the Approov SDK to enable easy integration when using the Volley HTTP stack for making the API calls that you wish to protect with Approov.

To be able to require the dependency you need to add the jitpack repository to the project level build.gradle file:

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

Now, synchronize Gradle and the Approov SDK will be ready to be used on your mobile app project.

The Approov Service needs to have Internet and Network permissions declared in the Android Manifest file to be able to work, thus if not already present add these permissions:

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

With the use of Approov dynamic certificate pinning the network_security_config.xml file can no longer have the <pin-set>...</pin-set> tag declared in it, nor the <trustkit-config>...</trustkit-config> one, because they will conflict with the Approov pinning implementation. So, go ahead and remove them from the network_security_config.xml file or if the only propose of this file in your project is for implementing pinning then remove the file altogether by deleting it and removing its declaration from the AndroidManifest.xml file. It can be found as an attribute in the <application ... android:networkSecurityConfig="@xml/network_security_config"> tag.

Approov SDK Usage

The Approov SDK needs to be initialized when your App starts and this same instance reused until the app is closed. For example, in the Currency Converter Demo app you can see In bold the required changes to add Approov to the VolleyQueueSingleton class:

package com.criticalblue.currencyconverterdemo;

import android.content.Context;
import com.android.volley.RequestQueue;
import com.android.volley.toolbox.BaseHttpStack;
import com.android.volley.toolbox.Volley;

import io.approov.service.volley.ApproovService;

public class VolleyQueueSingleton {
private static Context appContext;

private static RequestQueue requestQueue;
private static ApproovService approovService;

public static synchronized void initialize(Context context) {
appContext = context;
approovService = new ApproovService(appContext, BuildConfig.APPROOV_CONFIG_STRING);
}

public static synchronized RequestQueue getRequestQueue() {
if (requestQueue == null) {
BaseHttpStack httpStack = approovService.getBaseHttpStack();
requestQueue = Volley.newRequestQueue(appContext, httpStack);
}

return requestQueue;
}
}

This is made easy by using the Approov Volley Service dependency, which will fetch and add the Approov token to every request made to the API server, and will deal with the over-the-air (OTA) updates for the Approov SDK Dynamic Configuration that can be present in the response for each Approov Token fetch request, and you can see how this is done here.

During the initialization of the Approov SDK, a configuration string needs to be given and this is the one provided in the onboarding email sent to you after signing up for the Approov trial. The initial configuration is not built in the SDK in order to provide downstream flexibility. The Approov configuration is also dynamic because we support OTA updates which enable us to configure the Approov SDK without the need to release a new mobile app version. Every app will include an initial Approov configuration which is obtained and updated as necessary via the Approov CLI tool.

For a complete overview of the code used to implement Approov in the Currency Converter demo app, we can use git to show us the differences.

Git difference for the Approov implementation in the mobile app:

git show 696a5a18b9cbdfce032309f0c2200b093e3bbcf1

The output:

Screenshot from the git difference for the Mobile app code.

The Git difference for the API server:

git show e4788a52aa2af5788a72f5227d59377636682f96

As you can see is also very simple:

Screenshot from the git difference for the API server code.

You can use this difference as a starting point to understand the basic changes needed to integrate Approov into your own mobile app project.

Mobile App Release

First, you start by letting Approov know what API domain(s) you want to protect with Approov, and this is easily done via the Approov CLI with:

approov api -add currency-converter.demo.approov.io

Adding the API domain(s) only needs to be done one time and should be done before you register your first mobile app APK.

Now, build a release:

./bin/build-release.bash

And then install it:

adb install -r app/build/outputs/apk/release/app-release.apk

Launch it on a real device (not on an emulator):

adb shell am start -n com.criticalblue.currencyconverterdemo/com.criticalblue.currencyconverterdemo.MainActivity

Note: By default Approov security rejection policies settings are configured to detect mobile apps running on emulators, thus issuing an invalid Approov token. The security policies can be changed by following these instructions.

Now try to make a conversion and you will get an error:

Screenshot from the mobile app screen when an error occurs due to the APK not being registered with the Approov cloud service.This happens because the APK has not been registered yet with the Approov cloud service. As a result, each time you try to do an attestation an invalid Approov token is issued, hence the API server rejects the request with a 401 response, therefore triggering a Volley authentication failure error.

Let’s register the APK in the Approov cloud service (adjust the path to your correct location):

approov registration -add app/build/outputs/apk/release/app-release.apk -expireAfter 1800s

The output should be similar to:

registering app Currency Converter Demo

LokZ+7MJdkHGINCwXAHo9JBwIKXE7UqtikeDbuhDt7I=com.criticalblue.currencyconverterdemo-1.0[1]-1212 SDK:Android(2.0.5)

registration successful, expires 2019-09-13 17:48:21

So if you try to repeat the currency conversion immediately you may see the same error, just because a short time is needed to propagate the changes. You can either wait or proactively relaunch the mobile app and the changes will be updated. If you then retry the currency conversion you will get a successful response:

Screenshot from the mobile app screen with the currency conversion calculated.

You may have noticed the -expireAfter flag which ensures that APK registration will only be recognised for 1800 seconds after it is first registered. This is very useful when you are in the development phase and you don’t want to manage the de-registration of temporary versions of the APK with the Approov cloud service. Of course, in production you should not use the -expireAfter flag.

As you can see, implementing Approov is very easy and it delivers a frictionless experience for your end customers.

 

Approov Certificate Pinning Protection in Action

Now that we have implemented Approov in the mobile app and API server we are ready to see how Approov protects us against attempts to bypass certificate pinning. For consistency with the previous article, we will take the same approach, which consists to unpack the APK, changing the code, repackaging it, and reinstalling it on our device. Always remember that this is not the only way of bypassing certificate pinning, thus I challenge you to also try Frida or Xposed as a homework assignment.

Setup to Bypass Certificate Pinning

You need to follow these setup instructions from the previous article about bypassing certificate pinning and then come back here when you have finished it, that it is before starting the section about tampering with the mobile app.

So, you must only return here after the mitmproxy server is running, and the mitmproxy setup in the Android emulator is completed, including having the emulator running with the proxy settings configured to the same WiFi IP address and port where mitmproxy is listening for requests.

Testing that Approov Detects a MitM Attack

Now that you have the mitproxy and the emulator running and configured to proxy the requests through it you can go ahead and build a release, install and launch it on the emulator to see if you can intercept the API requests as shown in the previous article. During the setup, the emulator was started with a writable file system and the mitmproxy certificate authority was added to the Android operating system trust store in order for the MitM attack to succeed, as we saw in the previous article. But will it work when the mobile app is being protected with an Approov certificate pinning implementation? Let's check it out....

Build the release:

./bin/build-release.bash

Install the release in the emulator:

adb install -r app/build/outputs/apk/release/app-release.apk

Launch it on the emulator:

adb shell am start -n com.criticalblue.currencyconverterdemo/com.criticalblue.currencyconverterdemo.MainActivity

Now try to make a conversion and you will see that the MitM attack is detected by Approov:

Screenshot from the mobile app screen with error showing the detection of a MitM attack by Approov.

 

Testing that Approov Detects Repackaged Mobile Apps

Now that we know that Approov can detect a MitM attack it's time to see if it can detect a repackaged mobile app. For example, an attacker wanting to bypass the Approov dynamic certificate pinning implementation or any of the other protections would have to unpack the mobile app binary, find the respective code, modify or remove it, repackage the mobile app and then try to run it again to evade the Approov protections.

Let's simulate the attacker's behaviour by unpacking the release, modifying it, repackaging it and then running it on an emulator, instead of on a real device. Attackers often resort to the use of emulators to conduct their reverse engineering attacks, so we will go with that approach. They also use real devices, but that requires rooting the device and installing some software on it to try to hide that it is rooted, and this approach is too complex and laborious to show in a single blog post.

Unpack and Modify the APK

To unpack the APK we can use this helper script:

./bin/unpack-apk.sh

The output:
… omitted output

---> APK decode into: .local/apktool/decoded-apk

Now that the APK is decompiled, it's possible to inspect what it does and to modify its behavior. The decompiled code is in Smali code, not Java, and therefore requires you to be familiar with Smali to be able to figure out what is going on.

Attackers of Android apps are usually fluent in Smali code and they can snoop around it and eventually find what they need to change in order to disable certificate pinning. But wait, as you may remember in the previous article, certificate pinning was disabled just by editing the network security config file. So why can’t it be done in the same way now? Well, that’s because Approov handles certificate pinning dynamically, and the initial pin is shipped with the Approov initial configuration, thus the network security file is not required for this purpose.

Approov Dynamic Pinning overview:

Approov Dynamic Pinning diagram

Given that targeting the network security config file is not an option for the attacker anymore, the next approach they might take is to find the Smali code that handles certificate pinning, remove it, repackage the app, and then launch a MitM attack to intercept the traffic between the mobile and the API server.

The process for finding the Smali code is out of scope for this article, but I can tell you that the attacker would eventually end up modifying the Smali code that relates to this Java code:

BaseHttpStack httpStack = approovService.getBaseHttpStack();

requestQueue = Volley.newRequestQueue(appContext, httpStack);

To be equivalent to this Java code:

requestQueue = Volley.newRequestQueue(appContext);

Yes, you saw it right, the attacker removed Approov from the mobile app altogether, but he will be surprised with the outcome.

NOTE: Since it's out of scope for this article to teach you how to find and modify the Smali code you can go ahead with all the remaining instructions to repackage the mobile app without modifying a single line of code in the decoded APK and you will be also surprised with the outcome.

Repackage and Reinstall the APK

Now that the Smali code has been modified to remove Approov from the equation, it's time to repackage the mobile app and reinstall it on the emulator.

We can repackage the mobile app with the help of this helper script:

./bin/repackage-apk.sh

Restart the emulator without the writable file system and without the proxy:

adb emu kill

emulator -avd pixel-android-api-29 &> /dev/null &

And then install the repackaged mobile app APK on the emulator with:

adb install -r .local/apktool/decoded-apk/repackaged-and-signed.apk

Approov Protection in Action

Now that the APK have been repackaged and reinstalled on the emulator we can launch it:

adb shell am start -n com.criticalblue.currencyconverterdemo/com.criticalblue.currencyconverterdemo.MainActivity

Next, if we try a new currency conversion we can see that we get an error:

Screenshot from the mobile app (code obfuscated) screen when the API server fails to validate an Approov token.

This error is an authentication exception thrown by Volley because the API server is refusing to fulfil the request and returning a 401 response to the mobile app. If the mobile app release was not built with code obfuscation you would see instead this error:

Screenshot from the mobile app (code un-obfuscated) screen for when the API server fails to validate the Approov token.

Note: To disable code obfuscation when building the release just set to false the minifyEnabled and shrinkResources on the release buildTypes in your app level build.gradle file and then follow again the steps to build the release, unpack and repackage it.

The API server saw an invalid Approov token in the request, thus it returned a 401 response, instead of returning the 200 response with the requested data. The token is invalid because the APK has been modified, and this is how Approov protects your data from being breached. Put another way, only genuine mobile app instances can communicate with your API endpoints, not repackaged apps, bots, or to someone trying to probe the API server from a tool like Postman.
 

To illustrate the depth of security of your API server, if you had just repackaged the APK without actually changing anything in it, the Approov token issued by the Approov cloud service would still have been invalid. This is because the repackaged app will have a different DNA signature to the original app you registered with the Approov cloud and would therefore not pass the Approov authentication process. It should also be noted that if you had attached Frida, Xposed or some other instrumentation framework to disable certificate pinning at runtime, this scenario would also result in a failed attestation from the Approov cloud service.

You might consider a couple of other ways to beat Approov, like writing a script to generate correctly formed API requests. However, the attacker would not be able to provide an Approov token to the API server and so the API requests would be refused. Secondly, you might think that the attacker could spoof the Approov tokens but this is not possible because they are signed JWT tokens with a very short lifetime, and the secret used to sign them is only known by the Approov cloud service and by the API server. Therefore without the mobile app knowing the secret to sign the Approov Tokens it can't check their validity, neither can the attacker manipulate the app to issue new ones.

Summary

Protecting against certificate pinning bypass is done by implementing the Mobile App Attestation concept which allows the API server to detect with high confidence if what is making the request is a genuine mobile app instance or not. This approach will block attackers from accessing data they are not meant to have. The data is protected, regardless if the attempt is made via a script, a bot, a tampered mobile app, or by attaching a run-time instrumentation framework, like Xposed or Frida.

The Mobile App Attestation approach used here is a proactive one, since what is making the request is authenticated before any requests to the API server have been made. Also, because the authentication decision is taken outside of the mobile app itself, it cannot be reverse engineered or circumvented without being detected. This contrasts with traditional API server defenses that either parse requests in realtime and try to decide if they can be trusted or not, or they search for patterns or signatures which have previously been found to be bad. Either way, these approaches are prone to false positives and false negatives, thus requiring constant monitoring in order to relax or increase the severity of the policies being used. The Mobile App Attestation concept used by Approov removes both the doubt about the authenticity of the API traffic and the burden of constant monitoring and fine tuning of policies being used.

You can learn more about how Approov works and protects the Mobile app and API server in our Whitepaper How to Prevent MitM Attacks between Mobile Apps and APIs or  if you prefer on the Webinar Make MitM Attacks a Thing of the Past.

Implementing Approov in your mobile app and API server is a straightforward process which does not impact your development and deployment processes, illustrated by the words of one of our fintech customers:

“Approov was a natural choice at the end of our research because of the extensive capabilities of the product. It required minimal integration work while providing maximum security and flexibility. The similar solutions we found were too rigid and required too much initial integration work.”

 

Paulo Renato

Paulo Renato is known more often than not as paranoid about security. He strongly believes that all software should be secure by default. He thinks security should be always opt-out instead of opt-in and be treated as a first class citizen in the software development cycle, instead of an after thought when the product is about to be finished or released.