Reversing DexGuard’s String Encryption

Posted by Ajin Abraham on Jun 11 2015

TL;DR

DexGuard is a commercial tool used for protecting android binaries (APK) mainly from reversing and tampering. It provides features like code obfuscation, class encryption, string encryption, asset/resource encryption, tamper protection, anti-debugger checks, VM/Environment checks, SSL pinning etc. This blog post explains the decryption/reversing of DexGuard 6.1's string encryption.

DexGuard is a commercial tool used for protecting android binaries (APK) mainly from reversing and tampering. It provides features like code obfuscation, class encryption, string encryption, asset/resource encryption, tamper protection, anti-debugger checks, VM/Environment checks, SSL pinning etc. This blog post explains the decryption or reversing of DexGuard's string encryption. My colleague, Sachinraj collaborated with me for this research. After analysis, we found that DexGuard string encryption is more of an obfuscation that can be reversed with some effort. A sample android application is compiled with DexGuard String and Class encryption enabled. As per DexGuard documentation, You can encrypt entire classes by specifying them with the option -encryptclasses. For example:

-encryptclasses mypackage.MySecretClass
 There are a few alternative ways to specify which constant strings in the code should be encrypted, with the option -encryptstrings. The shortest way is to specify the strings literally:

-encryptstrings "Some secret string", "Some other secret string"
The sample android application contains two activity. The first activity contains an OK button which on press will invoke the second activity using Intent. We pass a hardcoded secret string from first activity to the second activity via Intent extra. The  source code of the first activity (MainActivity.java) is shown below:

package opensecurity.dexreversedemo;
import android.content.Intent;
import android.os.Bundle;
import android.support.v7.app.ActionBarActivity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.Button;
public class MainActivity extends ActionBarActivity 
{
public Button bt;
 private final static String secret="SuperZS3cur!ty0R0CK3S";
 @Override
  protected void onCreate(Bundle savedInstanceState) 
   {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    bt=(Button)findViewById(R.id.button);
    bt.setOnClickListener(new View.OnClickListener() {
 @Override
 public void onClick(View v)
 {
   Intent ask = new Intent(MainActivity.this, Secret.class);
   ask.putExtra("SECURE",secret);
  startActivity(ask);
  }
 });
 }
-----SNIPPED------
}

 We defined the following rules in dexguard-project.txt

-encryptstrings "SuperZS3cur!ty0R0CK3S"
-encryptclasses opensecurity.dexreversedemo.MainActivity,opensecurity.dexreversedemo.Secret
The above rules should encrypt the string SuperZS3cur!ty0R0CK3S and encrypt the classes MainActivity and Secret. We compiled the code with DexGuard enabled and analysed the APK using Mobile Security Framework which is configured to use the CFR decompiler. You can also use CFR decompiler in standalone to decompile the APK. Due to the protection enforced by DexGuard, we got large chunk of decompiled java files with strange names.

java_source

Since we have the MainActivity.java, we started our analysis from there.

mobile_security_framework_decompile

Here if you observe the above decompiled code, there are two strange imports. import o.\u062f; import o.\u706c;
As the decompiled source contains the following code, public class MainActivity extends \u062f { public Button \u02c9;

button_bt

If you compare it with our initial source code, you can easily conclude that  \u062f is nothing but ActionBarActivity and \u02c9 is the button variable bt.
Lets look into the second import file o.\u706c. If you convert \u706c to unicode text, you will get 灬. Now we need to find the file o/灬.java

obfs_file

The file contains the following code

package o;
import android.content.Context;
import android.content.Intent;
import android.view.View;
import o.\ufb59$\u0640;
import opensecurity.dexreversedemo.MainActivity;
public class \u706c
implements View.OnClickListener {
    private static final byte[] \u02cb = new byte[]{110, -49, 71, -112, 33, -6, -12, 12, -25, -8, -33, 47, 17, -4, -82, 82, 4, -74, 33, -35, 18, 7, -25, 31};
    private static int \u02ce = 62;
    final /* synthetic */ MainActivity \u02ca;
    public \u706c(MainActivity mainActivity) {
        this.\u02ca = mainActivity;
    }
    /*
     * Unable to fully structure code
     * Enabled aggressive block sorting
     * Lifted jumps to return sites
     */
    private static String \u02ca(intvar0, intvar1_1, intvar2_2) {
        var2_2 = var2_2 * 4+ 4;
        var7_3 = var0 * 3+ 83;
        var6_4 = -1;
        var3_5 = \u706c.\u02cb;
        var8_6 = var1_1 * 3+ 21;
        var4_7 = newbyte[var8_6];
        var1_1 = var6_4;
        var5_8 = var7_3;
        var0 = var2_2;
        if(var3_5 != null) ** GOTO lbl19
        var0 = var2_2;
        var5_8 = var7_3;
        var7_3 = var2_2;
        var1_1 = var6_4;
        var2_2 = var0;
        do{
            var0 = var7_3 + 1;
            var5_8 = var2_2 + var5_8 + 1;
lbl19: // 2 sources:
            var4_7[++var1_1] = (byte)var5_8;
            if(var1_1 == var8_6 - 1) {
                return new String(var4_7, 0);
            }
            var2_2 = var5_8;
            var5_8 = var3_5[var0];
            var7_3 = var0;
        } while(true);
    }
    public void onClick(View view) {
        view = newIntent((Context)this.\u02ca, \ufb59$\u0640.\u141d("o.\ufb59"));
        view.putExtra("SECURE", \u706c.\u02ca(0, 0, 0).intern());
        this.\u02ca.startActivity((Intent)view);
    }
}
This decompiled code looks like CFR decompiler failed at some places to decompile it completely but gave us a good hint about the logic. If you analyse the code, you can see the line which contains the code.

view.putExtra("SECURE", \u706c.\u02ca(0, 0, 0).intern());

This looks similar to ask.putExtra("SECURE",secret); in our initial source code. Now it's clear that \u706c.\u02ca(0, 0, 0).intern() method is responsible for decrypting the encrypted secret at Runtime. If we can successfully recreate this method, then we will be able to decrypt the secret. CFR decompiler did half the job for us and remaining is on us. We created a Java program with the above decryption method and it welcomed us with lots of errors. We added appropriate data types but still we had some missing code there. After looking into this code, We figured out the pseudo logic of the decryption method. This line if(var3_5 != null) ** GOTO lbl19 gave us a hint about the logic. var3_5 is nothing but the byte array \u02cb. It will never become null. So the following code (marked in red) will never get invoked.

looping

So we removed those unnecessary code and again if you carefully observe the code, you can see that GOTO lbl19 goes inside the do-while loop and there is two lines of code (marked in blue) which is skipped for the first time execution of the loop and when the first loop is finished, the successive loops will include the code. So we quickly wrote a PoC with that logic. File: DexRev.java


public class DexRev {
private static final byte[] \u02cb = new byte[]{110, -49, 71, -112, 33, -6, -12, 12, -25, -8, -33, 47, 17, -4, -82, 82, 4, -74, 33, -35, 18, 7, -25, 31};
    public static void main(String args[]) throws Exception
    {
    System.out.println("Decrypted: " + DexRev.\u02ca(0, 0, 0).intern());
    }
    private static String \u02ca(int var0, int var1_1, int var2_2) {
        var2_2 = var2_2 * 4 + 4;
        int var7_3 = var0 * 3 + 83;
        int var6_4 = -1;
        byte[] var3_5 = DexRev.\u02cb;
        int var8_6 = var1_1 * 3 + 21;
        byte [] var4_7 = new byte[var8_6];
        var1_1 = var6_4;
        int var5_8 = var7_3;
        var0 = var2_2;
        boolean firsttime=true;
        do {
        if(!firsttime){
            var0 = var7_3 + 1;
            var5_8 = var2_2 + var5_8 + 1;
        }
        firsttime=false;
            var4_7[++var1_1] = (byte)var5_8;
            if (var1_1 == var8_6 - 1) {
                return new String(var4_7, 0);
            }
            var2_2 = var5_8;
            var5_8 = var3_5[var0];
            var7_3 = var0;
        } while (true);
    }
}
If you run the above PoC, you will get the output.

Decrypted: SuperZS3cur!ty0R0CK3S

And DexGuard's so called String Encryption is reversed!. NOTE Later we came to know about an old blog that talks about string decryption in DexGuard. We also found that the blog was talking about the issue identified in an older version of DexGuard and we saw the following comment from the author of DexGuard. eric Disclosure This issue was reported to Eric, the founder of ProGuard and DexGuard. Eric Acknowledged the Report asked for a PoC and mentioned that string encryption is lightweight and when employed with class encryption, it will be difficult to obtain the plaintext information.


  • Tags: 
  • breaking dexguard string encryption
  • decrypting dexguard
  • dexguard
  • dexguard reversing
  • reversing dexguard
  • dexguard string encryption

Ajin Abraham

  • |
  • |
  • |

Ajin Abraham is a Security Engineer with 10+ years of experience in Application Security, Research and Engineering. He is passionate about building and maintaining open source security tools and communities. Some of his contributions to Hacker's arsenal include Mobile Security Framework (MobSF), nodejsscan, OWASP Xenotix, etc. Areas of interest include runtime security instrumentation, offensive security, web and mobile application security, code and architectural reviews, cloud-native runtime security, security tool development, security automation, breaking and fixing security products, reverse engineering, and exploit development.