engineering

January 09, 2020   |   6min read

LeakCanary—Deobfuscation Feature Explained

Security is very important but also a complex topic. Among many techniques, obfuscation is one of the simplest to apply in the app. Every Android app we make at Polidea is obfuscated. Getting Proguard or R8 config right may not be easy sometimes but we have tools such as Android Studio APK Analyzer to help us with this task. Almost every Android library contains the config so that developers don’t have to do anything. It means that in most cases the cost of obfuscation isn’t high.

A memory leak is also an important (but often ignored) topic. When an app is leaking memory, then, in the best-case scenario, it doesn’t perform well but, more often, it crashes. This is especially true for older and low-end devices. Such devices are very popular in emerging markets (Nokia 1, for example, has only 1GB of RAM memory). Users deserve well-behaving apps and that means we should do everything we can to avoid memory leaks. Fortunately, LeakCanary simplifies detecting and debugging leaks in a very straightforward way.

LeakCanary and obfuscation

Obfuscation can lead to unexpected errors at runtime. A common source of errors are JSON models where field names after obfuscation don’t match JSON keys anymore. It’s very important to test an app after obfuscation. It’s a good practice to have a special variant that is as close to production as possible and run tests on it. Such a variant is a perfect candidate for checking if it causes memory leaks. But what happens if you run LeakCanary on an obfuscated app?

text

Well, it works but, as you can see, LeakCanary shows a leak trace with obfuscated names which isn’t very helpful. I found this problem to be quite interesting, and so I decided to contribute and add deobfuscation capability to LeakCanary. It’s available on Github, starting from version 2.1. In this post, I will explain how to use this feature and how it works.

Deobfuscation

Deobfuscation is a process that takes obfuscated code and restores it to its original form. When obfuscation is enabled, then during the build a tool called R8 (or Proguard/Dexguard) transforms the code in a way that it’s not readable anymore. It also generates a mapping file, which, as the name suggests, contains the mapping from obfuscated name to the original name. If you use a crash reporting tool such as Crashlytics then this mapping file is automatically uploaded to Firebase during the build. It’s done by its gradle plugin. Given this file, Crashlytics will automatically deobfuscate crash stack traces and display them in a human-readable form. It turns out that LeakCanary can also make use of this mapping file to deobfuscate leak traces.

Passing the mapping file to LeakCanary

LeakCanary is a tool that runs within the app. However, the mapping file is generated during the building, so it’s not available at runtime. We need a way to put this file inside an APK so that LeakCanary can read it. To achieve this, we created a gradle plugin that puts the mapping file inside the APK (in assets directory). You might be thinking— “wait a minute, if I put a mapping file inside my app then the whole obfuscation has no sense because a malicious user can unzip the APK, get the mapping file and deobfuscate the code”. And you’re right, that’s why you should not use this feature in your production build. You should have a special obfuscated pre-production build for testing purposes, and that’s where you should also be using this plugin. Getting this wrong is a serious mistake. That’s why I wrote this post to unveil what LeakCanary does under the hood so you can use it consciously.

LeakCanary Plugin Configuration

To use the plugin, you have to add the dependency to your app. You can do it by adding this in your root build.gradle file:

buildscript {
  dependencies {
    classpath 'com.squareup.leakcanary:leakcanary-deobfuscation-gradle-plugin:2.1'
  }
}

Then you need to apply it in your app-specific build.gradle file:

apply plugin: 'com.android.application'
// LeakCanary plugin should be applied after android application or android library plugin
apply plugin: 'com.squareup.leakcanary.deobfuscation'

The last and most important step is the actual configuration. This way, you tell LeakCanary which variant of your app should have the mapping file copied into the APK:

leakCanary {
  // LeakCanary needs to know which variants have obfuscation turned on, but don’t put production variant here
  filterObfuscatedVariants { variant ->
    variant.name == “debug”
  }
}

You can have more than one of such a variant, but if you put a non-obfuscated variant name here, you’ll get a build error. So pay attention to this step and remember not to apply this plugin on a production build.

All those steps are described in LeakCanary’s fantastic documentation.

What does the plugin do?

LeakCanary deobfuscation plugin waits until the R8, or Proguard task is finished. Then for each variant specified in filterObfuscatedVariants block above, it copies this file and places it in the assets directory of the final APK file. And that’s all. There is nothing magical about it. You can verify it yourself—open the final APK with APK Analyzer (you can do this by dragging and dropping the file into Android Studio). It should contain a .txt file with obfuscation mapping.

text

Even though the plugin only copies a single file, it’s also cacheable and compliant with Gradle Task Configuration Avoidance so it won’t negatively affect build times.

What does LeakCanary do with the mapping file?

There is a tool called Shark, which is a part of LeakCanary (but it can also be used as a standalone command line tool). This tool can be used on any Android or JVM project. It reads and analyzes a memory dump which is stored in an .hprof file. LeakCanary creates such a memory dump and passes it to Shark. To speed up the process this tool creates a high-performance map that contains the names of all fields, classes etc. This is a perfect place to hook in with deobfuscation.

Shark has to parse the mapping file and deobfuscate the names when they’re being read from the index. And that’s exactly what happens. Mapping parser and deobfuscation is a part of Shark tool so you can use it without LeakCanary. You can use the following command to analyze an obfuscated .hprof:

analyze-hprof HPROF_FILE_PATH —obfuscation-mapping MAPPING_FILE_PATH

As you can see, the deobfuscation process consists of 2 parts: copying the mapping file and actual deobfuscation. Please try it out and report any issues. I hope my open-source contribution will help make apps memory efficient and secure at the same time.

Contributing to LeakCanary was a great adventure. I encourage everyone to go and contribute to open-source projects you like. I can guarantee that you’ll learn a lot! It’s also a unique chance to work with great, talented people.

I wouldn’t be able to do it without Pierre and Stephane—thank you for the support and for proofreading this post!

Michał Zieliński

Senior Software Engineer

Did you enjoy the read?

If you have any questions, don’t hesitate to ask!