Skip to content

Commit

Permalink
add paramorphism
Browse files Browse the repository at this point in the history
  • Loading branch information
GraxCode committed May 17, 2020
1 parent 0225814 commit 72f0d7d
Show file tree
Hide file tree
Showing 7 changed files with 305 additions and 8 deletions.
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ plugins {
id 'eclipse'
}

version = '2.5.2'
version = '2.6.0'
sourceCompatibility = 1.8
targetCompatibility = 1.8

Expand Down
4 changes: 2 additions & 2 deletions src/me/nov/threadtear/execution/ExecutionCategory.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package me.nov.threadtear.execution;

public enum ExecutionCategory {
GENERIC("Generic"), CLEANING("Cleaning"), ANALYSIS("Analysis"), TOOLS("Tools"), STRINGER("Obfuscators.Stringer Obfuscator"), ZKM("Obfuscators.ZKM Obfuscator"),
ALLATORI("Obfuscators.Allatori Obfuscator"), SB27("Obfuscators.Superblaubeere27 Obfuscator"), DASHO("Obfuscators.DashO Obfuscator");
GENERIC("Generic"), CLEANING("Cleaning"), ANALYSIS("Analysis"), TOOLS("Tools"), STRINGER("Obfuscators.Stringer"), ZKM("Obfuscators.ZKM"),
ALLATORI("Obfuscators.Allatori"), SB27("Obfuscators.Superblaubeere27"), DASHO("Obfuscators.DashO"), PARAMORPHISM("Obfuscators.Paramorphism");

public final String name;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package me.nov.threadtear.execution.paramorphism;

import java.lang.invoke.*;
import java.lang.reflect.Method;
import java.util.*;

import org.objectweb.asm.*;
import org.objectweb.asm.tree.*;

import me.nov.threadtear.Threadtear;
import me.nov.threadtear.execution.*;
import me.nov.threadtear.util.asm.Instructions;
import me.nov.threadtear.util.reflection.DynamicReflection;
import me.nov.threadtear.vm.*;

public class AccessObfusationParamorphism extends Execution implements IVMReferenceHandler {

private static final String PARAMORPHISM_INVOKEDYNAMIC_HANDLE_DESC = "\\(Ljava/lang/invoke/MethodHandles\\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;[JI]+\\)Ljava/lang/invoke/CallSite;";
private static final String DEPTH_TEST_METHOD = "([Ljava/lang/StackTraceElement;I)I";
private Map<String, Clazz> classes;
private int encrypted;
private int decrypted;
private boolean verbose;
private VM vm;

public AccessObfusationParamorphism() {
super(ExecutionCategory.PARAMORPHISM, "Access obfuscation removal", "Tested on version 2.1.<br>This is unfinished: Doesn't work on constructors and static initializers.", ExecutionTag.RUNNABLE,
ExecutionTag.POSSIBLY_MALICIOUS);
}

@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
this.verbose = verbose;
this.classes = classes;
this.encrypted = 0;
this.decrypted = 0;
logger.info("Decrypting all invokedynamic references");
logger.warning("Make sure all required libraries or dynamic classes are in the jar itself, or else some invokedynamics cannot be deobfuscated!");
classes.values().stream().forEach(this::patchThrowableDepth);
logger.info("Make sure to remove bad attributes first!");
logger.info("Starting decryption, this could take some time!");
classes.values().stream().forEach(this::decrypt);
if (encrypted == 0) {
logger.error("No access obfuscation matching Paramorphism 2.1 have been found!");
return false;
}
float decryptionRatio = Math.round((decrypted / (float) encrypted) * 100);
logger.errorIf("Of a total {} encrypted references, {}% were successfully decrypted", decryptionRatio <= 0.25, encrypted, decryptionRatio);
return decryptionRatio > 0.25;
}

private void patchThrowableDepth(Clazz c) {
c.node.methods.forEach(m -> {
if (m.desc.equals(DEPTH_TEST_METHOD)) {
InsnList il = new InsnList();
il.add(new InsnNode(ICONST_2));
il.add(new InsnNode(IRETURN));
Instructions.updateInstructions(m, null, il);
logger.info("Patched depth test method in {}", c.node.name);
}
});
}

private void decrypt(Clazz c) {
logger.collectErrors(c);
ClassNode cn = c.node;
try {
cn.methods.forEach(m -> {
for (int i = 0; i < m.instructions.size(); i++) {
AbstractInsnNode ain = m.instructions.get(i);
if (ain.getOpcode() == INVOKEDYNAMIC) {
// we need an own VM for each invokedynamic. this slows down everything but is the only option.
this.vm = VM.constructVM(this);
vm.setDummyLoading(true);
InvokeDynamicInsnNode idin = (InvokeDynamicInsnNode) ain;
if (idin.bsm != null) {
Handle bsm = idin.bsm;
if (bsm.getDesc().matches(PARAMORPHISM_INVOKEDYNAMIC_HANDLE_DESC)) {
if (!classes.containsKey(bsm.getOwner())) {
logger.error("Missing decryption class: {}", bsm.getOwner());
continue;
}
encrypted++;
try {
allowReflection(true);
if (!vm.isLoaded(bsm.getOwner().replace('/', '.')))
vm.explicitlyPreload(classes.get(bsm.getOwner()).node, false); // WITH clinit
CallSite callsite = loadCallSiteFromVM(cn, m, idin, bsm);
if (callsite != null) {
MethodHandleInfo methodInfo = DynamicReflection.revealMethodInfo(callsite.getTarget());
m.instructions.set(ain, DynamicReflection.getInstructionFromHandleInfo(methodInfo));
decrypted++;
}
allowReflection(false);
} catch (Throwable t) {
if (verbose) {
logger.error("Throwable", t);
}
logger.error("Failed to get callsite using classloader in {}, {}", referenceString(cn, m), shortStacktrace(t));
}
} else if (verbose) {
logger.warning("Other bootstrap type in " + cn.name + ": " + bsm + " " + bsm.getOwner().equals(cn.name) + " " + bsm.getDesc().equals(PARAMORPHISM_INVOKEDYNAMIC_HANDLE_DESC));
}
}
}
}
});
} catch (Throwable t) {
if (verbose) {
logger.error("Throwable", t);
}
logger.error("Failed load proxy for {}, {}", referenceString(cn, null), shortStacktrace(t));
}
}

//TODO seems to throw some exception sometimes, but works 90%
private CallSite loadCallSiteFromVM(ClassNode cn, MethodNode m, InvokeDynamicInsnNode idin, Handle bsm) throws Throwable {
ClassNode proxy = Sandbox.createClassProxy(cn.name); // paramorphism checks for method name and class name

InsnList invoker = new InsnList();
Type[] types = Type.getArgumentTypes(bsm.getDesc());
int var = 0;
for (int i = 0; i < types.length; i++) {
invoker.add(new VarInsnNode(types[i].getOpcode(ILOAD), var));
var += types[i].getSize();
}
invoker.add(new MethodInsnNode(INVOKESTATIC, bsm.getOwner(), bsm.getName(), bsm.getDesc())); // invokedynamic fake
invoker.add(new InsnNode(ARETURN)); // return callsite
String name = m.name.startsWith("<") ? '\0' + m.name : m.name;
proxy.methods.add(Sandbox.createMethodProxy(invoker, name, bsm.getDesc())); // same desc
vm.explicitlyPreload(proxy);
Class<?> loadedProxy = vm.loadClass(proxy.name.replace('/', '.'));
try {
List<Object> args = new ArrayList<>(Arrays.asList(DynamicReflection.getTrustedLookup(), idin.name, MethodType.fromMethodDescriptorString(idin.desc, vm)));
for (int i = 0; i < idin.bsmArgs.length; i++) { // extra arguments
Object value = idin.bsmArgs[i]; // paramorphism stores those extra parameters in bsmArgs
if (value instanceof Long) {
args.add((long) value);
} else {
args.add((int) value);
}
}
Method bootstrapBridge = loadedProxy.getDeclaredMethods()[0];
return (CallSite) bootstrapBridge.invoke(null, args.toArray());
} catch (IllegalArgumentException e) {
e.printStackTrace();
Threadtear.logger.error("One or more classes not in jar file: {}, cannot decrypt!", idin.desc);
} catch (Throwable e) {
if (verbose)
Threadtear.logger.error("CallSite exception", e);
}
return null;
}

@Override
public ClassNode tryClassLoad(String name) {
if (classes.containsKey(name)) {
ClassNode node = classes.get(name).node;
return node;
}
if (verbose)
logger.warning("Unresolved: {}, decryption might fail", name);
return null;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package me.nov.threadtear.execution.paramorphism;

import java.util.*;

import me.nov.threadtear.execution.*;

public class BadAttributeRemover extends Execution {

public BadAttributeRemover() {
super(ExecutionCategory.PARAMORPHISM, "Remove bad attributes", "Removes all inner and outer class attributes.", ExecutionTag.POSSIBLE_DAMAGE);
}

@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
classes.values().stream().map(c -> c.node).forEach(c -> {
c.innerClasses = new ArrayList<>();
c.outerClass = null;
c.outerMethod = null;
c.outerMethodDesc = null;
});
// TODO check if bytecode is compatible
logger.info("Removed all inner and outer class attributes.");
return true;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
package me.nov.threadtear.execution.paramorphism;

import java.lang.reflect.Method;
import java.util.Map;

import org.objectweb.asm.tree.*;

import me.nov.threadtear.Threadtear;
import me.nov.threadtear.execution.*;
import me.nov.threadtear.util.Strings;
import me.nov.threadtear.vm.*;

public class StringObfuscationParamorphism extends Execution implements IVMReferenceHandler {
private boolean verbose;
private Map<String, Clazz> classes;
private int encrypted;
private int decrypted;
private VM vm;

public StringObfuscationParamorphism() {
super(ExecutionCategory.PARAMORPHISM, "String obfuscation removal", "Tested on version 2.1<br>Make sure to decrypt access obfuscation first.", ExecutionTag.RUNNABLE,
ExecutionTag.POSSIBLY_MALICIOUS);
}

@Override
public boolean execute(Map<String, Clazz> classes, boolean verbose) {
this.verbose = verbose;
this.classes = classes;
this.encrypted = 0;
this.decrypted = 0;
this.vm = VM.constructVM(this);
classes.values().stream().forEach(this::decrypt);
if (encrypted == 0) {
logger.error("No strings matching Paramorphism 2.1 string obfuscation have been found!");
return false;
}
float decryptionRatio = Math.round((decrypted / (float) encrypted) * 100);
logger.info("Of a total {} encrypted strings, {}% were successfully decrypted", encrypted, decryptionRatio);
return decryptionRatio > 0.25;
}

private void decrypt(Clazz c) {
logger.collectErrors(c);
ClassNode cn = c.node;
cn.methods.forEach(m -> {
for (int i = 0; i < m.instructions.size(); i++) {
AbstractInsnNode ain = m.instructions.get(i);
if (ain.getOpcode() == INVOKESTATIC) {
MethodInsnNode min = (MethodInsnNode) ain;
if (min.desc.equals("()Ljava/lang/String;") && classes.containsKey(min.owner)) {
if (classes.get(min.owner).node.fields.stream().filter(f -> f.desc.equals("Ljava/util/Map;")).count() > 5) {
encrypted++;
String string = invokeVM(cn, m, min);
if (string != null) {
if (Strings.isHighUTF(string)) {
logger.warning("String may have not decrypted correctly in {}", referenceString(cn, m));
}
this.decrypted++;
m.instructions.set(ain, new LdcInsnNode(string));
}
}
}
}
}
});
}

private String invokeVM(ClassNode cn, MethodNode m, MethodInsnNode min) {
ClassNode proxy = Sandbox.createClassProxy(cn.name); // paramorphism checks for method name and class name

InsnList invoker = new InsnList();
invoker.add(min.clone(null)); // clone method
invoker.add(new InsnNode(ARETURN)); // return callsite
String name = m.name.startsWith("<") ? '\0' + m.name : m.name;
proxy.methods.add(Sandbox.createMethodProxy(invoker, name, min.desc)); // same desc
if (!vm.isLoaded(proxy.name.replace('/', '.')))
vm.explicitlyPreload(proxy, true); // we need no clinit here
try {
Class<?> loadedProxy = vm.loadClass(proxy.name.replace('/', '.'));
Method stringGetterBridge = loadedProxy.getDeclaredMethods()[0];
return (String) stringGetterBridge.invoke(null);
} catch (Throwable e) {
if (verbose)
Threadtear.logger.error("Throwable", e);
}
return null;
}

@Override
public ClassNode tryClassLoad(String name) {
if (classes.containsKey(name)) {
ClassNode node = classes.get(name).node;
return node;
}
if (verbose)
logger.warning("Unresolved: {}, decryption might fail", name);
return null;
}
}
5 changes: 5 additions & 0 deletions src/me/nov/threadtear/swing/dialog/ExecutionSelection.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import me.nov.threadtear.execution.cleanup.remove.RemoveUnnecessary;
import me.nov.threadtear.execution.dasho.StringObfuscationDashO;
import me.nov.threadtear.execution.generic.*;
import me.nov.threadtear.execution.paramorphism.*;
import me.nov.threadtear.execution.stringer.*;
import me.nov.threadtear.execution.tools.*;
import me.nov.threadtear.execution.zkm.*;
Expand Down Expand Up @@ -102,6 +103,10 @@ public ExecutionSelectionTree() {

addExecution(root, new StringObfuscationDashO());

addExecution(root, new BadAttributeRemover());
addExecution(root, new StringObfuscationParamorphism());
addExecution(root, new AccessObfusationParamorphism());

addExecution(root, new Java7Compatibility());
addExecution(root, new Java8Compatibility());
addExecution(root, new IsolatePossiblyMalicious());
Expand Down
12 changes: 7 additions & 5 deletions src/me/nov/threadtear/util/asm/Instructions.java
Original file line number Diff line number Diff line change
Expand Up @@ -280,11 +280,13 @@ public static boolean opcodesMatch(InsnList list1, InsnList list2) {
public static void updateInstructions(MethodNode m, Map<LabelNode, LabelNode> labels, InsnList rewrittenCode) {
m.instructions.clear();
m.instructions = rewrittenCode;
m.tryCatchBlocks.forEach(tcb -> {
tcb.start = labels.get(tcb.start);
tcb.end = labels.get(tcb.end);
tcb.handler = labels.get(tcb.handler);
});
if (m.tryCatchBlocks != null) {
m.tryCatchBlocks.forEach(tcb -> {
tcb.start = labels.get(tcb.start);
tcb.end = labels.get(tcb.end);
tcb.handler = labels.get(tcb.handler);
});
}
if (m.localVariables != null) {
m.localVariables.forEach(lv -> {
lv.start = labels.get(lv.start);
Expand Down

0 comments on commit 72f0d7d

Please sign in to comment.