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: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.
-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------
}
-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.
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);
}
}
view.putExtra("SECURE", \u706c.\u02ca(0, 0, 0).intern());
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.
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. 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.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.