Prevent Reverse Engineering (RE) of your Android application

From this article, you’ll learn how to implement protection measures against reverse engineering in your android application.

Łukasz Bobrek 2021.12.15   –   8 MIN read

TL;DR

  • Do not hesitate to implement as many anti-RE protections as possible.
  • Implementation cost of such protections is usually low, but they significantly make the attacker lifes harder.
  • Use source code obfuscation. 
  • Consider adding an additional encryption layer for high risk applications.
  • When designing mobile apps, try to follow current best security standards. Take a look at our Guidelines on mobile application security – Android edition.

Introduction

In this article I will cover both basic and more advanced techniques used on Android to protect applications from reverse engineering and tampering by the attackers. Basically, the goal is to implement mechanisms that would make it harder to analyse the source code and dynamic behavior of the application. Making the potential attacker’s life harder significantly reduces the risk that they will select your application. After all, why should they attack the application with several protection mechanisms, when they can just pick as a target application with little or no safeguards, thus saving themselves a lot of time? 

Having that said, keep in mind that all below-mentioned mechanisms are implemented on the application side, so they can be bypassed. But like said before, bypassing them is time consuming and can be tricky, so most attackers would simply skip your application and pick some other as a target. 

Types of Anti-RE Protections on Android

Integrity Protection

On Android, it is fairly easy to decompile an application into smali code. Smali is a hybrid language, it uses opcode (assembler style) but is also semi-readable. Smali can be analysed and patched in order to modify application behavior, for instance to disable certificate pinning or root check. Patched applications need to be recompiled and resigned by an attacker.

Reverse engineering android protection

Integrity protection mitigates smali code patching by verifying whether the signing certificate matches the expected developers certificate. What happens inside the code is that we can compare our developers certificate fingerprint with the certificate that has actually been used to sign the application. In case they do not match, we know that the application was tampered with and we can handle it accordingly. Below, sample code with demonstration of this mechanism: 

private boolean checkSignature() throws PackageManager.NameNotFoundException, NoSuchAlgorithmException {
   final String devSignature = "<redacted>";
   final PackageInfo packageInfo = getPackageManager().getPackageInfo(getPackageName(), PackageManager.GET_SIGNING_CERTIFICATES);
   final Signature[] signatures = packageInfo.signingInfo.getApkContentsSigners();
   for(Signature signature : signatures) {
       byte[] signatureBytes = signature.toByteArray();
       MessageDigest md = MessageDigest.getInstance("SHA");
       md.update(signatureBytes);
       final String currentSignature = Base64.encodeToString(md.digest(), Base64.DEFAULT);
       if (currentSignature.equals(devSignature)) {
           return true;
       }
   }
   return false;
}

Vendor Verification

In most cases, there is only one valid way to install a production application by the final user, which is Google Play Store. We do not want users to install our application from any third party store or with adb install. There is simple mechanism, which allows us to verify which vendor was used to install the application:  

private boolean checkVendor() {
   PackageManager pm;
   pm = getApplicationContext().getPackageManager();
   String installer = pm.getInstallerPackageName("<our package name>");
   if (installer == null) {
       return false;
   } else return installer.equals("com.android.vending");
}

If the installer is null (which means that the application was installed with adb install) or if the vendor is different from com.android.vending, we know that it was installed from an untrusted source and we can behave accordingly. 

Root check and SafetyNet

  • Check for su binaries, 
  • Check for root management applications,
  • Check for potentially dangerous apps,
  • Check for root cloaking apps,
  • Check fo test keys,
  • Check for dangerous props,
  • Check for busybox binary,
  • Check if su exists,
  • Check for the RW system.

To use below mentioned library, you should add dependency to gradle.build: 

implementation 'com.scottyab:rootbeer-lib:0.0.8'

And simply add the following code into your application: 

private boolean checkRoot() {
   RootBeer rootBeer = new RootBeer(this);
   return rootBeer.isRooted();
}

Note that, as all the other mechanisms presented in this article, local root check can be bypassed with frida or with smali patching, so it is highly recommended to use SafetyNet, which when implemented properly is not bypassable and also provides several additional checks, which guarantees device security. More information about SafetyNet can be found in this article:

Debugging detection

In some very unfortunate cases, developers may accidentally build the application in debug mode. To self protect against such eventuality, we can simply put following code into the project:

private boolean checkDebuggerAttached() {
   return Debug.isDebuggerConnected();
}
 
private boolean checkDebugger() {
   return (BuildConfig.DEBUG) || ((this.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0);
}

When the application detects debug mode it can either terminate or notify the application server about detected build.

Source code obfuscation

Android Runtime (ART) executes .dex files, which are part of the APK package. Dalvik bytecod (.dex) can be translated to equivalent Java bytecode. Conversion is not perfect and cannot be reversed, but Java code can be easily read and analyzed. Understanding the code, specifically implemented security mechanisms gives the attacker great advantage and  significantly increases the chance of exploiting the application. 

Reverse engineering Android Security

To mitigate that risk, developers can obfuscate the source code. Obfuscation is a process of making a code difficult to understand by humans, but without changing its semantics and functionality. The most typical techniques used by obfuscators are changing methods/parameters names, modifying the flow of the code and encrypting string and assets. 

Most popular obfuscators for Android code are ProGuard and DexGuard. The first one is available for free, but offers less protection against reverse engineering. 

For demonstration purposes, there is piece of not obfuscated Java code after decompilation process: 

private boolean checkVendor() {
      PackageManager var1 = this.getApplicationContext().getPackageManager();
      String var2 = var1.getInstallerPackageName("com.lbobrek.secretskeeper");
      return var2 == null ? false : var2.equals("com.android.vending");
   }

In order to enable obfuscation, build.gradle needs to be modified: 

buildTypes {
   release {
       minifyEnabled true
       proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
   }
}

Finally, the same code after basic obfuscation looks as follows: 

public void u() {
      String var2 = this.getApplicationContext().getPackageManager().getInstallerPackageName("com.lbobrek.secretskeeper");
      int var3 = 0;
      String var4 = null;
      boolean var5;
      String var6;
      boolean var7;
      if (var2 == null) {
         var5 = false;
         var6 = null;
      } else {
         var6 = "com.android.vending";
         var7 = var2.equals(var6);
         var5 = var7;
      }

Additional encryption layer for HTTP requests

For high risk applications it may also be a good idea to implement additional networking hardening. The most common is additional encryption of sent HTTP requests body. All the parameters and data sent within the request are encrypted and thus unreadable to the attacker, even after successful MITM and intercepting cleartext communication. In such a case, the attacker also needs to decrypt the data in order for the HTTP body. 

An example encrypted request looks as follows:

POST /api/data HTTP/1.1
Host: securing.biz
Connection: close
Authorization: [...]
Content-Length: x
 
encrypted binary data .......

Usually, the goal of the attacker is to modify HTTP parameters. In the case of an additional encryption layer, the attacker also needs to properly encrypt modified data. When the data is encrypted using asymmetric cryptography (e.g. RSA) a sophisticated proxy has to be used. It will require changing an encryption key on the device, decrypt it on the beginning of the proxy, modify the data and then again encrypt using the server’s encryption key. Such effort required to break the encryption may scare off potential attackers.

Summary 

In this article I presented you some basic and more advanced techniques used to make your application harder to reverse engineer by the attacker. Remember that usually the implementation cost of such protections is low, but the effort needed by the attacker to break them is significant. Also, it is a good practice to trigger implemented protections several times, for instance on application load and during login attempts. It may deter attackers, even when they patch the code to bypass your protections. 

Last but not least, remember that all of the above-mentioned protections are implemented in the application, so they can be in one way or another circumvented. Despite client side protections, secure coding and proper server-side checks must be also in place.

While hardening applications is a good idea, don’t forget to code your app securely as well. It’s just another layer of comprehensive defense. We’ve put together a handy guide to help:

Feel free to reach me out. You can find me on Twitter on LinkedIn

Łukasz Bobrek
Łukasz Bobrek Principal IT Security Consultant
Head of Cloud Security