Skip to content

Commit

Permalink
Implement stacktrace deobfuscation
Browse files Browse the repository at this point in the history
  • Loading branch information
JustRed23 committed Jun 30, 2024
1 parent 9636c55 commit efceeae
Show file tree
Hide file tree
Showing 5 changed files with 307 additions and 0 deletions.
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ dependencies {
api 'org.slf4j:slf4j-api:1.8.0-beta4'
implementation 'org.spongepowered:mixin:0.8.5'
implementation 'org.jetbrains:annotations:24.0.0'
implementation 'it.unimi.dsi:fastutil:8.5.12'

//ASM
implementation 'org.ow2.asm:asm:9.7'
implementation 'org.ow2.asm:asm-tree:9.7'
implementation 'org.ow2.asm:asm-util:9.7'
implementation 'org.ow2.asm:asm-commons:9.7'
implementation 'org.ow2.asm:asm-analysis:9.7'
}

jar {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.kettingpowered.ketting.common.deobf;

import java.util.Map;

public record ClassMapping(
String obfName,
String deobfName,
Map<String, String> methods
) {}
12 changes: 12 additions & 0 deletions src/main/java/org/kettingpowered/ketting/common/deobf/Mapping.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.kettingpowered.ketting.common.deobf;

public record Mapping(String obfMethod, String obfDesc, String mojang) {

static Mapping from(String obfMethod, String obfDesc, String mojang) {
return new Mapping(obfMethod, obfDesc, mojang);
}

public String obf() {
return obfMethod + obfDesc;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.kettingpowered.ketting.common.deobf;

import net.fabricmc.mappingio.MappingReader;
import net.fabricmc.mappingio.format.MappingFormat;
import net.fabricmc.mappingio.tree.MappingTree;
import net.fabricmc.mappingio.tree.MemoryMappingTree;
import org.jetbrains.annotations.Nullable;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

public class MappingLoader {

public final Map<String, ClassMapping> mappings;
public final Map<String, List<Mapping>> bukkitMappings = new HashMap<>();

MappingLoader(File mojmap, Map<String, String> bukkitMethods) throws IOException {
mappings = loadMappingsIfPresent(mojmap);
bukkitMethods.forEach(this::parseBukkitMappings);
}

private void parseBukkitMappings(String obf, String mojang) {
String raw = obf.substring(0, obf.indexOf('(') - 1);
int lastSlash = raw.lastIndexOf('/');
String classPath = raw.substring(0, lastSlash);
String methodName = raw.substring(lastSlash + 1);
String desc = obf.substring(obf.indexOf(methodName) + methodName.length() + 1).trim();
bukkitMappings.computeIfAbsent(classPath.replace('/', '.'), k -> new ArrayList<>()).add(Mapping.from(methodName, desc, mojang));
}

private @Nullable Map<String, ClassMapping> loadMappingsIfPresent(File mojmap) throws IOException {
try (final @Nullable InputStream mappingsInputStream = new FileInputStream(mojmap)) {
final MemoryMappingTree tree = new MemoryMappingTree();
MappingReader.read(new InputStreamReader(mappingsInputStream, StandardCharsets.UTF_8), MappingFormat.PROGUARD_FILE, tree);

String LEFT = "named";
String RIGHT = "official";

// Since the mapping works like 'named -> official', the source is technically the right side
tree.setSrcNamespace(RIGHT);
tree.setDstNamespaces(Collections.singletonList(LEFT));

final Set<ClassMapping> classes = new HashSet<>();

final StringPool pool = new StringPool();
for (final MappingTree.ClassMapping cls : tree.getClasses()) {
final Map<String, String> methods = new HashMap<>();

for (final MappingTree.MethodMapping methodMapping : cls.getMethods()) {
methods.put(
pool.string(methodKey(
methodMapping.getName(LEFT),
methodMapping.getDesc(LEFT)
)),
pool.string(methodMapping.getName(RIGHT))
);
}

final ClassMapping map = new ClassMapping(
cls.getName(LEFT).replace('/', '.'),
cls.getName(RIGHT).replace('/', '.'),
Map.copyOf(methods)
);
classes.add(map);
}

return Set.copyOf(classes).stream().collect(Collectors.toUnmodifiableMap(ClassMapping::deobfName, map -> map));
}
}

public String methodKey(final String obfName, final String obfDescriptor) {
return obfName + obfDescriptor;
}

public String getFromMojang(String className, String mojangName, String desc) {
if (mappings == null || mappings.isEmpty() || mojangName == null || mojangName.isBlank())
return mojangName;

ClassMapping mapping = mappings.get(className);
if (mapping == null)
return mojangName;

return mapping.methods().getOrDefault(methodKey(mojangName, desc), mojangName);
}

private static final class StringPool {
private final Map<String, String> pool = new HashMap<>();

public String string(final String string) {
return this.pool.computeIfAbsent(string, Function.identity());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package org.kettingpowered.ketting.common.deobf;

import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.util.*;

public class StacktraceDeobfuscator {

private static final Logger LOGGER = LoggerFactory.getLogger(StacktraceDeobfuscator.class);
private static MappingLoader mappingLoader;

private static boolean enabled;

private static final Map<Class<?>, Map<String, IntList>> lineMapCache = Collections.synchronizedMap(new LinkedHashMap<>(128, 0.75f, true) {
@Override
protected boolean removeEldestEntry(final Map.Entry<Class<?>, Map<String, IntList>> eldest) {
return this.size() > 127;
}
});

public static void init(File mojmap, Map<String, String> bukkitMethods) {
if (mappingLoader != null) return;

try {
mappingLoader = new MappingLoader(mojmap, bukkitMethods);
} catch (IOException e) {
LOGGER.error("Failed to load mappings", e);
return;
}

int mojmapSize = mappingLoader.mappings.size();
int bukkitSize = mappingLoader.bukkitMappings.values().stream().mapToInt(List::size).sum();
LOGGER.info("Loaded {} mappings [{} mojmap, {} bukkit]", mojmapSize + bukkitSize, mojmapSize, bukkitSize);
enabled = true;
}

public static void setEnabled(boolean enabled) {
StacktraceDeobfuscator.enabled = enabled;
}

public static void deobf(Throwable throwable) {
if (!enabled || mappingLoader == null) return;

throwable.setStackTrace(deobf(throwable.getStackTrace()));

final Throwable cause = throwable.getCause();
if (cause != null) deobf(cause);

Arrays.stream(throwable.getSuppressed()).forEach(StacktraceDeobfuscator::deobf);
}

public static StackTraceElement[] deobf(StackTraceElement[] stacktrace) {
if (!enabled || mappingLoader == null || stacktrace.length == 0) return stacktrace;

final StackTraceElement[] deobf = new StackTraceElement[stacktrace.length];
for (int i = 0; i < stacktrace.length; i++) {
final StackTraceElement element = stacktrace[i];
final String className = element.getClassName();

final List<Mapping> classMappings = mappingLoader.bukkitMappings.get(className);
if (classMappings == null) {
deobf[i] = element;
continue;
}

final Class<?> clazz;
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
LOGGER.error("Failed to find class for {}", className, e);
deobf[i] = element;
continue;
}

final String methodKey = determineMethodForLine(clazz, element.getLineNumber());

if (methodKey == null) {
deobf[i] = element;
continue;
}

final Mapping map = classMappings.stream().filter(m -> m.obf().equals(methodKey)).findFirst().orElse(null);

if (map == null) {
deobf[i] = element;
continue;
}

final String methodName = mappingLoader.getFromMojang(className, map.mojang(), map.obfDesc());

deobf[i] = new StackTraceElement(
element.getClassLoaderName(),
element.getModuleName(),
element.getModuleVersion(),
className,
Objects.equals(methodName, map.mojang()) ? element.getMethodName() : methodName,
sourceFileName(className),
element.getLineNumber()
);
}
return deobf;
}

private static @Nullable String determineMethodForLine(final Class<?> clazz, final int lineNumber) {
final Map<String, IntList> lineMap = lineMapCache.computeIfAbsent(clazz, StacktraceDeobfuscator::buildLineMap);
for (final var entry : lineMap.entrySet()) {
final String methodKey = entry.getKey();
final IntList lines = entry.getValue();
for (int i = 0, linesSize = lines.size(); i < linesSize; i++) {
final int num = lines.getInt(i);
if (num == lineNumber) {
return methodKey;
}
}
}
return null;
}

private static String sourceFileName(final String fullClassName) {
final int dot = fullClassName.lastIndexOf('.');
final String className = dot == -1
? fullClassName
: fullClassName.substring(dot + 1);
final String rootClassName = className.split("\\$")[0];
return rootClassName + ".java";
}

private static Map<String, IntList> buildLineMap(final Class<?> key) {
final Map<String, IntList> lineMap = new HashMap<>();
final class LineCollectingMethodVisitor extends MethodVisitor {
private final IntList lines = new IntArrayList();
private final String name;
private final String descriptor;

LineCollectingMethodVisitor(String name, String descriptor) {
super(Opcodes.ASM9);
this.name = name;
this.descriptor = descriptor;
}

@Override
public void visitLineNumber(int line, Label start) {
super.visitLineNumber(line, start);
this.lines.add(line);
}

@Override
public void visitEnd() {
super.visitEnd();
lineMap.put(mappingLoader.methodKey(this.name, this.descriptor), this.lines);
}
}
final ClassVisitor classVisitor = new ClassVisitor(Opcodes.ASM9) {
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
return new LineCollectingMethodVisitor(name, descriptor);
}
};
try {
final @Nullable InputStream inputStream = StacktraceDeobfuscator.class.getClassLoader()
.getResourceAsStream(key.getName().replace('.', '/') + ".class");
if (inputStream == null) {
throw new IllegalStateException("Could not find class file: " + key.getName());
}
final byte[] classData;
try (inputStream) {
classData = inputStream.readAllBytes();
}
final ClassReader reader = new ClassReader(classData);
reader.accept(classVisitor, 0);
} catch (final IOException ex) {
throw new RuntimeException(ex);
}
return lineMap;
}
}

0 comments on commit efceeae

Please sign in to comment.