Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More tests and a bit of refactoring #75

Merged
merged 9 commits into from
Aug 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
16 changes: 12 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -19,28 +19,36 @@ 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.

## 🧰 Recommended tools
- [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) - IDE for Java development
- [Recaf](https://github.com/Col-E/Recaf) - Modern java bytecode editor. Use it to analyze obfuscated classes.

## 🪄 Transformers
### What are transformers?
Whole deobfuscation process is based on transformers. Transformers are smaller classes that are responsible for deobfuscating specific obfuscation techniques. In simple words - transformers are transforming obfuscated code into a more readable form.
Whole deobfuscation process is based on transformers. Transformers are smaller pieces that are responsible for deobfuscating specific obfuscation techniques. In simple words - transformers are transforming obfuscated code into a more readable form.

### How to create your own transformer?
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)
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
> <br>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,6 +418,23 @@ public boolean isNumberOperator() {
|| (this.getOpcode() >= I2L && this.getOpcode() <= I2S);
}

public boolean isVarLoad() {
return this.getOpcode() >= ILOAD && this.getOpcode() <= ALOAD;
}

public boolean isVarStore() {
return this.getOpcode() >= ISTORE && this.getOpcode() <= ASTORE;
}

public int sizeOnStack() {
if (this.isLong() || this.isDouble()) {
// Only long and double values take up two stack values
return 2;
} else {
return 1;
}
}

public boolean isJump() {
return this instanceof JumpInsnNode;
}
Expand Down Expand Up @@ -474,6 +491,15 @@ public AbstractInsnNode getPrevious(int offset) {
return current;
}

public InsnNode toPop() {
if (this.getOpcode() == LSTORE || this.getOpcode() == DSTORE || this.sizeOnStack() == 2) {
// Long and double values take up two stack values. Need to use POP2
return new InsnNode(POP2);
} else {
return new InsnNode(POP);
}
}

public <T extends AbstractInsnNode> T getPreviousAs() {
return (T) getPrevious();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,11 +207,11 @@ public OriginalSourceValue merge(final OriginalSourceValue value1, final Origina
return value1;
} else {
// OriginalSourceInterpreter start
OriginalSourceValue parentValue = null;
if (value1.copiedFrom != null && value2.copiedFrom != null) {
parentValue = this.merge(value1.copiedFrom, value2.copiedFrom);
OriginalSourceValue copiedFrom = null;
if (setUnion.size() == 1 && value1.copiedFrom != null && value2.copiedFrom != null) {
copiedFrom = this.merge(value1.copiedFrom, value2.copiedFrom);
}
return new OriginalSourceValue(Math.min(value1.size, value2.size), setUnion, parentValue);
return new OriginalSourceValue(Math.min(value1.size, value2.size), setUnion, copiedFrom);
// OriginalSourceInterpreter end
}
}
Expand All @@ -223,11 +223,11 @@ public OriginalSourceValue merge(final OriginalSourceValue value1, final Origina
setUnion.addAll(value2.insns);

// OriginalSourceInterpreter start
OriginalSourceValue parentValue = null;
if (value1.copiedFrom != null && value2.copiedFrom != null) {
parentValue = this.merge(value1.copiedFrom, value2.copiedFrom);
OriginalSourceValue copiedFrom = null;
if (setUnion.size() == 1 && value1.copiedFrom != null && value2.copiedFrom != null) {
copiedFrom = this.merge(value1.copiedFrom, value2.copiedFrom);
}
return new OriginalSourceValue(Math.min(value1.size, value2.size), setUnion, parentValue);
return new OriginalSourceValue(Math.min(value1.size, value2.size), setUnion, copiedFrom);
// OriginalSourceInterpreter end
}
return value1;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -32,4 +35,23 @@ public static void loadFilesFromZip(Path path, BiConsumer<String, byte[]> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.MethodNode;
import org.objectweb.asm.tree.analysis.Analyzer;
import org.objectweb.asm.tree.analysis.AnalyzerException;
import org.objectweb.asm.tree.analysis.BasicValue;
import org.objectweb.asm.tree.analysis.BasicVerifier;
import org.objectweb.asm.tree.analysis.SimpleVerifier;
import uwu.narumi.deobfuscator.api.asm.ClassWrapper;
import uwu.narumi.deobfuscator.api.context.Context;
import uwu.narumi.deobfuscator.api.exception.TransformerException;
Expand Down Expand Up @@ -79,6 +85,16 @@ private static boolean transform(
}

LOGGER.info("Ended {} transformer in {} ms", transformer.name(), (System.currentTimeMillis() - start));

// Bytecode verification
/*if (oldInstance == null && changed) {
// Verify if code is valid
try {
verifyBytecode(scope, context);
} catch (RuntimeException e) {
LOGGER.error("Transformer {} produced invalid bytecode", transformer.name(), e);
}
}*/
} catch (TransformerException e) {
LOGGER.error("! {}: {}", transformer.name(), e.getMessage());
} catch (Exception e) {
Expand All @@ -88,4 +104,20 @@ private static boolean transform(

return changed;
}

/**
* Verifies if the bytecode is valid
*/
private static void verifyBytecode(@Nullable ClassWrapper scope, Context context) throws IllegalStateException {
for (ClassWrapper classWrapper : context.classes(scope)) {
for (MethodNode methodNode : classWrapper.methods()) {
Analyzer<BasicValue> analyzer = new Analyzer<>(new BasicVerifier());
try {
analyzer.analyzeAndComputeMaxs(classWrapper.name(), methodNode);
} catch (AnalyzerException e) {
throw new IllegalStateException("Invalid bytecode in " + classWrapper.name() + "#" + methodNode.name + methodNode.desc, e);
}
}
}
}
}
Loading