From 72f0d7d6aec1aff4b0d105fd8ec84c0d9d37ab2c Mon Sep 17 00:00:00 2001 From: GraxCode Date: Sun, 17 May 2020 14:10:42 +0200 Subject: [PATCH] add paramorphism --- build.gradle | 2 +- .../execution/ExecutionCategory.java | 4 +- .../AccessObfusationParamorphism.java | 166 ++++++++++++++++++ .../paramorphism/BadAttributeRemover.java | 25 +++ .../StringObfuscationParamorphism.java | 99 +++++++++++ .../swing/dialog/ExecutionSelection.java | 5 + .../nov/threadtear/util/asm/Instructions.java | 12 +- 7 files changed, 305 insertions(+), 8 deletions(-) create mode 100644 src/me/nov/threadtear/execution/paramorphism/AccessObfusationParamorphism.java create mode 100644 src/me/nov/threadtear/execution/paramorphism/BadAttributeRemover.java create mode 100644 src/me/nov/threadtear/execution/paramorphism/StringObfuscationParamorphism.java diff --git a/build.gradle b/build.gradle index bb6de7f..a92862f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { id 'eclipse' } -version = '2.5.2' +version = '2.6.0' sourceCompatibility = 1.8 targetCompatibility = 1.8 diff --git a/src/me/nov/threadtear/execution/ExecutionCategory.java b/src/me/nov/threadtear/execution/ExecutionCategory.java index b7e0e54..aea0d63 100644 --- a/src/me/nov/threadtear/execution/ExecutionCategory.java +++ b/src/me/nov/threadtear/execution/ExecutionCategory.java @@ -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; diff --git a/src/me/nov/threadtear/execution/paramorphism/AccessObfusationParamorphism.java b/src/me/nov/threadtear/execution/paramorphism/AccessObfusationParamorphism.java new file mode 100644 index 0000000..31f7a76 --- /dev/null +++ b/src/me/nov/threadtear/execution/paramorphism/AccessObfusationParamorphism.java @@ -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 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.
This is unfinished: Doesn't work on constructors and static initializers.", ExecutionTag.RUNNABLE, + ExecutionTag.POSSIBLY_MALICIOUS); + } + + @Override + public boolean execute(Map 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 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; + } + +} \ No newline at end of file diff --git a/src/me/nov/threadtear/execution/paramorphism/BadAttributeRemover.java b/src/me/nov/threadtear/execution/paramorphism/BadAttributeRemover.java new file mode 100644 index 0000000..65d6fa4 --- /dev/null +++ b/src/me/nov/threadtear/execution/paramorphism/BadAttributeRemover.java @@ -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 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; + } +} \ No newline at end of file diff --git a/src/me/nov/threadtear/execution/paramorphism/StringObfuscationParamorphism.java b/src/me/nov/threadtear/execution/paramorphism/StringObfuscationParamorphism.java new file mode 100644 index 0000000..f081960 --- /dev/null +++ b/src/me/nov/threadtear/execution/paramorphism/StringObfuscationParamorphism.java @@ -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 classes; + private int encrypted; + private int decrypted; + private VM vm; + + public StringObfuscationParamorphism() { + super(ExecutionCategory.PARAMORPHISM, "String obfuscation removal", "Tested on version 2.1
Make sure to decrypt access obfuscation first.", ExecutionTag.RUNNABLE, + ExecutionTag.POSSIBLY_MALICIOUS); + } + + @Override + public boolean execute(Map 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; + } +} diff --git a/src/me/nov/threadtear/swing/dialog/ExecutionSelection.java b/src/me/nov/threadtear/swing/dialog/ExecutionSelection.java index af0a295..a6d03e2 100644 --- a/src/me/nov/threadtear/swing/dialog/ExecutionSelection.java +++ b/src/me/nov/threadtear/swing/dialog/ExecutionSelection.java @@ -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.*; @@ -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()); diff --git a/src/me/nov/threadtear/util/asm/Instructions.java b/src/me/nov/threadtear/util/asm/Instructions.java index 21cc1ce..f73f5b3 100644 --- a/src/me/nov/threadtear/util/asm/Instructions.java +++ b/src/me/nov/threadtear/util/asm/Instructions.java @@ -280,11 +280,13 @@ public static boolean opcodesMatch(InsnList list1, InsnList list2) { public static void updateInstructions(MethodNode m, Map 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);