-
Notifications
You must be signed in to change notification settings - Fork 124
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
GraxCode
committed
May 17, 2020
1 parent
0225814
commit 72f0d7d
Showing
7 changed files
with
305 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
166 changes: 166 additions & 0 deletions
166
src/me/nov/threadtear/execution/paramorphism/AccessObfusationParamorphism.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
|
||
} |
25 changes: 25 additions & 0 deletions
25
src/me/nov/threadtear/execution/paramorphism/BadAttributeRemover.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
99 changes: 99 additions & 0 deletions
99
src/me/nov/threadtear/execution/paramorphism/StringObfuscationParamorphism.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters