From 8cb76f323975cf76fc4d257aee960f8365a78f50 Mon Sep 17 00:00:00 2001 From: EpicPlayerA10 Date: Sat, 24 Aug 2024 20:02:51 +0200 Subject: [PATCH] More tests and a bit of refactoring --- .gitignore | 4 +- CONTRIBUTING.md | 10 +- README.md | 1 + .../deobfuscator/api/asm/ClassWrapper.java | 7 +- .../deobfuscator/api/helper/ClassHelper.java | 17 ++- .../deobfuscator/api/helper/FileHelper.java | 22 +++ .../FramedInstructionsTransformer.java | 8 +- .../uwu/narumi/deobfuscator/Deobfuscator.java | 134 ++++++++++++------ .../src/test/java/Bootstrap.java | 2 +- .../deobfuscator/TestDeobfuscation.java | 13 +- .../base/AssertingResultSaver.java | 14 +- .../base/ClassWrapperContextSource.java | 74 ---------- .../base/SingleClassContextSource.java | 79 +++++++++++ .../base/TestDeobfuscationBase.java | 66 ++++++--- .../ComposedGeneralCleanTransformer.java | 2 + .../ComposedGeneralFlowTransformer.java | 9 +- .../impl/clean/DeadCodeCleanTransformer.java | 126 ---------------- .../impl/clean/PeepholeCleanTransformer.java | 24 ++++ .../peephole/DeadCodeCleanTransformer.java | 41 ++++++ .../{ => peephole}/NopCleanTransformer.java | 2 +- .../UnUsedLabelCleanTransformer.java | 2 +- .../UnUsedLocalVariablesCleanTransformer.java | 47 ++++++ .../UselessJumpsCleanTransformer.java | 91 ++++++++++++ .../peephole/UselessPopCleanTransformer.java | 34 +++++ .../pool/InlineLocalVariablesTransformer.java | 12 -- .../pool/InlineStaticFieldTransformer.java | 13 -- .../number/MathOperationsTransformer.java | 10 +- .../results/custom-classes/FlowObfSample.dec | 6 +- .../results/java/TestInlineStaticFields.dec | 15 ++ .../java/TestSimpleFlowObfuscation.dec | 10 +- .../java/TestUniversalNumberTransformer.dec | 17 +++ .../src/main/java/TestInlineStaticFields.java | 16 +++ .../main/java/TestSimpleFlowObfuscation.java | 15 ++ .../java/TestUniversalNumberTransformer.java | 19 +++ 34 files changed, 639 insertions(+), 323 deletions(-) delete mode 100644 deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/ClassWrapperContextSource.java create mode 100644 deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/SingleClassContextSource.java delete mode 100644 deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/DeadCodeCleanTransformer.java create mode 100644 deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/PeepholeCleanTransformer.java create mode 100644 deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/DeadCodeCleanTransformer.java rename deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/{ => peephole}/NopCleanTransformer.java (92%) rename deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/{ => peephole}/UnUsedLabelCleanTransformer.java (97%) create mode 100644 deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLocalVariablesCleanTransformer.java create mode 100644 deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessJumpsCleanTransformer.java create mode 100644 deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessPopCleanTransformer.java create mode 100644 testData/results/java/TestInlineStaticFields.dec create mode 100644 testData/results/java/TestUniversalNumberTransformer.dec create mode 100644 testData/src/java/src/main/java/TestInlineStaticFields.java create mode 100644 testData/src/java/src/main/java/TestUniversalNumberTransformer.java diff --git a/.gitignore b/.gitignore index b83e4f4..dd23291 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,7 @@ buildNumber.properties /work/ /test/ -# Don't add compiled classes from java code +# Don't track compiled classes from java code /testData/compiled/java/ +# Don't track deobfuscated files. They are added as decompiled code +/testData/deobfuscated/ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9d3dd7..f2d8e9b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -3,7 +3,7 @@ ## ✅ How to run deobfuscator 1. Navigate to class [`Bootstrap.java`](./deobfuscator-impl/src/test/java/Bootstrap.java) 2. In this class edit the deobfuscator configuration - - `input` - Your input jar file + - `inputJar` - Your input jar file - `transformers` - Pick transformers that you want to run. You can find them in [`deobfuscator-transformers`](./deobfuscator-transformers) module. 3. Run this class manually from your IDE @@ -19,6 +19,7 @@ The project is structured as follows: - [`src/java`](./testData/src/java) - You can write your java code to test transformers - [`compiled/custom-classes`](./testData/compiled/custom-classes) - Compiled classes to test transformers. You can throw here classes from your obfuscated jars. - [`compiled/custom-jars`](./testData/compiled/custom-jars) - Jars to test transformers. You can throw here your obfuscated jars. + - [`deobfuscated`] - Raw classes after deobfuscation process. Useful when debugging. - [`results`](./testData/results) - Expected results that are auto-generated decompiled java code. - [`TestDeobfuscation.java`](./deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/TestDeobfuscation.java) - Class where each test sample is registered. - [`Bootstrap.java`](./deobfuscator-impl/src/test/java/Bootstrap.java) - Class where you can run deobfuscator manually. @@ -31,16 +32,19 @@ Whole deobfuscation process is based on transformers. Transformers are smaller c 1. Create a new class in [`deobfuscator-transformers`](./deobfuscator-transformers) module. 2. Pick `Transformer`-like class you would like to implement: - `Transformer` - Basic transformer that transforms classes. - - `FramedInstructionsTransformer` - Transformer that mainly transforms instructions in methods. If you need you can also access values in the stack. + - `FramedInstructionsTransformer` - Transformer that mainly transforms instructions in methods. If you need you can also access values from the stack. - `ComposedTransformer` - Transformer that consists of multiple transformers. 3. You can start coding! ## 🧪 Testing -### How these test work? +### How these tests work? 1. The registered samples are transformed using corresponding transformers. 2. The output gets decompiled using Vineflower. 3. The output gets compared with the expected output. +### How to run tests? +Just run command `mvn test` in the root directory of the project. + ### How to create your own tests? You can create your own tests for transformers. There are a few ways to do it: - If the obfuscation is simple enough, you can write your own sample in [`testData/src/java`](./testData/src/java) diff --git a/README.md b/README.md index fa3f3bf..7c07a81 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ > - Porting old transformers to new code base > - Testing InstructionMatcher > - Implementing/Improving transformers +> - Writing tests > - Safety checks on putstatic in FieldInlineTransformers (overriding values) > - Feedback on how the new api presents itself (mainly InstructionMatcher) >
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 72b70e5..6f579ae 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 @@ -18,20 +18,23 @@ public class ClassWrapper implements Cloneable { protected static final Logger LOGGER = LogManager.getLogger(ClassWrapper.class); + /** + * Path for saving purposes. + */ private final String path; private final ClassNode classNode; private final FieldCache fieldCache; private final ConstantPool constantPool; private final int classWriterFlags; - public ClassWrapper(String path, ClassReader classReader, int readerMode, int classWriterFlags) throws Exception { + public ClassWrapper(String path, ClassReader classReader, int classReaderFlags, int classWriterFlags) throws Exception { this.path = path; this.classNode = new ClassNode(); this.constantPool = new ConstantPool(classReader); this.fieldCache = new FieldCache(); this.classWriterFlags = classWriterFlags; - classReader.accept(this.classNode, readerMode); + classReader.accept(this.classNode, classReaderFlags); } private ClassWrapper(String path, ClassNode classNode, FieldCache fieldCache, ConstantPool constantPool, int classWriterFlags) { 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 c3a8de4..0a48c2d 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 @@ -26,12 +26,21 @@ public static boolean isClass(byte[] bytes) { .equals("CAFEBABE"); } - public static ClassWrapper loadClass(String path, byte[] bytes, int readerMode, int classWriterFlags) throws Exception { - return loadClass(path, bytes, readerMode, classWriterFlags, false); + public static ClassWrapper loadClass(String path, byte[] bytes, int classReaderFlags, int classWriterFlags) throws Exception { + return loadClass(path, bytes, classReaderFlags, classWriterFlags, false); } - public static ClassWrapper loadClass(String path, byte[] bytes, int readerMode, int classWriterFlags, boolean fix) throws Exception { - return new ClassWrapper(path, new ClassReader(fix ? fixClass(bytes) : bytes), readerMode, classWriterFlags); + /** + * Load class from bytes + * + * @param path 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 path, byte[] bytes, int classReaderFlags, int classWriterFlags, boolean fix) throws Exception { + return new ClassWrapper(path, new ClassReader(fix ? fixClass(bytes) : bytes), classReaderFlags, classWriterFlags); } public static byte[] fixClass(byte[] bytes) throws InvalidClassException { diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/FileHelper.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/FileHelper.java index 3837757..1451238 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/FileHelper.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/helper/FileHelper.java @@ -1,5 +1,8 @@ package uwu.narumi.deobfuscator.api.helper; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.function.BiConsumer; import java.util.jar.JarFile; @@ -32,4 +35,23 @@ public static void loadFilesFromZip(Path path, BiConsumer consum LOGGER.debug("Error", e); } } + + public static void deleteDirectory(File file) { + if (!file.exists()) { + return; + } + if (file.isDirectory()) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + deleteDirectory(f); + } + } + } + try { + Files.delete(file.toPath()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } } diff --git a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/transformer/FramedInstructionsTransformer.java b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/transformer/FramedInstructionsTransformer.java index 0a7324f..75b0dc1 100644 --- a/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/transformer/FramedInstructionsTransformer.java +++ b/deobfuscator-api/src/main/java/uwu/narumi/deobfuscator/api/transformer/FramedInstructionsTransformer.java @@ -9,13 +9,14 @@ import java.util.Arrays; import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; /** * Transformer that will iterate instructions along with their current {@link Frame}s */ public abstract class FramedInstructionsTransformer extends Transformer { - private boolean changed = false; + private AtomicInteger changed = new AtomicInteger(0); /** * Transform instruction @@ -72,11 +73,12 @@ protected boolean transform(ClassWrapper scope, Context context) throws Exceptio // Run the instruction transformer boolean transformerChanged = transformInstruction(classWrapper, methodNode, insn, frame); if (transformerChanged) { - changed = true; + changed.incrementAndGet(); } }); })); - return changed; + LOGGER.info("Transformed {} instructions", changed.get()); + return changed.get() > 0; } } 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 ec98679..18b562c 100644 --- a/deobfuscator-impl/src/main/java/uwu/narumi/deobfuscator/Deobfuscator.java +++ b/deobfuscator-impl/src/main/java/uwu/narumi/deobfuscator/Deobfuscator.java @@ -2,12 +2,12 @@ import dev.xdark.ssvm.VirtualMachine; -import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.file.Files; import java.nio.file.Path; import java.util.*; import java.util.function.Supplier; @@ -20,12 +20,10 @@ import org.objectweb.asm.ClassWriter; import uwu.narumi.deobfuscator.api.asm.ClassWrapper; import uwu.narumi.deobfuscator.api.context.Context; -import uwu.narumi.deobfuscator.api.execution.SandBox; import uwu.narumi.deobfuscator.api.helper.ClassHelper; import uwu.narumi.deobfuscator.api.helper.FileHelper; import uwu.narumi.deobfuscator.api.library.Library; import uwu.narumi.deobfuscator.api.library.LibraryClassLoader; -import uwu.narumi.deobfuscator.api.library.LibraryClassWriter; import uwu.narumi.deobfuscator.api.transformer.Transformer; public class Deobfuscator { @@ -36,25 +34,30 @@ public class Deobfuscator { private final List> transformers = new ArrayList<>(); @Nullable - private final Path input; + private final Path inputJar; @Nullable - private final Path output; - private final List classes; + private final Path outputJar; + @Nullable + private final Path outputDir; + private final List classes; + private final int classReaderFlags; private final int classWriterFlags; private final boolean consoleDebug; private Deobfuscator(Builder builder) throws FileNotFoundException { - if (builder.input == null && builder.classes.isEmpty()) { + if (builder.inputJar == null && builder.classes.isEmpty()) { throw new FileNotFoundException("No input files provided"); } - if (builder.output != null && builder.output.toFile().exists()) + if (builder.outputJar != null && builder.outputJar.toFile().exists()) LOGGER.warn("Output file already exist, data will be overwritten"); - this.input = builder.input; - this.output = builder.output; + this.inputJar = builder.inputJar; + this.outputJar = builder.outputJar; + this.outputDir = builder.outputDir; this.classes = builder.classes; + this.transformers.addAll(builder.transformers); this.classReaderFlags = builder.classReaderFlags; this.classWriterFlags = builder.classWriterFlags; @@ -102,23 +105,23 @@ public Context getContext() { } private void loadInput() { - if (input != null) { - LOGGER.info("Loading jar file: {}", input); + if (inputJar != null) { + LOGGER.info("Loading jar file: {}", inputJar); // Load jar - FileHelper.loadFilesFromZip(input, this::loadClass); - LOGGER.info("Loaded jar file: {}\n", input); + FileHelper.loadFilesFromZip(inputJar, this::loadClass); + LOGGER.info("Loaded jar file: {}\n", inputJar); } - for (Path clazz : classes) { - LOGGER.info("Loading class: {}", clazz); + for (ExternalClass clazz : classes) { + LOGGER.info("Loading class: {}", clazz.relativePath); - try (InputStream inputStream = new FileInputStream(clazz.toFile())) { + try (InputStream inputStream = new FileInputStream(clazz.path.toFile())) { // Load class - this.loadClass(clazz.toString(), inputStream.readAllBytes()); + this.loadClass(clazz.relativePath, inputStream.readAllBytes()); - LOGGER.info("Loaded class: {}\n", clazz); + LOGGER.info("Loaded class: {}\n", clazz.relativePath); } catch (IOException e) { - LOGGER.error("Could not load class: {}", clazz, e); + LOGGER.error("Could not load class: {}", clazz.relativePath, e); } } } @@ -154,13 +157,45 @@ public void transform(List> transformers) { transformers.forEach(transformerSupplier -> Transformer.transform(transformerSupplier, null, this.context)); } + /** + * Saves deobfuscation output result + */ private void saveOutput() { - if (this.output == null) return; + if (outputJar != null) { + saveToJar(); + } else if (outputDir != null) { + saveClassesToDir(); + } else { + throw new IllegalStateException("No output file or directory provided"); + } + } + + private void saveClassesToDir() { + LOGGER.info("Saving classes to output directory: {}", outputDir); + + context + .getClasses() + .forEach((ignored, classWrapper) -> { + try { + byte[] data = classWrapper.compileToBytes(this.context); + + Path path = this.outputDir.resolve(classWrapper.getPath() + ".class"); + Files.createDirectories(path.getParent()); + Files.write(path, data); + } catch (Exception e) { + LOGGER.error("Could not save class: {}", classWrapper.name(), e); + } + + context.getOriginalClasses().remove(classWrapper.name()); + context.getClasses().remove(classWrapper.name()); + }); + } - LOGGER.info("Saving output file: {}", output); + private void saveToJar() { + LOGGER.info("Saving output file: {}", outputJar); try (ZipOutputStream zipOutputStream = - new ZipOutputStream(new FileOutputStream(output.toFile()))) { + new ZipOutputStream(new FileOutputStream(outputJar.toFile()))) { zipOutputStream.setLevel(9); context @@ -216,22 +251,24 @@ private void saveOutput() { context.getFiles().remove(name); }); } catch (Exception e) { - LOGGER.error("Could not save output file: {}", output); + LOGGER.error("Could not save output file: {}", outputJar); LOGGER.debug("Error", e); if (consoleDebug) e.printStackTrace(); } - LOGGER.info("Saved output file: {}\n", output); + LOGGER.info("Saved output file: {}\n", outputJar); } public static class Builder { private final Set libraries = new HashSet<>(); @Nullable - private Path input = null; + private Path inputJar = null; @Nullable - private Path output = null; - private List classes = new ArrayList<>(); + private Path outputJar = null; + @Nullable + private Path outputDir = null; + private List classes = new ArrayList<>(); private List> transformers = List.of(); private int classReaderFlags = ClassReader.SKIP_FRAMES; @@ -243,40 +280,48 @@ public static class Builder { private Builder() {} - public Builder input(@Nullable Path input) { - this.input = input; - if (this.input != null) { - String fullName = input.getFileName().toString(); + public Builder inputJar(@Nullable Path inputJar) { + this.inputJar = inputJar; + if (this.inputJar != null) { + String fullName = inputJar.getFileName().toString(); int dot = fullName.lastIndexOf('.'); - this.output = - input + this.outputJar = + inputJar .getParent() .resolve( dot == -1 ? fullName + "-out" : fullName.substring(0, dot) + "-out" + fullName.substring(dot)); - this.libraries.add(input); + this.libraries.add(inputJar); } return this; } - public Builder output(@Nullable Path output) { - this.output = output; + public Builder outputJar(@Nullable Path outputJar) { + this.outputJar = outputJar; return this; } - public Builder libraries(Path... paths) { - this.libraries.addAll(List.of(paths)); + /** + * Output dir for deobfuscated classes + */ + public Builder outputDir(@Nullable Path outputDir) { + this.outputDir = outputDir; return this; } - public Builder classes(Path... paths) { - this.classes = Arrays.asList(paths); + public Builder libraries(Path... paths) { + this.libraries.addAll(List.of(paths)); return this; } - public Builder clazz(Path path) { - this.classes.add(path); + /** + * Add external class to deobfuscate + * @param path Path to external class + * @param relativePath Relative path for saving purposes + */ + public Builder clazz(Path path, String relativePath) { + this.classes.add(new ExternalClass(path, relativePath)); return this; } @@ -310,4 +355,7 @@ public Deobfuscator build() throws FileNotFoundException { return new Deobfuscator(this); } } + + public record ExternalClass(Path path, String relativePath) { + } } diff --git a/deobfuscator-impl/src/test/java/Bootstrap.java b/deobfuscator-impl/src/test/java/Bootstrap.java index f51ffe0..fdfba9c 100644 --- a/deobfuscator-impl/src/test/java/Bootstrap.java +++ b/deobfuscator-impl/src/test/java/Bootstrap.java @@ -8,7 +8,7 @@ public class Bootstrap { public static void main(String[] args) throws Exception { Deobfuscator.builder() - .input(Path.of("work", "obf-test.jar")) + .inputJar(Path.of("work", "obf-test.jar")) .virtualMachine( new VirtualMachine() { // you can do shit diff --git a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/TestDeobfuscation.java b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/TestDeobfuscation.java index d78f9cd..18941b2 100644 --- a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/TestDeobfuscation.java +++ b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/TestDeobfuscation.java @@ -1,8 +1,11 @@ package uwu.narumii.deobfuscator; import uwu.narumi.deobfuscator.core.other.composed.ComposedGeneralFlowTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.PeepholeCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UnUsedLocalVariablesCleanTransformer; import uwu.narumi.deobfuscator.core.other.impl.pool.InlineLocalVariablesTransformer; -import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalFlowTransformer; +import uwu.narumi.deobfuscator.core.other.impl.pool.InlineStaticFieldTransformer; +import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalNumberTransformer; import uwu.narumii.deobfuscator.base.TestDeobfuscationBase; import java.util.List; @@ -11,8 +14,14 @@ public class TestDeobfuscation extends TestDeobfuscationBase { @Override protected void registerAll() { - register("Inlining local variables", InputType.JAVA_CODE, List.of(InlineLocalVariablesTransformer::new), "TestInlineLocalVariables"); + register("Inlining local variables", InputType.JAVA_CODE, List.of( + InlineLocalVariablesTransformer::new, + PeepholeCleanTransformer::new + ), "TestInlineLocalVariables"); + // TODO: Deobfuscate useless tricky while loops register("Simple flow obfuscation", InputType.JAVA_CODE, List.of(ComposedGeneralFlowTransformer::new), "TestSimpleFlowObfuscation"); + register("Universal Number Transformer", InputType.JAVA_CODE, List.of(UniversalNumberTransformer::new), "TestUniversalNumberTransformer"); + register("Inline static fields", InputType.JAVA_CODE, List.of(InlineStaticFieldTransformer::new), "TestInlineStaticFields"); // Samples // TODO: Deobfuscate switches diff --git a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/AssertingResultSaver.java b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/AssertingResultSaver.java index 6c72abf..36a1b5c 100644 --- a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/AssertingResultSaver.java +++ b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/AssertingResultSaver.java @@ -6,7 +6,6 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Map; import java.util.jar.Manifest; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -17,15 +16,13 @@ public class AssertingResultSaver implements IResultSaver { private final TestDeobfuscationBase.InputType inputType; - private final Map sourcePathToSourceName; - private final String inputJar; + private final String jarSource; private boolean savedContent = false; - public AssertingResultSaver(TestDeobfuscationBase.InputType inputType, Map sourcePathToSourceName, String inputJar) { + public AssertingResultSaver(TestDeobfuscationBase.InputType inputType, String jarSource) { this.inputType = inputType; - this.sourcePathToSourceName = sourcePathToSourceName; - this.inputJar = inputJar; + this.jarSource = jarSource; } @Override @@ -45,10 +42,9 @@ public void copyFile(String source, String path, String entryName) { public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { Path saveTo; if (this.inputType == TestDeobfuscationBase.InputType.CUSTOM_JAR) { - saveTo = Path.of(TestDeobfuscationBase.RESULTS_CLASSES_PATH.toString(), inputType.directory(), this.inputJar, qualifiedName + ".dec"); + saveTo = Path.of(TestDeobfuscationBase.RESULTS_CLASSES_PATH.toString(), inputType.directory(), this.jarSource, entryName + ".dec"); } else { - String sourceName = sourcePathToSourceName.get(path); - saveTo = Path.of(TestDeobfuscationBase.RESULTS_CLASSES_PATH.toString(), inputType.directory(), sourceName + ".dec"); + saveTo = Path.of(TestDeobfuscationBase.RESULTS_CLASSES_PATH.toString(), inputType.directory(), entryName + ".dec"); } File fileSaveTo = saveTo.toFile(); diff --git a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/ClassWrapperContextSource.java b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/ClassWrapperContextSource.java deleted file mode 100644 index 6ab7eed..0000000 --- a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/ClassWrapperContextSource.java +++ /dev/null @@ -1,74 +0,0 @@ -package uwu.narumii.deobfuscator.base; - -import org.jetbrains.java.decompiler.main.extern.IContextSource; -import org.jetbrains.java.decompiler.main.extern.IResultSaver; -import uwu.narumi.deobfuscator.api.asm.ClassWrapper; -import uwu.narumi.deobfuscator.api.context.Context; - -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.util.List; - -public class ClassWrapperContextSource implements IContextSource { - private final ClassWrapper classWrapper; - private final Context context; - - public ClassWrapperContextSource(ClassWrapper classWrapper, Context context) { - this.classWrapper = classWrapper; - this.context = context; - } - - @Override - public String getName() { - return this.classWrapper.name(); - } - - @Override - public Entries getEntries() { - String qualifiedName = this.classWrapper.name().replace('/', '.'); - return new Entries( - List.of( - Entry.atBase(qualifiedName) - ), - List.of(), - List.of() - ); - } - - @Override - public InputStream getInputStream(String resource) throws IOException { - return new ByteArrayInputStream(this.classWrapper.compileToBytes(this.context)); - } - - @Override - public IOutputSink createOutputSink(IResultSaver saver) { - return new IOutputSink() { - @Override - public void begin() { - - } - - @Override - public void acceptClass(String qualifiedName, String fileName, String content, int[] mapping) { - // Call IResultSaver - saver.saveClassFile(classWrapper.getPath(), qualifiedName, classWrapper.name(), content, mapping); - } - - @Override - public void acceptDirectory(String directory) { - - } - - @Override - public void acceptOther(String path) { - - } - - @Override - public void close() throws IOException { - - } - }; - } -} diff --git a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/SingleClassContextSource.java b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/SingleClassContextSource.java new file mode 100644 index 0000000..b3e0ee9 --- /dev/null +++ b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/SingleClassContextSource.java @@ -0,0 +1,79 @@ +package uwu.narumii.deobfuscator.base; + +import org.jetbrains.java.decompiler.main.extern.IContextSource; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; +import org.jetbrains.java.decompiler.util.InterpreterUtil; +import org.objectweb.asm.ClassReader; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.helper.ClassHelper; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.List; + +public class SingleClassContextSource implements IContextSource { + private final File file; + private final String relativePath; + private final String qualifiedName; + private final byte[] contents; + + public SingleClassContextSource(File file, String relativePath) { + this.file = file; + this.relativePath = relativePath; + try { + // Get qualified name + this.contents = InterpreterUtil.getBytes(file); + ClassWrapper classWrapper = ClassHelper.loadClass(relativePath, this.contents, ClassReader.SKIP_FRAMES, 0); + this.qualifiedName = classWrapper.name(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + @Override + public String getName() { + return "file " + this.file; + } + + @Override + public Entries getEntries() { + return new Entries(List.of(Entry.atBase(this.qualifiedName)), List.of(), List.of()); + } + + @Override + public InputStream getInputStream(String resource) throws IOException { + return new ByteArrayInputStream(this.contents); + } + + @Override + public IOutputSink createOutputSink(IResultSaver saver) { + return new IOutputSink() { + @Override + public void begin() { + + } + + @Override + public void acceptClass(String qualifiedName, String fileName, String content, int[] mapping) { + saver.saveClassFile("", qualifiedName, relativePath, content, mapping); + } + + @Override + public void acceptDirectory(String directory) { + + } + + @Override + public void acceptOther(String path) { + + } + + @Override + public void close() throws IOException { + + } + }; + } +} diff --git a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/TestDeobfuscationBase.java b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/TestDeobfuscationBase.java index 3eac4d5..d81f1cd 100644 --- a/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/TestDeobfuscationBase.java +++ b/deobfuscator-impl/src/test/java/uwu/narumii/deobfuscator/base/TestDeobfuscationBase.java @@ -2,18 +2,25 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.core.config.Configurator; +import org.apache.logging.log4j.core.util.FileUtils; +import org.jetbrains.annotations.Nullable; import org.jetbrains.java.decompiler.api.Decompiler; +import org.jetbrains.java.decompiler.main.extern.IContextSource; import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DynamicTest; import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.Timeout; import org.opentest4j.TestAbortedException; import uwu.narumi.deobfuscator.Deobfuscator; import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.helper.FileHelper; import uwu.narumi.deobfuscator.api.transformer.Transformer; +import java.io.File; import java.io.FileNotFoundException; +import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.HashMap; @@ -22,9 +29,11 @@ import java.util.function.Supplier; import java.util.stream.Stream; +@Timeout(60) public abstract class TestDeobfuscationBase { public static final Path TEST_DATA_PATH = Path.of("..", "testData"); public static final Path COMPILED_CLASSES_PATH = Path.of(TEST_DATA_PATH.toString(), "compiled"); + public static final Path DEOBFUSCATED_CLASSES_PATH = Path.of(TEST_DATA_PATH.toString(), "deobfuscated"); public static final Path RESULTS_CLASSES_PATH = Path.of(TEST_DATA_PATH.toString(), "results"); private final List registeredTests = new ArrayList<>(); @@ -57,6 +66,8 @@ public static void setup() { @DisplayName("Test deobfuscation") public Stream testDeobfuscation() { this.registeredTests.clear(); + FileHelper.deleteDirectory(DEOBFUSCATED_CLASSES_PATH.toFile()); + this.registerAll(); return this.registeredTests.stream().map(RegisteredTest::buildTest); } @@ -77,40 +88,46 @@ private void runTest() { Deobfuscator.Builder deobfuscatorBuilder = Deobfuscator.builder() .transformers(this.transformers.toArray(new Supplier[0])); - String inputJar = null; + File deobfuscatedJar = null; + String jarSource = null; // Get sources paths - Map sourcePathToSourceName = new HashMap<>(); if (this.inputType == InputType.CUSTOM_JAR) { if (sources.length > 1) { throw new IllegalArgumentException("Cannot use multiple sources with a jar input"); } - inputJar = sources[0]; + jarSource = sources[0]; + + Path relativePath = Path.of(this.inputType.directory(), sources[0] + ".jar"); // Add jar input - deobfuscatorBuilder.input( - Path.of(COMPILED_CLASSES_PATH.toString(), inputType.directory(), sources[0] + ".jar") - ); + Path inputJarPath = Path.of(COMPILED_CLASSES_PATH.toString(), relativePath.toString()); + deobfuscatorBuilder.inputJar(inputJarPath); + + deobfuscatedJar = Path.of(DEOBFUSCATED_CLASSES_PATH.toString(), relativePath.toString()).toFile(); } else { for (String sourceName : sources) { - Path path = Path.of(COMPILED_CLASSES_PATH.toString(), inputType.directory(), sourceName + ".class"); + Path relativePath = Path.of(this.inputType.directory(), sourceName + ".class"); + Path compiledClassPath = Path.of(COMPILED_CLASSES_PATH.toString(), relativePath.toString()); - if (!path.toFile().exists()) { - throw new IllegalArgumentException("File not found: " + path.toAbsolutePath().normalize()); + if (!compiledClassPath.toFile().exists()) { + throw new IllegalArgumentException("File not found: " + compiledClassPath.toAbsolutePath().normalize()); } // Add class - deobfuscatorBuilder.clazz(path); - sourcePathToSourceName.put(path.toString(), sourceName); + deobfuscatorBuilder.clazz(compiledClassPath, sourceName); } } + Path outputDir = Path.of(DEOBFUSCATED_CLASSES_PATH.toString(), this.inputType.directory()); + Deobfuscator deobfuscator; try { // Build deobfuscator deobfuscator = deobfuscatorBuilder - .output(null) + .outputJar(null) + .outputDir(outputDir) .build(); } catch (FileNotFoundException e) { throw new RuntimeException(e); @@ -119,12 +136,23 @@ private void runTest() { // Run deobfuscator! deobfuscator.start(); + // Init context sources + List contextSources = new ArrayList<>(); + if (this.inputType != InputType.CUSTOM_JAR) { + for (String sourceName : sources) { + Path relativePath = Path.of(this.inputType.directory(), sourceName + ".class"); + Path deobfuscatedClassPath = Path.of(DEOBFUSCATED_CLASSES_PATH.toString(), relativePath.toString()); + + contextSources.add(new SingleClassContextSource(deobfuscatedClassPath.toFile(), sourceName)); + } + } + // Assert output - this.assertOutput(deobfuscator, sourcePathToSourceName, inputJar); + this.assertOutput(contextSources, deobfuscatedJar, jarSource); } - private void assertOutput(Deobfuscator deobfuscator, Map sourcePathToSourceName, String inputJar) { - AssertingResultSaver assertingResultSaver = new AssertingResultSaver(this.inputType, sourcePathToSourceName, inputJar); + private void assertOutput(List contextSources, @Nullable File inputJar, @Nullable String jarSource) { + AssertingResultSaver assertingResultSaver = new AssertingResultSaver(this.inputType, jarSource); // Decompile classes Decompiler.Builder decompilerBuilder = Decompiler.builder() @@ -132,8 +160,12 @@ private void assertOutput(Deobfuscator deobfuscator, Map sourceP .output(assertingResultSaver); // Assert output // Add sources - for (ClassWrapper classWrapper : deobfuscator.getContext().classes()) { - decompilerBuilder.inputs(new ClassWrapperContextSource(classWrapper, deobfuscator.getContext())); + if (this.inputType == InputType.CUSTOM_JAR) { + decompilerBuilder.inputs(inputJar); + } else { + for (IContextSource contextSource : contextSources) { + decompilerBuilder.inputs(contextSource); + } } // Decompile diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralCleanTransformer.java index 396d46a..79c34b2 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralCleanTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralCleanTransformer.java @@ -2,6 +2,8 @@ import uwu.narumi.deobfuscator.api.transformer.ComposedTransformer; import uwu.narumi.deobfuscator.core.other.impl.clean.*; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.NopCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UnUsedLabelCleanTransformer; import uwu.narumi.deobfuscator.core.other.impl.universal.StackOperationResolveTransformer; import uwu.narumi.deobfuscator.core.other.impl.universal.TryCatchRepairTransformer; diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralFlowTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralFlowTransformer.java index bb1dd97..e2f5067 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralFlowTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/composed/ComposedGeneralFlowTransformer.java @@ -1,9 +1,9 @@ package uwu.narumi.deobfuscator.core.other.composed; import uwu.narumi.deobfuscator.api.transformer.ComposedTransformer; -import uwu.narumi.deobfuscator.core.other.impl.clean.DeadCodeCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.PeepholeCleanTransformer; import uwu.narumi.deobfuscator.core.other.impl.clean.LineNumberCleanTransformer; -import uwu.narumi.deobfuscator.core.other.impl.clean.UnUsedLabelCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UnUsedLabelCleanTransformer; import uwu.narumi.deobfuscator.core.other.impl.pool.InlineLocalVariablesTransformer; import uwu.narumi.deobfuscator.core.other.impl.pool.InlineStaticFieldTransformer; import uwu.narumi.deobfuscator.core.other.impl.universal.UniversalNumberTransformer; @@ -19,13 +19,12 @@ public ComposedGeneralFlowTransformer() { () -> new InlineStaticFieldTransformer(true, true), InlineLocalVariablesTransformer::new, - () -> new ComposedTransformer(true, - // Rerun if changed + () -> new ComposedTransformer(true, // Rerun if changed UniversalNumberTransformer::new, UniversalFlowTransformer::new ), - DeadCodeCleanTransformer::new + PeepholeCleanTransformer::new ); } } diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/DeadCodeCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/DeadCodeCleanTransformer.java deleted file mode 100644 index a947757..0000000 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/DeadCodeCleanTransformer.java +++ /dev/null @@ -1,126 +0,0 @@ -package uwu.narumi.deobfuscator.core.other.impl.clean; - -import org.objectweb.asm.tree.AbstractInsnNode; -import org.objectweb.asm.tree.JumpInsnNode; -import org.objectweb.asm.tree.LabelNode; -import org.objectweb.asm.tree.LookupSwitchInsnNode; -import org.objectweb.asm.tree.MethodNode; -import org.objectweb.asm.tree.TableSwitchInsnNode; -import org.objectweb.asm.tree.analysis.Analyzer; -import org.objectweb.asm.tree.analysis.AnalyzerException; -import org.objectweb.asm.tree.analysis.BasicInterpreter; -import org.objectweb.asm.tree.analysis.Frame; -import uwu.narumi.deobfuscator.api.asm.ClassWrapper; -import uwu.narumi.deobfuscator.api.context.Context; -import uwu.narumi.deobfuscator.api.transformer.Transformer; - -import java.util.Arrays; -import java.util.List; - -public class DeadCodeCleanTransformer extends Transformer { - - private boolean changed = false; - - @Override - protected boolean transform(ClassWrapper scope, Context context) throws Exception { - context.classes(scope).forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { - removeDeadCode(classWrapper, methodNode); - - try { - changed |= new UnUsedLabelCleanTransformer().transform(classWrapper, context); - } catch (Exception e) { - throw new RuntimeException(e); - } - - removeUselessGotoJumps(methodNode); - })); - - return changed; - } - - // Remove useless GOTO jumps - /* - A: - ... - goto B - B: - ... - */ - private void removeUselessGotoJumps(MethodNode methodNode) { - for (AbstractInsnNode insn : methodNode.instructions.toArray()) { - if (insn.getOpcode() == GOTO) { - JumpInsnNode jumpInsn = (JumpInsnNode) insn; - if (jumpInsn.getNext() instanceof LabelNode labelNode && jumpInsn.label == labelNode) { - - // Check if the label is used only by the jump instruction - if (!isLabelUsedOnlyByInstructions(methodNode, jumpInsn.label)) continue; - - List labelUsedInsns = Arrays.stream(methodNode.instructions.toArray()) - .filter(newInsn -> checkIfJumpHappens(newInsn, labelNode)) - .toList(); - - boolean labelUsedOnlyOnce = labelUsedInsns.size() == 1; - - if (labelUsedOnlyOnce) { - // Remove the goto and the label - methodNode.instructions.remove(labelNode); - methodNode.instructions.remove(jumpInsn); - changed = true; - } - } - } - } - } - - private void removeDeadCode(ClassWrapper classWrapper, MethodNode methodNode) { - Analyzer analyzer = new Analyzer<>(new BasicInterpreter()); - try { - analyzer.analyze(classWrapper.name(), methodNode); - } catch (AnalyzerException e) { - // Ignore - return; - } - Frame[] frames = analyzer.getFrames(); - AbstractInsnNode[] insns = methodNode.instructions.toArray(); - for (int i = 0; i < frames.length; i++) { - AbstractInsnNode insn = insns[i]; - if (frames[i] == null && insn.getType() != AbstractInsnNode.LABEL) { - methodNode.instructions.remove(insn); - insns[i] = null; - changed = true; - } - } - } - - private boolean isLabelUsedOnlyByInstructions(MethodNode methodNode, LabelNode labelNode) { - if (methodNode.tryCatchBlocks != null) { - boolean usedByTryCatchBlock = methodNode.tryCatchBlocks.stream() - .anyMatch(tryCatchBlockNode -> tryCatchBlockNode.start == labelNode || tryCatchBlockNode.end == labelNode || tryCatchBlockNode.handler == labelNode); - - if (usedByTryCatchBlock) { - return false; - } - } - - if (methodNode.localVariables != null) { - boolean usedByLocalVariable = methodNode.localVariables.stream() - .anyMatch(localVariableNode -> localVariableNode.start == labelNode || localVariableNode.end == labelNode); - - if (usedByLocalVariable) { - return false; - } - } - return true; - } - - private boolean checkIfJumpHappens(AbstractInsnNode insn, LabelNode labelNode) { - if (insn instanceof JumpInsnNode jumpInsn) { - return jumpInsn.label == labelNode; - } else if (insn instanceof LookupSwitchInsnNode lookupSwitchInsn) { - return lookupSwitchInsn.labels.contains(labelNode) || lookupSwitchInsn.dflt == labelNode; - } else if (insn instanceof TableSwitchInsnNode tableSwitchInsn) { - return tableSwitchInsn.labels.contains(labelNode) || tableSwitchInsn.dflt == labelNode; - } - return false; - } -} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/PeepholeCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/PeepholeCleanTransformer.java new file mode 100644 index 0000000..7ff78ce --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/PeepholeCleanTransformer.java @@ -0,0 +1,24 @@ +package uwu.narumi.deobfuscator.core.other.impl.clean; + +import uwu.narumi.deobfuscator.api.transformer.ComposedTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.DeadCodeCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.NopCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UnUsedLabelCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UnUsedLocalVariablesCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UselessJumpsCleanTransformer; +import uwu.narumi.deobfuscator.core.other.impl.clean.peephole.UselessPopCleanTransformer; + +public class PeepholeCleanTransformer extends ComposedTransformer { + + public PeepholeCleanTransformer() { + super( + DeadCodeCleanTransformer::new, + NopCleanTransformer::new, + UnUsedLabelCleanTransformer::new, + UselessJumpsCleanTransformer::new, + + UnUsedLocalVariablesCleanTransformer::new, + UselessPopCleanTransformer::new + ); + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/DeadCodeCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/DeadCodeCleanTransformer.java new file mode 100644 index 0000000..8392425 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/DeadCodeCleanTransformer.java @@ -0,0 +1,41 @@ +package uwu.narumi.deobfuscator.core.other.impl.clean.peephole; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.analysis.Analyzer; +import org.objectweb.asm.tree.analysis.AnalyzerException; +import org.objectweb.asm.tree.analysis.BasicInterpreter; +import org.objectweb.asm.tree.analysis.Frame; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.context.Context; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +public class DeadCodeCleanTransformer extends Transformer { + private boolean changed = false; + + @Override + protected boolean transform(ClassWrapper scope, Context context) throws Exception { + context.classes(scope).forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { + Analyzer analyzer = new Analyzer<>(new BasicInterpreter()); + try { + analyzer.analyze(classWrapper.name(), methodNode); + } catch (AnalyzerException e) { + // Ignore + return; + } + Frame[] frames = analyzer.getFrames(); + AbstractInsnNode[] insns = methodNode.instructions.toArray(); + for (int i = 0; i < frames.length; i++) { + AbstractInsnNode insn = insns[i]; + + // If frame is null then it means that the code is unreachable + if (frames[i] == null && insn.getType() != AbstractInsnNode.LABEL) { + methodNode.instructions.remove(insn); + insns[i] = null; + changed = true; + } + } + })); + + return changed; + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/NopCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/NopCleanTransformer.java similarity index 92% rename from deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/NopCleanTransformer.java rename to deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/NopCleanTransformer.java index b1cc7d4..e4b5a8b 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/NopCleanTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/NopCleanTransformer.java @@ -1,4 +1,4 @@ -package uwu.narumi.deobfuscator.core.other.impl.clean; +package uwu.narumi.deobfuscator.core.other.impl.clean.peephole; import java.util.Arrays; import uwu.narumi.deobfuscator.api.asm.ClassWrapper; diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/UnUsedLabelCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLabelCleanTransformer.java similarity index 97% rename from deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/UnUsedLabelCleanTransformer.java rename to deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLabelCleanTransformer.java index f70cf4f..e2da826 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/UnUsedLabelCleanTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLabelCleanTransformer.java @@ -1,4 +1,4 @@ -package uwu.narumi.deobfuscator.core.other.impl.clean; +package uwu.narumi.deobfuscator.core.other.impl.clean.peephole; import java.util.Arrays; import java.util.HashSet; diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLocalVariablesCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLocalVariablesCleanTransformer.java new file mode 100644 index 0000000..a7b3781 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UnUsedLocalVariablesCleanTransformer.java @@ -0,0 +1,47 @@ +package uwu.narumi.deobfuscator.core.other.impl.clean.peephole; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.VarInsnNode; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.context.Context; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class UnUsedLocalVariablesCleanTransformer extends Transformer { + private final AtomicInteger removedVars = new AtomicInteger(0); + + @Override + protected boolean transform(ClassWrapper scope, Context context) throws Exception { + context.classes(scope).forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { + + List localVariablesInUse = new ArrayList<>(); + + // Find all local variables in use + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof VarInsnNode varInsnNode && insn.getOpcode() >= ILOAD && insn.getOpcode() <= SALOAD) { + localVariablesInUse.add(varInsnNode.var); + } + } + + // Remove all local variables that are not in use + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn instanceof VarInsnNode varInsnNode && insn.getOpcode() >= ISTORE && insn.getOpcode() <= SASTORE) { + if (!localVariablesInUse.contains(varInsnNode.var)) { + // Pop the value from the stack + methodNode.instructions.set(insn, new InsnNode(POP)); + + removedVars.incrementAndGet(); + } + } + } + })); + + LOGGER.info("Removed {} unused local variables", removedVars.get()); + + return removedVars.get() > 0; + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessJumpsCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessJumpsCleanTransformer.java new file mode 100644 index 0000000..44c95a6 --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessJumpsCleanTransformer.java @@ -0,0 +1,91 @@ +package uwu.narumi.deobfuscator.core.other.impl.clean.peephole; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.JumpInsnNode; +import org.objectweb.asm.tree.LabelNode; +import org.objectweb.asm.tree.LookupSwitchInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.TableSwitchInsnNode; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.context.Context; +import uwu.narumi.deobfuscator.api.transformer.Transformer; + +import java.util.Arrays; +import java.util.List; + +/** + * Remove useless GOTO jumps + *
+ * A:
+ *   ...
+ *   goto B
+ * B:
+ *   ...
+ * 
+ */ +public class UselessJumpsCleanTransformer extends Transformer { + private boolean changed = false; + + @Override + protected boolean transform(ClassWrapper scope, Context context) throws Exception { + context.classes(scope).forEach(classWrapper -> classWrapper.methods().forEach(methodNode -> { + for (AbstractInsnNode insn : methodNode.instructions.toArray()) { + if (insn.getOpcode() == GOTO) { + JumpInsnNode jumpInsn = (JumpInsnNode) insn; + if (jumpInsn.getNext() instanceof LabelNode labelNode && jumpInsn.label == labelNode) { + + // Check if the label is used only by the jump instruction + if (!isLabelUsedOnlyByInstructions(methodNode, jumpInsn.label)) continue; + + List labelUsedInsns = Arrays.stream(methodNode.instructions.toArray()) + .filter(newInsn -> checkIfJumpHappens(newInsn, labelNode)) + .toList(); + + boolean labelUsedOnlyOnce = labelUsedInsns.size() == 1; + + if (labelUsedOnlyOnce) { + // Remove the goto and the label + methodNode.instructions.remove(labelNode); + methodNode.instructions.remove(jumpInsn); + changed = true; + } + } + } + } + })); + + return changed; + } + + private boolean isLabelUsedOnlyByInstructions(MethodNode methodNode, LabelNode labelNode) { + if (methodNode.tryCatchBlocks != null) { + boolean usedByTryCatchBlock = methodNode.tryCatchBlocks.stream() + .anyMatch(tryCatchBlockNode -> tryCatchBlockNode.start == labelNode || tryCatchBlockNode.end == labelNode || tryCatchBlockNode.handler == labelNode); + + if (usedByTryCatchBlock) { + return false; + } + } + + if (methodNode.localVariables != null) { + boolean usedByLocalVariable = methodNode.localVariables.stream() + .anyMatch(localVariableNode -> localVariableNode.start == labelNode || localVariableNode.end == labelNode); + + if (usedByLocalVariable) { + return false; + } + } + return true; + } + + private boolean checkIfJumpHappens(AbstractInsnNode insn, LabelNode labelNode) { + if (insn instanceof JumpInsnNode jumpInsn) { + return jumpInsn.label == labelNode; + } else if (insn instanceof LookupSwitchInsnNode lookupSwitchInsn) { + return lookupSwitchInsn.labels.contains(labelNode) || lookupSwitchInsn.dflt == labelNode; + } else if (insn instanceof TableSwitchInsnNode tableSwitchInsn) { + return tableSwitchInsn.labels.contains(labelNode) || tableSwitchInsn.dflt == labelNode; + } + return false; + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessPopCleanTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessPopCleanTransformer.java new file mode 100644 index 0000000..ab0573c --- /dev/null +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/clean/peephole/UselessPopCleanTransformer.java @@ -0,0 +1,34 @@ +package uwu.narumi.deobfuscator.core.other.impl.clean.peephole; + +import org.objectweb.asm.tree.AbstractInsnNode; +import org.objectweb.asm.tree.MethodNode; +import org.objectweb.asm.tree.analysis.Frame; +import org.objectweb.asm.tree.analysis.OriginalSourceValue; +import uwu.narumi.deobfuscator.api.asm.ClassWrapper; +import uwu.narumi.deobfuscator.api.transformer.FramedInstructionsTransformer; + +import java.util.stream.Stream; + +public class UselessPopCleanTransformer extends FramedInstructionsTransformer { + + @Override + protected Stream getInstructionsStream(Stream stream) { + return stream + .filter(insn -> insn.getOpcode() == POP); + } + + @Override + protected boolean transformInstruction(ClassWrapper classWrapper, MethodNode methodNode, AbstractInsnNode insn, Frame frame) { + OriginalSourceValue sourceValue = frame.getStack(frame.getStackSize() - 1); + for (AbstractInsnNode producer : sourceValue.insns) { + // If the producer is a constant, remove the pop and the constant + if (producer.isConstant()) { + methodNode.instructions.remove(producer); + methodNode.instructions.remove(insn); + return true; + } + } + + return false; + } +} diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineLocalVariablesTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineLocalVariablesTransformer.java index cb8e3f7..3b2e173 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineLocalVariablesTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineLocalVariablesTransformer.java @@ -36,8 +36,6 @@ private void inlineLocalVariables(ClassWrapper classWrapper, MethodNode methodNo Map> frames = analyzeOriginalSource(classWrapper.getClassNode(), methodNode); if (frames == null) return; - List toRemoveInsns = new ArrayList<>(); - // Inline static local variables for (AbstractInsnNode insn : methodNode.instructions.toArray()) { if (insn.getOpcode() == ILOAD) { @@ -59,19 +57,9 @@ private void inlineLocalVariables(ClassWrapper classWrapper, MethodNode methodNo AbstractInsnNode clone = valueInsn.clone(null); methodNode.instructions.set(insn, clone); - if (!toRemoveInsns.contains(storeVarSourceValue.getProducer())) { - toRemoveInsns.add(storeVarSourceValue.getProducer()); - } - if (!toRemoveInsns.contains(valueSourceValue.getProducer())) { - toRemoveInsns.add(valueSourceValue.getProducer()); - } - changed = true; } } } - - // Cleanup - toRemoveInsns.forEach(methodNode.instructions::remove); } } diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineStaticFieldTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineStaticFieldTransformer.java index ac25eff..a22d9b0 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineStaticFieldTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/pool/InlineStaticFieldTransformer.java @@ -100,8 +100,6 @@ protected boolean transform(ClassWrapper scope, Context context) throws Exceptio } }); - List toRemove = new ArrayList<>(); - LOGGER.info("Collected {} numbers from {} classes", index.get(), context.classes().size()); context.classes(scope).stream() .flatMap(classWrapper -> classWrapper.methods().stream()) @@ -133,20 +131,9 @@ protected boolean transform(ClassWrapper scope, Context context) throws Exceptio methodNode.instructions.set(node, getNumber(character)); } - FieldRef fieldRef = new FieldRef(node.owner, node.name, node.desc); - if (!toRemove.contains(fieldRef)) { - toRemove.add(fieldRef); - } - inline.incrementAndGet(); })); - // Cleanup - toRemove.forEach(fieldRef -> { - ClassWrapper owner = context.get(fieldRef.owner).get(); - owner.fields().removeIf(fieldNode -> fieldNode.name.equals(fieldRef.name) && fieldNode.desc.equals(fieldRef.desc)); - }); - // values.clear(); LOGGER.info("Inlined {} numbers in {} classes", inline.get(), context.classes().size()); diff --git a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/universal/number/MathOperationsTransformer.java b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/universal/number/MathOperationsTransformer.java index 160358c..3efb8e9 100644 --- a/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/universal/number/MathOperationsTransformer.java +++ b/deobfuscator-transformers/src/main/java/uwu/narumi/deobfuscator/core/other/impl/universal/number/MathOperationsTransformer.java @@ -36,7 +36,15 @@ protected boolean transformInstruction(ClassWrapper classWrapper, MethodNode met Number value1 = value1Insn.asNumber(); Number value2 = value2Insn.asNumber(); - methodNode.instructions.set(insn, AsmHelper.getNumber(AsmMathHelper.mathOperation(value1, value2, insn.getOpcode()))); + Number result; + try { + result = AsmMathHelper.mathOperation(value1, value2, insn.getOpcode()); + } catch (ArithmeticException e) { + // Skip division by zero + return false; + } + + methodNode.instructions.set(insn, AsmHelper.getNumber(result)); methodNode.instructions.remove(value1SourceValue.getProducer()); methodNode.instructions.remove(value2SourceValue.getProducer()); diff --git a/testData/results/custom-classes/FlowObfSample.dec b/testData/results/custom-classes/FlowObfSample.dec index 2861acb..fb67904 100644 --- a/testData/results/custom-classes/FlowObfSample.dec +++ b/testData/results/custom-classes/FlowObfSample.dec @@ -3,6 +3,8 @@ import org.bukkit.event.Listener; public class IIIIIIIIlIlIIIIlIIlIIIIIIIIlIlIllllIIIlIIIlIIllIllllIIIIllIIIIllIIllllIIIIllllIIIIIIIllIllIIIlllIlIIllIlIIllIlIIIIIlIIIIIllllIlIIllIllllIIlIIIIIIlIllIlIIIIlIlIIlIIlllIIIIlIIlIIllIlIIIlIlIlIIIIllIlllll implements Listener { + static long llllIIIIlllIlllIlIllIIlllIlIIlIIlllIlllIlllIIIllIlIIIIlIIIllIlIIlIIlIllIIlIlIIIIIIllIlIlIlIlIIllIlIllIlIIIllllllIlIIllIIllIlIlIIlIllIllIIIlIIlllIIIIIIIlIIIIIIIllIlIIIIIIlIIlIlIIlllIIIlllllIIIIlIIIlIlI; + public static int lIIIIlllIlIIllIIIlllllIlllIIllIllIIlIIllIIIllIIlIllllIIIIllIIlIIIIlIllIIlllIllIllllIIIlllIllIllIlllIlllllIIlIIIlllllIIIlIlIllllIllllIllllIIlIIllIlIIllIIllIIlIllIIllllIlllIIIIlIIllIIIIIIIIllllIlIllllIl( int var0 ) { @@ -67,7 +69,6 @@ public class IIIIIIIIlIlIIIIlIIlIIIIIIIIlIlIllllIIIlIIIlIIllIllllIIIIllIIIIllIIl while (true) { switch ((int)var10001) { case -1254534848: - byte var5 = -1; var10001 = -6562666170258139970L; break; case -253412202: @@ -75,15 +76,12 @@ public class IIIIIIIIlIlIIIIlIIlIIIIIIIIlIlIllllIIIlIIIlIIllIllllIIIIllIIIIllIIl super(); return; case 485227004: - boolean var4 = true; var10001 = 8779745570450297940L; break; case 1712111765: - boolean var3 = true; var10001 = -8855161723369167601L; } - boolean var6 = true; var10001 /= 8016067872500700090L; } } diff --git a/testData/results/java/TestInlineStaticFields.dec b/testData/results/java/TestInlineStaticFields.dec new file mode 100644 index 0000000..d396e4c --- /dev/null +++ b/testData/results/java/TestInlineStaticFields.dec @@ -0,0 +1,15 @@ +public class TestInlineStaticFields { + public static int TEST1 = 123; + public static String TEST2 = "placki"; + public static boolean TEST3 = true; + + public static void test() { + byte var10001 = 123; + String var10003 = "placki"; + boolean var10005 = true; + } + + public static void modifyStatic() { + TEST1 = 321; + } +} diff --git a/testData/results/java/TestSimpleFlowObfuscation.dec b/testData/results/java/TestSimpleFlowObfuscation.dec index f8d5080..d14a946 100644 --- a/testData/results/java/TestSimpleFlowObfuscation.dec +++ b/testData/results/java/TestSimpleFlowObfuscation.dec @@ -1,8 +1,16 @@ public class TestSimpleFlowObfuscation { public void testFlow() { - int a = 3; if (System.currentTimeMillis() == 123L) { System.out.println("123"); } } + + public void compareTest() { + int a = 123; + System.out.println("a is not 100"); + + while (a * 321 == 100) { + a++; + } + } } diff --git a/testData/results/java/TestUniversalNumberTransformer.dec b/testData/results/java/TestUniversalNumberTransformer.dec new file mode 100644 index 0000000..64cb9c4 --- /dev/null +++ b/testData/results/java/TestUniversalNumberTransformer.dec @@ -0,0 +1,17 @@ +public class TestUniversalNumberTransformer { + public void testNumbers1() { + int a = 3; + int b = 19; + double c = 1.5159303447561417E10; + float d = 9977.5625F; + System.out.println(1.5159313447123917E10); + } + + public void divideByZero() { + int a = 2; + if (a == 0) { + int b = 9 / 0; + System.out.println(b); + } + } +} diff --git a/testData/src/java/src/main/java/TestInlineStaticFields.java b/testData/src/java/src/main/java/TestInlineStaticFields.java new file mode 100644 index 0000000..df1965f --- /dev/null +++ b/testData/src/java/src/main/java/TestInlineStaticFields.java @@ -0,0 +1,16 @@ +public class TestInlineStaticFields { + public static int TEST1 = 123; + public static String TEST2 = "placki"; + public static boolean TEST3 = true; + + public static void test() { + System.out.println(TEST1); + System.out.println(TEST2); + System.out.println(TEST3); + } + + public static void modifyStatic() { + // TODO: Account for field modification + TEST1 = 321; + } +} diff --git a/testData/src/java/src/main/java/TestSimpleFlowObfuscation.java b/testData/src/java/src/main/java/TestSimpleFlowObfuscation.java index c6590f1..074f2a4 100644 --- a/testData/src/java/src/main/java/TestSimpleFlowObfuscation.java +++ b/testData/src/java/src/main/java/TestSimpleFlowObfuscation.java @@ -30,4 +30,19 @@ public void testFlow() { throw new RuntimeException(); } } + + public void compareTest() { + int a = 123; + + if (a == 100) { + System.out.println("a is 100"); + } else { + System.out.println("a is not 100"); + } + + // TODO: Simplify also this + while (a * 321 == 100) { + a += 1; + } + } } diff --git a/testData/src/java/src/main/java/TestUniversalNumberTransformer.java b/testData/src/java/src/main/java/TestUniversalNumberTransformer.java new file mode 100644 index 0000000..654bc9a --- /dev/null +++ b/testData/src/java/src/main/java/TestUniversalNumberTransformer.java @@ -0,0 +1,19 @@ +public class TestUniversalNumberTransformer { + public void testNumbers1() { + int a = 1 + 2; + int b = 235434535 / 12323432; + double c = 123123.123123 * 123123.123123; + float d = 123123.123123f / 12.34f; + + System.out.println(a + b + c + d); + } + + public void divideByZero() { + int a = 2; + if (a == 0) { + // Transformer shouldn't touch it + int b = 9 / 0; + System.out.println(b); + } + } +}