Reversing dProtect

In this post, we’re having a look at the first release of dProtect (v 1.0) by Romain Thomas. dProtect is a fork of ProGuard that provides four additional self-explanatory configuration flags:

  • -obfuscate-strings
  • -obfuscate-constants
  • -obfuscate-arithmetic
  • -obfuscate-control-flow (via flattening & opaque predicates — unfortunately, I was unable to get this flag to work, so it’s something we’ll have to revisit in the future.)

Let’s see how JEB’s dexdec’s built-in optimizers as well as custom IR plugins can be used to defeat some implementations of strings obfuscation, constants obfuscation, and arithmetic operations obfuscation.

Strings Obfuscation

The test method is as follows:

// targeted by: -obfuscate-strings
public String provideString() {
    return "hello dProtect";
}

Let’s disable dexdec’s built-in deobfuscators (CTRL+TAB to decompile, untick “Enable deobfuscators”) to get a chance to look at the obfuscated code. It decompiles to:

public static String a(String arg14) {
    StringBuilder v0 = new StringBuilder();
    int v1 = ((int)DPTest1.b[4]) ^ 1684628051;
label_8:
    while(v1 < arg14.length()) {
        int v2 = arg14.charAt(v1);
        while(true) {
            int v9 = v2 ^ -1;
            v0.append(((char)((((int)DPTest1.b[10]) ^ 0x2AE022E9) + v2 + (((int)DPTest1.b[3]) ^ 0x35A299BD) + (((int)DPTest1.b[10]) ^ 0x2AE022E9 ^ -1 | v9) - ((((int)DPTest1.b[10]) ^ 0x2AE022E9) + v2 - ((((int)DPTest1.b[10]) ^ 0x2AE022E9) + v2 + (((int)DPTest1.b[3]) ^ 0x35A299BD) + (((int)DPTest1.b[10]) ^ 0x2AE022E9 ^ -1 | v9))))));
            long[] v3 = DPTest1.b;
            int v6 = v1 ^ -1;
            v1 = v1 + (((int)v3[3]) ^ 0x35A299BD) + (((int)v3[3]) ^ 0x35A299BD) + (((int)v3[3]) ^ 0x35A299BD ^ -1 | v6) + ((((int)v3[3]) ^ 0x35A299BD) + v1 - ((((int)v3[3]) ^ 0x35A299BD) + v1 + (((int)v3[3]) ^ 0x35A299BD) + (((int)v3[3]) ^ 0x35A299BD ^ -1 | v6)));
            if((DPTest1.a + (((int)v3[3]) ^ 0x35A299BD)) % (((int)v3[7]) ^ 0x2B0F969A) != 0) {
                continue label_8;
            }
        }
    }

    return v0.toString();
}

public String provideString() {
    return DPTest1.a("歬歡歨歨歫欤歠歔歶歫歰歡歧歰");
}

A decryptor method a(String):String was generated by dProtect. It performs various computations to decrypt the input string.

One built-in optimizer that ships with JEB’s dexdec uses the IDState object to perform emulation (explained in a previous blog). It cleans up such code automatically:

provideString() is auto-deobfuscated by JEB’s dexdec

Arithmetic Operations Obfuscation

The test method is as follows:

// targeted by: -obfuscate-arithmetic
public int calculate(int x) {
    return 100 + x;
}

With standard JEB settings (re-tick “Enable deobfuscators” if you had disabled it), the obfuscated code decompiles to:

static {
    long[] v0 = new long[12];
    DPTest1.b = v0;
    v0[0] = 0x371C2961L;
    v0[1] = 0x13DD5724L;
    v0[2] = 0x17EB3014L;
    v0[3] = 0x35A299BCL;
    v0[4] = 1684628051L;
    v0[5] = 1720310111L;
    v0[6] = 0x576F77CBL;
    v0[7] = 0x2B0F9698L;
    v0[8] = 360862103L;
    v0[9] = 0x5A9D6037L;
    v0[10] = 0x2AE049EDL;
    v0[11] = 2060383159L;
    DPTest1.a = ((int)v0[11]) ^ 1305664179;
}

public int calculate(int arg4) {
    return arg4 + (((int)DPTest1.b[0]) ^ 0x371C2905);
}

As can be seen, the constant 100 has been replaced by an arithmetic operation, here, a XOR operating on an immediate and a static array element set up in the class initializer.

JEB does not ship with overly complex deobfuscators operating on arrays, because it is near-impossible in the general case to assess their finality (i.e. answer the question “will values be changed during the program execution?” definitively). However, to solve particular cases of obfuscation, writing a custom IR plugin to tackle this obfuscation is an acceptable solution. (Have a look at this post to get started on dexdec IR plugins.)

Let’s check DOptUnsafeArrayAccessSubst.java, a sample IR plugin that ships with JEB (folder coreplugins/scripts/) and does does exactly what we need: detecting the use of static array elements and replacing them by their actual values. We can enable the plugin by removing the “.DISABLED” extension. Now redecompile (CTRL+TAB). And… well, nothing has changed! It is time to examine the plugin code carefully, maybe even use your favorite IDE to troubleshoot and augment it. Here is what prevented the original plugin from kicking in: the plugin was looking for IR elements such as: IDArrayElt ^ IDImm. However, the IR it got was: (<int>IDArrayElt) ^ IDImm, that is, the array element was cast to int, making the IR expression an IDOperation, not an IDArrayElt.

The DOptUnsafeArrayAccessSubstV2.java plugin takes care of that (refer to isLikeArrayElt method).

Now we can redecompile. and things were deobfuscated as expected:

calculate() is deobfuscated by DOptUnsafeArrayAccessSubstV2

Constants Scrambling

Finally, let’s have a look at how constants obfuscation is achieved. The documentation gives examples of cryptographic-like S-boxes being initialized. The test method is as follows:

// targeted by: -obfuscate-constants
public void initArray(int[] a) {
    a[0] = 0x61707865;
    a[1] = 0x3320646e;
    a[2] = 0x79622d32;
    a[3] = 0x6b206574;
}

Out of the box, JEB decompiles the obfuscated code to:

static {
    long[] v0 = new long[12];
    DPTest1.b = v0;
    v0[0] = 0x371C2961L;
    v0[1] = 0x13DD5724L;
    v0[2] = 0x17EB3014L;
    v0[3] = 0x35A299BCL;
    v0[4] = 1684628051L;
    v0[5] = 1720310111L;
    v0[6] = 0x576F77CBL;
    v0[7] = 0x2B0F9698L;
    v0[8] = 360862103L;
    v0[9] = 0x5A9D6037L;
    v0[10] = 0x2AE049EDL;
    v0[11] = 2060383159L;
    DPTest1.a = ((int)v0[11]) ^ 1305664179;
}

public void initArray(int[] arg5) {
    long[] v0 = DPTest1.b;
    arg5[1684628051 ^ ((int)v0[4])] = 133800250 ^ ((int)v0[5]);
    arg5[0x35A299BD ^ ((int)v0[3])] = 0x644F13A5 ^ ((int)v0[6]);
    arg5[0x2B0F969A ^ ((int)v0[7])] = 0x6CE07CA5 ^ ((int)v0[8]);
    arg5[0x13DD5727 ^ ((int)v0[1])] = ((int)v0[9]) ^ 0x31BD0543;
}

Note that the use of synthetic static arrays is made, as was the case for the arithmetic operations obfuscation pass. Therefore, let’s try the DOptUnsafeArrayAccessSubstV2 plugin. As careful examination of the above code may give in, the plugin fails to deobfuscate this code on the first go. The reason: if you examine the IR produced while debugging the plugin, you will notice that the static array elements are accessed via a variable (v0, above). In IR, those elements are IDVar. Therefore, we need to check whether this variable references a static array. We will do that by using the data flow analysis facility made available to all dexdec plugins (public field dfa of optimizers sub-classing AbstractDOptimizer):

...
analyzeChains();  // initialize the `dfa` member field
Long defaddr = dfa.checkSingleDef(insnAddress, varid);  // use-def chains
...

The improved plugin can be found here: DOptUnsafeArrayAccessSubstV3.java

The obfuscated code is now processed as expected, and dexdec generates the following decompilation:

initArray() is deobfuscated by DOptUnsafeArrayAccessSubstV3

Conclusion and Future Work

dProtect is a great project to provide code obfuscation for the masses. Its compatibility with ProGuard makes integration into new and existing Android projects a breeze. I have little doubt many developers will try it out in the future. Let’s see how upcoming upgrades to the obfuscators fare against the decompiler!

In future blogs, we will have a look at dProtect’s control-flow obfuscation (once I’ve got it to work!) and we will see how O-MVLL, the LLVM-based native code obfuscator counterpart, does against JEB’s gendec (generic decompiler for native code).

Until next time! – Nicolas