From fe861df0e3a6d62773ed6b5129f0e0bf8f4fede0 Mon Sep 17 00:00:00 2001 From: EpicPlayerA10 Date: Wed, 9 Oct 2024 22:40:07 +0200 Subject: [PATCH] getCommonSuperClass - move away from custom classloader and use InheritanceGraph this should resolve issues with libraries which were compiled against a newer java version than the obfuscator was running. Other changes: - Update cafedude, use fork of SSVM to be able to update cafedude - Use SLF4J bridge - Make Classpath immutable - More uses for cafedude --- .editorconfig | 2 +- deobfuscator-api/pom.xml | 36 +- .../deobfuscator/api/asm/ClassWrapper.java | 10 +- .../deobfuscator/api/classpath/Classpath.java | 157 +++--- .../api/classpath/ClasspathClassLoader.java | 30 -- .../api/classpath/ClasspathClassWriter.java | 18 - .../api/classpath/InheritanceClassWriter.java | 21 + .../api/classpath/JvmClasspath.java | 47 ++ .../deobfuscator/api/context/Context.java | 43 +- .../api/context/DeobfuscatorOptions.java | 31 +- .../api/execution/ClasspathDataSupplier.java | 4 +- .../deobfuscator/api/execution/SandBox.java | 2 +- .../deobfuscator/api/helper/ClassHelper.java | 77 ++- .../api/inheritance/InheritanceGraph.java | 410 ++++++++++++++++ .../api/inheritance/InheritanceVertex.java | 459 ++++++++++++++++++ .../deobfuscator/api/inheritance/Streams.java | 43 ++ .../uwu/narumi/deobfuscator/Deobfuscator.java | 48 +- .../src/test/java/Bootstrap.java | 5 +- .../base/SingleClassContextSource.java | 6 +- .../impl/hp888/HP888PackerTransformer.java | 5 +- 20 files changed, 1243 insertions(+), 211 deletions(-) delete mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassLoader.java delete mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassWriter.java create mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/InheritanceClassWriter.java create mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/JvmClasspath.java create mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceGraph.java create mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceVertex.java create mode 100644 deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/Streams.java diff --git a/.editorconfig b/.editorconfig index 46862800..57821c84 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,4 @@ -[*] +[*.java] charset = utf-8 end_of_line = lf indent_style = space diff --git a/deobfuscator-api/pom.xml b/deobfuscator-api/pom.xml index 11adcdd1..449476df 100644 --- a/deobfuscator-api/pom.xml +++ b/deobfuscator-api/pom.xml @@ -28,8 +28,8 @@ 2.23.1 2.0.13 - 1.10.2 - d559ea90bb + 83ea274ff8 + 4d3f2bc142 205d8eaa1f @@ -51,88 +51,82 @@ ${jlinker.version} + - com.github.xxDark.SSVM + com.github.EpicPlayerA10.SSVM mirrors ${ssvm.version} - - com.github.xxDark.SSVM + com.github.EpicPlayerA10.SSVM ssvm-core ${ssvm.version} - - com.github.xxDark.SSVM + com.github.EpicPlayerA10.SSVM ssvm-invoke ${ssvm.version} - - com.github.xxDark.SSVM + com.github.EpicPlayerA10.SSVM ssvm-io ${ssvm.version} + org.ow2.asm asm-commons ${asm.version} - org.ow2.asm asm-util ${asm.version} - org.ow2.asm asm-tree ${asm.version} - org.ow2.asm asm-analysis ${asm.version} - org.ow2.asm asm ${asm.version} + org.apache.logging.log4j log4j-api ${log4j.version} - org.apache.logging.log4j log4j-core ${log4j.version} - + org.slf4j slf4j-api - ${slf4j.version} + 2.0.16 - - org.slf4j - slf4j-simple - ${slf4j.version} + org.apache.logging.log4j + log4j-slf4j2-impl + ${log4j.version} com.github.Col-E CAFED00D - ${kafedjud.version} + ${cafedude.version} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/ClassWrapper.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/ClassWrapper.java index 08022ee0..83d3666e 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/ClassWrapper.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/asm/ClassWrapper.java @@ -10,9 +10,9 @@ import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; import org.objectweb.asm.tree.*; -import uwu.narumi.deobfuscator.api.context.Context; import uwu.narumi.deobfuscator.api.helper.ClassHelper; -import uwu.narumi.deobfuscator.api.classpath.ClasspathClassWriter; +import uwu.narumi.deobfuscator.api.classpath.InheritanceClassWriter; +import uwu.narumi.deobfuscator.api.inheritance.InheritanceGraph; public class ClassWrapper implements Cloneable { @@ -27,7 +27,7 @@ public class ClassWrapper implements Cloneable { private final ConstantPool constantPool; private final int classWriterFlags; - public ClassWrapper(String pathInJar, ClassReader classReader, int classReaderFlags, int classWriterFlags) throws Exception { + public ClassWrapper(String pathInJar, ClassReader classReader, int classReaderFlags, int classWriterFlags) { this.pathInJar = pathInJar; this.classNode = new ClassNode(); this.constantPool = new ConstantPool(classReader); @@ -129,9 +129,9 @@ public String canonicalName() { /** * Compiles class to bytes. */ - public byte[] compileToBytes(Context context) { + public byte[] compileToBytes(InheritanceGraph inheritanceGraph) { try { - ClassWriter classWriter = new ClasspathClassWriter(this.classWriterFlags, context.getClasspath()); + ClassWriter classWriter = new InheritanceClassWriter(this.classWriterFlags, inheritanceGraph); this.classNode.accept(classWriter); return classWriter.toByteArray(); diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/Classpath.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/Classpath.java index df1a8ee3..03433cc7 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/Classpath.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/Classpath.java @@ -2,93 +2,114 @@ import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collections; +import java.util.HashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; -import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; import uwu.narumi.deobfuscator.api.context.DeobfuscatorOptions; import uwu.narumi.deobfuscator.api.helper.ClassHelper; import uwu.narumi.deobfuscator.api.helper.FileHelper; /** - * All sources for deobfuscated jar + * Immutable classpath + * + * @param classesInfo Class nodes that hold only the class information, not code */ -public class Classpath { +public record Classpath( + Map rawClasses, + Map files, + Map classesInfo +) { + + public static Builder builder() { + return new Builder(); + } - private static final Logger LOGGER = LogManager.getLogger(Classpath.class); + public static class Builder { + private static final Logger LOGGER = LogManager.getLogger(); - private final Map files = new ConcurrentHashMap<>(); - private final Map classes = new ConcurrentHashMap<>(); + private final Map rawClasses = new HashMap<>(); + private final Map files = new HashMap<>(); - private final int classWriterFlags; + private final Map classesInfo = new HashMap<>(); - public Classpath(int classWriterFlags) { - this.classWriterFlags = classWriterFlags; - } + private Builder() { + } - /** - * Adds jar to classpath - * - * @param jarPath Jar path - */ - public void addJar(@NotNull Path jarPath) { - int prevSize = classes.size(); - - FileHelper.loadFilesFromZip( - jarPath, - (classPath, bytes) -> { - if (!ClassHelper.isClass(classPath, bytes)) { - files.putIfAbsent(classPath, bytes); - return; - } - - try { - String className = ClassHelper.loadClass( - classPath, - bytes, - ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG, - classWriterFlags - ).name(); - - classes.putIfAbsent(className, bytes); - } catch (Exception e) { - LOGGER.error("Could not load {} class from {} library", classPath, jarPath, e); - } - }); - - LOGGER.info("Loaded {} classes from {}", classes.size() - prevSize, jarPath.getFileName()); - } + /** + * Adds jar to classpath + * + * @param jarPath Jar path + */ + @Contract("_ -> this") + public Builder addJar(@NotNull Path jarPath) { + FileHelper.loadFilesFromZip(jarPath, (classPath, bytes) -> { + if (!ClassHelper.isClass(classPath, bytes)) { + files.putIfAbsent(classPath, bytes); + return; + } + + try { + ClassNode classNode = ClassHelper.loadUnknownClassInfo(bytes); + String className = classNode.name; + + rawClasses.putIfAbsent(className, bytes); + classesInfo.putIfAbsent(className, classNode); + } catch (Exception e) { + LOGGER.error("Could not load {} class from {} library", classPath, jarPath, e); + } + }); + + return this; + } + + /** + * Adds {@link DeobfuscatorOptions.ExternalClass} to classpath + * + * @param externalClass External class + */ + @Contract("_ -> this") + public Builder addExternalClass(DeobfuscatorOptions.ExternalClass externalClass) { + try { + byte[] classBytes = Files.readAllBytes(externalClass.path()); + + ClassNode classNode = ClassHelper.loadUnknownClassInfo(classBytes); - /** - * Adds {@link DeobfuscatorOptions.ExternalClass} to classpath - * - * @param externalClass External class - */ - public void addExternalClass(DeobfuscatorOptions.ExternalClass externalClass) { - try { - byte[] classBytes = Files.readAllBytes(externalClass.path()); - String className = ClassHelper.loadClass( - externalClass.pathInJar(), - classBytes, - ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG, - classWriterFlags - ).name(); - - // Add class to classpath - classes.putIfAbsent(className, classBytes); - } catch (Exception e) { - throw new RuntimeException(e); + String className = classNode.name; + + // Add class to classpath + rawClasses.putIfAbsent(className, classBytes); + classesInfo.putIfAbsent(className, classNode); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return this; } - } - public Map getFiles() { - return this.files; - } + /** + * Adds another classpath to this classpath + */ + @Contract("_ -> this") + public Builder addClasspath(Classpath classpath) { + this.rawClasses.putAll(classpath.rawClasses); + this.files.putAll(classpath.files); + this.classesInfo.putAll(classpath.classesInfo); - public Map getClasses() { - return this.classes; + return this; + } + + public Classpath build() { + return new Classpath( + Collections.unmodifiableMap(rawClasses), + Collections.unmodifiableMap(files), + Collections.unmodifiableMap(classesInfo) + ); + } } } diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassLoader.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassLoader.java deleted file mode 100644 index b356bf72..00000000 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassLoader.java +++ /dev/null @@ -1,30 +0,0 @@ -package uwu.narumi.deobfuscator.api.classpath; - -/** - * A {@link ClassLoader} that holds all classpath of the current deobfuscation context - */ -public class ClasspathClassLoader extends ClassLoader { - private final Classpath classpath; - - public ClasspathClassLoader(Classpath classpath) { - this.classpath = classpath; - } - - @Override - protected Class findClass(String name) throws ClassNotFoundException { - String internalName = name.replace('.', '/'); - - // Find class in classPath - byte[] classBytes = null; - if (this.classpath.getClasses().containsKey(internalName)) { - // Find in normal classes - classBytes = this.classpath.getClasses().get(internalName); - } - - if (classBytes != null) { - // If found then return it - return defineClass(name, classBytes, 0, classBytes.length); - } - return super.findClass(name); - } -} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassWriter.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassWriter.java deleted file mode 100644 index 1b6e7cdb..00000000 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/ClasspathClassWriter.java +++ /dev/null @@ -1,18 +0,0 @@ -package uwu.narumi.deobfuscator.api.classpath; - -import org.objectweb.asm.ClassWriter; - -public class ClasspathClassWriter extends ClassWriter { - - private final ClasspathClassLoader loader; - - public ClasspathClassWriter(int flags, Classpath classpath) { - super(flags); - this.loader = new ClasspathClassLoader(classpath); - } - - @Override - protected ClassLoader getClassLoader() { - return loader; - } -} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/InheritanceClassWriter.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/InheritanceClassWriter.java new file mode 100644 index 00000000..e01bc4e5 --- /dev/null +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/InheritanceClassWriter.java @@ -0,0 +1,21 @@ +package uwu.narumi.deobfuscator.api.classpath; + +import org.objectweb.asm.ClassWriter; +import uwu.narumi.deobfuscator.api.inheritance.InheritanceGraph; + +/** + * A {@link ClassWriter} that uses a {@link InheritanceGraph} to determine the common superclass of two classes. + */ +public class InheritanceClassWriter extends ClassWriter { + private final InheritanceGraph inheritanceGraph; + + public InheritanceClassWriter(int flags, InheritanceGraph inheritanceGraph) { + super(flags); + this.inheritanceGraph = inheritanceGraph; + } + + @Override + protected String getCommonSuperClass(String first, String second) { + return this.inheritanceGraph.getCommon(first, second); + } +} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/JvmClasspath.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/JvmClasspath.java new file mode 100644 index 00000000..9ccba836 --- /dev/null +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/classpath/JvmClasspath.java @@ -0,0 +1,47 @@ +package uwu.narumi.deobfuscator.api.classpath; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.tree.ClassNode; +import uwu.narumi.deobfuscator.api.helper.ClassHelper; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * Classpath that fetches default JVM classes + */ +public class JvmClasspath { + private static final Logger LOGGER = LogManager.getLogger(); + + private static final Map cache = new ConcurrentHashMap<>(); + + @Nullable + public static ClassNode getClassNode(String name) { + if (cache.containsKey(name)) { + return cache.get(name); + } + + // Try to find it in classloader + byte[] value = null; + try (InputStream in = ClassLoader.getSystemResourceAsStream(name + ".class")) { + if (in != null) { + value = in.readAllBytes(); + } + } catch (IOException ex) { + LOGGER.error("Failed to fetch runtime bytecode of class: {}", name, ex); + } + + if (value == null) return null; + + ClassNode classNode = ClassHelper.loadClassInfo(value); + // Cache it! + cache.put(name, classNode); + + return classNode; + } +} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/Context.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/Context.java index c6db2d43..559e3261 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/Context.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/Context.java @@ -19,13 +19,29 @@ public class Context { private final Map files = new ConcurrentHashMap<>(); private final DeobfuscatorOptions options; - private final Classpath classpath; + + private final Classpath primaryClasspath; + private final Classpath libClasspath; + private final Classpath combinedClasspath; private SandBox globalSandBox = null; - public Context(DeobfuscatorOptions options, Classpath classpath) { + /** + * Creates a new {@link Context} instance from its options + * + * @param options Deobfuscator options + * @param primaryClasspath Classpath which has only primary jar in it + * @param libClasspath Classpath filled with libs + */ + public Context(DeobfuscatorOptions options, Classpath primaryClasspath, Classpath libClasspath) { this.options = options; - this.classpath = classpath; + + this.primaryClasspath = primaryClasspath; + this.libClasspath = libClasspath; + this.combinedClasspath = Classpath.builder() + .addClasspath(primaryClasspath) + .addClasspath(libClasspath) + .build(); } /** @@ -43,8 +59,25 @@ public DeobfuscatorOptions getOptions() { return options; } - public Classpath getClasspath() { - return classpath; + /** + * Classpath for primary jar + */ + public Classpath getPrimaryClasspath() { + return primaryClasspath; + } + + /** + * Classpath filled with libs + */ + public Classpath getLibClasspath() { + return libClasspath; + } + + /** + * {@link #getPrimaryClasspath()} and {@link #getLibClasspath()} combined + */ + public Classpath getCombinedClasspath() { + return this.combinedClasspath; } public Collection classes() { diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java index 23f82891..f60ea8a3 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/context/DeobfuscatorOptions.java @@ -1,8 +1,8 @@ package uwu.narumi.deobfuscator.api.context; +import org.intellij.lang.annotations.MagicConstant; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.Nullable; -import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import uwu.narumi.deobfuscator.api.transformer.Transformer; @@ -31,8 +31,7 @@ public record DeobfuscatorOptions( List> transformers, - int classReaderFlags, - int classWriterFlags, + @MagicConstant(flagsFromClass = ClassWriter.class) int classWriterFlags, boolean printStacktraces, boolean continueOnError, @@ -69,7 +68,7 @@ public static class Builder { private final List> transformers = new ArrayList<>(); // Other config - private int classReaderFlags = ClassReader.SKIP_FRAMES; + @MagicConstant(flagsFromClass = ClassWriter.class) private int classWriterFlags = ClassWriter.COMPUTE_FRAMES; private boolean printStacktraces = true; @@ -116,6 +115,11 @@ public DeobfuscatorOptions.Builder outputDir(@Nullable Path outputDir) { return this; } + /** + * Add libraries to the classpath. You can pass here files or directories. + * + * @param paths Paths to libraries + */ @Contract("_ -> this") public DeobfuscatorOptions.Builder libraries(Path... paths) { for (Path path : paths) { @@ -178,20 +182,14 @@ public final DeobfuscatorOptions.Builder transformers(Supplier... t } /** - * Flags for {@link ClassReader} - */ - @Contract("_ -> this") - public DeobfuscatorOptions.Builder classReaderFlags(int classReaderFlags) { - this.classReaderFlags = classReaderFlags; - return this; - } - - /** - * Flags for {@link ClassWriter}. When you set it to {@code 0} you will disable checking the validity - * of the bytecode. Although this is not recommended. + * Flags for {@link ClassWriter}. + *
    + *
  • 0 - Deobfuscated jar can't be run
  • + *
  • {@link ClassWriter#COMPUTE_FRAMES} - Makes a runnable deobfuscated jar
  • + *
*/ @Contract("_ -> this") - public DeobfuscatorOptions.Builder classWriterFlags(int classWriterFlags) { + public DeobfuscatorOptions.Builder classWriterFlags(@MagicConstant(flagsFromClass = ClassWriter.class) int classWriterFlags) { this.classWriterFlags = classWriterFlags; return this; } @@ -250,7 +248,6 @@ public DeobfuscatorOptions build() { // Transformers transformers, // Flags - classReaderFlags, classWriterFlags, // Other config printStacktraces, diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/ClasspathDataSupplier.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/ClasspathDataSupplier.java index 204a191c..0c1d6736 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/ClasspathDataSupplier.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/ClasspathDataSupplier.java @@ -13,11 +13,11 @@ public ClasspathDataSupplier(Classpath classpath) { @Override public byte[] getClass(String className) { - return classpath.getClasses().get(className.replace('.', '/')); + return classpath.rawClasses().get(className.replace('.', '/')); } @Override public byte[] getResource(String resourcePath) { - return classpath.getFiles().get(resourcePath); + return classpath.files().get(resourcePath); } } diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java index f00f809b..976670e4 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/execution/SandBox.java @@ -41,7 +41,7 @@ public class SandBox { public SandBox(Context context) { // Install all classes from deobfuscator context - this(new ClasspathDataSupplier(context.getClasspath())); + this(new ClasspathDataSupplier(context.getCombinedClasspath())); } public SandBox(SupplyingClassLoaderInstaller.DataSupplier dataSupplier) { diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/ClassHelper.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/ClassHelper.java index fafd2efa..90caff17 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/ClassHelper.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/ClassHelper.java @@ -1,10 +1,12 @@ package uwu.narumi.deobfuscator.api.helper; import java.util.ArrayList; -import me.coley.cafedude.InvalidClassException; -import me.coley.cafedude.classfile.ClassFile; -import me.coley.cafedude.io.ClassFileReader; -import me.coley.cafedude.io.ClassFileWriter; + +import software.coley.cafedude.InvalidClassException; +import software.coley.cafedude.classfile.ClassFile; +import software.coley.cafedude.io.ClassFileReader; +import software.coley.cafedude.io.ClassFileWriter; +import org.intellij.lang.annotations.MagicConstant; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.tree.ClassNode; @@ -14,7 +16,8 @@ public final class ClassHelper { - private ClassHelper() {} + private ClassHelper() { + } public static boolean isClass(String fileName, byte[] bytes) { return isClass(bytes) && (fileName.endsWith(".class") || fileName.endsWith(".class/")); @@ -22,32 +25,74 @@ public static boolean isClass(String fileName, byte[] bytes) { public static boolean isClass(byte[] bytes) { return bytes.length >= 4 - && String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]) - .equals("CAFEBABE"); + && String.format("%02X%02X%02X%02X", bytes[0], bytes[1], bytes[2], bytes[3]).equals("CAFEBABE"); } - public static ClassWrapper loadClass(String pathInJar, byte[] bytes, int classReaderFlags, int classWriterFlags) throws Exception { - return loadClass(pathInJar, bytes, classReaderFlags, classWriterFlags, true); + /** + * Load class from bytes + * + * @param pathInJar Relative path of a class in a jar + * @param bytes Class bytes + * @param classReaderFlags {@link ClassReader} flags + * @param classWriterFlags {@link ClassWriter} flags + */ + public static ClassWrapper loadClass( + String pathInJar, + byte[] bytes, + @MagicConstant(flagsFromClass = ClassReader.class) int classReaderFlags, + @MagicConstant(flagsFromClass = ClassWriter.class) int classWriterFlags + ) { + return new ClassWrapper(pathInJar, new ClassReader(bytes), classReaderFlags, classWriterFlags); } /** - * Load class from bytes + * Loads class from unknown sources. Applies fixes to bytecode using CAFED00D. * - * @param pathInJar Relative path of a class in a jar - * @param bytes Class bytes + * @param pathInJar Relative path of a class in a jar + * @param bytes Class bytes * @param classReaderFlags {@link ClassReader} flags * @param classWriterFlags {@link ClassWriter} flags - * @param fix Fix class using CAFED00D */ - public static ClassWrapper loadClass(String pathInJar, byte[] bytes, int classReaderFlags, int classWriterFlags, boolean fix) throws Exception { + public static ClassWrapper loadUnknownClass( + String pathInJar, + byte[] bytes, + @MagicConstant(flagsFromClass = ClassReader.class) int classReaderFlags, + @MagicConstant(flagsFromClass = ClassWriter.class) int classWriterFlags + ) throws InvalidClassException { // Fix class - bytes = fix ? fixClass(bytes) : bytes; + bytes = fixClass(bytes); - return new ClassWrapper(pathInJar, new ClassReader(bytes), classReaderFlags, classWriterFlags); + return loadClass(pathInJar, bytes, classReaderFlags, classWriterFlags); + } + + /** + * Loads only class info (like class name, superclass, interfaces, etc.) without any code. + * + * @param bytes Class bytes + * @return {@link ClassNode} with class info only + */ + public static ClassNode loadClassInfo(byte[] bytes) { + ClassNode classNode = new ClassNode(); + new ClassReader(bytes).accept(classNode, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES | ClassReader.SKIP_DEBUG); + return classNode; + } + + /** + * Loads class info from unknown sources. Applies fixes to bytecode using CAFED00D. + * + * @param bytes Class bytes + * @return {@link ClassNode} with class info only + */ + public static ClassNode loadUnknownClassInfo(byte[] bytes) throws InvalidClassException { + // Fix class + bytes = fixClass(bytes); + + return loadClassInfo(bytes); } public static byte[] fixClass(byte[] bytes) throws InvalidClassException { ClassFileReader classFileReader = new ClassFileReader(); + classFileReader.setDropDupeAnnotations(false); ClassFile classFile = classFileReader.read(bytes); bytes = new ClassFileWriter().write(classFile); diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceGraph.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceGraph.java new file mode 100644 index 00000000..6a1eacde --- /dev/null +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceGraph.java @@ -0,0 +1,410 @@ +package uwu.narumi.deobfuscator.api.inheritance; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import uwu.narumi.deobfuscator.api.classpath.Classpath; +import uwu.narumi.deobfuscator.api.classpath.JvmClasspath; + +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Class inheritance graph utility. + * + * @author Matt Coley + */ +// Copied from https://github.com/Col-E/Recaf/blob/ac6e07cbaf168a1f2093e71a39215bda8a00402d/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceGraph.java +public class InheritanceGraph { + private static final Logger LOGGER = LogManager.getLogger(); + + /** Vertex used for classes that are not found in the workspace. */ + private static final InheritanceVertex STUB = new InheritanceStubVertex(); + private static final String OBJECT = "java/lang/Object"; + private final Map> parentToChild = new ConcurrentHashMap<>(); + private final Map vertices = new ConcurrentHashMap<>(); + private final Set stubs = ConcurrentHashMap.newKeySet(); + private final Function vertexProvider = createVertexProvider(); + private final Classpath classpath; + + /** + * Create an inheritance graph. + */ + public InheritanceGraph(@NotNull Classpath classpath) { + this.classpath = classpath; + + // Populate downwards (parent --> child) lookup + refreshChildLookup(); + } + + /** + * Refresh parent-to-child lookup. + */ + private void refreshChildLookup() { + // Clear + parentToChild.clear(); + + // Repopulate + classpath.classesInfo().values().forEach(this::populateParentToChildLookup); + } + + /** + * Populate a references from the given child class to the parent class. + * + * @param name + * Child class name. + * @param parentName + * Parent class name. + */ + private void populateParentToChildLookup(@NotNull String name, @NotNull String parentName) { + parentToChild.computeIfAbsent(parentName, k -> ConcurrentHashMap.newKeySet()).add(name); + } + + /** + * Populate all references from the given child class to its parents. + * + * @param info + * Child class. + */ + private void populateParentToChildLookup(@NotNull ClassNode info) { + populateParentToChildLookup(info, Collections.newSetFromMap(new IdentityHashMap<>())); + } + + /** + * Populate all references from the given child class to its parents. + * + * @param info + * Child class. + * @param visited + * Classes already visited in population. + */ + private void populateParentToChildLookup(@NotNull ClassNode info, @NotNull Set visited) { + // Skip if already visited + if (!visited.add(info)) + return; + + // Skip module classes + if ((info.access & Opcodes.ACC_MODULE) != 0) + return; + + // Add direct parent + String name = info.name; + String superName = info.superName; + if (superName != null) + populateParentToChildLookup(name, superName); + + // Visit parent + InheritanceVertex superVertex = vertexProvider.apply(superName); + if (superVertex != null && !superVertex.isJavaLangObject() && !superVertex.isLoop()) + populateParentToChildLookup(superVertex.getValue(), visited); + + // Add direct interfaces + for (String itf : info.interfaces) { + populateParentToChildLookup(name, itf); + + // Visit interfaces + InheritanceVertex interfaceVertex = vertexProvider.apply(itf); + if (interfaceVertex != null) + populateParentToChildLookup(interfaceVertex.getValue(), visited); + } + } + + /** + * Remove all references from the given child class to its parents. + * + * @param info + * Child class. + */ + private void removeParentToChildLookup(@NotNull ClassNode info) { + String superName = info.superName; + if (superName != null) + removeParentToChildLookup(info.name, superName); + for (String itf : info.interfaces) + removeParentToChildLookup(info.name, itf); + } + + /** + * Remove a references from the given child class to the parent class. + * + * @param name + * Child class name. + * @param parentName + * Parent class name. + */ + private void removeParentToChildLookup(@NotNull String name, @NotNull String parentName) { + Set children = parentToChild.get(parentName); + if (children != null) + children.remove(name); + InheritanceVertex parentVertex = getVertex(parentName); + InheritanceVertex childVertex = getVertex(name); + if (parentVertex != null) parentVertex.clearCachedVertices(); + if (childVertex != null) childVertex.clearCachedVertices(); + } + + /** + * Removes the given class from the graph. + * + * @param cls + * Class that was removed. + */ + private void removeClass(@NotNull ClassNode cls) { + removeParentToChildLookup(cls); + + String name = cls.name; + vertices.remove(name); + } + + + /** + * @param parent + * Parent to find children of. + * + * @return Direct extensions/implementations of the given parent. + */ + @NotNull + private Set getDirectChildren(@NotNull String parent) { + return parentToChild.getOrDefault(parent, Collections.emptySet()); + } + + /** + * @param name + * Class name. + * + * @return Vertex in graph of class. {@code null} if no such class was found in the inputs. + */ + @Nullable + public InheritanceVertex getVertex(@NotNull String name) { + InheritanceVertex vertex = vertices.get(name); + if (vertex == null && !stubs.contains(name)) { + // Vertex does not exist and was not marked as a stub. + // We want to look up the vertex for the given class and figure out if its valid or needs to be stubbed. + InheritanceVertex provided = vertexProvider.apply(name); + if (provided == STUB || provided == null) { + // Provider yielded either a stub OR no result. Discard it. + stubs.add(name); + } else { + // Provider yielded a valid vertex. Update the return value and record it in the map. + vertices.put(name, provided); + vertex = provided; + } + } + return vertex; + } + + /** + * @param name + * Class name. + * @param includeObject + * {@code true} to include {@link Object} as a vertex. + * + * @return Complete inheritance family of the class. + */ + @NotNull + public Set getVertexFamily(@NotNull String name, boolean includeObject) { + InheritanceVertex vertex = getVertex(name); + if (vertex == null) + return Collections.emptySet(); + if (vertex.isModule()) + return Collections.singleton(vertex); + return vertex.getFamily(includeObject); + } + + /** + * @param first + * First class name. + * @param second + * Second class name. + * + * @return Common parent of the classes. + */ + @NotNull + public String getCommon(@NotNull String first, @NotNull String second) { + // Full upwards hierarchy for the first + InheritanceVertex vertex = getVertex(first); + if (vertex == null) { + printCantFindClass(first); + return OBJECT; + } + if (OBJECT.equals(first) || OBJECT.equals(second)) { + return OBJECT; + } + + Set firstParents = vertex.allParents() + .map(InheritanceVertex::getName) + .collect(Collectors.toCollection(LinkedHashSet::new)); + firstParents.add(first); + + // Ensure 'Object' is last + firstParents.remove(OBJECT); + firstParents.add(OBJECT); + + // Base case + if (firstParents.contains(second)) + return second; + + // Iterate over second's parents via breadth-first-search + Queue queue = new LinkedList<>(); + queue.add(second); + do { + // Item to fetch parents of + String next = queue.poll(); + if (next == null || next.equals(OBJECT)) + break; + + InheritanceVertex nextVertex = getVertex(next); + if (nextVertex == null) { + printCantFindClass(next); + break; + } + + for (String parent : nextVertex.getParents().stream() + .map(InheritanceVertex::getName).toList()) { + if (!parent.equals(OBJECT)) { + // Parent in the set of visited classes? Then its valid. + if (firstParents.contains(parent)) + return parent; + // Queue up the parent + queue.add(parent); + } + } + } while (!queue.isEmpty()); + + // Fallback option + return OBJECT; + } + + private void printCantFindClass(String className) { + LOGGER.warn("Can't find class '{}'. Computed frames might be wrong. " + + "If you want a runnable deobfuscated jar then add a missing lib using 'DeobfuscatorOptions#libraries'.", className); + } + + @NotNull + private Function createVertexProvider() { + return name -> { + // Edge case handling for 'java/lang/Object' doing a parent lookup. + // There is no parent, do not use STUB. + if (name == null) + return null; + + // Edge case handling for arrays. There is no object typing of arrays. + if (name.isEmpty() || name.charAt(0) == '[') + return null; + + // Find class in workspace, if not found yield stub. + ClassNode result = this.getClassNode(name); + if (result == null) { + return STUB; + } + + // Map class to vertex. + //ResourcePathNode resourcePath = result.getPathOfType(WorkspaceResource.class); + //boolean isPrimary = resourcePath != null && resourcePath.isPrimary(); + //ClassInfo info = result.getValue(); + return new InheritanceVertex(result, this::getVertex, this::getDirectChildren); + }; + } + + @Nullable + private ClassNode getClassNode(String name) { + ClassNode result = classpath.classesInfo().get(name); + if (result != null) return result; + + // Try to find it in classloader + return JvmClasspath.getClassNode(name); + } + + private static class InheritanceStubVertex extends InheritanceVertex { + private InheritanceStubVertex() { + super(new ClassNode(), in -> null, in -> null); + } + + @Override + public boolean hasField(@NotNull String name, @NotNull String desc) { + return false; + } + + @Override + public boolean hasMethod(@NotNull String name, @NotNull String desc) { + return false; + } + + @Override + public boolean isJavaLangObject() { + return false; + } + + @Override + public boolean isParentOf(@NotNull InheritanceVertex vertex) { + return false; + } + + @Override + public boolean isChildOf(@NotNull InheritanceVertex vertex) { + return false; + } + + @Override + public boolean isIndirectFamilyMember(@NotNull InheritanceVertex vertex) { + return false; + } + + @Override + public boolean isIndirectFamilyMember(@NotNull Set family, @NotNull InheritanceVertex vertex) { + return false; + } + + @NotNull + @Override + public Set getFamily(boolean includeObject) { + return Collections.emptySet(); + } + + @NotNull + @Override + public Set getAllParents() { + return Collections.emptySet(); + } + + @NotNull + @Override + public Stream allParents() { + return Stream.empty(); + } + + @NotNull + @Override + public Set getParents() { + return Collections.emptySet(); + } + + @NotNull + @Override + public Set getAllChildren() { + return Collections.emptySet(); + } + + @NotNull + @Override + public Set getChildren() { + return Collections.emptySet(); + } + + @NotNull + @Override + public Set getAllDirectVertices() { + return Collections.emptySet(); + } + + @NotNull + @Override + public String getName() { + return "$$STUB$$"; + } + } +} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceVertex.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceVertex.java new file mode 100644 index 00000000..139e8b6d --- /dev/null +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/InheritanceVertex.java @@ -0,0 +1,459 @@ +package uwu.narumi.deobfuscator.api.inheritance; + +import org.jetbrains.annotations.NotNull; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.MethodNode; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Graph element for a class inheritance hierarchy. + * + * @author Matt Coley + */ +// Copied from https://github.com/Col-E/Recaf/blob/ac6e07cbaf168a1f2093e71a39215bda8a00402d/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceVertex.java +public class InheritanceVertex { + private final Function lookup; + private final Function> childrenLookup; + //private final boolean isPrimary; + private volatile Set parents; + private volatile Set children; + private ClassNode value; + + /** + * @param value + * The wrapped value. + * @param lookup + * Class vertex lookup. + * @param childrenLookup + * Class child lookup. + */ + public InheritanceVertex(@NotNull ClassNode value, + @NotNull Function lookup, + @NotNull Function> childrenLookup) { + this.value = value; + this.lookup = lookup; + this.childrenLookup = childrenLookup; + //this.isPrimary = isPrimary; + } + + /** + * @param name + * Field name. + * @param desc + * Field descriptor. + * + * @return If the field exists in the current vertex. + */ + public boolean hasField(@NotNull String name, @NotNull String desc) { + for (FieldNode fn : value.fields) + if (fn.name.equals(name) && fn.desc.equals(desc)) + return true; + return false; + } + + /** + * @param name + * Field name. + * @param desc + * Field descriptor. + * + * @return If the field exists in the current vertex or in any parent vertex. + */ + public boolean hasFieldInSelfOrParents(@NotNull String name, @NotNull String desc) { + if (hasField(name, desc)) + return true; + return allParents() + .filter(v -> v != this) + .anyMatch(parent -> parent.hasFieldInSelfOrParents(name, desc)); + } + + /** + * @param name + * Field name. + * @param desc + * Field descriptor. + * + * @return If the field exists in the current vertex or in any child vertex. + */ + public boolean hasFieldInSelfOrChildren(@NotNull String name, @NotNull String desc) { + if (hasField(name, desc)) + return true; + return allChildren() + .filter(v -> v != this) + .anyMatch(parent -> parent.hasFieldInSelfOrChildren(name, desc)); + } + + /** + * @param name + * Method name. + * @param desc + * Method descriptor. + * + * @return If the method exists in the current vertex. + */ + public boolean hasMethod(@NotNull String name, @NotNull String desc) { + for (MethodNode mn : value.methods) + if (mn.name.equals(name) && mn.desc.equals(desc)) + return true; + return false; + } + + /** + * @param name + * Method name. + * @param desc + * Method descriptor. + * + * @return If the method exists in the current vertex or in any parent vertex. + */ + public boolean hasMethodInSelfOrParents(@NotNull String name, @NotNull String desc) { + if (hasMethod(name, desc)) + return true; + return allParents() + .filter(v -> v != this) + .anyMatch(parent -> parent.hasMethodInSelfOrParents(name, desc)); + } + + /** + * @param name + * Method name. + * @param desc + * Method descriptor. + * + * @return If the method exists in the current vertex or in any child vertex. + */ + public boolean hasMethodInSelfOrChildren(@NotNull String name, @NotNull String desc) { + if (hasMethod(name, desc)) + return true; + return allChildren() + .filter(v -> v != this) + .anyMatch(parent -> parent.hasMethodInSelfOrChildren(name, desc)); + } + + /** + * @return {@code true} if the class represented by this vertex is a library class. + * This means a class that does not belong to the primary {@link WorkspaceResource} + * of a {@link Workspace}. + */ + /*public boolean isLibraryVertex() { + return !isPrimary; + }*/ + + /** + * @return {@code true} when the current vertex represents {@link Object}. + */ + public boolean isJavaLangObject() { + return getName().equals("java/lang/Object"); + } + + /** + * @return {@code true} when a parent of this vertex, is this vertex. + */ + public boolean isLoop() { + String name = getName(); + return allParents() + .anyMatch(v -> name.equals(v.getName())); + } + + /** + * @return {@code true} when the current vertex represents a {@code module-info}. + */ + public boolean isModule() { + return (getValue().access & Opcodes.ACC_MODULE) != 0 && getValue().superName == null; + } + + /** + * @param name + * Method name. + * @param desc + * Method descriptor. + * + * @return {@code true} if method is an extension of an outside class's methods and thus should not be renamed. + * {@code false} if the method is safe to rename. + */ + /*public boolean isLibraryMethod(@NotNull String name, @NotNull String desc) { + // Check against this definition + if (!isPrimary && hasMethod(name, desc)) + return true; + + // Check parents. + // If we extend a class with a library definition then it should be considered a library method. + for (InheritanceVertex parent : getParents()) + if (parent.isLibraryMethod(name, desc)) + return true; + + // No library definition found, so its safe to rename. + return false; + }*/ + + /** + * @param vertex + * Supposed child vertex. + * + * @return {@code true} if the vertex is of a child type to this vertex's {@link #getName() type}. + */ + public boolean isParentOf(@NotNull InheritanceVertex vertex) { + return vertex.getAllParents().contains(this); + } + + /** + * @param vertex + * Supposed parent vertex. + * + * @return {@code true} if the vertex is of a parent type to this vertex's {@link #getName() type}. + */ + public boolean isChildOf(@NotNull InheritanceVertex vertex) { + return getAllParents().contains(vertex); + } + + /** + * @param vertex + * Supposed vertex that belongs in the family. + * + * @return {@code true} if the vertex is a family member, but is not a child or parent of the current vertex. + */ + public boolean isIndirectFamilyMember(@NotNull InheritanceVertex vertex) { + return isIndirectFamilyMember(getFamily(true), vertex); + } + + /** + * @param family + * Family to check in. + * @param vertex + * Supposed vertex that belongs in the family. + * + * @return {@code true} if the vertex is a family member, but is not a child or parent of the current vertex. + */ + public boolean isIndirectFamilyMember(@NotNull Set family, @NotNull InheritanceVertex vertex) { + return this != vertex && + family.contains(vertex) && + !isChildOf(vertex) && + !isParentOf(vertex); + } + + /** + * @param name + * Name of parent type. + * + * @return {@code true} when this vertex has the given parent. + */ + public boolean hasParent(@NotNull String name) { + for (InheritanceVertex parent : getAllParents()) + if (name.equals(parent.getName())) + return true; + + return false; + } + + /** + * @param name + * Name of child type. + * + * @return {@code true} when this vertex has the given child. + */ + public boolean hasChild(@NotNull String name) { + for (InheritanceVertex child : getAllChildren()) + if (name.equals(child.getName())) + return true; + + return false; + } + + /** + * @param includeObject + * {@code true} to include {@link Object} as a vertex. + * + * @return The entire class hierarchy. + */ + @NotNull + public Set getFamily(boolean includeObject) { + Set vertices = new LinkedHashSet<>(); + visitFamily(vertices); + if (!includeObject) + vertices.removeIf(InheritanceVertex::isJavaLangObject); + return vertices; + } + + private void visitFamily(@NotNull Set vertices) { + if (isModule()) + return; + if (vertices.add(this) && !isJavaLangObject()) + for (InheritanceVertex vertex : getAllDirectVertices()) + vertex.visitFamily(vertices); + } + + /** + * @return All classes this extends or implements. + */ + @NotNull + public Set getAllParents() { + return allParents().collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * @return All classes this extends or implements. + */ + @NotNull + public Stream allParents() { + // Skip 1 to skip ourselves (which we use as the seed vertex) + return Streams.recurseWithoutCycles(this, InheritanceVertex::getParents) + .skip(1); + } + + /** + * @return Classes this directly extends or implements. + */ + @NotNull + public Set getParents() { + Set parents = this.parents; + if (parents == null) { + synchronized (this) { + if (isModule()) { + parents = Collections.emptySet(); + this.parents = parents; + return parents; + } + parents = this.parents; + if (parents == null) { + String name = getName(); + parents = new LinkedHashSet<>(); + String superName = value.superName; + if (superName != null && !name.equals(superName)) { + InheritanceVertex parentVertex = lookup.apply(superName); + if (parentVertex != null) + parents.add(parentVertex); + } + for (String itf : value.interfaces) { + InheritanceVertex itfVertex = lookup.apply(itf); + if (itfVertex != null && !name.equals(itf)) + parents.add(itfVertex); + } + this.parents = parents; + } + } + } + return parents; + } + + /** + * @return All classes extending or implementing this type. + */ + @NotNull + public Set getAllChildren() { + return allChildren().collect(Collectors.toCollection(LinkedHashSet::new)); + } + + /** + * @return Stream of all classes extending or implementing this type. + */ + @NotNull + public Stream allChildren() { + // Skip 1 to skip ourselves (which we use as the seed vertex) + return Streams.recurseWithoutCycles(this, InheritanceVertex::getChildren) + .skip(1); + } + + /** + * @return Classes that extend or implement this class. + */ + @NotNull + public Set getChildren() { + Set children = this.children; + if (children == null) { + synchronized (this) { + if (isModule()) { + children = Collections.emptySet(); + this.children = children; + return children; + } + children = this.children; + if (children == null) { + String name = getName(); + children = childrenLookup.apply(value.name) + .stream() + .filter(childName -> !name.equals(childName)) + .map(lookup) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(LinkedHashSet::new)); + this.children = children; + } + } + } + return children; + } + + /** + * @return All direct parents and child vertices. + */ + @NotNull + public Set getAllDirectVertices() { + Set set = new HashSet<>(getParents()); + set.addAll(getChildren()); + return set; + } + + /** + * Clears cached {@link #getParents()} and {@link #getChildren()} values. + */ + public void clearCachedVertices() { + synchronized (this) { + parents = null; + children = null; + } + } + + + /** + * @return {@link #getValue() wrapped class's} name + */ + @NotNull + public String getName() { + return value.name; + } + + /** + * @return Wrapped class info. + */ + @NotNull + public ClassNode getValue() { + return value; + } + + /** + * @param value + * New wrapped class info. + */ + public void setValue(@NotNull ClassNode value) { + this.value = value; + + // Reset children & parent sets + synchronized (this) { + children = null; + parents = null; + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InheritanceVertex vertex = (InheritanceVertex) o; + return Objects.equals(getName(), vertex.getName()); + } + + @Override + public int hashCode() { + return getName().hashCode(); + } + + @Override + public String toString() { + return getName(); + } +} diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/Streams.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/Streams.java new file mode 100644 index 00000000..f22fe4e1 --- /dev/null +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/inheritance/Streams.java @@ -0,0 +1,43 @@ +package uwu.narumi.deobfuscator.api.inheritance; + +import java.util.ArrayDeque; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +public class Streams { + public static Stream recurseWithoutCycles(T seed, Function> flatMap) { + Deque> vertices = new ArrayDeque<>(); + Set visited = new HashSet<>(); + vertices.push(Collections.singletonList(seed).iterator()); + return StreamSupport.stream(new Spliterators.AbstractSpliterator<>(Long.MAX_VALUE, Spliterator.IMMUTABLE | Spliterator.NONNULL) { + @Override + public boolean tryAdvance(Consumer action) { + while (true) { + Iterator iterator = vertices.peek(); + if (iterator == null) { + return false; + } + if (!iterator.hasNext()) { + vertices.poll(); + continue; + } + T vertex = iterator.next(); + if (visited.add(vertex)) { + action.accept(vertex); + vertices.push(flatMap.apply(vertex).iterator()); + return true; + } + } + } + }, false); + } +} diff --git a/deobfuscator-impl/src/main/java/uwu/narumi/deobfuscator/Deobfuscator.java b/deobfuscator-impl/src/main/java/uwu/narumi/deobfuscator/Deobfuscator.java index c7ead000..cd4420bd 100644 --- a/deobfuscator-impl/src/main/java/uwu/narumi/deobfuscator/Deobfuscator.java +++ b/deobfuscator-impl/src/main/java/uwu/narumi/deobfuscator/Deobfuscator.java @@ -12,12 +12,14 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.objectweb.asm.ClassReader; import uwu.narumi.deobfuscator.api.asm.ClassWrapper; import uwu.narumi.deobfuscator.api.context.Context; import uwu.narumi.deobfuscator.api.context.DeobfuscatorOptions; import uwu.narumi.deobfuscator.api.helper.ClassHelper; import uwu.narumi.deobfuscator.api.helper.FileHelper; import uwu.narumi.deobfuscator.api.classpath.Classpath; +import uwu.narumi.deobfuscator.api.inheritance.InheritanceGraph; import uwu.narumi.deobfuscator.api.transformer.Transformer; public class Deobfuscator { @@ -45,26 +47,35 @@ private Deobfuscator(DeobfuscatorOptions options) { LOGGER.warn("Output file already exist, data will be overwritten"); } - Classpath classpath = this.buildClasspath(); + Classpath primaryClasspath = buildPrimaryClasspath(); + LOGGER.info("Loaded {} classes from a primary source", primaryClasspath.rawClasses().size()); - this.context = new Context(options, classpath); - } + Classpath libClasspath = buildLibClasspath(); + LOGGER.info("Loaded {} classes from libraries", libClasspath.rawClasses().size()); - private Classpath buildClasspath() { - Classpath classpath = new Classpath(this.options.classWriterFlags()); + this.context = new Context(options, primaryClasspath, libClasspath); + } - // Add libraries - options.libraries().forEach(classpath::addJar); + public Classpath buildPrimaryClasspath() { + Classpath.Builder builder = Classpath.builder(); // Add input jar as a library if (options.inputJar() != null) { - classpath.addJar(options.inputJar()); + builder.addJar(options.inputJar()); } // Add raw classes as a library if (!options.classes().isEmpty()) { - options.classes().forEach(classpath::addExternalClass); + options.classes().forEach(builder::addExternalClass); } - return classpath; + return builder.build(); + } + + public Classpath buildLibClasspath() { + Classpath.Builder builder = Classpath.builder(); + // Add libraries + options.libraries().forEach(builder::addJar); + + return builder.build(); } public void start() { @@ -102,12 +113,11 @@ private void loadInput() { private void loadClass(String pathInJar, byte[] bytes) { try { if (ClassHelper.isClass(pathInJar, bytes)) { - ClassWrapper classWrapper = ClassHelper.loadClass( + ClassWrapper classWrapper = ClassHelper.loadUnknownClass( pathInJar, bytes, - this.options.classReaderFlags(), - this.options.classWriterFlags(), - true + ClassReader.SKIP_FRAMES, + this.options.classWriterFlags() ); context.getClasses().putIfAbsent(classWrapper.name(), classWrapper); } else if (!context.getFiles().containsKey(pathInJar)) { @@ -144,11 +154,13 @@ private void saveOutput() { private void saveClassesToDir() { LOGGER.info("Saving classes to output directory: {}", this.options.outputDir()); + InheritanceGraph inheritanceGraph = new InheritanceGraph(this.context.getCombinedClasspath()); + context .getClasses() .forEach((ignored, classWrapper) -> { try { - byte[] data = classWrapper.compileToBytes(this.context); + byte[] data = classWrapper.compileToBytes(inheritanceGraph); Path path = this.options.outputDir().resolve(classWrapper.getPathInJar()); Files.createDirectories(path.getParent()); @@ -173,6 +185,8 @@ private void saveToJar() { } } + InheritanceGraph inheritanceGraph = new InheritanceGraph(this.context.getCombinedClasspath()); + try (ZipOutputStream zipOutputStream = new ZipOutputStream(Files.newOutputStream(this.options.outputJar()))) { zipOutputStream.setLevel(9); @@ -181,7 +195,7 @@ private void saveToJar() { .forEach( (ignored, classWrapper) -> { try { - byte[] data = classWrapper.compileToBytes(this.context); + byte[] data = classWrapper.compileToBytes(inheritanceGraph); zipOutputStream.putNextEntry(new ZipEntry(classWrapper.name() + ".class")); zipOutputStream.write(data); @@ -191,7 +205,7 @@ private void saveToJar() { try { // Save original class as a fallback - byte[] data = context.getClasspath().getClasses().get(classWrapper.name()); + byte[] data = context.getPrimaryClasspath().rawClasses().get(classWrapper.name()); zipOutputStream.putNextEntry(new ZipEntry(classWrapper.name() + ".class")); zipOutputStream.write(data); diff --git a/deobfuscator-impl/src/test/java/Bootstrap.java b/deobfuscator-impl/src/test/java/Bootstrap.java index cd3f1aa5..0984b8f8 100644 --- a/deobfuscator-impl/src/test/java/Bootstrap.java +++ b/deobfuscator-impl/src/test/java/Bootstrap.java @@ -1,5 +1,4 @@ import java.nio.file.Path; -import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import uwu.narumi.deobfuscator.Deobfuscator; import uwu.narumi.deobfuscator.api.context.DeobfuscatorOptions; @@ -10,13 +9,13 @@ public class Bootstrap { public static void main(String[] args) { Deobfuscator.from( DeobfuscatorOptions.builder() - .inputJar(Path.of("work", "obf-test.jar")) + .inputJar(Path.of("work", "obf-test.jar")) // Specify your input jar here + //.libraries(Path.of("work", "libs")) // Specify your libraries here if needed .transformers( // Pick your transformers here () -> new ComposedGeneralFlowTransformer() ) .continueOnError() - .classReaderFlags(ClassReader.SKIP_FRAMES) .classWriterFlags(ClassWriter.COMPUTE_FRAMES) .build() ).start(); diff --git a/deobfuscator-impl/src/test/java/uwu/narumi/deobfuscator/base/SingleClassContextSource.java b/deobfuscator-impl/src/test/java/uwu/narumi/deobfuscator/base/SingleClassContextSource.java index ed0c49d6..4096aa75 100644 --- a/deobfuscator-impl/src/test/java/uwu/narumi/deobfuscator/base/SingleClassContextSource.java +++ b/deobfuscator-impl/src/test/java/uwu/narumi/deobfuscator/base/SingleClassContextSource.java @@ -4,8 +4,6 @@ import java.nio.file.Path; import org.jetbrains.java.decompiler.main.extern.IContextSource; import org.jetbrains.java.decompiler.main.extern.IResultSaver; -import org.objectweb.asm.ClassReader; -import uwu.narumi.deobfuscator.api.asm.ClassWrapper; import uwu.narumi.deobfuscator.api.helper.ClassHelper; import java.io.ByteArrayInputStream; @@ -25,8 +23,8 @@ public SingleClassContextSource(Path file, String relativePath) { try { // Get qualified name this.contents = Files.readAllBytes(file); - ClassWrapper classWrapper = ClassHelper.loadClass(relativePath, this.contents, ClassReader.SKIP_FRAMES, 0); - this.qualifiedName = classWrapper.name(); + + this.qualifiedName = ClassHelper.loadUnknownClassInfo(this.contents).name; } catch (Exception e) { throw new RuntimeException(e); } diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/hp888/HP888PackerTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/hp888/HP888PackerTransformer.java index 1310b0c3..02c1f776 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/hp888/HP888PackerTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/hp888/HP888PackerTransformer.java @@ -61,11 +61,10 @@ protected void transform(ClassWrapper scope, Context context) throws Exception { // Decrypt! byte[] decrypted = cipher.doFinal(bytes); - newClasses.put(cleanFileName, ClassHelper.loadClass(cleanFileName, + newClasses.put(cleanFileName, ClassHelper.loadUnknownClass(cleanFileName, decrypted, ClassReader.SKIP_FRAMES, - ClassWriter.COMPUTE_MAXS, - true + ClassWriter.COMPUTE_MAXS )); markChange(); } catch (Exception e) {