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.

Android Developer (f/m/d)

  • adjoe
  • Programmatic Supply
  • Full-time
adjoe is a leading mobile ad platform developing cutting-edge advertising and monetization solutions that take its app partners’ business to the next level. Part of the applike group ecosystem, adjoe is home to an advanced tech stack, powerful financial backing from Bertelsmann, and a highly motivated workforce to be reckoned with.

Meet Your Team: Programmatic Supply

In a competitive adtech market, adjoe stands for greater transparency and fairness for app publishers and advertisers – and a more relevant and enjoyable experience for users. 

It’s exactly for this reason that adjoe’s Programmatic team has built its own mediation platform WAVE. It’s not only driven by a backend application that decides which ad to show but also by our Android and iOS SDKs. These render the ads for the user and provide useful tracking events to the backend.

The WAVE SDK serves millions of in-app ads a day in different formats, and our backend system handles a few billion auctions in real-time every day to help game developers monetize their apps.

Want to be a part of this industry-first adtech solution? Join our discussions, explore implementation, and put your problem-solving skills to the test in our cross-functional Programmatic team!

Our mobile tech stack

Android Studio, Kotlin, Java, Gradle, okHTTP, Wire, gRPC, Protobuf, XCode, Swift, Objective-C, Cocoapods, SPM, Unity, C#, React Native, JavaScript/TypeScript, GitLab CI, Maven, Sentry, Google Firebase.

And we’re always open to new technologies & tools that will benefit the product.
What You Will Do
  • You will develop the WAVE SDK by creating new features and innovative ad formats. The views you build will be seen by billions of mobile users all over the world every day.
  • You will be responsible for cross-platform mobile development by ensuring our SDKs’ compatibility with other development frameworks like Unity, React Native, and Flutter. Thanks to that, our product will be integrated into top mobile games and apps in the industry.
  • You will implement mechanisms to improve the technical stability, performance, and maintenance of WAVE SDK. Your goal is to make it as easy and smooth as possible for app developers to integrate and utilize adjoe’s technologies.
  • You will use your system design knowledge to come up with the optimal solutions compatible with different frameworks and our backend.  
  • You will align with advertising industry standards & best practices to keep our product up to date.
  • You will contribute to our CI/CD pipelines to efficiently and effortlessly distribute our SDK to thousands of app developers.
  • Who You Are
  • You have 3+ years of work experience as a mobile developer working with Kotlin.
  • You have hands-on experience with developing Android libraries and SDKs.
  • You keep up to date with trends in mobile development and are aware of UX & UI best practices.
  • You are open to relocating to Hamburg, Germany.
  • Plus: You have basic knowledge of C++ and working with the NDK.
  • Plus: You have knowledge of React Native (JavaScript), Unity (C#), Flutter (Dart), or similar cross-platform development frameworks.
  • Plus: You have iOS development experience as well.
  • Plus: You have experience with build/deployment tooling (such as Gradle, Maven, and GitLab CI).
  • Heard of Our Perks?
  • Work-Life Package: 2 remote days per week, 30 vacation days, 3 weeks per year of remote work, flexible working hours, dog-friendly kick-ass office in the center of the city.
  • Relocation Package: Visa & legal support, relocation bonus, reimbursement of German Classes costs and more.
  • Happy Belly Package: Monthly company lunch, tons of free snacks and drinks, free breakfast & fresh delicious pastries every Monday
  • Physical & Mental Health Package: In-house gym with personal trainer, various classes like Yoga with expert teachers.
  • Activity Package: Regular team and company events, hackathons.
  • Education Package: Opportunities to boost your professional development with courses and trainings directly connected to your career goals 
  • Wealth building: virtual stock options for all our regular employees
  • Skip writing cover letters. Tell us about your most passionate personal project, your desired salary and your earliest possible start date. We are looking forward to your application!

    We welcome applications from people who will contribute to the diversity of our company.

    We’re programmed to succeed

    See vacancies