/******************************************************************************* * 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.google.common.collect.Maps; import com.google.common.collect.Sets; 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.bytecode.ClassProtectifier; import cuchaz.enigma.bytecode.ClassPublifier; import cuchaz.enigma.mapping.*; import cuchaz.enigma.mapping.entry.*; import cuchaz.enigma.throwables.IllegalNameException; import cuchaz.enigma.utils.Utils; import oml.ast.transformers.ObfuscatedEnumSwitchRewriterTransform; import oml.ast.transformers.RemoveObjectCasts; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; import java.io.*; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarOutputStream; public class Deobfuscator { private final ReferencedEntryPool entryPool = new ReferencedEntryPool(); private final ParsedJar parsedJar; private final DecompilerSettings settings; private final JarIndex jarIndex; private final MappingsRenamer renamer; private final Map translatorCache; private Mappings mappings; public Deobfuscator(ParsedJar jar) { this.parsedJar = jar; // build the jar index this.jarIndex = new JarIndex(entryPool); this.jarIndex.indexJar(this.parsedJar, true); // 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 defaults this.translatorCache = Maps.newTreeMap(); this.renamer = new MappingsRenamer(this.jarIndex, null, this.entryPool); // init mappings setMappings(new Mappings()); } public Deobfuscator(JarFile jar) throws IOException { this(new ParsedJar(jar)); } public ParsedJar getJar() { return this.parsedJar; } public JarIndex getJarIndex() { return this.jarIndex; } public Mappings getMappings() { return this.mappings; } public void setMappings(Mappings val) { setMappings(val, true); } public void setMappings(Mappings val, boolean warnAboutDrops) { if (val == null) { val = new Mappings(); } // drop mappings that don't match the jar MappingsChecker checker = new MappingsChecker(this.jarIndex); checker.dropBrokenMappings(val); if (warnAboutDrops) { for (Map.Entry mapping : checker.getDroppedClassMappings().entrySet()) { System.out.println("WARNING: Couldn't find class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); } for (Map.Entry mapping : checker.getDroppedInnerClassMappings().entrySet()) { System.out.println("WARNING: Couldn't find inner class entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); } for (Map.Entry mapping : checker.getDroppedFieldMappings().entrySet()) { System.out.println("WARNING: Couldn't find field entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); } for (Map.Entry mapping : checker.getDroppedMethodMappings().entrySet()) { System.out.println("WARNING: Couldn't find behavior entry " + mapping.getKey() + " (" + mapping.getValue().getDeobfName() + ") in jar. Mapping was dropped."); } } this.mappings = val; this.renamer.setMappings(mappings); this.translatorCache.clear(); } public Translator getTranslator(TranslationDirection direction) { return this.translatorCache.computeIfAbsent(direction, k -> this.mappings.getTranslator(direction, this.jarIndex.getTranslationIndex())); } public void getSeparatedClasses(List obfClasses, List deobfClasses) { for (ClassEntry obfClassEntry : this.jarIndex.getObfClassEntries()) { // skip inner classes if (obfClassEntry.isInnerClass()) { continue; } // separate the classes ClassEntry deobfClassEntry = deobfuscateEntry(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, getTranslator(TranslationDirection.OBFUSCATING), getTranslator(TranslationDirection.DEOBFUSCATING) ); } public CompilationUnit getSourceTree(String className) { return getSourceTree(className, createTypeLoader()); } public CompilationUnit getSourceTree(String className, ITranslatingTypeLoader loader) { // 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 // first, assume class name is deobf String deobfClassName = className; // if it wasn't actually deobf, then we can find a mapping for it and get the deobf name ClassMapping classMapping = this.mappings.getClassByObf(className); if (classMapping != null && classMapping.getDeobfName() != null) { deobfClassName = classMapping.getDeobfName(); } // set the desc loader this.settings.setTypeLoader(loader); // see if procyon can find the desc TypeReference type = new MetadataSystem(loader).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, null); } public SourceIndex getSourceIndex(CompilationUnit sourceTree, String source, Boolean ignoreBadTokens) { // build the source index SourceIndex index; if (ignoreBadTokens != null) { index = new SourceIndex(source, ignoreBadTokens); } else { index = new SourceIndex(source); } sourceTree.acceptVisitor(new SourceIndexVisitor(entryPool), index); // DEBUG // sourceTree.acceptVisitor( new TreeDumpVisitor( new File( "tree.txt" ) ), null ); // resolve all the classes in the source references for (Token token : index.referenceTokens()) { EntryReference deobfReference = index.getDeobfReference(token); // get the obfuscated entry Entry obfEntry = obfuscateEntry(deobfReference.entry); // try to resolve the class ClassEntry resolvedObfClassEntry = this.jarIndex.getTranslationIndex().resolveEntryOwner(obfEntry); if (resolvedObfClassEntry != null && !resolvedObfClassEntry.equals(obfEntry.getOwnerClassEntry())) { // change the class of the entry obfEntry = obfEntry.updateOwnership(resolvedObfClassEntry); // save the new deobfuscated reference deobfReference.entry = deobfuscateEntry(obfEntry); index.replaceDeobfReference(token, deobfReference); } // DEBUG // System.out.println( token + " -> " + reference + " -> " + index.getReferenceToken( reference ) ); } 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 = Sets.newHashSet(); for (ClassEntry obfClassEntry : this.jarIndex.getObfClassEntries()) { // skip inner classes if (obfClassEntry.isInnerClass()) { continue; } classEntries.add(obfClassEntry); } 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()); // DEOBFUSCATE ALL THE THINGS!! @_@ Stopwatch stopwatch = Stopwatch.createStarted(); AtomicInteger count = new AtomicInteger(); classEntries.parallelStream().forEach(obfClassEntry -> { ClassEntry deobfClassEntry = deobfuscateEntry(new ClassEntry(obfClassEntry)); if (progress != null) { progress.onProgress(count.getAndIncrement(), deobfClassEntry.toString()); } try { // get the source CompilationUnit sourceTree = getSourceTree(obfClassEntry.getName(), typeLoader); // 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 deobfuscate class " + deobfClassEntry + " (" + obfClassEntry + ")"); t.printStackTrace(System.err); } }); stopwatch.stop(); System.out.println("writeSources Done in : " + stopwatch.toString()); if (progress != null) { progress.onProgress(count.get(), "Done:"); } } private void addAllPotentialAncestors(Set classEntries, ClassEntry classObfEntry) { for (ClassEntry interfaceEntry : jarIndex.getTranslationIndex().getInterfaces(classObfEntry)) { if (classEntries.add(interfaceEntry)) { addAllPotentialAncestors(classEntries, interfaceEntry); } } ClassEntry superClassEntry = jarIndex.getTranslationIndex().getSuperclass(classObfEntry); if (superClassEntry != null && classEntries.add(superClassEntry)) { addAllPotentialAncestors(classEntries, superClassEntry); } } public boolean isMethodProvider(MethodEntry methodEntry) { Set classEntries = new HashSet<>(); addAllPotentialAncestors(classEntries, methodEntry.getOwnerClassEntry()); for (ClassEntry parentEntry : classEntries) { MethodEntry ancestorMethodEntry = entryPool.getMethod(parentEntry, methodEntry.getName(), methodEntry.getDesc()); if (jarIndex.containsObfMethod(ancestorMethodEntry)) { return false; } } return true; } @Deprecated public boolean isMethodProvider(ClassEntry classObfEntry, MethodEntry methodEntry) { Set classEntries = new HashSet<>(); addAllPotentialAncestors(classEntries, classObfEntry); for (ClassEntry parentEntry : classEntries) { MethodEntry ancestorMethodEntry = entryPool.getMethod(parentEntry, methodEntry.getName(), methodEntry.getDesc()); if (jarIndex.containsObfMethod(ancestorMethodEntry)) { return false; } } return true; } public void rebuildMethodNames(ProgressListener progress) { int i = 0; Map> renameClassMap = new HashMap<>(); progress.init(getMappings().classes().size() * 3, "Rebuilding method names"); for (ClassMapping classMapping : Lists.newArrayList(getMappings().classes())) { progress.onProgress(i++, classMapping.getDeobfName()); rebuildMethodNames(classMapping, renameClassMap); } for (Map.Entry> renameClassMapEntry : renameClassMap.entrySet()) { progress.onProgress(i++, renameClassMapEntry.getKey().getDeobfName()); for (Map.Entry entry : renameClassMapEntry.getValue().entrySet()) { Entry obfEntry = entry.getKey(); removeMapping(obfEntry); } } for (Map.Entry> renameClassMapEntry : renameClassMap.entrySet()) { progress.onProgress(i++, renameClassMapEntry.getKey().getDeobfName()); for (Map.Entry entry : renameClassMapEntry.getValue().entrySet()) { Entry obfEntry = entry.getKey(); String name = entry.getValue(); if (name != null) { try { rename(obfEntry, name); } catch (IllegalNameException exception) { System.out.println("WARNING: " + exception.getMessage()); } } } } } private void rebuildMethodNames(ClassMapping classMapping, Map> renameClassMap) { Map renameEntries = new HashMap<>(); for (MethodMapping methodMapping : Lists.newArrayList(classMapping.methods())) { ClassEntry classObfEntry = classMapping.getObfEntry(); MethodEntry obfEntry = methodMapping.getObfEntry(classObfEntry); boolean isProvider = isMethodProvider(obfEntry); if (hasDeobfuscatedName(obfEntry) && !(methodMapping.getDeobfName().equals(methodMapping.getObfName()))) { renameEntries.put(obfEntry, isProvider ? methodMapping.getDeobfName() : null); } if (isProvider) { for (LocalVariableMapping localVariableMapping : methodMapping.arguments()) { Entry argObfEntry = localVariableMapping.getObfEntry(obfEntry); if (hasDeobfuscatedName(argObfEntry)) { renameEntries.put(argObfEntry, deobfuscateEntry(argObfEntry).getName()); } } } } classMapping.markDirty(); renameClassMap.put(classMapping, renameEntries); for (ClassMapping innerClass : classMapping.innerClasses()) { rebuildMethodNames(innerClass, renameClassMap); } } public void writeJar(File out, ProgressListener progress) { transformJar(out, progress, createTypeLoader()::transformInto); } public void protectifyJar(File out, ProgressListener progress) { transformJar(out, progress, (node, writer) -> { node.accept(new ClassProtectifier(Opcodes.ASM5, writer)); return node.name; }); } public void publifyJar(File out, ProgressListener progress) { transformJar(out, progress, (node, writer) -> { node.accept(new ClassPublifier(Opcodes.ASM5, writer)); return node.name; }); } 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.visit(node -> { if (progress != null) { progress.onProgress(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.onProgress(i.get(), "Done!"); } } catch (IOException ex) { throw new Error("Unable to write to Jar file!"); } } public T obfuscateEntry(T deobfEntry) { if (deobfEntry == null) { return null; } T translatedEntry = getTranslator(TranslationDirection.OBFUSCATING).getTranslatedEntry(deobfEntry); if (translatedEntry == null) { return deobfEntry; } return translatedEntry; } public T deobfuscateEntry(T obfEntry) { if (obfEntry == null) { return null; } T translatedEntry = getTranslator(TranslationDirection.DEOBFUSCATING).getTranslatedEntry(obfEntry); if (translatedEntry == null) { return obfEntry; } return translatedEntry; } public EntryReference obfuscateReference(EntryReference deobfReference) { if (deobfReference == null) { return null; } return new EntryReference<>(obfuscateEntry(deobfReference.entry), obfuscateEntry(deobfReference.context), deobfReference); } public EntryReference deobfuscateReference(EntryReference obfReference) { if (obfReference == null) { return null; } return new EntryReference<>(deobfuscateEntry(obfReference.entry), deobfuscateEntry(obfReference.context), obfReference); } public boolean isObfuscatedIdentifier(Entry obfEntry) { return isObfuscatedIdentifier(obfEntry, false); } public boolean isObfuscatedIdentifier(Entry obfEntry, boolean hack) { 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; } // FIXME: HACK EVEN MORE HACK! if (hack && this.jarIndex.containsObfEntry(obfEntry.getOwnerClassEntry())) return true; } return this.jarIndex.containsObfEntry(obfEntry); } public boolean isRenameable(EntryReference obfReference, boolean activeHack) { return obfReference.isNamed() && isObfuscatedIdentifier(obfReference.getNameableEntry(), activeHack); } public boolean isRenameable(EntryReference obfReference) { return isRenameable(obfReference, false); } public boolean hasDeobfuscatedName(Entry obfEntry) { Translator translator = getTranslator(TranslationDirection.DEOBFUSCATING); if (obfEntry instanceof ClassEntry) { ClassEntry obfClass = (ClassEntry) obfEntry; List mappingChain = this.mappings.getClassMappingChain(obfClass); ClassMapping classMapping = mappingChain.get(mappingChain.size() - 1); return classMapping != null && classMapping.getDeobfName() != null; } else if (obfEntry instanceof FieldEntry) { return translator.hasFieldMapping((FieldEntry) obfEntry); } else if (obfEntry instanceof MethodEntry) { MethodEntry methodEntry = (MethodEntry) obfEntry; if (methodEntry.isConstructor()) { return false; } return translator.hasMethodMapping(methodEntry); } else if (obfEntry instanceof LocalVariableEntry) { return translator.hasLocalVariableMapping((LocalVariableEntry) obfEntry); } else { throw new Error("Unknown entry desc: " + obfEntry.getClass().getName()); } } public void rename(Entry obfEntry, String newName) { rename(obfEntry, newName, true); } // NOTE: these methods are a bit messy... oh well public void rename(Entry obfEntry, String newName, boolean clearCache) { if (obfEntry instanceof ClassEntry) { this.renamer.setClassName((ClassEntry) obfEntry, newName); } else if (obfEntry instanceof FieldEntry) { this.renamer.setFieldName((FieldEntry) obfEntry, newName); } else if (obfEntry instanceof MethodEntry) { if (((MethodEntry) obfEntry).isConstructor()) { throw new IllegalArgumentException("Cannot rename constructors"); } this.renamer.setMethodTreeName((MethodEntry) obfEntry, newName); } else if (obfEntry instanceof LocalVariableEntry) { this.renamer.setLocalVariableTreeName((LocalVariableEntry) obfEntry, newName); } else { throw new Error("Unknown entry desc: " + obfEntry.getClass().getName()); } // clear caches if (clearCache) this.translatorCache.clear(); } public void removeMapping(Entry obfEntry) { if (obfEntry instanceof ClassEntry) { this.renamer.removeClassMapping((ClassEntry) obfEntry); } else if (obfEntry instanceof FieldEntry) { this.renamer.removeFieldMapping((FieldEntry) obfEntry); } else if (obfEntry instanceof MethodEntry) { if (((MethodEntry) obfEntry).isConstructor()) { throw new IllegalArgumentException("Cannot rename constructors"); } this.renamer.removeMethodTreeMapping((MethodEntry) obfEntry); } else if (obfEntry instanceof LocalVariableEntry) { this.renamer.removeLocalVariableMapping((LocalVariableEntry) obfEntry); } else { throw new Error("Unknown entry desc: " + obfEntry); } // clear caches this.translatorCache.clear(); } public void markAsDeobfuscated(Entry obfEntry) { if (obfEntry instanceof ClassEntry) { this.renamer.markClassAsDeobfuscated((ClassEntry) obfEntry); } else if (obfEntry instanceof FieldEntry) { this.renamer.markFieldAsDeobfuscated((FieldEntry) obfEntry); } else if (obfEntry instanceof MethodEntry) { MethodEntry methodEntry = (MethodEntry) obfEntry; if (methodEntry.isConstructor()) { throw new IllegalArgumentException("Cannot rename constructors"); } this.renamer.markMethodTreeAsDeobfuscated(methodEntry); } else if (obfEntry instanceof LocalVariableEntry) { this.renamer.markArgumentAsDeobfuscated((LocalVariableEntry) obfEntry); } else { throw new Error("Unknown entry desc: " + obfEntry); } // clear caches this.translatorCache.clear(); } public void changeModifier(Entry entry, Mappings.EntryModifier modifierEntry) { Entry obfEntry = obfuscateEntry(entry); if (obfEntry instanceof ClassEntry) this.renamer.setClassModifier((ClassEntry) obfEntry, modifierEntry); else if (obfEntry instanceof FieldEntry) this.renamer.setFieldModifier((FieldEntry) obfEntry, modifierEntry); else if (obfEntry instanceof MethodEntry) this.renamer.setMethodModifier((MethodEntry) obfEntry, modifierEntry); else throw new Error("Unknown entry desc: " + obfEntry); } public Mappings.EntryModifier getModifier(Entry obfEntry) { Entry entry = obfuscateEntry(obfEntry); if (entry != null) obfEntry = entry; if (obfEntry instanceof ClassEntry) return this.renamer.getClassModifier((ClassEntry) obfEntry); else if (obfEntry instanceof FieldEntry) return this.renamer.getFieldModifier((FieldEntry) obfEntry); else if (obfEntry instanceof MethodEntry) return this.renamer.getMethodModfifier((MethodEntry) obfEntry); else throw new Error("Unknown entry desc: " + obfEntry); } public static void runCustomTransforms(AstBuilder builder, DecompilerContext context){ List transformers = Arrays.asList( new ObfuscatedEnumSwitchRewriterTransform(context), new RemoveObjectCasts(context) ); for (IAstTransform transform : transformers){ transform.run(builder.getCompilationUnit()); } } public interface ProgressListener { void init(int totalWork, String title); void onProgress(int numDone, String message); } public interface ClassTransformer { String transform(ClassNode node, ClassWriter writer); } }