/*******************************************************************************
* 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.Stopwatch;
import com.google.common.collect.Lists;
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.DecompilerContext;
import com.strobel.decompiler.DecompilerSettings;
import com.strobel.decompiler.PlainTextOutput;
import com.strobel.decompiler.languages.java.JavaOutputVisitor;
import com.strobel.decompiler.languages.java.ast.AstBuilder;
import com.strobel.decompiler.languages.java.ast.CompilationUnit;
import com.strobel.decompiler.languages.java.ast.InsertParenthesesVisitor;
import com.strobel.decompiler.languages.java.ast.transforms.IAstTransform;
import cuchaz.enigma.analysis.*;
import cuchaz.enigma.analysis.index.JarIndex;
import cuchaz.enigma.api.EnigmaPlugin;
import cuchaz.enigma.translation.mapping.*;
import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
import cuchaz.enigma.translation.mapping.tree.EntryTree;
import cuchaz.enigma.translation.representation.ReferencedEntryPool;
import cuchaz.enigma.translation.representation.entry.ClassEntry;
import cuchaz.enigma.translation.representation.entry.Entry;
import cuchaz.enigma.translation.representation.entry.MethodEntry;
import cuchaz.enigma.utils.Utils;
import oml.ast.transformers.*;
import org.objectweb.asm.ClassWriter;
import org.objectweb.asm.tree.ClassNode;
import java.io.*;
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 ReferencedEntryPool entryPool = new ReferencedEntryPool();
private final ParsedJar parsedJar;
private final DecompilerSettings settings;
private final JarIndex jarIndex;
private final IndexTreeBuilder indexTreeBuilder;
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...");
// config the decompiler
this.settings = DecompilerSettings.javaDefaults();
this.settings.setMergeVariables(Utils.getSystemPropertyAsBoolean("enigma.mergeVariables", true));
this.settings.setForceExplicitImports(Utils.getSystemPropertyAsBoolean("enigma.forceExplicitImports", true));
this.settings.setForceExplicitTypeArguments(
Utils.getSystemPropertyAsBoolean("enigma.forceExplicitTypeArguments", true));
// DEBUG
this.settings.setShowDebugLineNumbers(Utils.getSystemPropertyAsBoolean("enigma.showDebugLineNumbers", false));
this.settings.setShowSyntheticMembers(Utils.getSystemPropertyAsBoolean("enigma.showSyntheticMembers", false));
// init mappings
mapper = new EntryRemapper(jarIndex);
}
public Deobfuscator(JarFile jar, Consumer listener) throws IOException {
this(new ParsedJar(jar), listener);
}
public Deobfuscator(ParsedJar jar) throws IOException {
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) {
if (mappings != null) {
Collection> dropped = dropMappings(mappings);
mapper = new EntryRemapper(jarIndex, mappings);
DeltaTrackingTree deobfToObf = mapper.getDeobfToObf();
for (Entry> entry : dropped) {
deobfToObf.trackDeletion(entry);
}
} else {
mapper = new EntryRemapper(jarIndex);
}
}
private Collection> dropMappings(EntryTree mappings) {
// drop mappings that don't match the jar
MappingsChecker checker = new MappingsChecker(jarIndex, mappings);
MappingsChecker.Dropped dropped = checker.dropBrokenMappings();
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(deobfClassEntry);
} 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 TranslatingTypeLoader createTypeLoader() {
return new TranslatingTypeLoader(
this.parsedJar,
this.jarIndex,
this.entryPool,
this.mapper.getObfuscator(),
this.mapper.getDeobfuscator()
);
}
public CompilationUnit getSourceTree(String className) {
return getSourceTree(className, createTypeLoader());
}
public CompilationUnit getSourceTree(String className, ITranslatingTypeLoader loader) {
return getSourceTree(className, loader, new NoRetryMetadataSystem(loader));
}
public CompilationUnit getSourceTree(String className, ITranslatingTypeLoader loader, MetadataSystem metadataSystem) {
// we don't know if this class name is obfuscated or deobfuscated
// we need to tell the decompiler the deobfuscated name so it doesn't get freaked out
// the decompiler only sees classes after deobfuscation, so we need to load it by the deobfuscated name if there is one
String deobfClassName = mapper.deobfuscate(new ClassEntry(className)).getFullName();
// set the desc loader
this.settings.setTypeLoader(loader);
// see if procyon can find the desc
TypeReference type = metadataSystem.lookupType(deobfClassName);
if (type == null) {
throw new Error(String.format("Unable to find desc: %s (deobf: %s)\nTried class names: %s",
className, deobfClassName, loader.getClassNamesToTry(deobfClassName)
));
}
TypeDefinition resolvedType = type.resolve();
// decompile it!
DecompilerContext context = new DecompilerContext();
context.setCurrentType(resolvedType);
context.setSettings(this.settings);
AstBuilder builder = new AstBuilder(context);
builder.addType(resolvedType);
builder.runTransformations(null);
runCustomTransforms(builder, context);
return builder.getCompilationUnit();
}
public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source) {
return getSourceIndex(sourceTree, source, true);
}
public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source, boolean ignoreBadTokens) {
// build the source index
SourceIndex index = new SourceIndex(source, ignoreBadTokens);
sourceTree.acceptVisitor(new SourceIndexVisitor(entryPool), index);
EntryResolver resolver = mapper.getDeobfResolver();
Collection tokens = Lists.newArrayList(index.referenceTokens());
// resolve all the classes in the source references
for (Token token : tokens) {
EntryReference, Entry>> deobfReference = index.getDeobfReference(token);
index.replaceDeobfReference(token, resolver.resolveFirstReference(deobfReference, ResolutionStrategy.RESOLVE_CLOSEST));
}
return index;
}
public String getSource(CompilationUnit sourceTree) {
// render the AST into source
StringWriter buf = new StringWriter();
sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null);
sourceTree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(buf), this.settings), null);
return buf.toString();
}
public void writeSources(File dirOut, ProgressListener progress) {
// get the classes to decompile
Set classEntries = jarIndex.getEntryIndex().getClasses().stream()
.filter(classEntry -> !classEntry.isInnerClass())
.collect(Collectors.toSet());
if (progress != null) {
progress.init(classEntries.size(), "Decompiling classes...");
}
//create a common instance outside the loop as mappings shouldn't be changing while this is happening
//synchronized to make sure the parallelStream doesn't CME with the cache
ITranslatingTypeLoader typeLoader = new SynchronizedTypeLoader(createTypeLoader());
MetadataSystem metadataSystem = new NoRetryMetadataSystem(typeLoader);
metadataSystem.setEagerMethodLoadingEnabled(true);//ensures methods are loaded on classload and prevents race conditions
// DEOBFUSCATE ALL THE THINGS!! @_@
Stopwatch stopwatch = Stopwatch.createStarted();
AtomicInteger count = new AtomicInteger();
classEntries.parallelStream().forEach(obfClassEntry -> {
ClassEntry deobfClassEntry = mapper.deobfuscate(obfClassEntry);
if (progress != null) {
progress.step(count.getAndIncrement(), deobfClassEntry.toString());
}
try {
// get the source
CompilationUnit sourceTree = getSourceTree(obfClassEntry.getName(), typeLoader, metadataSystem);
// write the file
File file = new File(dirOut, deobfClassEntry.getName().replace('.', '/') + ".java");
file.getParentFile().mkdirs();
try (Writer writer = new BufferedWriter(new FileWriter(file))) {
sourceTree.acceptVisitor(new InsertParenthesesVisitor(), null);
sourceTree.acceptVisitor(new JavaOutputVisitor(new PlainTextOutput(writer), settings), null);
}
} 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 " + deobfClassEntry + " (" + obfClassEntry + ")");
t.printStackTrace(System.err);
}
});
stopwatch.stop();
System.out.println("writeSources Done in : " + stopwatch.toString());
if (progress != null) {
progress.step(count.get(), "Done:");
}
}
public void writeJar(File out, ProgressListener progress) {
transformJar(out, progress, createTypeLoader()::transformInto);
}
public void transformJar(File out, ProgressListener progress, ClassTransformer transformer) {
try (JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) {
if (progress != null) {
progress.init(parsedJar.getClassCount(), "Transforming classes...");
}
AtomicInteger i = new AtomicInteger();
parsedJar.visitNode(node -> {
if (progress != null) {
progress.step(i.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);
}
});
if (progress != null) {
progress.step(i.get(), "Done!");
}
} 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 isObfuscatedIdentifier(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;
}
}
return this.jarIndex.getEntryIndex().hasEntry(obfEntry);
}
public boolean isRenameable(EntryReference, Entry>> obfReference) {
return obfReference.isNamed() && isObfuscatedIdentifier(obfReference.getNameableEntry());
}
public boolean hasDeobfuscatedName(Entry> obfEntry) {
return mapper.hasDeobfMapping(obfEntry);
}
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 static void runCustomTransforms(AstBuilder builder, DecompilerContext context) {
List transformers = Arrays.asList(
new ObfuscatedEnumSwitchRewriterTransform(context),
new VarargsFixer(context),
new RemoveObjectCasts(context),
new Java8Generics(),
new InvalidIdentifierFix()
);
for (IAstTransform transform : transformers) {
transform.run(builder.getCompilationUnit());
}
}
public interface ClassTransformer {
String transform(ClassNode node, ClassWriter writer);
}
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;
}
public synchronized TypeDefinition resolve(final TypeReference type) {
return super.resolve(type);
}
}
}