My mission as a mobile security researcher at NowSecure and during my doctorate days back at the University of California, Riverside, is to research the privacy and security issues in Android and iOS mobile apps. Lately I’ve been troubled by the reemergence of mobile app developers’ use of Security By Obscurity in many apps with underlying vulnerabilities. The concept of Security by Obscurity refers to design or implementation secrecy as the main mechanism for security.
Some companies would have you believe the No. 1 vulnerability for mobile apps is failing to use code obfuscation. Unfortunately, even if a developer tries to sweep app vulnerabilities under the rug, they’ll still be discovered and potentially exploited. In fact, our automated mobile application security tools routinely find vulnerabilities in apps that employ obfuscation, and there’s no shortage of mobile attack vectors.
The first priority of any developer must be to ensure that your app is being tested for security vulnerabilities and privacy issues.Only after finding and fixing these issues should developers invest time in making an attacker’s life difficult. They usually accomplish this by obfuscating the app logic or some sensitive hardcoded values which likely should not have been hardcoded in the first place. This code complexity may affect the mobile device battery, slow app performance or have other negative effects.
Obfuscation & Kotlin Android Apps
Having said that, let’s examine how Android apps programmed using Kotlin could render Security By Obscurity ineffective. Kotlin is a statically-typed, general purpose language which was designed to interoperate fully with Java and the Java Virtual Machine. Android initially supported Kotlin in 2017 and it recently emerged as the preferred language Google recommends for Android app development.
The diagram above shows how the compilation procedure looks when a developer creates an Android app in Kotlin/Java. First the Java compiler will compile all the Java source files, while the Kotlin compiler will do the same for the Kotlin ones. The generated .class files together with any third-party library will be obfuscated with ProGuard or a similar obfuscator (if applicable) and will be transformed to dex files with the help of D8. Aapt2 produces the resulting APK by packaging the .dex files, the AndroidManifest.xml and any available resources.
It is safe to assume that if developers decide to use ProGuard or a similar obfuscation solution, their code will have obfuscated method/class/field names, thus making the reverse engineering process more difficult and time consuming. To make matters worse, if they follow the notion of Security Through Obscurity, they will wrongly assume that their code is relatively safe as well.
However, Kotlin has an interesting annotation that can be presented in any class by the Kotlin compiler. This metadata annotation, as can be seen from the source code, contains a field with the name d2, which is an array of strings that occur in the original class written in plaintext. As you might have already guessed, those strings don’t get obfuscated.
That means if this annotation exists inside the APK’s dex files, someone will be able to match the obfuscated names with the original ones and substantially ease the reverse engineering process. (Read on to see how someone can do that.) For example, a reverse engineer will be able to write appropriate Frida hooks for relevant obfuscated methods or in general understand much more about the app’s inner workings. Not that this does not necessarily make the app less secure, but if the developers of the app rely solely on obfuscation as a security measure, then that could potentially be the case.
Static Analysis of Source Code vs Binary
Kotlin Android apps offer a great example of why static analysis of binaries is better than static analysis of source code. Not only can the NowSecure mobile application testing solution deobfuscate method/field signatures that make our analysis faster, but as was described above, the generated dex files of a Kotlin Android app are no different than the ones written in Java. This means that the NowSecure platform’s static application mobile app security testing solution security testing (SAST) capabilities for binaries will work out of the box for Kotlin Android apps. By contrast, whereas any vendor that provides SAST of source code will have to support a whole new programming language, which will take them a lot of time and money.
How to Reverse Engineer Obfuscated Code
To detect if an app contains Kotlin code, you can extract the contents of the APK using apktool and try to find if there is a package with the name kotlin inside the folders that contain smali code.
If the Kotlin package is there it is probably safe to assume that the app contains Kotlin code. Next, we can try to find which classes contain Kotlin metadata. Note that sometimes this Metadata class is obfuscated as well, but it will always be a runtime annotation that starts with “Lkotlin/”. For this example, let’s use the Evernote Android App with SHA-256 4EA721F971272CE93A05DBEA67489CB885D508F9098ECCEC9441DAB45D77FCCF
. After unpacking the APK with apktool, run inside the resulting folder the following one-line command:
grep -R "annotation runtime Lkotlin/Metadata;" smali*
This will match a little more than 3,000 classes. From all the results, we picked smali_classes2/com/evernote/ui/notesharing/c/m.smali
which is a relatively small obfuscated class. This class contain the following fields (in smali):
.field private final a:Lcom/evernote/ui/notesharing/c/j; .field private final b:Lcom/evernote/ui/notesharing/c/n;
And the following methods:
.method public constructor <init>(Lcom/evernote/ui/notesharing/c/n;)V .method public a()Lcom/evernote/ui/notesharing/c/j; .method public b()Lcom/evernote/ui/notesharing/c/n; .method public synthetic c()Ljava/lang/Object; .method public equals(Ljava/lang/Object;)Z .method public hashCode()I</code> .method public toString()Ljava/lang/String;
Clearly the names are obfuscated. But the Metadata annotation contains the following:
.annotation runtime Lkotlin/Metadata; bv = { 0x1, 0x0, 0x3 } d1 = { "u00000nu0002u0018u0002nu0002u0018u0002nu0002u0018u0002nu0002u0008u0005nu0002u0018u0002nu0002u0008u0005nu0002u0010u000bnu0000nu0002u0010u0000nu0000nu0002u0010u0008nu0000nu0002u0010u000enu0000u0008u0086u0008u0018u00002u0008u0012u0004u0012u00020u00020u0001Bru0012u0006u0010u0003u001au00020u0002u00a2u0006u0002u0010u0004Jtu0010u000bu001au00020u0002Hu00c6u0003Ju0013u0010u000cu001au00020u00002u0008u0008u0002u0010u0003u001au00020u0002Hu00c6u0001Ju0013u0010ru001au00020u000e2u0008u0010u000fu001au0004u0018u00010u0010Hu00d6u0003Jtu0010u0011u001au00020u0012Hu00d6u0001Jtu0010u0013u001au00020u0014Hu00d6u0001Ru0014u0010u0003u001au00020u0002Xu0096u0004u00a2u0006u0008nu0000u001au0004u0008u0005u0010u0006Ru0014u0010u0007u001au00020u0008Xu0096u0004u00a2u0006u0008nu0000u001au0004u0008tu0010nu00a8u0006u0015" } d2 = { "Lcom/evernote/ui/notesharing/recipientitems/SingleNoteRecipientItem;", "Lcom/evernote/ui/notesharing/recipientitems/Recipient;", "Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;", "data", "(Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;)V", "getData", "()Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;", "viewType", "Lcom/evernote/ui/notesharing/recipientitems/RecipientItemViewType;", "getViewType", "()Lcom/evernote/ui/notesharing/recipientitems/RecipientItemViewType;", "component1", "copy", "equals", "", "other", "", "hashCode", "", "toString", "", "evernote_armv7EvernoteRelease" } k = 0x1 mv = { 0x1, 0x1, 0xd } .end annotation
As we can see, the d2 field contains plaintext strings which are related to the obfuscated names that we saw before. The strings though don’t seem to properly display method/field signatures, and that’s because the Kotlin compiler tries to optimize the size of the resulting annotation. d1 contains the information to convert the plaintext strings to actual method and field signatures.
To accomplish that, we need to write a small program in Kotlin that uses the kotlinx-metadata-jvm library, as seen below:
import kotlinx.metadata.jvm.* fun main(args: Array<String>) { val k = 0x1 val numbers: IntArray = intArrayOf(0x1, 0x1, 0xd) val bv: IntArray = intArrayOf(0x1, 0x0, 0x3) val d1 = arrayOf("u00000nu0002u0018u0002nu0002u0018u0002nu0002u0018u0002nu0002u0008u0005nu0002u0018u0002nu0002u0008u0005nu0002u0010u000bnu0000nu0002u0010u0000nu0000nu0002u0010u0008nu0000nu0002u0010u000enu0000u0008u0086u0008u0018u00002u0008u0012u0004u0012u00020u00020u0001Bru0012u0006u0010u0003u001au00020u0002u00a2u0006u0002u0010u0004Jtu0010u000bu001au00020u0002Hu00c6u0003Ju0013u0010u000cu001au00020u00002u0008u0008u0002u0010u0003u001au00020u0002Hu00c6u0001Ju0013u0010ru001au00020u000e2u0008u0010u000fu001au0004u0018u00010u0010Hu00d6u0003Jtu0010u0011u001au00020u0012Hu00d6u0001Jtu0010u0013u001au00020u0014Hu00d6u0001Ru0014u0010u0003u001au00020u0002Xu0096u0004u00a2u0006u0008nu0000u001au0004u0008u0005u0010u0006Ru0014u0010u0007u001au00020u0008Xu0096u0004u00a2u0006u0008nu0000u001au0004u0008tu0010nu00a8u0006u0015") val d2 = arrayOf("Lcom/evernote/ui/notesharing/recipientitems/SingleNoteRecipientItem;", "Lcom/evernote/ui/notesharing/recipientitems/Recipient;", "Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;", "data", "(Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;)V", "getData", "()Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;", "viewType", "Lcom/evernote/ui/notesharing/recipientitems/RecipientItemViewType;", "getViewType", "()Lcom/evernote/ui/notesharing/recipientitems/RecipientItemViewType;", "component1", "copy", "equals", "", "other", "", "hashCode", "", "toString", "", "evernote_armv7EvernoteRelease") val header = KotlinClassHeader( k, numbers, bv, d1, d2, "", "", 0 ); val metadata = KotlinClassMetadata.read(header) as KotlinClassMetadata.Class; val klass = metadata.toKmClass(); println(klass.functions.map { it.signature }) println(klass.properties.map { it.fieldSignature }) println(klass.constructors.map { it.signature }) }
All in all, the above code creates the KotlinClassHeader with the appropriate values extracted from the KotlinMetadata annotation and obtains the KmClass instance from the KotlinClassMetadata instance. Next,t it prints its functions’, fields’ and constructors’ signatures. Running it yields the following results:
[component1()Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;, copy(Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;)Lcom/evernote/ui/notesharing/recipientitems/SingleNoteRecipientItem;, equals(Ljava/lang/Object;)Z, hashCode()I, toString()Ljava/lang/String;] [data:Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;, viewType:Lcom/evernote/ui/notesharing/recipientitems/RecipientItemViewType;] [<init>(Lcom/evernote/ui/notesharing/recipientitems/SingleNoteShareRecipientData;)V]
Having that data enables us to easily correlate signatures with the obfuscated ones and simplify the reverse engineering process.
Conclusion
The example above demonstrates a few things:
- Relying solely on obfuscation can be dangerous
- The implications of new languages such as Kotlin on static analysis techniques
- The constant evolution of the mobile space.
There are myriad ways of defeating obfuscation. In the particular example above, we showed how one can do it with the help of an actual compiler. To reduce risk in the mobile apps your team develops, we recommend incorporating automated mobile application security testing into the dev pipeline to find and fix security and privacy flaws faster.