One of the most difficult aspects of creating a good Android application is making sure the app is secure. Certificate pinning and license verification are used commonly in many Android projects to improve security. Unfortunately, these techniques might be easily bypassed by reverse engineering—attackers are even likely to use tools that bypass these security measures automatically. To make attackers' lives much more difficult we encourage you to use a less common security measure: build integrity verification.
A build integrity verification brings many benefits to your project. Consider these advantages:
We want to be able to detect changes to application source code and terminate the application once such changes occur. To do this, we can implement a cyclic redundancy check on the application’s bytecode. We will calculate the hash of the classes.dex file and then store it in strings.xml. Then at some point when the application is running, we can invoke the isCodeModified() function.
This function will retrieve the previously cached checksum and then compare it with the current classes.dex checksum value. If those values differ, then we know that the source code was modified and we can terminate the application process. One important point here is that the cached checksum should be stored in application resources, not directly in the source code. Otherwise the classes.dex checksum would change when you modify the cached crc value.
Now that we know how to implement the build integrity verification one question remains: how and when should we store the checksum in strings.xml? Once we are ready for an application release, we could use this simplistic algorithm:
This process would be cumbersome if we had to perform these steps every time we want to release a new update to Google Play Store. Also, it’s common these days to use a continuous integration system to automate application builds. To address these issues, we will create two gradle tasks:
We left out certain implementation details as an exercise for you.
Now that we have those gradle tasks, we can utilize them in the bash script that will be executed by the CI system:
Here we simply build an application and then verify the checksum. If the checksum is incorrect, then we update it and then build apk once again. Now the checksum should be correct, and we can safely distribute the build to our users.
It’s important to notice that we execute a “clean” task before each build. Otherwise the calculated checksums would differ and our algorithm wouldn’t work correctly.
Unfortunately, the nature of mobile applications renders them impossible to be resilient to every attack. Potential attackers will always have a possibility to decompile your code and modify it. However, the build integrity verification will make potential attacks much more difficult. Hopefully this article will prove valuable to you, and you will consider implementing this security measure in your future projects. Happy coding!