Skip to content

Commit

Permalink
Refactor and improve testing framework. Simplify tests creation and a…
Browse files Browse the repository at this point in the history
…llow to decompile additional classes that were added during deobfuscation
  • Loading branch information
EpicPlayerA10 committed Oct 13, 2024
1 parent e22d8f2 commit 0841ae4
Show file tree
Hide file tree
Showing 33 changed files with 283 additions and 213 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -69,21 +69,24 @@ public Builder addJar(@NotNull Path jarPath) {
}

/**
* Adds {@link DeobfuscatorOptions.ExternalClass} to classpath
* Adds {@link DeobfuscatorOptions.ExternalFile} to classpath
*
* @param externalClass External class
* @param externalFile External class
*/
@Contract("_ -> this")
public Builder addExternalClass(DeobfuscatorOptions.ExternalClass externalClass) {
public Builder addExternalFile(DeobfuscatorOptions.ExternalFile externalFile) {
try {
byte[] classBytes = Files.readAllBytes(externalClass.path());

ClassNode classNode = ClassHelper.loadUnknownClassInfo(classBytes);
byte[] bytes = Files.readAllBytes(externalFile.path());
if (!ClassHelper.isClass(externalFile.pathInJar(), bytes)) {
files.putIfAbsent(externalFile.pathInJar(), bytes);
return this;
}

ClassNode classNode = ClassHelper.loadUnknownClassInfo(bytes);
String className = classNode.name;

// Add class to classpath
rawClasses.putIfAbsent(className, classBytes);
rawClasses.putIfAbsent(className, bytes);
classesInfo.putIfAbsent(className, classNode);
} catch (Exception e) {
throw new RuntimeException(e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
*/
public record DeobfuscatorOptions(
@Nullable Path inputJar,
List<ExternalClass> classes,
List<ExternalFile> externalFiles,
Set<Path> libraries,

@Nullable Path outputJar,
Expand All @@ -43,10 +43,10 @@ public static DeobfuscatorOptions.Builder builder() {
}

/**
* @param path Path to the raw .class file
* @param pathInJar Relative path to .class file if it were in .jar
* @param path Path to the raw file
* @param pathInJar Relative path to file as if it were in .jar
*/
public record ExternalClass(Path path, String pathInJar) {
public record ExternalFile(Path path, String pathInJar) {
}

/**
Expand All @@ -56,7 +56,7 @@ public static class Builder {
// Inputs
@Nullable
private Path inputJar = null;
private final List<DeobfuscatorOptions.ExternalClass> classes = new ArrayList<>();
private final List<ExternalFile> externalFiles = new ArrayList<>();
private final Set<Path> libraries = new HashSet<>();

// Outputs
Expand Down Expand Up @@ -100,20 +100,36 @@ public DeobfuscatorOptions.Builder inputJar(@Nullable Path inputJar) {
}

/**
* Output jar for deobfuscated classes. Automatically filled when input jar is set
* Add an external file to the deobfuscation context. You can add raw .class files or files that would be in .jar
*
* @param path Path to an external file
* @param pathInJar Relative path to file if it were in .jar
*/
@Contract("_ -> this")
public DeobfuscatorOptions.Builder outputJar(@Nullable Path outputJar) {
this.outputJar = outputJar;
@Contract("_,_ -> this")
public DeobfuscatorOptions.Builder externalFile(Path path, String pathInJar) {
this.externalFiles.add(new ExternalFile(path, pathInJar));
return this;
}

/**
* Set output dir it if you want to output raw compiled classes instead of jar file
* Adds all files from the directory to the deobfuscation context
*
* @param path Path to the directory
*/
@Contract("_ -> this")
public DeobfuscatorOptions.Builder outputDir(@Nullable Path outputDir) {
this.outputDir = outputDir;
public DeobfuscatorOptions.Builder inputDir(Path path) {
try {
Files.walkFileTree(path, new SimpleFileVisitor<>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
String pathInJar = path.relativize(file).toString();
externalFile(file, pathInJar);
return FileVisitResult.CONTINUE;
}
});
} catch (IOException e) {
throw new RuntimeException(e);
}
return this;
}

Expand Down Expand Up @@ -146,14 +162,20 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO
}

/**
* Add external class to deobfuscate
*
* @param path Path to external class
* @param relativePath Relative path for saving purposes
* Output jar for deobfuscated classes. Automatically filled when input jar is set
*/
@Contract("_,_ -> this")
public DeobfuscatorOptions.Builder clazz(Path path, String relativePath) {
this.classes.add(new DeobfuscatorOptions.ExternalClass(path, relativePath));
@Contract("_ -> this")
public DeobfuscatorOptions.Builder outputJar(@Nullable Path outputJar) {
this.outputJar = outputJar;
return this;
}

/**
* Set output dir it if you want to output raw compiled classes instead of jar file
*/
@Contract("_ -> this")
public DeobfuscatorOptions.Builder outputDir(@Nullable Path outputDir) {
this.outputDir = outputDir;
return this;
}

Expand Down Expand Up @@ -238,7 +260,7 @@ public DeobfuscatorOptions.Builder skipFiles() {
*/
public DeobfuscatorOptions build() {
// Verify some options
if (this.inputJar == null && this.classes.isEmpty()) {
if (this.inputJar == null && this.externalFiles.isEmpty()) {
throw new IllegalStateException("No input files provided");
}
if (this.outputJar == null && this.outputDir == null) {
Expand All @@ -251,7 +273,7 @@ public DeobfuscatorOptions build() {
return new DeobfuscatorOptions(
// Input
inputJar,
classes,
externalFiles,
libraries,
// Output
outputJar,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,13 +59,13 @@ private Deobfuscator(DeobfuscatorOptions options) {

public Classpath buildPrimaryClasspath() {
Classpath.Builder builder = Classpath.builder();
// Add input jar as a library
// Add input jar
if (options.inputJar() != null) {
builder.addJar(options.inputJar());
}
// Add raw classes as a library
if (!options.classes().isEmpty()) {
options.classes().forEach(builder::addExternalClass);
// Add external files
if (!options.externalFiles().isEmpty()) {
options.externalFiles().forEach(builder::addExternalFile);
}

return builder.build();
Expand Down Expand Up @@ -97,16 +97,16 @@ private void loadInput() {
LOGGER.info("Loaded jar file: {}", this.options.inputJar());
}

for (DeobfuscatorOptions.ExternalClass clazz : this.options.classes()) {
LOGGER.info("Loading class: {}", clazz.pathInJar());
for (DeobfuscatorOptions.ExternalFile externalFile : this.options.externalFiles()) {
LOGGER.info("Loading external file: {}", externalFile.pathInJar());

try (InputStream inputStream = new FileInputStream(clazz.path().toFile())) {
try (InputStream inputStream = new FileInputStream(externalFile.path().toFile())) {
// Load class
this.loadClassOrFile(clazz.pathInJar(), inputStream.readAllBytes());
this.loadClassOrFile(externalFile.pathInJar(), inputStream.readAllBytes());

LOGGER.info("Loaded class: {}", clazz.pathInJar());
LOGGER.info("Loaded external file: {}", externalFile.pathInJar());
} catch (IOException e) {
LOGGER.error("Could not load class: {}", clazz.pathInJar(), e);
LOGGER.error("Could not load external file: {}", externalFile.pathInJar(), e);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,49 +12,58 @@
import uwu.narumi.deobfuscator.base.TestDeobfuscationBase;
import uwu.narumi.deobfuscator.transformer.TestSandboxSecurityTransformer;

import java.util.List;
import java.util.Map;

public class TestDeobfuscation extends TestDeobfuscationBase {

@Override
protected void registerAll() {
register("Inlining local variables", InputType.JAVA_CODE, List.of(
InlineLocalVariablesTransformer::new,
ComposedPeepholeCleanTransformer::new
), Source.of("TestInlineLocalVariables"));
register("Simple flow obfuscation", InputType.JAVA_CODE, List.of(ComposedGeneralFlowTransformer::new), Source.of("TestSimpleFlowObfuscation"));
register("Universal Number Transformer", InputType.JAVA_CODE, List.of(UniversalNumberTransformer::new), Source.of("TestUniversalNumberTransformer"));
test("Inlining local variables")
.transformers(InlineLocalVariablesTransformer::new, ComposedPeepholeCleanTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.JAVA_CODE, "TestInlineLocalVariables.class")
.register();
test("Simple flow obfuscation")
.transformers(ComposedGeneralFlowTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.JAVA_CODE, "TestSimpleFlowObfuscation.class")
.register();
test("Universal number transformer")
.transformers(UniversalNumberTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.JAVA_CODE, "TestUniversalNumberTransformer.class")
.register();
// TODO: Uninitialized static fields should replace with 0?
register("Inline static fields", InputType.JAVA_CODE, List.of(
InlineStaticFieldTransformer::new,
UselessPopCleanTransformer::new
), Source.of("TestInlineStaticFields"));
register("Inline static fields with modification", InputType.JAVA_CODE, List.of(
InlineStaticFieldTransformer::new,
UselessPopCleanTransformer::new
), Source.of("TestInlineStaticFieldsWithModification"));
test("Inline static fields")
.transformers(InlineStaticFieldTransformer::new, UselessPopCleanTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.JAVA_CODE, "TestInlineStaticFields.class")
.register();
test("Inline static fields with modification")
.transformers(InlineStaticFieldTransformer::new, UselessPopCleanTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.JAVA_CODE, "TestInlineStaticFieldsWithModification.class")
.register();

// Test sandbox security (e.g. not allowing dangerous calls)
register("Sandbox security", InputType.JAVA_CODE, List.of(TestSandboxSecurityTransformer::new), Source.of("sandbox/TestSandboxSecurity", false));
test("Sandbox security")
.transformers(TestSandboxSecurityTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.JAVA_CODE, "sandbox/TestSandboxSecurity.class")
.noDecompile()
.register();

// JSR Inlining
register("JSR Inlining", InputType.CUSTOM_CLASS, List.of(JsrInlinerTransformer::new), Source.of("JSR"));
test("JSR Inlining")
.transformers(JsrInlinerTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.CUSTOM_CLASS, "JSR.class")
.register();

// Samples
register("Some flow obf sample", InputType.CUSTOM_CLASS, List.of(ComposedGeneralFlowTransformer::new), Source.of("FlowObfSample"));
test("Some flow obf sample")
.transformers(ComposedGeneralFlowTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.CUSTOM_CLASS, "FlowObfSample.class")
.register();

// Zelix
register("Zelix (22.0.3) Sample 1", InputType.CUSTOM_CLASS, List.of(() -> new ComposedZelixTransformer(true)),
Source.of("zkm/sample1/ExampleClass"),
Source.of("zkm/sample1/ILongDecrypter", false),
Source.of("zkm/sample1/LongDecrypter1", false),
Source.of("zkm/sample1/LongDecrypter2", false),
// These are required to compute frames
Source.of("zkm/sample1/ByteBufUtil", true),
Source.of("zkm/sample1/ByteBufUtil_7", true),
Source.of("zkm/sample1/ByteBufUtil_8", true)
);
test("Zelix (22.0.3) Sample 1")
.transformers(() -> new ComposedZelixTransformer(true))
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_CLASS, "zkm/sample1")
.register();
// Obfuscated using this ZKM config (https://www.zelix.com/klassmaster/docs/langZKMScript.html):
/*
obfuscate changeLogFileIn=""
Expand All @@ -73,22 +82,14 @@ protected void registerAll() {
methodParameterChanges=flowObfuscate
obfuscateParameters=normal;
*/
register("Zelix (22.0.3) Sample 2 - Class initialization order", InputType.CUSTOM_CLASS,
List.of(
() -> new ComposedZelixTransformer(true,
// During obfuscation was specified classInitializationOrder option,
// so we need to also pass it here for correct decrypted values
Map.of("a.a.a.a.a4", "a.a.a.a.bc")
)
),
Source.of("zkm/sample2/bc"),
Source.of("zkm/sample2/a4"),
Source.of("zkm/sample2/ba"),
Source.of("zkm/sample2/a_"),
Source.of("zkm/sample2/ILongDecrypter", false),
Source.of("zkm/sample2/SimpleLongDecrypter", false),
Source.of("zkm/sample2/FallbackLongDecrypter", false)
);
test("Zelix (22.0.3) Sample 2 - Class initialization order")
.transformers(() -> new ComposedZelixTransformer(true,
// During obfuscation was specified classInitializationOrder option,
// so we need to also pass it here for correct decrypted values
Map.of("a.a.a.a.a4", "a.a.a.a.bc")
))
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_CLASS, "zkm/sample2")
.register();
// Obfuscated using the following ZKM config (https://www.zelix.com/klassmaster/docs/langZKMScript.html):
/*
obfuscate changeLogFileIn=""
Expand All @@ -108,10 +109,10 @@ protected void registerAll() {
methodParameterChanges=flowObfuscate
obfuscateParameters=normal;
*/
register("Zelix (21) Snake Game", InputType.CUSTOM_JAR,
List.of(() -> new ComposedZelixTransformer(true)),
Source.of("SnakeGame-obf-zkm")
);
test("Zelix (22.0.3) Sample 3 - Snake Game")
.transformers(() -> new ComposedZelixTransformer(true))
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_JAR, "SnakeGame-obf-zkm.jar")
.register();

// Zelix (22.0.3)
/*
Expand All @@ -129,21 +130,24 @@ protected void registerAll() {
autoReflectionHandling=normal
obfuscateReferences=none;
*/
register("Zelix (22.0.3) - String Encryption - Enhanced - Some strings", InputType.CUSTOM_CLASS,
List.of(ComposedZelixTransformer::new),
Source.of("zkm/EnhancedStringEncSomeStrings")
);
register("Zelix (22.0.3) - String Encryption - Enhanced - Many strings", InputType.CUSTOM_CLASS,
List.of(ComposedZelixTransformer::new),
Source.of("zkm/EnhancedStringEncManyStrings")
);
test("Zelix (22.0.3) - String Encryption - Enhanced - Some strings")
.transformers(ComposedZelixTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.CUSTOM_CLASS, "zkm/EnhancedStringEncSomeStrings.class")
.register();
test("Zelix (22.0.3) - String Encryption - Enhanced - Many strings")
.transformers(ComposedZelixTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.CUSTOM_CLASS, "zkm/EnhancedStringEncManyStrings.class")
.register();

// Example HP888 classes. Without packer.
register("HP888", InputType.CUSTOM_CLASS, List.of(ComposedHP888Transformer::new),
Source.of("hp888/ExampleClass"),
Source.of("hp888/Strings") // Obfuscated strings
);
test("HP888")
.transformers(ComposedHP888Transformer::new)
.input(OutputType.MULTIPLE_CLASSES, InputType.CUSTOM_CLASS, "hp888")
.register();

register("POP2 Sample", InputType.CUSTOM_CLASS, List.of(UselessPopCleanTransformer::new), Source.of("Pop2Sample"));
test("POP2 Sample")
.transformers(UselessPopCleanTransformer::new)
.input(OutputType.SINGLE_CLASS, InputType.CUSTOM_CLASS, "Pop2Sample.class")
.register();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,12 @@
*/
public class AssertingResultSaver implements IResultSaver {

private final Path root;
private final Path outputDir;

private boolean savedContent = false;

public AssertingResultSaver(TestDeobfuscationBase.InputType inputType, String jarRelativePath) {
this.root = inputType == TestDeobfuscationBase.InputType.CUSTOM_JAR
? TestDeobfuscationBase.RESULTS_CLASSES_PATH.resolve(inputType.directory()).resolve(jarRelativePath)
: TestDeobfuscationBase.RESULTS_CLASSES_PATH.resolve(inputType.directory());
public AssertingResultSaver(Path outputDir) {
this.outputDir = outputDir;
}

@Override
Expand All @@ -42,12 +40,10 @@ public void saveClassFile(String path, String qualifiedName, String entryName, S
// Replace CRLF with LF
content = content.replace("\r\n", "\n");

// The Vineflower implementations of IContextSource append .java
if (entryName.endsWith(".java")) {
entryName = entryName.substring(0, entryName.length() - 5);
}
// Remove file extension
entryName = entryName.substring(0, entryName.lastIndexOf('.'));

Path saveTo = this.root.resolve(entryName + ".dec");
Path saveTo = this.outputDir.resolve(entryName + ".dec");

try {
if (Files.exists(saveTo)) {
Expand Down
Loading

0 comments on commit 0841ae4

Please sign in to comment.