diff --git a/.gitignore b/.gitignore index dacd1690..325c59aa 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ # Ignore Gradle build output directory build +.gui/gradle/wrapper/ # Ignore logs *.log diff --git a/build.gradle.kts b/build.gradle.kts index d5cc2dfe..c6449389 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -62,8 +62,8 @@ allprojects { plugins.withType { configure { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.toVersion(19) + targetCompatibility = JavaVersion.toVersion(19) } if (!skipAutostyle) { @@ -88,7 +88,7 @@ allprojects { include("**/*.properties") filteringCharset = "UTF-8" // apply native2ascii conversion since Java 8 expects properties to have ascii symbols only - filter(org.apache.tools.ant.filters.EscapeUnicode::class) +// filter(org.apache.tools.ant.filters.EscapeUnicode::class) } } @@ -103,13 +103,13 @@ allprojects { windowTitle = "Threadtear ${project.name} API" header = "Threadtear" addBooleanOption("Xdoclint:none", true) - addStringOption("source", "8") - if (JavaVersion.current().isJava9Compatible) { - addBooleanOption("html5", true) - links("https://docs.oracle.com/javase/9/docs/api/") - } else { - links("https://docs.oracle.com/javase/8/docs/api/") - } + addStringOption("source", "22") +// if (JavaVersion.current().isJava9Compatible) { +// addBooleanOption("html5", true) +// links("https://docs.oracle.com/javase/9/docs/api/") +// } else { +// links("https://docs.oracle.com/javase/8/docs/api/") +// } } } @@ -132,10 +132,10 @@ allprojects { // This includes either project-specific license or a default one if (file("$projectDir/LICENSE").exists()) { textFrom("$projectDir/LICENSE") - rename { s -> "${project.name.toUpperCase()}_LICENSE" } + rename { "${project.name.toUpperCase()}_LICENSE" } } else { textFrom("$rootDir/LICENSE") - rename { s -> "${rootProject.name.toUpperCase()}_LICENSE" } + rename { "${rootProject.name.toUpperCase()}_LICENSE" } } } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index f5892078..999b8081 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -19,7 +19,9 @@ dependencies { implementation("org.ow2.asm:asm-commons") implementation("com.github.leibnitz27:cfr") { isChanging = true } + implementation("org.vineflower:vineflower:1.10.1") implementation("ch.qos.logback:logback-classic") + implementation("software.coley:cafedude-core:2.1.1") - externalLib("fernflower-15-05-20") +// externalLib("fernflower-15-05-20") } diff --git a/core/src/main/java/me/nov/threadtear/ThreadtearCore.java b/core/src/main/java/me/nov/threadtear/ThreadtearCore.java index 6f81675e..e7a33109 100644 --- a/core/src/main/java/me/nov/threadtear/ThreadtearCore.java +++ b/core/src/main/java/me/nov/threadtear/ThreadtearCore.java @@ -4,8 +4,15 @@ import me.nov.threadtear.execution.Execution; import me.nov.threadtear.logging.LogWrapper; import me.nov.threadtear.security.VMSecurityManager; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; import org.slf4j.LoggerFactory; +import software.coley.cafedude.InvalidClassException; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; +import java.io.IOException; import java.lang.management.ManagementFactory; import java.lang.management.RuntimeMXBean; import java.lang.reflect.Field; @@ -15,12 +22,6 @@ import java.util.stream.Collectors; public class ThreadtearCore { - public static void configureEnvironment() throws Exception { - System.setProperty("file.encoding", "UTF-8"); - Field charset = Charset.class.getDeclaredField("defaultCharset"); - charset.setAccessible(true); - charset.set(null, null); - } public static void configureLoggers() { LogWrapper.logger.addLogger(LoggerFactory.getLogger("logfile")); @@ -32,13 +33,14 @@ public static void run(List classes, List executions, boolean LogWrapper.logger.info("Executing {} tasks on {} classes!", executions.size(), classes.size()); if (!disableSecurity) { LogWrapper.logger.info("Initializing security manager if something goes horribly wrong"); - System.setSecurityManager(new VMSecurityManager()); } else { LogWrapper.logger.warning("Starting without security manager!"); } List ignoredClasses = classes.stream().filter(c -> !c.transform).collect(Collectors.toList()); LogWrapper.logger.warning("{} classes will be ignored", ignoredClasses.size()); classes.removeIf(c -> !c.transform); + + Map map = classes.stream().collect(Collectors.toMap(c -> c.node.name, c -> c, (c1, c2) -> { LogWrapper.logger.warning("Warning: Duplicate class definition of {}, one class may not get decrypted", c1.node.name); return c1; @@ -70,7 +72,6 @@ public static void run(List classes, List executions, boolean } catch (InterruptedException e1) { } LogWrapper.logger.info("Successful completion!"); - System.setSecurityManager(null); } // TODO: make a CLI diff --git a/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java b/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java index 8a7bd035..591c4867 100644 --- a/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java +++ b/core/src/main/java/me/nov/threadtear/decompiler/DecompilerInfo.java @@ -19,7 +19,7 @@ public String toString() { public static List> getDecompilerInfos() { List> list = new ArrayList<>(3); list.add(new CFRBridge.CFRDecompilerInfo()); - list.add(new FernflowerBridge.FernflowerDecompilerInfo()); + list.add(new VineFlowerBridge.FernflowerDecompilerInfo()); list.add(new KrakatauBridge.KrakatauDecompilerInfo()); return list; } diff --git a/core/src/main/java/me/nov/threadtear/decompiler/FernflowerBridge.java b/core/src/main/java/me/nov/threadtear/decompiler/VineFlowerBridge.java similarity index 91% rename from core/src/main/java/me/nov/threadtear/decompiler/FernflowerBridge.java rename to core/src/main/java/me/nov/threadtear/decompiler/VineFlowerBridge.java index 1b82dace..990030ed 100644 --- a/core/src/main/java/me/nov/threadtear/decompiler/FernflowerBridge.java +++ b/core/src/main/java/me/nov/threadtear/decompiler/VineFlowerBridge.java @@ -10,7 +10,7 @@ import me.nov.threadtear.io.JarIO; -public class FernflowerBridge implements IDecompilerBridge, IBytecodeProvider, IResultSaver { +public class VineFlowerBridge implements IDecompilerBridge, IBytecodeProvider, IResultSaver { protected static final Map options = new HashMap<>(); @@ -68,7 +68,7 @@ public String decompile(File archive, String name, byte[] bytez) { return sw.toString(); } if (result == null || result.trim().isEmpty()) { - result = "No Fernflower output received\n\nOutput log:\n" + new String(log.toByteArray()); + result = "No VineFlower output received\n\nOutput log:\n" + new String(log.toByteArray()); } return result; } @@ -112,21 +112,21 @@ public void saveClassEntry(String path, String archiveName, String qualifiedName public void closeArchive(String path, String archiveName) { } - public static class FernflowerDecompilerInfo extends DecompilerInfo { + public static class FernflowerDecompilerInfo extends DecompilerInfo { @Override public String getName() { - return "Fernflower"; + return "VineFlower"; } @Override public String getVersionInfo() { - return "15-08-20"; + return "1.10.1"; } @Override - public FernflowerBridge createDecompilerBridge() { - return new FernflowerBridge(); + public VineFlowerBridge createDecompilerBridge() { + return new VineFlowerBridge(); } } } diff --git a/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java b/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java index fc823a5d..46355a9b 100644 --- a/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java +++ b/core/src/main/java/me/nov/threadtear/execution/ExecutionLink.java @@ -1,77 +1,34 @@ package me.nov.threadtear.execution; -import me.nov.threadtear.execution.allatori.ExpirationDateRemoverAllatori; -import me.nov.threadtear.execution.allatori.JunkRemoverAllatori; -import me.nov.threadtear.execution.allatori.StringObfuscationAllatori; -import me.nov.threadtear.execution.analysis.*; -import me.nov.threadtear.execution.cleanup.InlineMethods; -import me.nov.threadtear.execution.cleanup.InlineUnchangedFields; -import me.nov.threadtear.execution.cleanup.remove.RemoveAttributes; -import me.nov.threadtear.execution.cleanup.remove.RemoveUnnecessary; -import me.nov.threadtear.execution.cleanup.remove.RemoveUnusedVariables; -import me.nov.threadtear.execution.dasho.StringObfuscationDashO; -import me.nov.threadtear.execution.generic.ConvertCompareInstructions; -import me.nov.threadtear.execution.generic.KnownConditionalJumps; -import me.nov.threadtear.execution.generic.ObfuscatedAccess; -import me.nov.threadtear.execution.generic.TryCatchObfuscationRemover; -import me.nov.threadtear.execution.generic.inliner.ArgumentInliner; -import me.nov.threadtear.execution.generic.inliner.JSRInliner; -import me.nov.threadtear.execution.paramorphism.AccessObfuscationParamorphism; -import me.nov.threadtear.execution.paramorphism.BadAttributeRemover; -import me.nov.threadtear.execution.paramorphism.StringObfuscationParamorphism; -import me.nov.threadtear.execution.stringer.AccessObfuscationStringer; -import me.nov.threadtear.execution.stringer.StringObfuscationStringer; -import me.nov.threadtear.execution.tools.*; -import me.nov.threadtear.execution.zkm.*; +import me.nov.threadtear.logging.LogWrapper; +import me.nov.threadtear.util.reflection.ReflectionUtil; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; public class ExecutionLink { - public static final List> executions = new ArrayList>() {{ - add(InlineMethods.class); - add(InlineUnchangedFields.class); - add(RemoveUnnecessary.class); - add(RemoveUnusedVariables.class); - add(RemoveAttributes.class); - - add(ArgumentInliner.class); - add(JSRInliner.class); - add(ObfuscatedAccess.class); - add(KnownConditionalJumps.class); - add(ConvertCompareInstructions.class); - - add(RestoreSourceFiles.class); - add(ReobfuscateClassNames.class); - add(ReobfuscateMembers.class); - add(ReobfuscateVariableNames.class); - add(RemoveMonitors.class); - add(RemoveTCBs.class); - - add(StringObfuscationStringer.class); - add(AccessObfuscationStringer.class); - - add(TryCatchObfuscationRemover.class); - add(StringObfuscationZKM.class); - add(AccessObfuscationZKM.class); - add(FlowObfuscationZKM.class); - add(DESObfuscationZKM.class); - - add(StringObfuscationAllatori.class); - add(ExpirationDateRemoverAllatori.class); - add(JunkRemoverAllatori.class); - - add(StringObfuscationDashO.class); - - add(BadAttributeRemover.class); - add(StringObfuscationParamorphism.class); - add(AccessObfuscationParamorphism.class); - - add(Java7Compatibility.class); - add(Java8Compatibility.class); - add(IsolatePossiblyMalicious.class); - add(AddLineNumbers.class); - add(LogAllExceptions.class); - add(RemoveMaxs.class); - }}; + public static final List> executions = new ArrayList>() {}; + static { + // Use reflection to get every class in the execution package and add it if it extends Execution + Class[] classes = ReflectionUtil.getClassInPackage("me.nov.threadtear.execution"); + LogWrapper.logger.info("Found " + classes.length + " classes in execution package"); + for(Class clazz : classes){ + // Skip the Execution class itself, abstract classes, and interfaces + if(Execution.class.isAssignableFrom(clazz) && clazz != Execution.class && !Modifier.isAbstract(clazz.getModifiers()) && !clazz.isInterface()){ + try { + // Check if the class has a public no-argument constructor + clazz.getConstructor(); + // Add the class directly to the list without instantiating + executions.add((Class) clazz); + } catch (NoSuchMethodException e) { + // The class doesn't have a public no-argument constructor; skip it + continue; + } + } + } + + } } diff --git a/core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java b/core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java deleted file mode 100644 index cd4c38da..00000000 --- a/core/src/main/java/me/nov/threadtear/execution/allatori/ExpirationDateRemoverAllatori.java +++ /dev/null @@ -1,55 +0,0 @@ -package me.nov.threadtear.execution.allatori; - -import java.util.*; -import java.util.Map.Entry; -import java.util.stream.*; - -import org.objectweb.asm.tree.LdcInsnNode; -import org.objectweb.asm.tree.analysis.BasicValue; - -import me.nov.threadtear.analysis.stack.*; -import me.nov.threadtear.execution.*; - -public class ExpirationDateRemoverAllatori extends Execution implements IConstantReferenceHandler { - - public ExpirationDateRemoverAllatori() { - super(ExecutionCategory.ALLATORI, "Remove expiry date", - "Allatori adds expiration dates to the code
that stop the obfuscated jar " + - "file from running after being passed.
They can be removed easily.", - ExecutionTag.POSSIBLE_DAMAGE); - } - - @Override - public boolean execute(Map classes, boolean verbose) { - try { - logger.info("Finding most common long ldc cst"); - long mostCommon = classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .map(m -> m.instructions.spliterator()).flatMap(insns -> StreamSupport.stream(insns, false)) - .filter(ain -> ain.getOpcode() == LDC && ((LdcInsnNode) ain).cst instanceof Long) - .map(ain -> (LdcInsnNode) ain) - .filter(ldc -> Math.abs((long) ldc.cst - System.currentTimeMillis()) < 157784760000L) - .collect(Collectors.groupingBy(ldc -> (long) ldc.cst, Collectors.counting())).entrySet().stream() - .max(Entry.comparingByValue()).map(Entry::getKey).orElseThrow(RuntimeException::new); - logger.info("Expiration date is " + new Date(mostCommon).toString() + ", replacing"); - classes.values().stream().map(c -> c.node.methods).flatMap(List::stream).map(m -> m.instructions.spliterator()) - .flatMap(insns -> StreamSupport.stream(insns, false)) - .filter(ain -> ain.getOpcode() == LDC && ((LdcInsnNode) ain).cst.equals(mostCommon)) - .map(ain -> (LdcInsnNode) ain).forEach(ldc -> ldc.cst = 1337133713371337L); - return true; - } catch (Exception e) { - logger.error("Failure", e); - return false; - } - } - - @Override - public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) { - return null; - } - - @Override - public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, - List values) { - return null; - } -} diff --git a/core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java b/core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java deleted file mode 100644 index 4261608d..00000000 --- a/core/src/main/java/me/nov/threadtear/execution/allatori/JunkRemoverAllatori.java +++ /dev/null @@ -1,63 +0,0 @@ -package me.nov.threadtear.execution.allatori; - -import me.nov.threadtear.execution.Clazz; -import me.nov.threadtear.execution.Execution; -import me.nov.threadtear.execution.ExecutionCategory; -import me.nov.threadtear.execution.ExecutionTag; -import me.nov.threadtear.util.asm.InstructionModifier; -import org.objectweb.asm.tree.*; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.stream.Collectors; -import java.util.stream.StreamSupport; - -public class JunkRemoverAllatori extends Execution { - public JunkRemoverAllatori() { - super(ExecutionCategory.ALLATORI, "Junk instruction remover", - "Removes junk instructions that create a lot of boolean variables when " + - "decompiled with Fernflower.", ExecutionTag.BETTER_DECOMPILE); - } - - @Override - public boolean execute(Map map, boolean verbose) { - int methodTotal = 0; - int methodModified = 0; - int removedTotal = 0; - final List classNodes = map.values().stream().map(c -> c.node).collect(Collectors.toList()); - for (ClassNode clazz : classNodes) { - for (MethodNode method : clazz.methods) { - methodTotal++; - int removed = processMethod(method); - removedTotal += removed; - if (removed > 0) { - methodModified++; - } - } - } - logger.info("Removed {} junk instructions from {}/{} methods.", removedTotal, methodModified, methodTotal); - return true; - } - - private int processMethod(MethodNode method) { - AtomicInteger removed = new AtomicInteger(); - InstructionModifier modifier = new InstructionModifier(); - StreamSupport.stream(method.instructions.spliterator(), false) - .filter(i -> i.getOpcode() == ICONST_1 && i.getNext() != null && i.getNext().getOpcode() == DUP && - i.getNext().getNext() != null && i.getNext().getNext().getOpcode() == POP2).map(i -> (InsnNode) i) - .forEach(i -> { - removed.getAndIncrement(); - modifier.remove(i); - modifier.remove(i.getNext()); - modifier.remove(i.getNext().getNext()); - }); - modifier.apply(method); - return removed.get(); - } - - @Override - public String getAuthor() { - return "ViRb3"; - } -} diff --git a/core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java b/core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java deleted file mode 100644 index afb78deb..00000000 --- a/core/src/main/java/me/nov/threadtear/execution/allatori/StringObfuscationAllatori.java +++ /dev/null @@ -1,186 +0,0 @@ -package me.nov.threadtear.execution.allatori; - -import java.lang.reflect.Method; -import java.util.*; - -import org.objectweb.asm.tree.*; -import org.objectweb.asm.tree.analysis.*; - -import me.nov.threadtear.analysis.stack.*; -import me.nov.threadtear.execution.*; -import me.nov.threadtear.util.asm.Instructions; -import me.nov.threadtear.util.format.Strings; -import me.nov.threadtear.vm.*; - -public class StringObfuscationAllatori extends Execution implements IVMReferenceHandler, IConstantReferenceHandler { - - private static final String ALLATORI_DECRPYTION_METHOD_DESC = "(Ljava/lang/String;)Ljava/lang/String;"; - private Map classes; - private int encrypted; - private int decrypted; - private boolean verbose; - - public StringObfuscationAllatori() { - super(ExecutionCategory.ALLATORI, "String obfuscation removal", - "Tested on version 7.3, should work for older versions too.", 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; - - classes.values().forEach(this::decrypt); - if (encrypted == 0) { - logger.error("No strings matching Allatori 7.3 string obfuscation have been found!"); - return false; - } - float decryptionRatio = Math.round((decrypted / (float) encrypted) * 100); - logger.info("Of a total " + encrypted + " encrypted strings, " + (decryptionRatio) + "% were " + - "successfully decrypted"); - return decryptionRatio > 0.25; - } - - private void decrypt(Clazz c) { - ClassNode cn = c.node; - logger.collectErrors(c); - cn.methods.forEach(m -> { - InsnList rewrittenCode = new InsnList(); - Map labels = Instructions.cloneLabels(m.instructions); - - // as we can't add instructions because frame index - // and instruction index - // wouldn't fit together anymore we have to do it - // this way - loopConstantFrames(cn, m, this, (ain, frame) -> { - for (AbstractInsnNode newInstr : tryReplaceMethods(cn, m, ain, frame)) { - rewrittenCode.add(newInstr.clone(labels)); - } - }); - if (rewrittenCode.size() > 0) { - Instructions.updateInstructions(m, labels, rewrittenCode); - } - }); - } - - private AbstractInsnNode[] tryReplaceMethods(ClassNode cn, MethodNode m, AbstractInsnNode ain, - Frame frame) { - if (ain.getOpcode() == INVOKESTATIC) { - MethodInsnNode min = (MethodInsnNode) ain; - if (min.desc.equals(ALLATORI_DECRPYTION_METHOD_DESC)) { - try { - encrypted++; - ConstantValue top = frame.getStack(frame.getStackSize() - 1); - if (top.isKnown() && top.isString()) { - String encryptedString = (String) top.getValue(); - // strings are not high utf and no high sdev, - // don't check - String realString = invokeProxy(cn, m, min, encryptedString); - if (realString != null) { - if (Strings.isHighUTF(realString)) { - logger.warning("String may have not decrypted correctly in " + cn.name + "." + m.name + m.desc); - } - this.decrypted++; - return new AbstractInsnNode[]{new InsnNode(POP), new LdcInsnNode(realString)}; - } else { - logger.error("Failed to decrypt string in " + cn.name + "." + m.name + m.desc); - } - } else if (verbose) { - logger.warning("Unknown top stack value in " + cn.name + "." + m.name + m.desc + ", skipping"); - } - } catch (Throwable e) { - if (verbose) { - logger.error("Throwable", e); - } - logger.error( - "Failed to decrypt string in " + cn.name + "." + m.name + m.desc + ": " + e.getClass().getName() + - ", " + e.getMessage()); - } - } - } - return new AbstractInsnNode[]{ain}; - } - - private String invokeProxy(ClassNode cn, MethodNode m, MethodInsnNode min, String encrypted) throws Exception { - VM vm = VM.constructNonInitializingVM(this); - createFakeClone(cn, m, min, encrypted); // create a - // duplicate of the current class, - // we need this because stringer checks for - // stacktrace method name and class - - final Clazz owner = classes.get(min.owner); - if (owner == null) { - logger.error("Could not find owner class in class list"); - return null; - } - ClassNode decryptionMethodOwner = owner.node; - if (decryptionMethodOwner == null) - return null; - vm.explicitlyPreload(fakeInvocationClone); // proxy - // class can't contain code in clinit other than the - // one we want to run - if (!vm.isLoaded(decryptionMethodOwner.name.replace('/', '.'))) // decryption class - // could be the same class - vm.explicitlyPreload(decryptionMethodOwner, true, (name, desc) -> !name.matches("java/lang/.*")); - Class loadedClone = vm.loadClass(fakeInvocationClone.name.replace('/', '.'), true); // load - // dupe class - - if (m.name.equals("")) { - loadedClone.newInstance(); // special case: - // constructors have to be invoked by newInstance. - // Sandbox.createMethodProxy automatically handles - // access and super call - } else { - for (Method reflectionMethod : loadedClone.getMethods()) { - if (reflectionMethod.getName().equals(m.name)) { - reflectionMethod.invoke(null); - break; - } - } - } - return (String) loadedClone.getDeclaredField("proxyReturn").get(null); - } - - private void createFakeClone(ClassNode cn, MethodNode m, MethodInsnNode min, String encrypted) { - ClassNode node = Sandbox.createClassProxy(cn.name); - InsnList instructions = new InsnList(); - instructions.add(new LdcInsnNode(encrypted)); - instructions.add(min.clone(null)); // we can clone - // original method here - instructions.add(new FieldInsnNode(PUTSTATIC, node.name, "proxyReturn", "Ljava/lang/String;")); - instructions.add(new InsnNode(RETURN)); - - node.fields.add(new FieldNode(ACC_PUBLIC | ACC_STATIC, "proxyReturn", "Ljava/lang/String;", null, null)); - node.methods.add(Sandbox.createMethodProxy(instructions, m.name, "()V")); // method should return real - // string - if (min.owner.equals(cn.name)) { - // decryption method is in own class - node.methods.add(Sandbox.copyMethod(getMethod(classes.get(min.owner).node, min.name, min.desc))); - } - fakeInvocationClone = node; - } - - private ClassNode fakeInvocationClone; - - @Override - public ClassNode tryClassLoad(String name) { - if (name.equals(fakeInvocationClone.name)) { - return fakeInvocationClone; - } - return classes.containsKey(name) ? classes.get(name).node : null; - } - - @Override - public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) { - return null; - } - - @Override - public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, - List values) { - return null; - } -} diff --git a/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java b/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java index 49ba89ce..b7350047 100644 --- a/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java +++ b/core/src/main/java/me/nov/threadtear/execution/analysis/ReobfuscateVariableNames.java @@ -18,13 +18,14 @@ public ReobfuscateVariableNames() { super(ExecutionCategory.ANALYSIS, "Reobfuscate variable names", "Reobfuscate method local variable names for easier analysis." + "
" + - "Gets rid of default names like a, a2, ... and obfuscated names like 恼人的名字.", + "Gets rid of default names like a, a2, ... and obfuscated names like 恼人的名字."+ + "
" + "refactored by neilhuang007", ExecutionTag.BETTER_DECOMPILE, ExecutionTag.POSSIBLE_DAMAGE); } @Override public String getAuthor() { - return "ViRb3"; + return "neilhuang007"; } private int getVariableCount(MethodNode method) { diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java new file mode 100644 index 00000000..f58ad3ed --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineArithmetics.java @@ -0,0 +1,24 @@ +package me.nov.threadtear.execution.cleanup; + +import me.nov.threadtear.execution.Clazz; +import me.nov.threadtear.execution.Execution; +import me.nov.threadtear.execution.ExecutionCategory; +import me.nov.threadtear.execution.ExecutionTag; + +import java.util.Map; + +public class InlineArithmetics extends Execution { + + public InlineArithmetics() { + super(ExecutionCategory.CLEANING, "Inline arithmetics", + "Inline arithmetic calculations.
Can be useful for deobfuscating arithmetic obfuscation used in flow.", + ExecutionTag.SHRINK, ExecutionTag.RUNNABLE); + } + + @Override + public boolean execute(Map classes, boolean verbose) { + + + return false; + } +} diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java index f0579684..6f01bbe0 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineMethods.java @@ -3,172 +3,380 @@ import java.util.*; import java.util.stream.StreamSupport; +import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; import me.nov.threadtear.execution.*; import me.nov.threadtear.util.asm.*; +import static org.objectweb.asm.Opcodes.*; + +/** + * This execution attempts to inline trivial methods (e.g., methods that only return a constant or throw an exception) into their call sites. + * It identifies such methods, then replaces their invocation instructions with the method's instructions. + * + * Key Improvements: + * - Properly handles 'this' and arguments for both static and non-static methods. + * - Remaps local variables correctly. + * - Handles return instructions so that return values remain on stack (if needed). + * - Skips constructors and class initializers (, ). + * - Skips methods that reference fields or contain complex instructions. + * - Enforces a maximum method size to ensure only trivial methods are inlined. + */ public class InlineMethods extends Execution { public InlineMethods() { - super(ExecutionCategory.CLEANING, "Inline static methods without invocation", - "Inline static methods that only return or throw.
Can be" + - " useful for deobfuscating try catch block obfuscation.", ExecutionTag.SHRINK, - ExecutionTag.RUNNABLE); + super( + ExecutionCategory.CLEANING, + "Inline trivial methods without invocation", + "Inline trivial methods (only return or throw) to simplify code.
" + + "Skips /, large methods, or those referencing fields.", + ExecutionTag.SHRINK, + ExecutionTag.RUNNABLE + ); } - public int inlines; + private int inlines; + + // Maximum allowed instructions for a method to be considered trivial enough to inline + private static final int MAX_METHOD_SIZE = 32; @Override public boolean execute(Map classes, boolean verbose) { - HashMap map = new HashMap<>(); - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.stream().filter(this::isUnnecessary) - .forEach(m -> map.put(c.name + "." + m.name + m.desc, m))); - logger.info("{} unnecessary methods found that could be inlined", map.size()); + // Map to hold methods eligible for inlining: key = "owner.name.desc", value = MethodNode + HashMap eligibleMethods = new HashMap<>(); + + // Identify candidate methods for inlining + classes.values().stream() + .map(c -> c.node) + .forEach(classNode -> classNode.methods.stream() + .filter(this::isEligibleForInlining) + .forEach(method -> eligibleMethods.put(classNode.name + "." + method.name + method.desc, method)) + ); + + logger.info("{} trivial methods found that could be inlined", eligibleMethods.size()); inlines = 0; - classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .forEach(m -> m.instructions.forEach(ain -> { - if (ain.getOpcode() == INVOKESTATIC) { // - // can't inline invokevirtual / special - // as object could only be superclass and - // real overrides - MethodInsnNode min = (MethodInsnNode) ain; - String key = min.owner + "." + min.name + min.desc; - if (map.containsKey(key)) { - inlineMethod(m, min, map.get(key)); - m.maxStack = Math.max(map.get(key).maxStack, m.maxStack); - m.maxLocals = Math.max(map.get(key).maxLocals, m.maxLocals); - inlines++; - } - } - })); - - // map.forEach((key, method) -> classes.get(key - // .substring(0, key.lastIndexOf('.'))).node.methods - // .removeIf(m -> m.equals(method) && !Access - // .isPublic(method.access))); - map.forEach((key, method) -> classes.get(key.substring(0, key.lastIndexOf('.'))).node.methods.remove(method)); + + // Inline all calls to these eligible methods + classes.values().stream() + .map(c -> c.node.methods) + .flatMap(List::stream) + .forEach(callerMethod -> { + List invokeInstructions = new ArrayList<>(); + + // Collect invoke instructions that target eligible methods + for (AbstractInsnNode ain : callerMethod.instructions.toArray()) { + int opcode = ain.getOpcode(); + if (opcode == INVOKESTATIC || opcode == INVOKEVIRTUAL || opcode == INVOKESPECIAL) { + // Ensure the instruction is a MethodInsnNode + if (!(ain instanceof MethodInsnNode)) { + continue; // Skip if not a MethodInsnNode + } + MethodInsnNode min = (MethodInsnNode) ain; + String key = min.owner + "." + min.name + min.desc; + if (eligibleMethods.containsKey(key)) { + invokeInstructions.add(ain); + } + } + } + + // Inline each collected invoke instruction + for (AbstractInsnNode invokeInsn : invokeInstructions) { + MethodInsnNode min = (MethodInsnNode) invokeInsn; + MethodNode calleeMethod = eligibleMethods.get(min.owner + "." + min.name + min.desc); + if (calleeMethod != null) { + inlineMethod(callerMethod, min, calleeMethod); + inlines++; + } + } + }); + + // Remove inlined methods from their respective classes + for (Map.Entry entry : eligibleMethods.entrySet()) { + String fullMethodName = entry.getKey(); // "owner.name.desc" + String className = fullMethodName.substring(0, fullMethodName.lastIndexOf('.')); + Clazz clazz = classes.get(className); + if (clazz != null) { + clazz.node.methods.remove(entry.getValue()); + } + } + logger.info("Inlined {} method references!", inlines); + return inlines > 0; + } + + /** + * Determines if a method is eligible for inlining based on several criteria: + * - Not a constructor or class initializer (, ). + * - Does not contain complex instructions like method calls, field accesses, jumps, or type instructions. + * - Has a number of instructions below the specified maximum threshold. + * - Ends with a return or throw instruction. + * + * @param method The method node to evaluate. + * @return True if the method is eligible for inlining; otherwise, false. + */ + private boolean isEligibleForInlining(MethodNode method) { + // Skip constructors and class initializers + if (method.name.equals("") || method.name.equals("")) { + return false; + } + + // Check method size + if (method.instructions.size() > MAX_METHOD_SIZE) { + return false; + } + + // Ensure no complex instructions (method calls, field accesses, type instructions, jumps) + if (containsComplexInstructions(method)) { + return false; + } + + // Ensure method ends with a return or throw + if (!endsWithReturnOrThrow(method)) { + return false; + } + return true; } - private void inlineMethod(MethodNode m, MethodInsnNode min, MethodNode method) { - InsnList copy = Instructions.copy(method.instructions); - StreamSupport.stream(copy.spliterator(), false) - .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) - .forEach(copy::remove); - removeReturn(copy); - - InsnList fakeVarList = createFakeVarList(method); - copy.insert(fakeVarList); - - StreamSupport.stream(copy.spliterator(), false).filter(ain -> ain.getType() == AbstractInsnNode.VAR_INSN) - .map(ain -> (VarInsnNode) ain).forEach(v -> v.var += m.maxLocals + 4); // - // offset local - // variables to not - // collide with existing ones - m.instructions.insert(min, copy); - m.instructions.remove(min); + /** + * Checks if a method contains complex instructions that would make inlining unsafe or non-trivial. + * Complex instructions include method calls, field accesses, dynamic invokes, type instructions, and jumps. + * + * @param method The method node to inspect. + * @return True if the method contains complex instructions; otherwise, false. + */ + private boolean containsComplexInstructions(MethodNode method) { + for (AbstractInsnNode ain : method.instructions.toArray()) { + switch (ain.getType()) { + case AbstractInsnNode.METHOD_INSN: + case AbstractInsnNode.FIELD_INSN: + case AbstractInsnNode.INVOKE_DYNAMIC_INSN: + case AbstractInsnNode.TYPE_INSN: + case AbstractInsnNode.JUMP_INSN: + return true; + default: + break; + } + } + return false; + } + + /** + * Checks if a method ends with a return or throw instruction. + * Skips any trailing line or frame nodes. + * + * @param method The method node to inspect. + * @return True if the method ends with a return or throw; otherwise, false. + */ + private boolean endsWithReturnOrThrow(MethodNode method) { + AbstractInsnNode last = method.instructions.getLast(); + while (last != null && + (last.getType() == AbstractInsnNode.LINE || last.getType() == AbstractInsnNode.FRAME)) { + last = last.getPrevious(); + } + if (last == null) return false; + int opcode = last.getOpcode(); + return opcode == RETURN || opcode == IRETURN || opcode == LRETURN || + opcode == FRETURN || opcode == DRETURN || opcode == ARETURN || opcode == ATHROW; } - private InsnList createFakeVarList(MethodNode m) { - InsnList fakeVarList = new InsnList(); + /** + * Inlines a callee method into the caller method at the location of the invoke instruction. + * Handles argument popping, variable remapping, and return instruction removal. + * + * @param caller The caller method where inlining occurs. + * @param invoke The invoke instruction to replace with inlined code. + * @param callee The callee method being inlined. + */ + private void inlineMethod(MethodNode caller, MethodInsnNode invoke, MethodNode callee) { + // Create a copy of the callee's instructions + InsnList calleeInstructions = Instructions.copy(callee.instructions); + + // Remove line and frame nodes for simplicity + StreamSupport.stream(calleeInstructions.spliterator(), false) + .filter(ain -> ain.getType() == AbstractInsnNode.LINE || ain.getType() == AbstractInsnNode.FRAME) + .forEach(calleeInstructions::remove); + + // Remove or adjust return instructions in the copied code + removeAndHandleReturns(calleeInstructions, callee); + + // Determine method signature details + Type methodType = Type.getMethodType(callee.desc); + Type[] argTypes = methodType.getArgumentTypes(); + boolean isStatic = (callee.access & ACC_STATIC) != 0; + + // Calculate the starting index for new local variables in the caller + int newLocalBase = caller.maxLocals; + + // Calculate total size needed for parameters + int paramSize = 0; + if (!isStatic) { + paramSize += 1; // 'this' reference + } + for (Type argType : argTypes) { + paramSize += (argType.getSize() == 2) ? 2 : 1; + } + + // Update caller's maxLocals and maxStack + caller.maxLocals += paramSize + 4; // Additional buffer for safety + caller.maxStack = Math.max(callee.maxStack, caller.maxStack); + + // Generate instructions to pop arguments from the stack into new local variables + InsnList argumentPoppers = new InsnList(); + + // Pop arguments in reverse order and store them into new locals + for (int i = argTypes.length - 1; i >= 0; i--) { + Type argType = argTypes[i]; + newLocalBase = storeArgument(argumentPoppers, newLocalBase, argType); + } + + // If the method is not static, pop the 'this' reference + if (!isStatic) { + Type thisType = Type.getObjectType(invoke.owner); + newLocalBase = storeArgument(argumentPoppers, newLocalBase, thisType); + } + + // Insert argument pop instructions at the beginning of the callee instructions + calleeInstructions.insert(argumentPoppers); + + // Remap local variable indices in the callee's instructions + remapLocalVariables(calleeInstructions, callee, newLocalBase - paramSize, isStatic); + + // Insert the inlined instructions into the caller method + caller.instructions.insert(invoke, calleeInstructions); + // Remove the original invoke instruction + caller.instructions.remove(invoke); - LinkedHashMap varTypes = getVarsAndTypesForDesc(m.desc.substring(1, m.desc.indexOf(')'))); - for (int var : varTypes.keySet()) { - fakeVarList.insert(new VarInsnNode(varTypes.get(var), var)); // make sure its reversed + // Optional Sanity Check: Ensure no return instructions remain + if (!postInlineSanityCheck(calleeInstructions)) { + logger.warning("Post-inline sanity check failed for inlined method {}.{}", callee.name, callee.desc); } - // pop object here for non static invoke: fakeVarList - // .add(new InsnNode(POP)); - return fakeVarList; } - public static LinkedHashMap getVarsAndTypesForDesc(String rawType) { - LinkedHashMap map = new LinkedHashMap<>(); - int var = 0; // would be 1 on non-static methods - - boolean object = false; - boolean array = false; - for (char c : rawType.toCharArray()) { - if (!object) { - if (array && c != 'L') { - map.put(var, ASTORE); // array type is astore - var++; - array = false; - continue; - } - switch (c) { - case 'L': - array = false; - map.put(var, ASTORE); - object = true; - var++; - break; - case 'I': - map.put(var, ISTORE); - var++; - break; - case 'D': - map.put(var, DSTORE); - var += 2; - break; - case 'F': - map.put(var, FSTORE); - var++; - break; - case 'J': - map.put(var, LSTORE); - var += 2; - break; - case '[': - array = true; - break; - } - } else if (c == ';') { - object = false; - } + /** + * Pops an argument from the stack and stores it into a new local variable. + * + * @param instructions The instruction list to append store instructions. + * @param localIndex The current local variable index. + * @param type The type of the argument to store. + * @return The next available local variable index. + */ + private int storeArgument(InsnList instructions, int localIndex, Type type) { + int storeOpcode; + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.BYTE: + case Type.CHAR: + case Type.SHORT: + case Type.INT: + storeOpcode = ISTORE; + break; + case Type.LONG: + storeOpcode = LSTORE; + break; + case Type.FLOAT: + storeOpcode = FSTORE; + break; + case Type.DOUBLE: + storeOpcode = DSTORE; + break; + case Type.ARRAY: + case Type.OBJECT: + default: + storeOpcode = ASTORE; + break; } - return map; + + instructions.add(new VarInsnNode(storeOpcode, localIndex)); + return localIndex + ((storeOpcode == LSTORE || storeOpcode == DSTORE) ? 2 : 1); } - private void removeReturn(InsnList copy) { - int i = copy.size() - 1; - while (i >= 0) { - AbstractInsnNode ain = copy.get(i); + /** + * Remaps local variable indices in the inlined callee instructions to avoid conflicts with caller's locals. + * + * @param instructions The instruction list containing the inlined code. + * @param callee The callee method being inlined. + * @param base The base index to offset local variables. + * @param isStatic Whether the callee method is static. + */ + private void remapLocalVariables(InsnList instructions, MethodNode callee, int base, boolean isStatic) { + for (AbstractInsnNode ain : instructions.toArray()) { + if (ain instanceof VarInsnNode) { + VarInsnNode vin = (VarInsnNode) ain; + vin.var += base; + } + } + } - if (ain.getOpcode() == ATHROW) { - // keep athrow, as it would still be in code - return; + /** + * Removes return instructions from the inlined method code or adjusts them so that return values remain on stack. + * Strategy: + * - If IRETURN, LRETURN, etc.: remove the return instruction, leaving the value on the stack. + * - If RETURN (void): remove it, leaving nothing on the stack. + * - If ATHROW: leave it intact because the method is supposed to throw. + * + * @param instructions The instruction list to modify. + * @param callee The callee method being inlined. + */ + private void removeAndHandleReturns(InsnList instructions, MethodNode callee) { + ListIterator iterator = instructions.iterator(); + while (iterator.hasNext()) { + AbstractInsnNode ain = iterator.next(); + int opcode = ain.getOpcode(); + if (opcode == IRETURN || opcode == FRETURN || opcode == ARETURN || + opcode == DRETURN || opcode == LRETURN) { + // Remove the return instruction, leaving the return value on the stack + iterator.remove(); + } else if (opcode == RETURN) { + // Remove the void return instruction + iterator.remove(); + } else if (opcode == ATHROW) { + // Leave ATHROW instructions intact } - copy.remove(ain); - switch (ain.getOpcode()) { - case RETURN: - case ARETURN: - case DRETURN: - case FRETURN: - case IRETURN: - case LRETURN: - return; - default: + } + } + + /** + * Performs a sanity check after inlining to ensure no invalid instructions remain. + * Specifically, it checks for leftover return instructions which should have been removed. + * + * @param instructions The instruction list to check. + * @return True if the sanity check passes; otherwise, false. + */ + private boolean postInlineSanityCheck(InsnList instructions) { + for (AbstractInsnNode ain : instructions.toArray()) { + int opcode = ain.getOpcode(); + // Check for any leftover return instructions + if (opcode == RETURN || opcode == IRETURN || opcode == LRETURN || + opcode == FRETURN || opcode == DRETURN || opcode == ARETURN) { + return false; // Sanity check failed } - i--; } - throw new RuntimeException("no return found to remove, invalid method?"); + return true; // Sanity check passed } + /** + * Determines if a method is unnecessary (i.e., can be inlined). + * Currently checks if it doesn't contain complex instructions like method calls, field accesses, etc., + * and ensures it ends with a return or throw. + * + * @param m The method node to evaluate. + * @return True if the method is unnecessary and eligible for inlining; otherwise, false. + */ public boolean isUnnecessary(MethodNode m) { - if (!Access.isStatic(m.access)) { - return false; - } else if (m.instructions.size() > 32) { - // do not inline huge methods - return false; - } else if (m.instructions.size() < 2) { - // abstract methods or similar - return false; - } - return StreamSupport.stream(m.instructions.spliterator(), false).noneMatch(this::isInvocationOrJump); + // This method is deprecated in favor of isEligibleForInlining + // Keeping it for backward compatibility; it simply delegates to isEligibleForInlining + return isEligibleForInlining(m); } + /** + * Determines if an instruction is an invocation or a jump. + * Used to identify methods that cannot be inlined. + * + * @param ain The instruction node to check. + * @return True if the instruction is an invocation or a jump; otherwise, false. + */ public boolean isInvocationOrJump(AbstractInsnNode ain) { switch (ain.getType()) { case AbstractInsnNode.METHOD_INSN: @@ -177,7 +385,8 @@ public boolean isInvocationOrJump(AbstractInsnNode ain) { case AbstractInsnNode.TYPE_INSN: case AbstractInsnNode.JUMP_INSN: return true; + default: + return false; } - return false; } } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java index 70c99f30..7c6a9e64 100644 --- a/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/InlineUnchangedFields.java @@ -1,68 +1,348 @@ package me.nov.threadtear.execution.cleanup; -import java.util.*; -import java.util.stream.*; - -import org.objectweb.asm.Type; -import org.objectweb.asm.tree.*; - import me.nov.threadtear.execution.*; import me.nov.threadtear.util.asm.*; +import org.objectweb.asm.*; +import org.objectweb.asm.tree.*; +import java.lang.reflect.Field; +import java.security.SecureClassLoader; +import java.util.*; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; + +import static org.objectweb.asm.Opcodes.*; + +/** + * This execution inlines static fields that: + * 1. Are assigned only once in . + * 2. Are never reassigned outside . + * 3. Have a known value after finishes running. + * + * Approach: + * - Load all classes into a custom ClassLoader. + * - This triggers for each class. + * - After that, reflect on the class to get the runtime values of the static fields. + * - If a field is never reassigned after , we treat its runtime value as a constant. + */ public class InlineUnchangedFields extends Execution { + private Map classes; + private List fieldAssignments; // All field assignment instructions + private int inlinedCount; + + // Map: className -> (fieldName+desc -> runtime value) + private Map> constantFieldValues = new HashMap<>(); + public InlineUnchangedFields() { - super(ExecutionCategory.CLEANING, "Inline unchanged fields", - "Inline fields that are not set anywhere in the code.
Can be useful for " + - "ZKM deobfuscation.", ExecutionTag.RUNNABLE, ExecutionTag.BETTER_DECOMPILE, - ExecutionTag.BETTER_DEOBFUSCATE); + super( + ExecutionCategory.CLEANING, + "Inline unchanged fields (Reflection Runtime) with merged ", + "Loads classes, merges multiple , runs , and uses reflection to determine static field values.
" + + "Then inlines these values in all references.", + ExecutionTag.RUNNABLE, + ExecutionTag.BETTER_DECOMPILE, + ExecutionTag.BETTER_DEOBFUSCATE + ); } - public int inlines; - private Map classes; - private List fieldPuts; - @Override public boolean execute(Map classes, boolean verbose) { this.classes = classes; - this.inlines = 0; - // TODO static initializer should be excluded, we can - // still calculate the field - // value - this.fieldPuts = classes.values().stream().map(c -> c.node.methods).flatMap(List::stream) - .map(m -> m.instructions.spliterator()).flatMap(insns -> StreamSupport.stream(insns, false)) - .filter(ain -> ain.getOpcode() == PUTFIELD || ain.getOpcode() == PUTSTATIC).map(ain -> (FieldInsnNode) ain) - .collect(Collectors.toList()); - - classes.values().stream().map(c -> c.node).filter(c -> !Access.isEnum(c.access)) - .forEach(c -> c.fields.stream().filter(f -> isNotReferenced(c, f)).forEach(f -> inline(c, f))); - logger.info("Inlined {} method references!", inlines); - return inlines > 0; + this.inlinedCount = 0; + + // Collect all field assignment instructions + this.fieldAssignments = classes.values().stream() + .map(c -> c.node.methods) + .flatMap(List::stream) + .map(m -> m.instructions.spliterator()) + .flatMap(insns -> StreamSupport.stream(insns, false)) + .filter(ain -> ain.getOpcode() == PUTFIELD || ain.getOpcode() == PUTSTATIC) + .map(ain -> (FieldInsnNode) ain) + .collect(Collectors.toList()); + + // Merge multiple methods if present + for (Clazz clazz : this.classes.values()) { + mergeClinitMethods(clazz.node); + } + + // Build the classes map + Map classesMap = buildClassesMap(this.classes); + + // Instantiate the custom class loader with the classes map + ReflectiveClassLoader loader = new ReflectiveClassLoader(getClass().getClassLoader(), classesMap); + + // Define all classes using the custom class loader + Map> loadedClasses = defineAllClasses(loader, this.classes); + + // Determine which fields can be considered constant + for (Clazz clazz : this.classes.values()) { + ClassNode cn = clazz.node; + if (!Access.isEnum(cn.access)) { + analyzeClinitForConstants(cn, loadedClasses); + } + } + + // Inline all references to these constant fields + inlineAllConstantFields(); + + logger.info("Inlined {} field references!", inlinedCount); + return inlinedCount > 0; } - private boolean isNotReferenced(ClassNode cn, FieldNode f) { - return fieldPuts.stream().noneMatch(fin -> isReferenceTo(cn, fin, f)); + /** + * Merges multiple methods into a single one if found. + * Java normally allows only one , but in manipulated bytecode, + * there might be multiple. We combine them for easier analysis. + */ + private void mergeClinitMethods(ClassNode cn) { + List clinitMethods = new ArrayList<>(); + for (MethodNode m : cn.methods) { + if (m.name.equals("") && m.desc.equals("()V")) { + clinitMethods.add(m); + } + } + + // If there's only one or none, nothing to do + if (clinitMethods.size() <= 1) { + return; + } + + // We have multiple methods. Let's merge them into the first one. + MethodNode primary = clinitMethods.get(0); + + for (int i = 1; i < clinitMethods.size(); i++) { + MethodNode extra = clinitMethods.get(i); + + // We'll merge instructions from 'extra' into 'primary' + // Just before primary's RETURN instruction (or at the end if no explicit return) + AbstractInsnNode insertionPoint = findMethodReturnOrEnd(primary); + + // Clone labels + Map labelMap = Instructions.cloneLabels(extra.instructions); + + // Copy instructions + InsnList extraCopy = new InsnList(); + for (AbstractInsnNode ain : extra.instructions) { + if (ain.getType() == AbstractInsnNode.LABEL || + ain.getType() == AbstractInsnNode.FRAME || + ain.getType() == AbstractInsnNode.LINE || + ain.getOpcode() != RETURN) { + extraCopy.add(ain.clone(labelMap)); + } + } + + // Insert the copied instructions before the return + primary.instructions.insertBefore(insertionPoint, extraCopy); + + // Merge try-catch blocks + if (extra.tryCatchBlocks != null) { + for (TryCatchBlockNode tcb : extra.tryCatchBlocks) { + TryCatchBlockNode copyTcb = new TryCatchBlockNode( + labelMap.get(tcb.start), + labelMap.get(tcb.end), + labelMap.get(tcb.handler), + tcb.type + ); + primary.tryCatchBlocks.add(copyTcb); + } + } + + // Merge local variables + if (extra.localVariables != null) { + if (primary.localVariables == null) { + primary.localVariables = new ArrayList<>(); + } + for (LocalVariableNode lv : extra.localVariables) { + LocalVariableNode copyLv = new LocalVariableNode( + lv.name, + lv.desc, + lv.signature, + labelMap.get(lv.start), + labelMap.get(lv.end), + lv.index + ); + primary.localVariables.add(copyLv); + } + } + } + + // Remove all extra methods + cn.methods.removeIf(m -> m.name.equals("") && m != primary); } - public void inline(ClassNode cn, FieldNode fn) { - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.forEach(m -> { - for (AbstractInsnNode ain : m.instructions) { - if (ain.getType() == AbstractInsnNode.FIELD_INSN) { - FieldInsnNode fin = (FieldInsnNode) ain; - if (isGetReferenceTo(cn, fin, fn)) { - m.instructions.set(ain, Instructions.makeNullPush(Type.getType(fn.desc))); - inlines++; - } + /** + * Finds a suitable point in the primary method to insert extra instructions. + * We prefer to insert before the RETURN instruction if found, else insert at the end. + */ + private AbstractInsnNode findMethodReturnOrEnd(MethodNode m) { + for (AbstractInsnNode ain = m.instructions.getLast(); ain != null; ain = ain.getPrevious()) { + int op = ain.getOpcode(); + if (op >= IRETURN && op <= RETURN) { + return ain; // found a return instruction + } + } + // No return found, insert at the very end + return m.instructions.getLast(); + } + + /** + * Builds a map of internal class names to their byte arrays. + * + * @param classes The map of class names to Clazz instances. + * @return A map of internal class names to byte arrays. + */ + private Map buildClassesMap(Map classes) { + Map classBytesMap = new HashMap<>(); + for (Map.Entry e : classes.entrySet()) { + String internalName = e.getKey(); // e.g., "com/example/MyClass" + ClassNode cn = e.getValue().node; + byte[] classBytes = Instructions.toByteArray(cn); + classBytesMap.put(internalName, classBytes); + } + return classBytesMap; + } + + /** + * Defines all classes into the provided ClassLoader. + * Once defined, will have run, so we can reflect on their static fields. + */ + private Map> defineAllClasses(ReflectiveClassLoader loader, Map classes) { + Map> result = new HashMap<>(); + for (Map.Entry e : classes.entrySet()) { + String internalName = e.getKey(); // e.g., "com/example/MyClass" + String className = internalName.replace('/', '.'); // e.g., "com.example.MyClass" + try { + Class definedClass = loader.loadClass(className); + result.put(internalName, definedClass); + } catch (ClassNotFoundException ex) { + logger.error("Failed to load class: {}", className, ex); + } + } + return result; + } + + /** + * Analyze a single class to determine which static fields can be considered constant. + * If a static field is never reassigned outside , we read its value via reflection. + */ + private void analyzeClinitForConstants(ClassNode cn, Map> loadedClasses) { + MethodNode clinit = null; + for (MethodNode m : cn.methods) { + if (m.name.equals("") && m.desc.equals("()V")) { + clinit = m; + break; + } + } + + if (clinit == null) return; + + Class runtimeClass = loadedClasses.get(cn.name); + if (runtimeClass == null) { + return; // Could not load class, skip + } + + // Check each static field to see if it's never assigned outside + for (FieldNode fn : cn.fields) { + if ((fn.access & ACC_STATIC) == 0) { + continue; // only handle static fields + } + + boolean assignedOutsideClinit = fieldAssignments.stream() + .anyMatch(fin -> isFieldReferenceTo(cn, fn, fin) && !isInClinit(cn, fin)); + if (!assignedOutsideClinit) { + try { + Field f = runtimeClass.getDeclaredField(fn.name); + f.setAccessible(true); + Object value = f.get(null); + + // Store discovered value + constantFieldValues + .computeIfAbsent(cn.name, k -> new HashMap<>()) + .put(fn.name + fn.desc, value); + } catch (NoSuchFieldException | IllegalAccessException ex) { + // Can't access field, skip } } - })); + } } - private boolean isGetReferenceTo(ClassNode cn, FieldInsnNode fin, FieldNode fn) { - return (fin.getOpcode() == GETSTATIC || fin.getOpcode() == GETFIELD) && isReferenceTo(cn, fin, fn); + /** + * Inline references to all fields determined to be constant. + */ + private void inlineAllConstantFields() { + for (Clazz c : classes.values()) { + ClassNode cn = c.node; + Map fieldMap = constantFieldValues.getOrDefault(cn.name, Collections.emptyMap()); + + if (!fieldMap.isEmpty()) { + for (MethodNode m : cn.methods) { + List toReplace = new ArrayList<>(); + for (AbstractInsnNode ain : m.instructions) { + if (ain.getType() == AbstractInsnNode.FIELD_INSN) { + FieldInsnNode fin = (FieldInsnNode) ain; + String key = fin.name + fin.desc; + if (fieldMap.containsKey(key) && fin.getOpcode() == GETSTATIC) { + toReplace.add(ain); + } + } + } + + for (AbstractInsnNode insn : toReplace) { + FieldInsnNode fin = (FieldInsnNode) insn; + Object constantValue = fieldMap.get(fin.name + fin.desc); + Type fieldType = Type.getType(fin.desc); + + AbstractInsnNode replacement = Instructions.makeConstantPush(constantValue, fieldType); + m.instructions.set(insn, replacement); + inlinedCount++; + logger.debug("Inlined field {}.{} in method {} of class {} with value {}", + fin.owner, fin.name, m.name, cn.name, constantValue); + } + } + } + } } - private boolean isReferenceTo(ClassNode cn, FieldInsnNode fin, FieldNode fn) { + private boolean isFieldReferenceTo(ClassNode cn, FieldNode fn, FieldInsnNode fin) { return fin.owner.equals(cn.name) && fin.name.equals(fn.name) && fin.desc.equals(fn.desc); } + + private boolean isInClinit(ClassNode cn, FieldInsnNode fin) { + for (MethodNode m : cn.methods) { + if (m.name.equals("") && m.desc.equals("()V")) { + for (AbstractInsnNode ain : m.instructions) { + if (ain == fin) { + return true; + } + } + } + } + return false; + } + + /** + * A custom ClassLoader that allows us to define classes from byte arrays. + * It holds a map of class names to byte arrays to resolve dependencies. + * + * Note: This class trusts the input. Real usage should consider security. + */ + public static class ReflectiveClassLoader extends SecureClassLoader { + private final Map classesMap; + + public ReflectiveClassLoader(ClassLoader parent, Map classesMap) { + super(parent); + this.classesMap = classesMap; + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + String internalName = name.replace('.', '/'); + byte[] classBytes = classesMap.get(internalName); + if (classBytes != null) { + return defineClass(name, classBytes, 0, classBytes.length); + } + return super.findClass(name); // Delegate to parent if not found in map + } + } } diff --git a/core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java b/core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java new file mode 100644 index 00000000..1d4a4c1a --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/execution/cleanup/simplify/SimplifyBitOperations.java @@ -0,0 +1,367 @@ +package me.nov.threadtear.execution.cleanup.simplify; + +import java.util.*; + +import me.nov.threadtear.analysis.stack.ConstantTracker; +import org.objectweb.asm.Type; +import org.objectweb.asm.tree.*; +import org.objectweb.asm.tree.analysis.*; +import me.nov.threadtear.execution.*; +import me.nov.threadtear.util.asm.Access; +import me.nov.threadtear.analysis.stack.ConstantValue; +import me.nov.threadtear.analysis.stack.IConstantReferenceHandler; + +/** + * Execution to simplify bitwise operations in Java bytecode. + * It performs constant folding and attempts to simplify common obfuscation patterns. + */ +public class SimplifyBitOperations extends Execution implements IConstantReferenceHandler { + + public SimplifyBitOperations() { + super( + ExecutionCategory.CLEANING, + "Simplify Bitwise Operations", + "Simplifies bitwise operations by performing constant folding and simplifying common obfuscation patterns." + ); + } + + private int simplifications = 0; + + @Override + public boolean execute(Map classes, boolean verbose) { + simplifications = 0; + + for (Clazz clazz : classes.values()) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + if (Access.isAbstract(method.access) || Access.isNative(method.access)) { + continue; // Skip abstract and native methods + } + + try { + simplifyMethod(classNode, method, verbose); + } catch (Exception e) { + clazz.addFail(e); + if (verbose) { + logger.error("Failed to simplify method {}.{}: {}", classNode.name, method.name, e.getMessage()); + } + } + } + } + + logger.info("Simplified {} bitwise operations.", simplifications); + return true; + } + + /** + * Simplifies bitwise operations within a method. + * + * @param classNode The class containing the method. + * @param method The method to simplify. + * @param verbose Flag to enable verbose logging. + * @throws AnalyzerException If bytecode analysis fails. + */ + private void simplifyMethod(ClassNode classNode, MethodNode method, boolean verbose) throws AnalyzerException { + Analyzer analyzer = new Analyzer<>(new ConstantTracker(this, Access.isStatic(method.access), method.maxLocals, method.desc, new Object[0])); + analyzer.analyze(classNode.name, method); + Frame[] frames = analyzer.getFrames(); + AbstractInsnNode[] insns = method.instructions.toArray(); + + for (int i = 0; i < insns.length; i++) { + AbstractInsnNode insn = insns[i]; + int opcode = insn.getOpcode(); + + if (isBitwiseOperation(opcode)) { + Frame frame = frames[i]; + if (frame == null) continue; // Dead code + + // Depending on the operation, retrieve operands + switch (opcode) { + case IAND: + case IOR: + case IXOR: + // Binary integer operations + simplifyBinaryIntOperation(method, insn, frame, opcode, Type.INT_TYPE, verbose); + break; + case LAND: + case LOR: + case LXOR: + // Binary long operations + simplifyBinaryIntOperation(method, insn, frame, opcode, Type.LONG_TYPE, verbose); + break; + case ISHL: + case ISHR: + case IUSHR: + // Shift operations + simplifyShiftOperation(method, insn, frame, opcode, Type.INT_TYPE, verbose); + break; + case LSHL: + case LSHR: + case LUSHR: + // Shift operations for long + simplifyShiftOperation(method, insn, frame, opcode, Type.LONG_TYPE, verbose); + break; + default: + break; + } + + // Attempt to simplify common bitmask patterns + simplifyBitMaskPattern(method, insn, frame, i, verbose); + } + } + } + + /** + * Checks if the opcode corresponds to a bitwise operation. + * + * @param opcode The opcode to check. + * @return True if it's a bitwise operation, else false. + */ + private boolean isBitwiseOperation(int opcode) { + return opcode == IAND || opcode == IOR || opcode == IXOR || + opcode == LAND || opcode == LOR || opcode == LXOR || + opcode == ISHL || opcode == ISHR || opcode == IUSHR || + opcode == LSHL || opcode == LSHR || opcode == LUSHR; + } + + /** + * Simplifies binary integer or long bitwise operations. + * + * @param method The method containing the instruction. + * @param insn The instruction to potentially replace. + * @param frame The current stack frame. + * @param opcode The opcode of the instruction. + * @param type The type of the operation (INT or LONG). + * @param verbose Verbose logging flag. + */ + private void simplifyBinaryIntOperation(MethodNode method, AbstractInsnNode insn, Frame frame, int opcode, Type type, boolean verbose) { + // For binary operations, the stack should have two operands + ConstantValue value2 = frame.getStack(frame.getStackSize() - 1); + ConstantValue value1 = frame.getStack(frame.getStackSize() - 2); + + if (value1.isKnown() && (value1.isInteger() || value1.isLong()) && + value2.isKnown() && (value2.isInteger() || value2.isLong())) { + // Perform the bitwise operation + long operand1 = ((Number) value1.getValue()).longValue(); + long operand2 = ((Number) value2.getValue()).longValue(); + long result = 0; + + switch (opcode) { + case IAND: + case LAND: + result = operand1 & operand2; + break; + case IOR: + case LOR: + result = operand1 | operand2; + break; + case IXOR: + case LXOR: + result = operand1 ^ operand2; + break; + default: + return; // Not a binary bitwise operation + } + + // Replace the bitwise operation with the constant result + AbstractInsnNode replacement = getConstantInsn(result, type); + method.instructions.set(insn, replacement); + simplifications++; + + if (verbose && logger != null) { // Ensure logger is not null + logger.debug("Simplified bitwise operation in method {}: {} {} {} -> {}", method.name, operand1, getOpcodeName(opcode), operand2, result); + } + } + } + + /** + * Simplifies shift operations. + * + * @param method The method containing the instruction. + * @param insn The instruction to potentially replace. + * @param frame The current stack frame. + * @param opcode The opcode of the instruction. + * @param type The type of the operation (INT or LONG). + * @param verbose Verbose logging flag. + */ + private void simplifyShiftOperation(MethodNode method, AbstractInsnNode insn, Frame frame, int opcode, Type type, boolean verbose) { + // For shift operations, the stack should have two operands: value and shift + ConstantValue shiftValue = frame.getStack(frame.getStackSize() - 1); + ConstantValue value = frame.getStack(frame.getStackSize() - 2); + + if (value.isKnown() && (value.isInteger() || value.isLong()) && + shiftValue.isKnown() && (shiftValue.isInteger() || shiftValue.isLong())) { + long operand = ((Number) value.getValue()).longValue(); + long shift = ((Number) shiftValue.getValue()).longValue(); + long result = 0; + + switch (opcode) { + case ISHL: + case LSHL: + result = operand << shift; + break; + case ISHR: + case LSHR: + result = operand >> shift; + break; + case IUSHR: + case LUSHR: + result = operand >>> shift; + break; + default: + return; // Not a shift operation + } + + // Replace the shift operation with the constant result + AbstractInsnNode replacement = getConstantInsn(result, type); + method.instructions.set(insn, replacement); + simplifications++; + + if (verbose && logger != null) { // Ensure logger is not null + logger.debug("Simplified shift operation in method {}: {} {} {}", method.name, operand, getOpcodeName(opcode), shift, result); + } + } + } + + /** + * Attempts to simplify common bitmask patterns. + * + * @param method The method containing the instruction. + * @param insn The current instruction. + * @param frame The current stack frame. + * @param index The index of the instruction. + * @param verbose Verbose logging flag. + */ + private void simplifyBitMaskPattern(MethodNode method, AbstractInsnNode insn, Frame frame, int index, boolean verbose) { + // Example pattern: (var0 >>> shift) & mask + // Attempt to simplify if possible + if (!(insn instanceof InsnNode)) return; + + int opcode = insn.getOpcode(); + if (opcode != IAND && opcode != LAND) return; + + // Get the previous instruction (should be the shift operation) + AbstractInsnNode prevInsn = insn.getPrevious(); + if (prevInsn == null) return; + + int prevOpcode = prevInsn.getOpcode(); + if (!(prevOpcode == ISHL || prevOpcode == ISHR || prevOpcode == IUSHR || + prevOpcode == LSHL || prevOpcode == LSHR || prevOpcode == LUSHR)) { + return; + } + + // Check if the shift operation has a known shift value + Frame shiftFrame = frame; + ConstantValue maskValue = frame.getStack(frame.getStackSize() - 1); + ConstantValue shiftResult = frame.getStack(frame.getStackSize() - 2); + + if (maskValue.isKnown() && (maskValue.isInteger() || maskValue.isLong())) { + long mask = ((Number) maskValue.getValue()).longValue(); + + // Attempt to determine if the mask is a power of two minus one (e.g., 1, 3, 7, 15, 31, 63, ...) + if (isPowerOfTwoMinusOne(mask)) { + int bitCount = Long.bitCount(mask); + // For example, mask = 63 (0x3F) -> bitCount = 6 + + // This pattern is often used to extract specific bits + // Without knowing var0's value, we cannot simplify further + // However, we can annotate or mark this pattern for manual review + + if (verbose) { + logger.debug("Detected bitmask pattern in method {}: mask = {}", method.name, mask); + } + + // Optionally, you can insert comments or metadata for manual analysis + // Since ASM does not support comments, this step is limited + } + } + } + + /** + * Checks if a number is a power of two minus one (e.g., 1, 3, 7, 15, 31, ...). + * + * @param number The number to check. + * @return True if the number is a power of two minus one, else false. + */ + private boolean isPowerOfTwoMinusOne(long number) { + return (number & (number + 1)) == 0 && number != 0; + } + + /** + * Creates a constant instruction node based on the type and value. + * + * @param value The constant value. + * @param type The type of the constant (INT or LONG). + * @return The corresponding instruction node. + */ + private AbstractInsnNode getConstantInsn(long value, Type type) { + if (type.equals(Type.INT_TYPE)) { + if (value >= -1 && value <= 5) { + switch ((int) value) { + case -1: return new InsnNode(ICONST_M1); + case 0: return new InsnNode(ICONST_0); + case 1: return new InsnNode(ICONST_1); + case 2: return new InsnNode(ICONST_2); + case 3: return new InsnNode(ICONST_3); + case 4: return new InsnNode(ICONST_4); + case 5: return new InsnNode(ICONST_5); + } + } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) { + return new IntInsnNode(BIPUSH, (int) value); + } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) { + return new IntInsnNode(SIPUSH, (int) value); + } else { + return new LdcInsnNode((int) value); + } + } else if (type.equals(Type.LONG_TYPE)) { + if (value == 0L || value == 1L) { + return new InsnNode(value == 0L ? LCONST_0 : LCONST_1); + } else { + return new LdcInsnNode(value); + } + } + return new LdcInsnNode(value); + } + + /** + * Retrieves the opcode name for logging purposes. + * + * @param opcode The opcode to get the name for. + * @return The name of the opcode. + */ + private String getOpcodeName(int opcode) { + switch (opcode) { + case IAND: return "IAND"; + case IOR: return "IOR"; + case IXOR: return "IXOR"; + case LAND: return "LAND"; + case LOR: return "LOR"; + case LXOR: return "LXOR"; + case ISHL: return "ISHL"; + case ISHR: return "ISHR"; + case IUSHR: return "IUSHR"; + case LSHL: return "LSHL"; + case LSHR: return "LSHR"; + case LUSHR: return "LUSHR"; + default: return "UNKNOWN"; + } + } + + /** + * Implementation of IConstantReferenceHandler interface methods. + * These methods are required for constant tracking but are not utilized in this execution. + */ + + @Override + public Object getFieldValueOrNull(BasicValue v, String owner, String name, String desc) { + // Not required for this execution + return null; + } + + @Override + public Object getMethodReturnOrNull(BasicValue v, String owner, String name, String desc, List values) { + return null; + } + + +} diff --git a/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java b/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java index 1a0ce802..12627e8f 100644 --- a/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java +++ b/core/src/main/java/me/nov/threadtear/execution/zkm/FlowObfuscationZKM.java @@ -4,55 +4,66 @@ import me.nov.threadtear.execution.Execution; import me.nov.threadtear.execution.ExecutionCategory; import me.nov.threadtear.execution.ExecutionTag; +import me.nov.threadtear.util.asm.Access; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.InsnNode; import org.objectweb.asm.tree.MethodNode; import java.util.ArrayList; import java.util.List; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Predicate; public class FlowObfuscationZKM extends Execution { private static final Predicate singleJump = op -> (op >= IFEQ && op <= IFLE) || op == IFNULL || op == IFNONNULL; - private int replaced; public FlowObfuscationZKM() { super(ExecutionCategory.ZKM, "Flow obfuscation removal", - "Tested on ZKM 14, could work on newer versions too.", ExecutionTag.POSSIBLE_DAMAGE, + "Rewritten and needs to be teseted", ExecutionTag.POSSIBLE_DAMAGE, ExecutionTag.BETTER_DECOMPILE); } @Override public boolean execute(Map classes, boolean verbose) { - replaced = 0; - logger.info("Removing all garbage jumps"); - classes.values().stream().map(c -> c.node).forEach(c -> c.methods.forEach(this::removeZKMJumps)); - logger.info("Removed {} jumps matching ZKM pattern in total", replaced); - return replaced > 0; - } + AtomicInteger counter = new AtomicInteger(); + + classes.values().forEach(clazz -> { + ClassNode classNode = clazz.node; + + classNode.methods.stream() + .filter(methodNode -> !Access.isAbstract(methodNode.access) && !Access.isNative(methodNode.access)) + .forEach(methodNode -> { + int originalTryCatchCount = methodNode.tryCatchBlocks.size(); + + methodNode.tryCatchBlocks.removeIf(tc -> { + AbstractInsnNode handlerNext = tc.handler.getNext(); + if (handlerNext == null) return false; + + int opcode = handlerNext.getOpcode(); + + // Check for INVOKESTATIC followed by ATHROW + if (opcode == Opcodes.INVOKESTATIC) { + AbstractInsnNode nextNext = handlerNext.getNext(); + return nextNext != null && nextNext.getOpcode() == Opcodes.ATHROW; + } + // Check for ATHROW directly + else if (opcode == Opcodes.ATHROW) { + return true; + } + return false; + }); + + int removedTryCatchCount = originalTryCatchCount - methodNode.tryCatchBlocks.size(); + counter.addAndGet(removedTryCatchCount); + }); + }); - public void removeZKMJumps(MethodNode mn) { - for (AbstractInsnNode ain : mn.instructions.toArray()) { - if (ain.getPrevious() != null && singleJump.test(ain.getOpcode())) { - AbstractInsnNode previous = ain.getPrevious(); - boolean shouldPop = false; - if (ain.getOpcode() == IFNULL || ain.getOpcode() == IFNONNULL) { //first case flow obfuscation scenario - if (previous.getOpcode() == ALOAD) { - shouldPop = true; - } - } else if (ain.getOpcode() >= IFEQ && ain.getOpcode() <= IFLE) { //second case - if (previous.getOpcode() == ILOAD) { - shouldPop = true; - } - } - if (shouldPop) { - mn.instructions.set(ain, new InsnNode(POP)); - replaced++; - } - } - } + logger.info("[ZKM] Removed {} fake try-catch blocks.", counter.get()); + return counter.get() > 0; } } diff --git a/core/src/main/java/me/nov/threadtear/io/JarIO.java b/core/src/main/java/me/nov/threadtear/io/JarIO.java index 45a14f96..6955588a 100644 --- a/core/src/main/java/me/nov/threadtear/io/JarIO.java +++ b/core/src/main/java/me/nov/threadtear/io/JarIO.java @@ -8,9 +8,15 @@ import me.nov.threadtear.logging.LogWrapper; import org.apache.commons.io.IOUtils; +import org.benf.cfr.reader.bytecode.analysis.opgraph.op4rewriters.IllegalGenericRewriter; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; import me.nov.threadtear.execution.Clazz; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; +import software.coley.cafedude.transform.IllegalStrippingTransformer; public final class JarIO { private JarIO() { @@ -25,15 +31,35 @@ public static ArrayList loadClasses(File jarFile) throws IOException { return classes; } + public static byte[] transformClazz(byte[] bytes) throws software.coley.cafedude.InvalidClassException { + + // Use Cafedude to strip attributes + ClassFileReader reader = new ClassFileReader(); + ClassFile classFile = reader.read(bytes); + // Modifies the 'cf' instance + new IllegalStrippingTransformer(classFile).transform(); + byte[] strippedBytecode = new ClassFileWriter().write(classFile); + + // Return a new Clazz object + return strippedBytecode; + } + private static ArrayList readEntry(JarFile jar, JarEntry en, ArrayList classes) { String name = en.getName(); try (InputStream jis = jar.getInputStream(en)) { - byte[] bytes = IOUtils.toByteArray(jis); + byte[] bytes = (IOUtils.toByteArray(jis)); if (isClassFile(bytes)) { try { - final ClassNode cn = Conversion.toNode(bytes); + final ClassNode cn = Conversion.toNode(transformClazz(bytes)); + if (cn != null && (cn.superName != null || (cn.name != null && cn.name.equals("java/lang/Object")))) { - classes.add(new Clazz(cn, en, jar)); + // transform using cafedood + try{ + classes.add(new Clazz(cn, en, jar)); + }catch (Exception e){ + LogWrapper.logger.error("Failed to transform class {}", e, name); + } + } } catch (Exception e) { LogWrapper.logger.error("Failed to load file {}", e, name); diff --git a/core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java b/core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java new file mode 100644 index 00000000..2b875b49 --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/util/ByteCodeUtil.java @@ -0,0 +1,105 @@ +package me.nov.threadtear.util; + + +import me.nov.threadtear.execution.Clazz; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.*; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ByteCodeUtil { + /** + * Finds all classes that import the specified class. + * + * @param classes The map of class names to Clazz objects. + * @param specificClass The name of the specific class to find imports for. + * @return A map of class names to Clazz objects that import the specific class. + */ + public static Map findallimports(Map classes, String specificClass) { + Map result = new HashMap<>(); + + for (Map.Entry entry : classes.entrySet()) { + Clazz clazz = entry.getValue(); + if (importsClass(clazz, specificClass)) { + result.put(entry.getKey(), clazz); + } + } + + return result; + } + + /** + * Checks if the given class imports the specified class. + * + * @param clazz The Clazz object to check. + * @param specificClass The name of the specific class to check for. + * @return True if the clazz imports the specific class, false otherwise. + */ + private static boolean importsClass(Clazz clazz, String specificClass) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + for (AbstractInsnNode instruction : method.instructions) { + if (instruction instanceof MethodInsnNode) { + MethodInsnNode methodInsn = (MethodInsnNode) instruction; + if (methodInsn.owner.equals(specificClass.replace('.', '/'))) { + return true; + } + } + } + } + return false; + } + + public static List findVariableModifications(Map classes, Clazz Class, int varIndex) { + List modifications = new ArrayList<>(); + String targetClassName = Class.node.name; + + // Retrieve the list of fields from the target class + List fields = Class.node.fields; + if (varIndex < 0 || varIndex >= fields.size()) { + // Invalid varIndex + return modifications; + } + FieldNode targetField = fields.get(varIndex); + String targetFieldName = targetField.name; + String targetFieldDesc = targetField.desc; + + for (Clazz clazz : classes.values()) { + ClassNode classNode = clazz.node; + for (MethodNode method : classNode.methods) { + InsnList instructions = method.instructions; + for (AbstractInsnNode instruction : instructions) { + if (instruction instanceof FieldInsnNode) { + FieldInsnNode fieldInsn = (FieldInsnNode) instruction; + if ((fieldInsn.getOpcode() == Opcodes.PUTFIELD || fieldInsn.getOpcode() == Opcodes.PUTSTATIC) + && fieldInsn.owner.equals(targetClassName) + && fieldInsn.name.equals(targetFieldName) + && fieldInsn.desc.equals(targetFieldDesc)) { + // Record the modification + String modificationDetail = "Field " + fieldInsn.owner.replace('/', '.') + "." + fieldInsn.name + + " modified in method " + method.name + " of class " + classNode.name.replace('/', '.'); + modifications.add(modificationDetail); + } + } + } + } + } + return modifications; + } + + + /** + * Checks if the given opcode corresponds to a variable modification. + * + * @param opcode The opcode to check. + * @return True if the opcode is a modification, false otherwise. + */ + private static boolean isModificationOpcode(int opcode) { + // Opcodes for variable modification (store instructions) + return opcode == Opcodes.ISTORE || opcode == Opcodes.LSTORE || opcode == Opcodes.FSTORE || + opcode == Opcodes.DSTORE || opcode == Opcodes.ASTORE; + } +} diff --git a/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java b/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java index 9028c340..cea1275e 100644 --- a/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java +++ b/core/src/main/java/me/nov/threadtear/util/asm/Instructions.java @@ -333,4 +333,86 @@ public static InsnList singleton(AbstractInsnNode ain) { list.add(ain); return list; } + + /** + * Converts a ClassNode back into a byte array. + * This uses ASM's ClassWriter and the accept() method on ClassNode. + */ + public static byte[] toByteArray(ClassNode cn) { + ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS | ClassWriter.COMPUTE_FRAMES); + cn.accept(cw); + return cw.toByteArray(); + } + + /** + * Creates instructions to push a given constant value onto the stack, based on its type. + * If value is null, or not compatible, it falls back to makeNullPush(). + */ + public static AbstractInsnNode makeConstantPush(Object value, Type type) { + if (value == null) { + return makeNullPush(type); + } + + switch (type.getSort()) { + case Type.BOOLEAN: + case Type.BYTE: + case Type.SHORT: + case Type.INT: + // Expect value to be a number + int intVal = ((Number) value).intValue(); + return pushInt(intVal); + + case Type.LONG: + long longVal = ((Number) value).longValue(); + if (longVal == 0L) return new InsnNode(LCONST_0); + if (longVal == 1L) return new InsnNode(LCONST_1); + return new LdcInsnNode(longVal); + + case Type.FLOAT: + float floatVal = ((Number) value).floatValue(); + if (floatVal == 0.0f) return new InsnNode(FCONST_0); + if (floatVal == 1.0f) return new InsnNode(FCONST_1); + if (floatVal == 2.0f) return new InsnNode(FCONST_2); + return new LdcInsnNode(floatVal); + + case Type.DOUBLE: + double doubleVal = ((Number) value).doubleValue(); + if (doubleVal == 0.0d) return new InsnNode(DCONST_0); + if (doubleVal == 1.0d) return new InsnNode(DCONST_1); + return new LdcInsnNode(doubleVal); + + case Type.OBJECT: + case Type.ARRAY: + // For objects (including String) just LDC them + return new LdcInsnNode(value); + + default: + // If we can't handle this type, fallback to null/zero + return makeNullPush(type); + } + } + + /** + * Helper method to push an int value efficiently. + */ + private static AbstractInsnNode pushInt(int val) { + switch (val) { + case -1: return new InsnNode(ICONST_M1); + case 0: return new InsnNode(ICONST_0); + case 1: return new InsnNode(ICONST_1); + case 2: return new InsnNode(ICONST_2); + case 3: return new InsnNode(ICONST_3); + case 4: return new InsnNode(ICONST_4); + case 5: return new InsnNode(ICONST_5); + default: + // For larger integers, use BIPUSH/SIPUSH or LDC + if (val >= Byte.MIN_VALUE && val <= Byte.MAX_VALUE) { + return new IntInsnNode(BIPUSH, val); + } else if (val >= Short.MIN_VALUE && val <= Short.MAX_VALUE) { + return new IntInsnNode(SIPUSH, val); + } else { + return new LdcInsnNode(val); + } + } + } } diff --git a/core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java b/core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java new file mode 100644 index 00000000..01daba64 --- /dev/null +++ b/core/src/main/java/me/nov/threadtear/util/reflection/ReflectionUtil.java @@ -0,0 +1,66 @@ +package me.nov.threadtear.util.reflection; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; +import java.util.jar.JarEntry; +import java.util.jar.JarFile; + +public class ReflectionUtil { + public static Class[] getClassInPackage(String packageName) { + List> classes = new ArrayList<>(); + String path = packageName.replace('.', '/'); + try { + Enumeration resources = Thread.currentThread().getContextClassLoader().getResources(path); + while (resources.hasMoreElements()) { + URL resource = resources.nextElement(); + if (resource.getProtocol().equals("file")) { + classes.addAll(findClasses(new File(resource.getFile()), packageName)); + } else if (resource.getProtocol().equals("jar")) { + String jarPath = resource.getPath().substring(5, resource.getPath().indexOf("!")); + classes.addAll(findClassesInJar(jarPath, path)); + } + } + } catch (IOException | ClassNotFoundException e) { + e.printStackTrace(); + } + return classes.toArray(new Class[0]); + } + + private static List> findClasses(File directory, String packageName) throws ClassNotFoundException { + List> classes = new ArrayList<>(); + if (!directory.exists()) { + return classes; + } + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + classes.addAll(findClasses(file, packageName + "." + file.getName())); + } else if (file.getName().endsWith(".class")) { + classes.add(Class.forName(packageName + '.' + file.getName().substring(0, file.getName().length() - 6))); + } + } + } + return classes; + } + + private static List> findClassesInJar(String jarPath, String packagePath) throws IOException, ClassNotFoundException { + List> classes = new ArrayList<>(); + JarFile jarFile = new JarFile(jarPath); + Enumeration entries = jarFile.entries(); + while (entries.hasMoreElements()) { + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); + if (entryName.startsWith(packagePath) && entryName.endsWith(".class") && !entry.isDirectory()) { + String className = entryName.replace('/', '.').substring(0, entryName.length() - 6); + classes.add(Class.forName(className)); + } + } + jarFile.close(); + return classes; + } +} diff --git a/gradle.properties b/gradle.properties index 04c0ca18..27458896 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,10 +16,10 @@ com.github.autostyle.version = 3.1 commons-io.version = 2.6 commons-configuration2.version = 2.7 commons-beanutils.version = 1.9.4 -darklaf.version = 2.6.1 -darklaf.extensions.version = 0.3.4 +darklaf.version = 3.0.2 +darklaf.extensions.version = 0.4.1 asm.version = 9.1 -cfr.version = -SNAPSHOT +cfr.version = 0.151 rsyntaxtextarea.version = 3.1.1 jgraphx.version = v4.0.0 logback-classic.version = 1.2.3 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 6623300b..c7d437bb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.4-bin.zip +networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gui/build.gradle.kts b/gui/build.gradle.kts index b213a94d..49906fa0 100644 --- a/gui/build.gradle.kts +++ b/gui/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation("com.github.weisj:darklaf-theme") { isChanging = true } implementation("com.github.weisj:darklaf-property-loader") { isChanging = true } implementation("com.github.weisj:darklaf-extensions-rsyntaxarea") + implementation("com.github.weisj:jsvg:1.6.0") implementation("com.fifesoft:rsyntaxtextarea") implementation("com.github.jgraph:jgraphx") @@ -17,9 +18,12 @@ dependencies { implementation("commons-io:commons-io") implementation("org.apache.commons:commons-configuration2") + implementation("software.coley:cafedude-core:2.1.1") } tasks.shadowJar { + val serviceEntries: Any? = null // No annotation needed here + archiveBaseName.set(project.name) duplicatesStrategy = DuplicatesStrategy.INCLUDE manifest { @@ -34,15 +38,15 @@ tasks.shadowJar { destinationPath = "META-INF/licenses/NOTICES.txt" include("META-INF/NOTICE", "META-INF/NOTICE.txt") } - relocate("META-INF", "META-INF/licenses") { - includes.addAll(listOf( - "META-INF/*LICENSE*", - "META-INF/*NOTICE*", - "META-INF/AL2.0", - "META-INF/LGPL2.1" - )) - exclude("META-INF/THREADTEAR_LICENSE") - } + //relocate("META-INF", "META-INF/licenses") { + // includes.addAll(listOf( + // "META-INF/*LICENSE*", + // "META-INF/*NOTICE*", + // "META-INF/AL2.0", + // "META-INF/LGPL2.1" + // )) + // exclude("META-INF/THREADTEAR_LICENSE") + //} } tasks.clean { @@ -73,5 +77,5 @@ val runGui by tasks.registering(JavaExec::class) { workingDir = File(project.rootDir, "dist") workingDir.mkdir() main = "me.nov.threadtear.Threadtear" - classpath("$rootDir/dist/threadtear-${project.version}.jar") + classpath = files("$rootDir/dist/threadtear-${project.version}.jar") } diff --git a/gui/gradle/wrapper/gradle-wrapper.properties b/gui/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..110609d3 --- /dev/null +++ b/gui/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Fri Sep 20 18:35:57 CST 2024 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gui/src/main/java/me/nov/threadtear/Threadtear.java b/gui/src/main/java/me/nov/threadtear/Threadtear.java index d6b7d768..c8740e36 100644 --- a/gui/src/main/java/me/nov/threadtear/Threadtear.java +++ b/gui/src/main/java/me/nov/threadtear/Threadtear.java @@ -32,8 +32,8 @@ public class Threadtear extends JFrame { public Threadtear() { logFrame = new LogFrame(); this.initBounds(); - this.setTitle("Threadtear " + CoreUtils.getVersion()); - this.setIconImage(SwingUtils.iconToFrameImage(SwingUtils.getIcon("threadtear.svg", true), this)); + this.setTitle("SkidSuite " + CoreUtils.getVersion()); + this.setIconImage(SwingUtils.iconToFrameImage(SwingUtils.getIcon("skidsuite.svg", true), this)); this.setDefaultCloseOperation(WindowConstants.DO_NOTHING_ON_CLOSE); this.addWindowListener(new ExitListener(this)); this.initializeFrame(); @@ -48,7 +48,7 @@ public static Threadtear getInstance() { public static void main(String[] args) throws Exception { LookAndFeel.init(); LookAndFeel.setLookAndFeel(); - ThreadtearCore.configureEnvironment(); +// ThreadtearCore.configureEnvironment(); ThreadtearCore.configureLoggers(); configureGUILoggers(); getInstance().setVisible(true); diff --git a/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java b/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java index 86116669..541773b5 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java +++ b/gui/src/main/java/me/nov/threadtear/swing/SwingUtils.java @@ -2,8 +2,12 @@ import com.github.weisj.darklaf.components.OverlayScrollPane; import com.github.weisj.darklaf.components.border.DarkBorders; -import com.github.weisj.darklaf.icons.IconLoader; + +import com.github.weisj.darklaf.properties.icons.IconLoader; import com.github.weisj.darklaf.ui.button.DarkButtonUI; +import com.github.weisj.jsvg.SVGDocument; +import com.github.weisj.jsvg.geometry.size.FloatSize; +import com.github.weisj.jsvg.parser.SVGLoader; import me.nov.threadtear.Threadtear; import me.nov.threadtear.swing.textarea.DecompilerTextArea; import org.fife.ui.rtextarea.RTextScrollPane; @@ -16,10 +20,13 @@ import javax.swing.tree.TreePath; import java.awt.*; import java.awt.event.ActionListener; +import java.awt.image.BufferedImage; +import java.net.URL; +import java.util.function.Consumer; public class SwingUtils { - private static final IconLoader ICON_LOADER = IconLoader.get(Threadtear.class); + public static SVGLoader loader = new SVGLoader(); public static TitledPanel withTitleAndBorder(String title, JComponent c) { Border border = DarkBorders.createLineBorder(1, 1, 1, 1); @@ -172,21 +179,33 @@ public static Image iconToFrameImage(Icon icon, Window window) { } public static Icon getIcon(String path) { - return getIcon(path, false); + return getIcon(path, 16, 16, false); // Default size and themed flag } public static Icon getIcon(String path, boolean themed) { - return ICON_LOADER.getIcon(path, themed); + return getIcon(path, 16, 16, themed); // Default size with themed flag } public static Icon getIcon(String path, int width, int height) { - return ICON_LOADER.getIcon(path, width, height, false); + return getIcon(path, width, height, false); // Default themed flag } public static Icon getIcon(String path, int width, int height, boolean themed) { - return ICON_LOADER.getIcon(path, width, height, themed); + URL svgUrl = Threadtear.class.getResource(path); + assert svgUrl != null; + SVGDocument svgDocument = loader.load(svgUrl); + BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + Graphics2D g = image.createGraphics(); + assert svgDocument != null; + g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE); + svgDocument.render(null, g); + g.dispose(); + + return new ImageIcon(image); } + public static JButton createSlimButton(Icon icon, ActionListener l) { JButton jButton = new JButton(icon); jButton.putClientProperty(DarkButtonUI.KEY_NO_BORDERLESS_OVERWRITE, true); diff --git a/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java b/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java index 528141ce..13d59904 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java +++ b/gui/src/main/java/me/nov/threadtear/swing/dialog/ExecutionSelection.java @@ -13,7 +13,6 @@ import com.github.weisj.darklaf.components.border.DarkBorders; import me.nov.threadtear.execution.Execution; import me.nov.threadtear.execution.ExecutionLink; -import me.nov.threadtear.execution.allatori.*; import me.nov.threadtear.execution.analysis.*; import me.nov.threadtear.execution.cleanup.*; import me.nov.threadtear.execution.cleanup.remove.RemoveAttributes; diff --git a/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java b/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java index d81697b5..52d81165 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/laf/LookAndFeel.java @@ -6,10 +6,11 @@ import com.github.weisj.darklaf.LafManager; import com.github.weisj.darklaf.theme.*; -import com.github.weisj.darklaf.theme.info.ColorToneRule; -import com.github.weisj.darklaf.theme.info.ContrastRule; import com.github.weisj.darklaf.theme.info.DefaultThemeProvider; -import com.github.weisj.darklaf.theme.info.PreferredThemeStyle; +import com.github.weisj.darklaf.theme.spec.ColorToneRule; +import com.github.weisj.darklaf.theme.spec.ContrastRule; +import com.github.weisj.darklaf.theme.spec.PreferredThemeStyle; + public class LookAndFeel { diff --git a/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java b/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java index 7c4d2211..cab91b44 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/panel/ConfigurationPanel.java @@ -109,20 +109,6 @@ private JPanel createBottomButtons() { JOptionPane.showMessageDialog(this, "You have to load a jar file first."); return; } - if (main.listPanel.executionList.getExecutions().size() > 0) { - JTextArea ta = new JTextArea(); - ta.setText("This project is entirely open-source and many hours have went into developing it.\n" + - "Please consider donating a small amount, if you are happy with your deobfuscation results.\n" + - "Every paid coffee will result in motivation to develop this tool, as it lives of it.\n" + - "You can also contact me on twitter (@graxcoding) for more options.\n" + - "Thank you.\n\n" + - "Bitcoin adress: 3LfBXghKn8KAj74tyetaUdJLic4NpGY3Vr"); - ta.setCaretPosition(0); - ta.setEditable(false); - JOptionPane.showMessageDialog(this, - ta, "Consider donating", - JOptionPane.INFORMATION_MESSAGE, SwingUtils.getIcon("bit_qr.png", 150, 150)); - } JFileChooser jfc = new JFileChooser(inputFile.getParentFile()); jfc.setAcceptAllFileFilterUsed(false); jfc.setSelectedFile(new File(FilenameUtils.removeExtension(inputFile.getAbsolutePath()) + ".jar")); diff --git a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java index 9050b1cf..bc1af1f7 100644 --- a/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java +++ b/gui/src/main/java/me/nov/threadtear/swing/tree/ClassTreePanel.java @@ -17,7 +17,11 @@ import me.nov.threadtear.swing.tree.renderer.ClassTreeCellRenderer; import me.nov.threadtear.util.format.Strings; import org.apache.commons.io.FilenameUtils; +import org.objectweb.asm.ClassReader; import org.objectweb.asm.tree.ClassNode; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; import javax.swing.*; import javax.swing.tree.*; @@ -26,6 +30,7 @@ import java.awt.event.MouseEvent; import java.io.File; import java.io.IOException; +import java.io.InvalidClassException; import java.nio.file.Files; import java.util.ArrayList; import java.util.Arrays; @@ -292,4 +297,8 @@ public void addToTree(ClassTreeNode current, Clazz c, String[] packages, int pck current.add(newChild); addToTree(newChild, c, packages, ++pckg); } + + + + } diff --git a/gui/src/main/resources/me/nov/threadtear/add.svg b/gui/src/main/resources/me/nov/threadtear/add.svg index fed1ab5b..397e9321 100644 --- a/gui/src/main/resources/me/nov/threadtear/add.svg +++ b/gui/src/main/resources/me/nov/threadtear/add.svg @@ -1,12 +1,4 @@ - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/add_disabled.svg b/gui/src/main/resources/me/nov/threadtear/add_disabled.svg index 9ea445ca..57eaaf56 100644 --- a/gui/src/main/resources/me/nov/threadtear/add_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/add_disabled.svg @@ -1,12 +1,4 @@ - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/analysis.svg b/gui/src/main/resources/me/nov/threadtear/analysis.svg index c3ee1f8f..d5a82d9c 100644 --- a/gui/src/main/resources/me/nov/threadtear/analysis.svg +++ b/gui/src/main/resources/me/nov/threadtear/analysis.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg b/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg index 61b0a16b..021b0c39 100644 --- a/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/analysis_disabled.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/bit_qr.png b/gui/src/main/resources/me/nov/threadtear/bit_qr.png deleted file mode 100644 index edad7ac0..00000000 Binary files a/gui/src/main/resources/me/nov/threadtear/bit_qr.png and /dev/null differ diff --git a/gui/src/main/resources/me/nov/threadtear/bytecode.svg b/gui/src/main/resources/me/nov/threadtear/bytecode.svg index dbd22baf..989c92a7 100644 --- a/gui/src/main/resources/me/nov/threadtear/bytecode.svg +++ b/gui/src/main/resources/me/nov/threadtear/bytecode.svg @@ -1,13 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg b/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg index 78ed8f26..6783e2e8 100644 --- a/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/bytecode_disabled.svg @@ -1,13 +1,19 @@ - - - - - - - - - - + + + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/class.svg b/gui/src/main/resources/me/nov/threadtear/class.svg index 7e0b5536..a8027940 100644 --- a/gui/src/main/resources/me/nov/threadtear/class.svg +++ b/gui/src/main/resources/me/nov/threadtear/class.svg @@ -1,9 +1,5 @@ - - - - - - \ No newline at end of file + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/decompile.svg b/gui/src/main/resources/me/nov/threadtear/decompile.svg index 90273fe3..139178b7 100644 --- a/gui/src/main/resources/me/nov/threadtear/decompile.svg +++ b/gui/src/main/resources/me/nov/threadtear/decompile.svg @@ -1,13 +1,4 @@ - - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg b/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg index 7da12ea3..cf4caeb2 100644 --- a/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/decompile_disabled.svg @@ -1,13 +1,4 @@ - - - - - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/enum.svg b/gui/src/main/resources/me/nov/threadtear/enum.svg index 2d989287..44d6e5cf 100644 --- a/gui/src/main/resources/me/nov/threadtear/enum.svg +++ b/gui/src/main/resources/me/nov/threadtear/enum.svg @@ -1,8 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/failure.svg b/gui/src/main/resources/me/nov/threadtear/failure.svg index b42d8126..6162ef02 100644 --- a/gui/src/main/resources/me/nov/threadtear/failure.svg +++ b/gui/src/main/resources/me/nov/threadtear/failure.svg @@ -1,13 +1,13 @@ - - - - - - - - - - - - + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/field.svg b/gui/src/main/resources/me/nov/threadtear/field.svg index 4c06576e..02a9b201 100644 --- a/gui/src/main/resources/me/nov/threadtear/field.svg +++ b/gui/src/main/resources/me/nov/threadtear/field.svg @@ -1,9 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/file.svg b/gui/src/main/resources/me/nov/threadtear/file.svg index 0133b676..3ebb0ee7 100644 --- a/gui/src/main/resources/me/nov/threadtear/file.svg +++ b/gui/src/main/resources/me/nov/threadtear/file.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - - + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/file_disabled.svg b/gui/src/main/resources/me/nov/threadtear/file_disabled.svg index 0fe1114f..cf07a4c1 100644 --- a/gui/src/main/resources/me/nov/threadtear/file_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/file_disabled.svg @@ -1,13 +1,18 @@ - - - - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/folder.svg b/gui/src/main/resources/me/nov/threadtear/folder.svg index b55af3df..56a65ef8 100644 --- a/gui/src/main/resources/me/nov/threadtear/folder.svg +++ b/gui/src/main/resources/me/nov/threadtear/folder.svg @@ -1,10 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/graph.svg b/gui/src/main/resources/me/nov/threadtear/graph.svg index d510edc2..c6f16e77 100644 --- a/gui/src/main/resources/me/nov/threadtear/graph.svg +++ b/gui/src/main/resources/me/nov/threadtear/graph.svg @@ -1,15 +1,8 @@ - - - - - - - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg b/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg index 3d4f1b03..ade54254 100644 --- a/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/graph_disabled.svg @@ -1,15 +1,8 @@ - - - - - - - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/ignore.svg b/gui/src/main/resources/me/nov/threadtear/ignore.svg index 3eaa68e2..8a12b67d 100644 --- a/gui/src/main/resources/me/nov/threadtear/ignore.svg +++ b/gui/src/main/resources/me/nov/threadtear/ignore.svg @@ -1,10 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg b/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg index 02a281a0..19a47825 100644 --- a/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/ignore_disabled.svg @@ -1,10 +1,16 @@ - - - - - - - - + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/innerClass.svg b/gui/src/main/resources/me/nov/threadtear/innerClass.svg index 583beb2f..d4cc1cb7 100644 --- a/gui/src/main/resources/me/nov/threadtear/innerClass.svg +++ b/gui/src/main/resources/me/nov/threadtear/innerClass.svg @@ -1,7 +1,4 @@ - - - - - - - \ No newline at end of file + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/interface.svg b/gui/src/main/resources/me/nov/threadtear/interface.svg index 94e68c31..53905c52 100644 --- a/gui/src/main/resources/me/nov/threadtear/interface.svg +++ b/gui/src/main/resources/me/nov/threadtear/interface.svg @@ -1,8 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/load_config.svg b/gui/src/main/resources/me/nov/threadtear/load_config.svg index c6bce312..2fd83e7e 100644 --- a/gui/src/main/resources/me/nov/threadtear/load_config.svg +++ b/gui/src/main/resources/me/nov/threadtear/load_config.svg @@ -1,13 +1,9 @@ - - - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/mainClass.svg b/gui/src/main/resources/me/nov/threadtear/mainClass.svg index b1aa1042..79b8d51d 100644 --- a/gui/src/main/resources/me/nov/threadtear/mainClass.svg +++ b/gui/src/main/resources/me/nov/threadtear/mainClass.svg @@ -1,9 +1,11 @@ - - - - - - + + + + + + + + + - \ No newline at end of file + diff --git a/gui/src/main/resources/me/nov/threadtear/method.svg b/gui/src/main/resources/me/nov/threadtear/method.svg index 0723ffc5..bca827ff 100644 --- a/gui/src/main/resources/me/nov/threadtear/method.svg +++ b/gui/src/main/resources/me/nov/threadtear/method.svg @@ -1,9 +1,5 @@ - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_down.svg b/gui/src/main/resources/me/nov/threadtear/move_down.svg index 46e7af04..70a43560 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_down.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_down.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg b/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg index d3441547..f8a84756 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_down_disabled.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_up.svg b/gui/src/main/resources/me/nov/threadtear/move_up.svg index e790d2aa..bb98e4ad 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_up.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_up.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg b/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg index df3ab3e3..c681a5d2 100644 --- a/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/move_up_disabled.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/package.svg b/gui/src/main/resources/me/nov/threadtear/package.svg index 224f87e7..4da77ad6 100644 --- a/gui/src/main/resources/me/nov/threadtear/package.svg +++ b/gui/src/main/resources/me/nov/threadtear/package.svg @@ -1,10 +1,5 @@ - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/refresh.svg b/gui/src/main/resources/me/nov/threadtear/refresh.svg index 67b4684f..24af79e3 100644 --- a/gui/src/main/resources/me/nov/threadtear/refresh.svg +++ b/gui/src/main/resources/me/nov/threadtear/refresh.svg @@ -1,10 +1,7 @@ - - - - - - - - + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg b/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg index 24258c92..35b97638 100644 --- a/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/refresh_disabled.svg @@ -1,10 +1,7 @@ - - - - - - - - + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/remove.svg b/gui/src/main/resources/me/nov/threadtear/remove.svg index 54c60745..89429c28 100644 --- a/gui/src/main/resources/me/nov/threadtear/remove.svg +++ b/gui/src/main/resources/me/nov/threadtear/remove.svg @@ -1,9 +1,4 @@ - - - - - - - - + + + diff --git a/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg b/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg index 327ae509..dd8e2949 100644 --- a/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/remove_disabled.svg @@ -1,9 +1,16 @@ - - - - - - - - + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/run.svg b/gui/src/main/resources/me/nov/threadtear/run.svg index a1aba0e4..bfaf56a2 100644 --- a/gui/src/main/resources/me/nov/threadtear/run.svg +++ b/gui/src/main/resources/me/nov/threadtear/run.svg @@ -1,4 +1,4 @@ - - - - \ No newline at end of file + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/save.svg b/gui/src/main/resources/me/nov/threadtear/save.svg index fafa508e..06841c5f 100644 --- a/gui/src/main/resources/me/nov/threadtear/save.svg +++ b/gui/src/main/resources/me/nov/threadtear/save.svg @@ -1,12 +1,7 @@ - - - - - - - - - - - + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/save_config.svg b/gui/src/main/resources/me/nov/threadtear/save_config.svg index 9aa8965b..a5c20a8a 100644 --- a/gui/src/main/resources/me/nov/threadtear/save_config.svg +++ b/gui/src/main/resources/me/nov/threadtear/save_config.svg @@ -1,9 +1,8 @@ - - - - - - - - + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg b/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg index a54150e3..0f031770 100644 --- a/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg +++ b/gui/src/main/resources/me/nov/threadtear/save_config_disabled.svg @@ -1,9 +1,17 @@ - - - - - - - - + + + + + + + + + + + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/skidsuite.svg b/gui/src/main/resources/me/nov/threadtear/skidsuite.svg new file mode 100644 index 00000000..155295f5 --- /dev/null +++ b/gui/src/main/resources/me/nov/threadtear/skidsuite.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/threadtear.svg b/gui/src/main/resources/me/nov/threadtear/threadtear.svg deleted file mode 100644 index 92c34fd6..00000000 --- a/gui/src/main/resources/me/nov/threadtear/threadtear.svg +++ /dev/null @@ -1,63 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/gui/src/main/resources/me/nov/threadtear/zoom_in.svg b/gui/src/main/resources/me/nov/threadtear/zoom_in.svg index 68f9cef5..39cb0f66 100644 --- a/gui/src/main/resources/me/nov/threadtear/zoom_in.svg +++ b/gui/src/main/resources/me/nov/threadtear/zoom_in.svg @@ -1,13 +1,6 @@ - - - - - - - - - - - - + + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/zoom_out.svg b/gui/src/main/resources/me/nov/threadtear/zoom_out.svg index 9afaff04..41c92b41 100644 --- a/gui/src/main/resources/me/nov/threadtear/zoom_out.svg +++ b/gui/src/main/resources/me/nov/threadtear/zoom_out.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg b/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg index f735a22e..e497a8f7 100644 --- a/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg +++ b/gui/src/main/resources/me/nov/threadtear/zoom_reset.svg @@ -1,14 +1,4 @@ - - - - - - - - - - - - - + + +