/*******************************************************************************
* Copyright (c) 2015 Jeff Martin.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Lesser General Public
* License v3.0 which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/lgpl.html
*
* Contributors:
* Jeff Martin - initial API and implementation
******************************************************************************/
package cuchaz.enigma;
import com.google.common.base.Functions;
import com.google.common.base.Stopwatch;
import com.strobel.assembler.metadata.ITypeLoader;
import com.strobel.assembler.metadata.MetadataSystem;
import com.strobel.assembler.metadata.TypeDefinition;
import com.strobel.assembler.metadata.TypeReference;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.decompiler.languages.java.ast.CompilationUnit;
import cuchaz.enigma.analysis.EntryReference;
import cuchaz.enigma.analysis.IndexTreeBuilder;
import cuchaz.enigma.analysis.ParsedJar;
import cuchaz.enigma.analysis.index.JarIndex;
import cuchaz.enigma.api.EnigmaPlugin;
import cuchaz.enigma.bytecode.translators.SourceFixVisitor;
import cuchaz.enigma.bytecode.translators.TranslationClassVisitor;
import cuchaz.enigma.translation.Translatable;
import cuchaz.enigma.translation.Translator;
import cuchaz.enigma.translation.mapping.*;
import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
import cuchaz.enigma.translation.mapping.tree.EntryTree;
import cuchaz.enigma.translation.representation.entry.ClassEntry;
import cuchaz.enigma.translation.representation.entry.Entry;
import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
import cuchaz.enigma.translation.representation.entry.MethodEntry;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.tree.ClassNode;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.JarOutputStream;
import java.util.stream.Collectors;
public class Deobfuscator {
private final ServiceLoader plugins = ServiceLoader.load(EnigmaPlugin.class);
private final ParsedJar parsedJar;
private final JarIndex jarIndex;
private final IndexTreeBuilder indexTreeBuilder;
private final SourceProvider obfSourceProvider;
private EntryRemapper mapper;
public Deobfuscator(ParsedJar jar, Consumer listener) {
this.parsedJar = jar;
// build the jar index
this.jarIndex = JarIndex.empty();
this.jarIndex.indexJar(this.parsedJar, listener);
listener.accept("Initializing plugins...");
for (EnigmaPlugin plugin : getPlugins()) {
plugin.onClassesLoaded(parsedJar.getClassDataMap(), parsedJar::getClassNode);
}
this.indexTreeBuilder = new IndexTreeBuilder(jarIndex);
listener.accept("Preparing...");
CompiledSourceTypeLoader typeLoader = new CompiledSourceTypeLoader(parsedJar);
typeLoader.addVisitor(visitor -> new SourceFixVisitor(Opcodes.ASM5, visitor, jarIndex));
this.obfSourceProvider = new SourceProvider(SourceProvider.createSettings(), typeLoader);
// init mappings
mapper = new EntryRemapper(jarIndex);
}
public Deobfuscator(JarFile jar, Consumer listener) throws IOException {
this(new ParsedJar(jar), listener);
}
public Deobfuscator(ParsedJar jar) {
this(jar, (msg) -> {
});
}
public Deobfuscator(JarFile jar) throws IOException {
this(jar, (msg) -> {
});
}
public ServiceLoader getPlugins() {
return plugins;
}
public ParsedJar getJar() {
return this.parsedJar;
}
public JarIndex getJarIndex() {
return this.jarIndex;
}
public IndexTreeBuilder getIndexTreeBuilder() {
return indexTreeBuilder;
}
public EntryRemapper getMapper() {
return this.mapper;
}
public void setMappings(EntryTree mappings) {
setMappings(mappings, ProgressListener.VOID);
}
public void setMappings(EntryTree mappings, ProgressListener progress) {
if (mappings != null) {
Collection> dropped = dropMappings(mappings, progress);
mapper = new EntryRemapper(jarIndex, mappings);
DeltaTrackingTree obfToDeobf = mapper.getObfToDeobf();
for (Entry> entry : dropped) {
obfToDeobf.trackChange(entry);
}
} else {
mapper = new EntryRemapper(jarIndex);
}
}
private Collection> dropMappings(EntryTree mappings, ProgressListener progress) {
// drop mappings that don't match the jar
MappingsChecker checker = new MappingsChecker(jarIndex, mappings);
MappingsChecker.Dropped dropped = checker.dropBrokenMappings(progress);
Map, String> droppedMappings = dropped.getDroppedMappings();
for (Map.Entry, String> mapping : droppedMappings.entrySet()) {
System.out.println("WARNING: Couldn't find " + mapping.getKey() + " (" + mapping.getValue() + ") in jar. Mapping was dropped.");
}
return droppedMappings.keySet();
}
public void getSeparatedClasses(List obfClasses, List deobfClasses) {
for (ClassEntry obfClassEntry : this.jarIndex.getEntryIndex().getClasses()) {
// skip inner classes
if (obfClassEntry.isInnerClass()) {
continue;
}
// separate the classes
ClassEntry deobfClassEntry = mapper.deobfuscate(obfClassEntry);
if (!deobfClassEntry.equals(obfClassEntry)) {
// if the class has a mapping, clearly it's deobfuscated
deobfClasses.add(obfClassEntry);
} else if (obfClassEntry.getPackageName() != null) {
// also call it deobufscated if it's not in the none package
deobfClasses.add(obfClassEntry);
} else {
// otherwise, assume it's still obfuscated
obfClasses.add(obfClassEntry);
}
}
}
public SourceProvider getObfSourceProvider() {
return obfSourceProvider;
}
public void writeSources(Path outputDirectory, ProgressListener progress) {
// get the classes to decompile
Collection classEntries = jarIndex.getEntryIndex().getClasses();
Stopwatch stopwatch = Stopwatch.createStarted();
try {
Translator deobfuscator = mapper.getDeobfuscator();
// deobfuscate everything first
Map translatedNodes = deobfuscateClasses(progress, classEntries, deobfuscator);
decompileClasses(outputDirectory, progress, translatedNodes);
} finally {
stopwatch.stop();
System.out.println("writeSources Done in : " + stopwatch.toString());
}
}
private Map deobfuscateClasses(ProgressListener progress, Collection classEntries, Translator translator) {
AtomicInteger count = new AtomicInteger();
if (progress != null) {
progress.init(classEntries.size(), "Deobfuscating classes...");
}
return classEntries.parallelStream()
.map(entry -> {
ClassEntry translatedEntry = translator.translate(entry);
if (progress != null) {
progress.step(count.getAndIncrement(), translatedEntry.toString());
}
ClassNode node = parsedJar.getClassNode(entry.getFullName());
if (node != null) {
ClassNode translatedNode = new ClassNode();
node.accept(new TranslationClassVisitor(translator, Opcodes.ASM5, translatedNode));
return translatedNode;
}
return null;
})
.filter(Objects::nonNull)
.collect(Collectors.toMap(n -> n.name, Functions.identity()));
}
private void decompileClasses(Path outputDirectory, ProgressListener progress, Map translatedClasses) {
Collection decompileClasses = translatedClasses.values().stream()
.filter(classNode -> classNode.name.indexOf('$') == -1)
.collect(Collectors.toList());
if (progress != null) {
progress.init(decompileClasses.size(), "Decompiling classes...");
}
//create a common instance outside the loop as mappings shouldn't be changing while this is happening
CompiledSourceTypeLoader typeLoader = new CompiledSourceTypeLoader(translatedClasses::get);
typeLoader.addVisitor(visitor -> new SourceFixVisitor(Opcodes.ASM5, visitor, jarIndex));
//synchronized to make sure the parallelStream doesn't CME with the cache
ITypeLoader synchronizedTypeLoader = new SynchronizedTypeLoader(typeLoader);
MetadataSystem metadataSystem = new Deobfuscator.NoRetryMetadataSystem(synchronizedTypeLoader);
//ensures methods are loaded on classload and prevents race conditions
metadataSystem.setEagerMethodLoadingEnabled(true);
DecompilerSettings settings = SourceProvider.createSettings();
SourceProvider sourceProvider = new SourceProvider(settings, synchronizedTypeLoader, metadataSystem);
AtomicInteger count = new AtomicInteger();
decompileClasses.parallelStream().forEach(translatedNode -> {
if (progress != null) {
progress.step(count.getAndIncrement(), translatedNode.name);
}
decompileClass(outputDirectory, translatedNode, sourceProvider);
});
}
private void decompileClass(Path outputDirectory, ClassNode translatedNode, SourceProvider sourceProvider) {
try {
// get the source
CompilationUnit sourceTree = sourceProvider.getSources(translatedNode.name);
Path path = outputDirectory.resolve(translatedNode.name.replace('.', '/') + ".java");
Files.createDirectories(path.getParent());
try (Writer writer = Files.newBufferedWriter(path)) {
sourceProvider.writeSource(writer, sourceTree);
}
} catch (Throwable t) {
// don't crash the whole world here, just log the error and keep going
// TODO: set up logback via log4j
System.err.println("Unable to decompile class " + translatedNode.name);
t.printStackTrace(System.err);
}
}
public void writeTransformedJar(File out, ProgressListener progress) {
Translator deobfuscator = mapper.getDeobfuscator();
writeTransformedJar(out, progress, (node, visitor) -> {
ClassEntry entry = new ClassEntry(node.name);
node.accept(new TranslationClassVisitor(deobfuscator, Opcodes.ASM5, visitor));
return deobfuscator.translate(entry).getFullName();
});
}
public void writeTransformedJar(File out, ProgressListener progress, ClassTransformer transformer) {
try (JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) {
if (progress != null) {
progress.init(parsedJar.getClassCount(), "Transforming classes...");
}
AtomicInteger count = new AtomicInteger();
parsedJar.visitNode(node -> {
if (progress != null) {
progress.step(count.getAndIncrement(), node.name);
}
try {
ClassWriter writer = new ClassWriter(0);
String transformedName = transformer.transform(node, writer);
outJar.putNextEntry(new JarEntry(transformedName.replace('.', '/') + ".class"));
outJar.write(writer.toByteArray());
outJar.closeEntry();
} catch (Throwable t) {
throw new Error("Unable to transform class " + node.name, t);
}
});
} catch (IOException ex) {
throw new Error("Unable to write to Jar file!");
}
}
public AccessModifier getModifier(Entry> entry) {
EntryMapping mapping = mapper.getDeobfMapping(entry);
if (mapping == null) {
return AccessModifier.UNCHANGED;
}
return mapping.getAccessModifier();
}
public void changeModifier(Entry> entry, AccessModifier modifier) {
EntryMapping mapping = mapper.getDeobfMapping(entry);
if (mapping != null) {
mapper.mapFromObf(entry, new EntryMapping(mapping.getTargetName(), modifier));
} else {
mapper.mapFromObf(entry, new EntryMapping(entry.getName(), modifier));
}
}
public boolean isRenamable(Entry> obfEntry) {
if (obfEntry instanceof MethodEntry) {
// HACKHACK: Object methods are not obfuscated identifiers
MethodEntry obfMethodEntry = (MethodEntry) obfEntry;
String name = obfMethodEntry.getName();
String sig = obfMethodEntry.getDesc().toString();
if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) {
return false;
} else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) {
return false;
} else if (name.equals("finalize") && sig.equals("()V")) {
return false;
} else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) {
return false;
} else if (name.equals("hashCode") && sig.equals("()I")) {
return false;
} else if (name.equals("notify") && sig.equals("()V")) {
return false;
} else if (name.equals("notifyAll") && sig.equals("()V")) {
return false;
} else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) {
return false;
} else if (name.equals("wait") && sig.equals("()V")) {
return false;
} else if (name.equals("wait") && sig.equals("(J)V")) {
return false;
} else if (name.equals("wait") && sig.equals("(JI)V")) {
return false;
}
} else if (obfEntry instanceof LocalVariableEntry && !((LocalVariableEntry) obfEntry).isArgument()) {
return false;
}
return this.jarIndex.getEntryIndex().hasEntry(obfEntry);
}
public boolean isRenamable(EntryReference, Entry>> obfReference) {
return obfReference.isNamed() && isRenamable(obfReference.getNameableEntry());
}
public boolean isRemapped(Entry> entry) {
EntryResolver resolver = mapper.getObfResolver();
DeltaTrackingTree mappings = mapper.getObfToDeobf();
return resolver.resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT).stream()
.anyMatch(mappings::contains);
}
public void rename(Entry> obfEntry, String newName) {
mapper.mapFromObf(obfEntry, new EntryMapping(newName));
}
public void removeMapping(Entry> obfEntry) {
mapper.removeByObf(obfEntry);
}
public void markAsDeobfuscated(Entry> obfEntry) {
mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName()));
}
public T deobfuscate(T translatable) {
return mapper.deobfuscate(translatable);
}
public interface ClassTransformer {
String transform(ClassNode node, ClassVisitor visitor);
}
public static class NoRetryMetadataSystem extends MetadataSystem {
private final Set _failedTypes = Collections.newSetFromMap(new ConcurrentHashMap<>());
public NoRetryMetadataSystem(final ITypeLoader typeLoader) {
super(typeLoader);
}
@Override
protected synchronized TypeDefinition resolveType(final String descriptor, final boolean mightBePrimitive) {
if (_failedTypes.contains(descriptor)) {
return null;
}
final TypeDefinition result = super.resolveType(descriptor, mightBePrimitive);
if (result == null) {
_failedTypes.add(descriptor);
}
return result;
}
@Override
public synchronized TypeDefinition resolve(final TypeReference type) {
return super.resolve(type);
}
}
}