adjoe Engineers’ Blog
 /  Android  /  Obfuscation in Android Apps
Purple illustration as decorative image
Android

Improving Code Obfuscation in Android Apps

We all have our secrets. This is true for individuals as well as companies. If a secret becomes known to the public, this usually leaves the party who tried to withhold the secret in a bad spot. 

For a company, the secret could be the key to its business. If it gets leaked, it could give their competitors an advantage and potentially ruin the company. This secret could be for example a new algorithm that a company is developing, a more advanced way of collecting information, or simply paths to internal APIs.

Mobile developers are forced to include the code for these secrets in their applications. If the code isn’t there, and if the code doesn’t contain the logic, it cannot do anything. At the same time, this means that the secrets can end up in the user’s hands, who can then do with them whatever they want. For example, they could call the internal APIs with fake parameters, which can potentially harm the business.

The best way to prevent this is, of course, not to add critical code to your mobile apps. But if that isn’t possible, then the only way to protect the code is through obfuscation. Effective code obfuscation is important for protecting your business secrets and, with that, your business.

In this article, I will explain the principles of code obfuscation and provide examples of implementing different types of obfuscation in Android apps.

Obsuf … Pobf … Obf … What???

What is obfuscation? Many Android developers are already familiar with the concept. If you are one of them, feel free to skip this paragraph. But if you are unsure about what obfuscation is or why you might need it, keep reading.

In essence, code obfuscation is a technique that scrambles your code. It doesn’t change the order in which the code is executed because that would most of the time change its functionality. But it changes the order in which the statements appear in the source code. 

Obfuscation can also change identifier names, conceal computations, and even add bogus code that doesn’t have any effect. All of this makes the code more difficult to understand for third parties and thus reduces the chance of someone else using your code.

So, How Does Obfuscation Work?

There are many ways to obfuscate code. However, an easy way to approach it is the following.

Think about all the principles of writing good, high-quality code: It should be readable; understandable; maintainable; and apply DRY, SOLID, KISS – and all those other principles. To obfuscate code, take the principles and reverse them!

val K = -3
fun v(v: List<Int>): Double {
   var (a, b) = 0 to 0.0;
   var c = 0
   if ((System.currentTimeMillis() shr 63) == 0L) a = v.size
   else b = v[0].toDouble()
   while (a > 0) {
       b += v[c + (a xor a)] * -((a xor -a) shr K)
       c += a / a--
   }
   return b / c
}
val K = 31

Ugly code, beautifully obfuscated: can you guess what it does? See below for the solution.

Why? Again, code obfuscation is essentially writing code that is unreadable and hard to understand. Therefore, by writing the ugliest code you effectively obfuscate the code.

There is one important thing to keep in mind, though. If you obfuscate source code, you obfuscate it not only for third parties that are not supposed to read it, but also for yourself and your coworkers. These are people who should still understand the code because they have to work with it!

To solve this problem, code obfuscation is usually applied not on the source code level, but on the bytecode level at compile time. This means that whatever effect the obfuscation has won’t be visible in the source code.

Bytecode is not easy to read, and it’s even harder to write. That’s why obfuscation on the bytecode level is typically not done manually but with the help of programs that apply the obfuscation automatically.

fun average(numbers: List<Int>): Double {
   return numbers.sum().toDouble() / numbers.size
}

The unobfuscated code from the example above. Now it’s clear that it calculates the average of a list of input numbers. To obfuscate it, I added additional variables, dead code and of course a good portion of bit shift magic.

There are many programs for obfuscation – both proprietary and paid as well as open source and free. Among the free and open-source ones, ProGuard is probably the most famous one for Android developers, since it is the default obfuscator and code shrinker for all Android apps.

ProGuard, however, doesn’t provide the most powerful obfuscation. To illustrate this, look at the following example and compare the unobfuscated code with the code that was obfuscated by ProGuard.

code snippet showing before and after code obfuscation in android apps

You will notice that all that ProGuard did is change the names of the method, its parameters, and local variables. While this is already quite useful – after all, names should always be chosen in a way that they describe what the variables or method is supposed to do – the rest of the method remains unobfuscated, and it is quite easy to determine what it does.

The reason for that is that ProGuard was not created with obfuscation in mind but rather with code minification and optimization. And it is really good at that! But if you want to have something that is really good at obfuscation you have to look further.

Integrating Third-Party Obfuscators

An easy way to add string and control-flow obfuscation to your Android app is through OpenObfuscator by dProtect. At the time of writing, this is the most popular free and open-source obfuscator specifically for Android. It has configuration capabilities similar to ProGuard and is fully compatible with ProGuard. The only downside is that it doesn’t seem to be actively supported anymore.

To add OpenObfuscator to your project, you simply need to add it as a Gradle plugin:

buildscript {
   dependencies {
       classpath 're.obfuscator:dprotect-gradle:1.0.0'
   }
   repositories {
       mavenCentral()
       maven {
           url = uri("https://maven.pkg.github.com/open-obfuscator/dProtect")
           credentials {
               username = "xxx"
               password = "xxx"
           }
       }
   }
}

Read this article to learn how you can generate the GitHub personal access token that you need to download the plugin.

With that done, all that’s left to do is create a configuration file. Next to string and control-flow obfuscation, OpenObfuscator also supports arithmetic and constant obfuscation (which can be seen as a special case of control-flow obfuscation). The following configuration file enables all of them for the entire project.

-obfuscate-arithmetic,high class io.adjoe.obfuscation.** { *; }
-obfuscate-constants class io.adjoe.obfuscation.** { *; }
-obfuscate-control-flow class io.adjoe.obfuscation.** { *; }
-obfuscate-strings class io.adjoe.obfuscation.** { *; }

Now it’s time to build your application. 

While that is running, let’s talk about performance for a second. As obfuscation almost always introduces additional code, it does impact the performance of your application negatively. However, you will find that the impact is usually not big and not noticeable by the user, unless you overdo it. It certainly will not cause ANRs

In the end, it is a tradeoff between security and performance. If performance is the most important thing for you, you can always scale the level of obfuscation down and only obfuscate those parts of the code that really require it (after all, is it really such a big secret how you change the button’s color to red?).

After the compilation has finished, we can inspect the results. Here you can see an example of control-flow, arithmetic, and constant obfuscation. Do you recognize the method?

fun a(n: Int): Int {
    var l: Long
    val lArray: LongArray = b
    var n2: Int
    while ((0x377A2C62.toInt() xor (lArray[0].toInt().also { n2 = it })) % (lArray[1].toInt() xor 0x52BE2870) != 0) {}
    var n3 = (lArray[2].toInt() xor 0x5B45B7AF)
    var n4 = (lArray[3].toInt() xor 0x780DEDF0)
    var n5 = (lArray[1].toInt() xor 0x52BE2870)
    n2 = n4
    if (n5 <= n) {
        n2 = n3
        while (true) {
            n3 += n4
            l = b[3]
            val n6 = l.toInt()
            n2 = n3 - (n6 xor 0x780DEDF0) - n3 + (n3 + (l.toInt() xor 0x780DEDF0) + n2)
            while (n5 != n) {
                lArray = b
                l = lArray[3]
                val n7 = l.toInt()
                n5 += (l.toInt() xor 0x780DEDF0) * 2 + (~(n5) or (n7 xor 0x780DEDF0)) + (n7 + n5 - ((l.toInt() xor 0x780DEDF0) + n5 + (l.toInt() xor 0x780DEDF0) + (~(l.toInt() xor 0x780DEDF0) or n6)))
                a = (lArray[9].toInt() xor 0x673E7E26).also { n3 = it }
                if ((n3 * n3 + n3 + (lArray[4].toInt() xor 0x1EDF9B91)) % (lArray[7].toInt() xor 0x14F34D4B) == 0) {
                    n4 = n2
                    continue
                }
                n3 = n4.also { n4 = n2 }
                n2 = n3
            }
            break
        }
    }
    return n2
}

Yes, this is the fibonacci method from above. Who would have guessed? Comparing this to the obfuscation that ProGuard applied to the same method, you can see what good obfuscation can do and how it can protect your application.

Create Your Own Obfuscator

Okay, we know how we can greatly improve the obfuscation of our code by using custom third-party obfuscators. But if others can do it, why shouldn’t we build our own obfuscator for Android? Is that possible? 

The answer is yes, it is possible. And it is even a really good idea. Not only because it’s a fun project, but also because you can implement custom obfuscation algorithms that are not yet known to deobfuscators. This makes your obfuscation even more secure.

But even though the concept of writing an obfuscator for Android is not that complicated, it would still be too much for this article. I will save this for a later article, so stay tuned!

Putting It All Together

You have learned what obfuscation is and why it’s beneficial to use code obfuscation in Android apps. We went over different obfuscation methods, from simple manual obfuscation over automatic obfuscation with ProGuard to more advanced techniques like control-flow obfuscation. You saw how easy it is to integrate an open-source obfuscator that adds more powerful obfuscation than ProGuard which improves the security of your application.

I recommend using obfuscation in all parts of your application that don’t contain publicly known information (for example, a simple Fibonacci number computation). You can hide your internal API paths using string obfuscation and other secret computations using, for example, control-flow obfuscation.

Keep in mind: This doesn’t make your app secure, but it makes it more secure and in the best case makes attackers lose their minds when reverse-engineering your app.

If you would like to see obfuscation in action or simply check out the code examples from this article again in detail, take a look at this GitHub repository. Here I have put everything together in a runnable application. It also implements OpenObfuscator, which will make it easier for you to get started with it in your own applications.

Programmatic Supply

Android Developer (f/m/d)

  • Full-time,
  • Hamburg

iOS Developer (f/m/d)

  • Full-time,
  • Hamburg

Product Lead (f/m/d)

  • Full-time,
  • Hamburg

Senior Product Manager (f/m/d)

  • Full-time,
  • Hamburg

Tech Lead (f/m/d)

  • Full-time,
  • Hamburg

We’re programmed to succeed

See vacancies