Menu Close Back

Decompiling obfuscated Android applications

Decompiling obfuscated Android applications

 
    Note: Watch the accompanying video here.
 

Android applications can be reverse engineered using a host of freely available tools such as Apktool, dex2jar, dexdump, Jadx, JD-GUI, CFR and Procyon. These tools are used to dump the executable code and, in some cases, reconstruct the source code. Once an attacker has retrieved the source code, he is able to steal parts of it, extract valuable information from it or modify it.

Code hardening (obfuscation and encryption) is an effective way of protecting mobile applications against reverse engineering. It transforms the code through various techniques without affecting the functionality to make it obscure and unintelligible. In some cases, it changes the bytecode so significantly that it’s impossible to revert it back to its original form.

To illustrate what code hardening does, we analyze a sample application (App.java) protected with DexGuard using basic and popular reverse engineering tools. We focus on three techniques: name obfuscation, string encryption and control flow obfuscation.

Name obfuscation

Renaming identifiers is a simple but powerful obfuscation technique. This technique consists in replacing the names of classes, fields, methods and resources with meaningless characters to make the code of application more difficult to read and interpret.

When we apply name obfuscation to the App.java class and try to decompile it with JD-GUI, we see that the output no longer contains any semantic information. We get comparable results when we use CFR or Proycon.

package o;

import android.app.Application;
import android.content.SharedPreferences;

public class ᵍ
  extends Application
{
  protected static ᵍ ˎ;
  private ᴮ ˊ;
  
  public ᵍ()
  {
    ˎ = this;
  }
  
  public static ᵍ ˎ()
  {
    return ˎ;
  }
  
  public SharedPreferences ˋ()
  {
    if (this.ˊ == null)
    {
      this.ˊ = new ᴮ(this, null, "my_prefs.xml");
      ᴮ.ˏ(true);
    }
    return this.ˊ;
  }
}

Code snippet 1: App.java decompiled with JD-GUI after applying name obfuscation

Some decompilers, like Deguard, try to overcome this problem by performing pattern recognition and semantic analysis to guess possible identifier names. Others use standard names like Class001, Class002 etc. to rename unknown identifiers. Such techniques can only aim to change the identifiers to more readable names, but they can’t recover the original identifiers since these are completely stripped from the binary.

package p000o;

import android.app.Application;
import android.content.SharedPreferences;

public class C0515 extends Application {
    protected static C0515 f1699;
    private C0457 f1700;

    public C0515() {
        f1699 = this;
    }

    public static C0515 m2781() {
        return f1699;
    }

    public SharedPreferences m2782() {
        if (this.f1700 == null) {
            this.f1700 = new C0457(this, null, "my_prefs.xml");
            C0457.m2435(true);
        }
        return this.f1700;
    }
}

Code snippet 2: App.java decompiled with jadx after applying name obfuscation

String encryption

Encryption is another obfuscation technique that a decompiler can’t overcome. Encrypted code is decrypted on-the-fly when the application is executed. Since basic decompilers perform static analysis or analysis of the code at rest, they can’t recover information that is only calculated at runtime. Recovering encrypted content requires advanced, often manual, analysis of the code or has to rely on emulation. For this reason, encryption is one of the strongest forms of protection of sensitive files and data against static analysis attacks.

Encryption can be used to protect various components of mobile applications: string encryption for API keys and authentication tokens, resource encryption for keystores etc. Here, we focus on string encryption. We encrypt the string “my_prefs.xml” in the App.java class. When we try to decompile the sample with CFR, we see that the decompiler is not able to retrieve the encrypted string and returns an error message. We get comparable results when we use jadx, JD-GUI or Proycon.

/*
 * Decompiled with CFR 0_115.
 * 
 * Could not load the following classes:
 *  android.app.Application
 *  android.content.SharedPreferences
 *  com.securepreferences.sample.App
 *  o.\u1d2e
 */
package com.securepreferences.sample;

import android.app.Application;
import android.content.SharedPreferences;
import o.\u1d2e;

public class App
extends Application {
    protected static App instance;
    private static int \u02bb;
    private static int \u02bd;
    private static int \u02ca;
    private static int \u02cb;
    private static short[] \u02ce;
    private static byte[] \u02cf;
    private static int \u0971;
    private \u1d2e mSecurePrefs;

    static {
        \u02bb = 0;
        \u02bd = 1;
        \u02cb = 822016385;
        \u0971 = 81;
        \u02ca = -1999280546;
        \u02cf = new byte[]{-1, -11, 74, -69, 13, 1, -13, 2, 17, -26, 12, 0};
    }

    public App() {
        instance = this;
    }

    /*
     * Exception decompiling
     */
    public static App get() {
        // This method has failed to decompile.  When submitting a bug report, please provide this stack trace, and (if you hold appropriate legal rights) the relevant class file.
        // org.benf.cfr.reader.util.ConfusedCFRException: Extractable last case doesn't follow previous
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.SwitchReplacer.examineSwitchContiguity(SwitchReplacer.java:486)
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.SwitchReplacer.replaceRawSwitches(SwitchReplacer.java:65)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysisInner(CodeAnalyser.java:425)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysisOrWrapFail(CodeAnalyser.java:220)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysis(CodeAnalyser.java:165)
        // org.benf.cfr.reader.entities.attributes.AttributeCode.analyse(AttributeCode.java:91)
        // org.benf.cfr.reader.entities.Method.analyse(Method.java:354)
        // org.benf.cfr.reader.entities.ClassFile.analyseMid(ClassFile.java:751)
        // org.benf.cfr.reader.entities.ClassFile.analyseTop(ClassFile.java:683)
        // org.benf.cfr.reader.Main.doClass(Main.java:46)
        // org.benf.cfr.reader.Main.main(Main.java:183)
        // the.bytecode.club.bytecodeviewer.decompilers.CFRDecompiler.decompileClassNode(CFRDecompiler.java:70)
        // the.bytecode.club.bytecodeviewer.gui.ClassViewer$14.doShit(ClassViewer.java:907)
        // the.bytecode.club.bytecodeviewer.gui.PaneUpdaterThread.run(PaneUpdaterThread.java:16)
        throw new IllegalStateException("Decompilation failed");
    }

    /*
     * Exception decompiling
     */
    private static String \u02ce(byte var0, short var1_1, int var2_2, int var3_3, int var4_4) {
        // This method has failed to decompile.  When submitting a bug report, please provide this stack trace, and (if you hold appropriate legal rights) the relevant class file.
        // org.benf.cfr.reader.util.ConfusedCFRException: Extractable last case doesn't follow previous
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.SwitchReplacer.examineSwitchContiguity(SwitchReplacer.java:486)
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.SwitchReplacer.replaceRawSwitches(SwitchReplacer.java:65)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysisInner(CodeAnalyser.java:425)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysisOrWrapFail(CodeAnalyser.java:220)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysis(CodeAnalyser.java:165)
        // org.benf.cfr.reader.entities.attributes.AttributeCode.analyse(AttributeCode.java:91)
        // org.benf.cfr.reader.entities.Method.analyse(Method.java:354)
        // org.benf.cfr.reader.entities.ClassFile.analyseMid(ClassFile.java:751)
        // org.benf.cfr.reader.entities.ClassFile.analyseTop(ClassFile.java:683)
        // org.benf.cfr.reader.Main.doClass(Main.java:46)
        // org.benf.cfr.reader.Main.main(Main.java:183)
        // the.bytecode.club.bytecodeviewer.decompilers.CFRDecompiler.decompileClassNode(CFRDecompiler.java:70)
        // the.bytecode.club.bytecodeviewer.gui.ClassViewer$14.doShit(ClassViewer.java:907)
        // the.bytecode.club.bytecodeviewer.gui.PaneUpdaterThread.run(PaneUpdaterThread.java:16)
        throw new IllegalStateException("Decompilation failed");
    }

    /*
     * Exception decompiling
     */
    public SharedPreferences getSharedPreferences() {
        // This method has failed to decompile.  When submitting a bug report, please provide this stack trace, and (if you hold appropriate legal rights) the relevant class file.
        // org.benf.cfr.reader.util.CannotPerformDecode: reachable test BLOCK was exited and re-entered.
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.Misc.getFarthestReachableInRange(Misc.java:143)
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.SwitchReplacer.examineSwitchContiguity(SwitchReplacer.java:385)
        // org.benf.cfr.reader.bytecode.analysis.opgraph.op3rewriters.SwitchReplacer.replaceRawSwitches(SwitchReplacer.java:65)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysisInner(CodeAnalyser.java:425)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysisOrWrapFail(CodeAnalyser.java:220)
        // org.benf.cfr.reader.bytecode.CodeAnalyser.getAnalysis(CodeAnalyser.java:165)
        // org.benf.cfr.reader.entities.attributes.AttributeCode.analyse(AttributeCode.java:91)
        // org.benf.cfr.reader.entities.Method.analyse(Method.java:354)
        // org.benf.cfr.reader.entities.ClassFile.analyseMid(ClassFile.java:751)
        // org.benf.cfr.reader.entities.ClassFile.analyseTop(ClassFile.java:683)
        // org.benf.cfr.reader.Main.doClass(Main.java:46)
        // org.benf.cfr.reader.Main.main(Main.java:183)
        // the.bytecode.club.bytecodeviewer.decompilers.CFRDecompiler.decompileClassNode(CFRDecompiler.java:70)
        // the.bytecode.club.bytecodeviewer.gui.ClassViewer$14.doShit(ClassViewer.java:907)
        // the.bytecode.club.bytecodeviewer.gui.PaneUpdaterThread.run(PaneUpdaterThread.java:16)
        throw new IllegalStateException("Decompilation failed");
    }
}

Code snippet 3: App.java decompiled with CFR after encrypting 'my_prefs.xml'

Some tools, like Apktool, simply spit out the encrypted file taken as input, without deciphering its contents. (Apktool does this to be able to correctly rebuild the application without the loss of unrecognized files.)

Control flow obfuscation

Obfuscators typically apply transformations directly to the application’s bytecode. Some of these bytecode changes cannot be reverted back to Java source code since there is no readable equivalent. Combined with simpler techniques like renaming and code optimization, these remove a lot of the information needed by decompiling tools to recreate the source code.

A good example of such an obfuscation technique is the scrambling of the application’s control-flow, also called control flow obfuscation. Another one is arithmetic obfuscation. In this blog, we focus on control flow obfuscation. We apply control flow obfuscation to the getSharedPreferences() method in App.java. We use the --cfg option of Jadx to generate the control flow diagrams.

The control flow graph of getSharedPreferences() before obfuscation looks like this:

DexGuard obfuscates the control flow of the method. This is most obvious when observing the inserted branch (in blue) and jump (in red) instructions that no longer follow a linear execution path.

Multiple layers of protection

Each of the discussed techniques makes it more difficult to decompile mobile applications using reverse engineering tools and to analyze them manually. For that reason, it is important to combine multiple layers of obfuscation and encryption. Apart from the code, obfuscation and encryption can also be applied to compiled resources, native libraries, asset files and the AndroidManifest.xml file, further adding to the complexity and strengthening the obfuscated applications against decompilers.