Home Defeating Epsilon Loader V0.34 Vol. 1: InvokeDynamic
Post
Cancel

Defeating Epsilon Loader V0.34 Vol. 1: InvokeDynamic

Epsilon Loader V0.34 had been considered as “STRONG obfuscated” as well as “uncrackable” by the 2B2T community for a long time.

It was also widely believed that the authentication and verification part of Epsilon is achieved in the DLL1.

So let’s look inside the DLL and the related JVM classes to determine what role the DLL plays and find out the way to exploit it.

Initial analysis of the DLL loading process

The first thing we want to figure out is the loading process of the DLL in JVM Bytecode-Level, so we search for the string “DLL” using Recaf.

stringSearchResult

It’s so lucky that the dll’s filename was not encrypted. Taking advantages of that we can find a suspicious class called ESKID and its static initizer clinit which contains the string.

Indy2 In Action

After analysing clinit a bit, we can observe that there are many occurences of invokedynamic instructions. (More information about invokedynamic: https://blogs.oracle.com/javamagazine/post/understanding-java-method-invocation-with-invokedynamic)

For instance, look at the following invokedynamic instruction:

1
2
3
4
5
6
7
8
9
INVOKEDYNAMIC i(Ljava/lang/Object;Ljava/lang/Object;)Ljava/io/InputStream; [
      // handle kind 0x6 : INVOKESTATIC
ESKID.a(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Integer;)Ljava/lang/invoke/CallSite;
      // arguments:
      "<str1>", 
      "<str2>", 
      "<str3>", 
      1
]

Based on the knowledge of the invokedynamic instruction, we can see ESKID.a method as the bootstrap method and dig deeper into it.

BSM3

Using Threadtear’s power, we can generate a CFG4 of the BSM3 (The graph below is optimized).

HandleType

Then we can easily find out that the role of the 4th integer argument is to specify the invoke-type:

ValueInvoke-Type
1Virtual
2Static
3Special

It is obvious that “str1”, “str2” and “str3” were obfuscated, however their functions can be identified easily:

str1target class’s name
str2target method’s name
str3target method’s signature

String Encryption Algorithm

After we know the roles the BSM plays , it’s time for us to deal with those obfuscated strings.

1
2
3
<push the obfuscated string to stack>
INVOKESTATIC ESKID.b (Ljava/lang/String;)Ljava/lang/String;
<working with the decoded string>

Bytecode above is the pattern of the string decryption.

It’s certain that ESKID.b is the method for string decryption in this case.

Then we can take a glance at the method ESKID.b :

decryption_process

(Screenshot above is the last part of ESKID.b’s CFG.)

As you can see, there are plenty of junk codes.

But after analyzing the crucial part of the CFG above, we can assume that there is a loop which traverses every char of the obfuscated string.

That loop turns out to be the encrypting routine.

A simple kotlin decryptor implementation for this case would look like this:

1
2
3
4
5
6
7
fun decrypt(enc: String): String {
    var dec=""
    for (c in enc){
        dec+= (c.code xor (1406090362 xor 1406085967 shl 2)).toChar()
    }
    return dec
}

visual_decryptor

Automation

With the information gathered from the previous section, we can finally get rid of the annoying invokedynamics and reveal the true invocation.

However, Every obfuscated class has a unique XOR key despite of the same decryption algorithm.

What’s worse, the XOR key is protected by junk code. That’s a stumbling block we have to deal with.

So our workaround is writing a custom transformer based on java-deobfuscator to automate the deobfuscation process.

First of all, we find BSM and decryptor method.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//find BSM
MethodNode bsm = classNode.methods
        .filter(method -> method.desc.equals(bootstrapDesc))
        .findFirst().orElse(null);
if (bsm == null) return;

//find decryptor method
final InstructionPattern decryptionPattern = new InstructionPattern(
        new InvocationStep(INVOKESTATIC, classNode.name, null, "(Ljava/lang/String;)Ljava/lang/String;", false),
        new InvocationStep(INVOKESTATIC, "java/lang/Class", "forName", "(Ljava/lang/String;)Ljava/lang/Class;", false)
);
MethodInsnNode callDecryptor = (MethodInsnNode) Arrays.stream(bsm.instructions.toArray())
        .filter(ain -> {
            InstructionMatcher matcher = decryptionPattern.matcher(ain);
            return matcher.find() && (matcher.getCapturedInstructions("all").get(0) == ain);
        }).findFirst().orElse(null);
if (callDecryptor == null) {
    logger.error("[ESKID] [StaticIndyTransformer] call decryptor pattern not match in class {}", classNode.name);
    return;
}
MethodNode decryptor = TransformerHelper.findMethodNode(classNode, callDecryptor.name, callDecryptor.desc);

During the previous section, we have known that the value of the top stack frame is the key when the last ixor instruction is about to be executed by JVM.

Therefore, we can analyze how the stack changes and grab the top-stack value as the XOR key.

So the following code shows how we filter out the last XOR instruction using Deobfuscator’s InstructionMatcher :

1
2
3
4
5
6
7
8
9
10
11
12
13
final InstructionPattern algorithmPattern = new InstructionPattern(
        new OpcodeStep(IXOR),
        new OpcodeStep(I2C),
        new InvocationStep(INVOKEVIRTUAL, "java/lang/StringBuilder", "append", "(C)Ljava/lang/StringBuilder;", false));
AbstractInsnNode lastIxor = Arrays.stream(decryptor.instructions.toArray())
        .filter(ain -> {
            InstructionMatcher matcher = algorithmPattern.matcher(ain);
            return matcher.find() && (matcher.getCapturedInstructions("all").get(0) == ain);
        }).findFirst().orElse(null);
if (lastIxor == null) {
    logger.error("[ESKID] [StaticIndyTransformer] algorithm pattern not match!");
    return;
}

Then we analyzed the frame statically using the power of SimAnalyzer and get the XOR key from the top of the stack:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
SimInterpreter interpreter = new SimInterpreter();
SimAnalyzer analyzer = new SimAnalyzer(interpreter);
Frame<AbstractValue>[] frames;
analyzer.setThrowUnresolvedAnalyzerErrors(false);
int xorKey;
try {
    frames = analyzer.analyze(classNode.name, decryptor);
    AbstractValue topValue = getTopStack(frames[decryptor.instructions.indexOf(lastIxor)]);
    //strict sanity check
    if (topValue == null
            || topValue.isValueUnresolved()
            || topValue.isNull()
            || topValue.isArray()
            || !topValue.isPrimitive()
            || topValue.getInsns().size() <= 1
            || topValue.getInsns().stream().anyMatch(a2 -> ILOAD <= a2.getOpcode() && a2.getOpcode() <= SASTORE)
            || !(topValue.getValue() instanceof Integer)) {
        logger.error("[ESKID] [StaticIndyTransformer] failed to get stack top value");
        return;
    }
    xorKey = (int) topValue.getValue();
    logger.debug("xor key found: {}", xorKey);
} catch (AnalyzerException e) {
    logger.error("failed to get frames in class: {} , method: {} , insn: {}", classNode.name, decryptor.name, TransformerHelper.insnToString(e.node));
    e.printStackTrace();
    return;
}

Finally we have obtained all the information we need to preform the deobfuscation, so the last part is the invocation type identification:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Object[] bsmArgs = ((InvokeDynamicInsnNode) indy).bsmArgs;
int opcode;
switch ((int) bsmArgs[3]) {
    case 1: {
        opcode = INVOKEVIRTUAL;
        break;
    }
    case 2: {
        opcode = INVOKESTATIC;
        break;
    }
    case 3: {
        opcode = INVOKESPECIAL;
        break;
    }
    default: {
        logger.error("[ESKID] [StaticIndyTransformer] failed to get invocation type");
        return;
    }
}

You can download the complete source code of this Indy transformer via this: StaticIndyTransformer.java

YOU NEED TO ADD SimAnalyzer DEPENDENCY FIRST TO RUN THIS TRANSFORMER!!

Final results

result

Screenshot above is a part of ESKID method.

Footnotes

  1. So-called Native Obfsucation 

  2. Invokedynamic 

  3. bootstrap method  2

  4. control flow graph 

This post is licensed under CC BY 4.0 by the author.