diff options
Diffstat (limited to 'src/main/java/cuchaz/enigma/Deobfuscator.java')
| -rw-r--r-- | src/main/java/cuchaz/enigma/Deobfuscator.java | 432 |
1 files changed, 0 insertions, 432 deletions
diff --git a/src/main/java/cuchaz/enigma/Deobfuscator.java b/src/main/java/cuchaz/enigma/Deobfuscator.java deleted file mode 100644 index b4736d8..0000000 --- a/src/main/java/cuchaz/enigma/Deobfuscator.java +++ /dev/null | |||
| @@ -1,432 +0,0 @@ | |||
| 1 | /******************************************************************************* | ||
| 2 | * Copyright (c) 2015 Jeff Martin. | ||
| 3 | * All rights reserved. This program and the accompanying materials | ||
| 4 | * are made available under the terms of the GNU Lesser General Public | ||
| 5 | * License v3.0 which accompanies this distribution, and is available at | ||
| 6 | * http://www.gnu.org/licenses/lgpl.html | ||
| 7 | * <p> | ||
| 8 | * Contributors: | ||
| 9 | * Jeff Martin - initial API and implementation | ||
| 10 | ******************************************************************************/ | ||
| 11 | |||
| 12 | package cuchaz.enigma; | ||
| 13 | |||
| 14 | import com.google.common.base.Functions; | ||
| 15 | import com.google.common.base.Stopwatch; | ||
| 16 | import com.strobel.assembler.metadata.ITypeLoader; | ||
| 17 | import com.strobel.assembler.metadata.MetadataSystem; | ||
| 18 | import com.strobel.assembler.metadata.TypeDefinition; | ||
| 19 | import com.strobel.assembler.metadata.TypeReference; | ||
| 20 | import com.strobel.decompiler.DecompilerSettings; | ||
| 21 | import com.strobel.decompiler.languages.java.ast.CompilationUnit; | ||
| 22 | import cuchaz.enigma.analysis.EntryReference; | ||
| 23 | import cuchaz.enigma.analysis.IndexTreeBuilder; | ||
| 24 | import cuchaz.enigma.analysis.ParsedJar; | ||
| 25 | import cuchaz.enigma.analysis.index.JarIndex; | ||
| 26 | import cuchaz.enigma.api.EnigmaPlugin; | ||
| 27 | import cuchaz.enigma.bytecode.translators.SourceFixVisitor; | ||
| 28 | import cuchaz.enigma.bytecode.translators.TranslationClassVisitor; | ||
| 29 | import cuchaz.enigma.translation.Translatable; | ||
| 30 | import cuchaz.enigma.translation.Translator; | ||
| 31 | import cuchaz.enigma.translation.mapping.*; | ||
| 32 | import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree; | ||
| 33 | import cuchaz.enigma.translation.mapping.tree.EntryTree; | ||
| 34 | import cuchaz.enigma.translation.representation.entry.ClassEntry; | ||
| 35 | import cuchaz.enigma.translation.representation.entry.Entry; | ||
| 36 | import cuchaz.enigma.translation.representation.entry.LocalVariableEntry; | ||
| 37 | import cuchaz.enigma.translation.representation.entry.MethodEntry; | ||
| 38 | import org.objectweb.asm.ClassVisitor; | ||
| 39 | import org.objectweb.asm.ClassWriter; | ||
| 40 | import org.objectweb.asm.Opcodes; | ||
| 41 | import org.objectweb.asm.tree.ClassNode; | ||
| 42 | |||
| 43 | import java.io.File; | ||
| 44 | import java.io.FileOutputStream; | ||
| 45 | import java.io.IOException; | ||
| 46 | import java.io.Writer; | ||
| 47 | import java.nio.file.Files; | ||
| 48 | import java.nio.file.Path; | ||
| 49 | import java.util.*; | ||
| 50 | import java.util.concurrent.ConcurrentHashMap; | ||
| 51 | import java.util.concurrent.atomic.AtomicInteger; | ||
| 52 | import java.util.function.Consumer; | ||
| 53 | import java.util.jar.JarEntry; | ||
| 54 | import java.util.jar.JarFile; | ||
| 55 | import java.util.jar.JarOutputStream; | ||
| 56 | import java.util.stream.Collectors; | ||
| 57 | |||
| 58 | public class Deobfuscator { | ||
| 59 | |||
| 60 | private final ServiceLoader<EnigmaPlugin> plugins = ServiceLoader.load(EnigmaPlugin.class); | ||
| 61 | private final ParsedJar parsedJar; | ||
| 62 | private final JarIndex jarIndex; | ||
| 63 | private final IndexTreeBuilder indexTreeBuilder; | ||
| 64 | |||
| 65 | private final SourceProvider obfSourceProvider; | ||
| 66 | |||
| 67 | private EntryRemapper mapper; | ||
| 68 | |||
| 69 | public Deobfuscator(ParsedJar jar, Consumer<String> listener) { | ||
| 70 | this.parsedJar = jar; | ||
| 71 | |||
| 72 | // build the jar index | ||
| 73 | this.jarIndex = JarIndex.empty(); | ||
| 74 | this.jarIndex.indexJar(this.parsedJar, listener); | ||
| 75 | |||
| 76 | listener.accept("Initializing plugins..."); | ||
| 77 | for (EnigmaPlugin plugin : getPlugins()) { | ||
| 78 | plugin.onClassesLoaded(parsedJar.getClassDataMap(), parsedJar::getClassNode); | ||
| 79 | } | ||
| 80 | |||
| 81 | this.indexTreeBuilder = new IndexTreeBuilder(jarIndex); | ||
| 82 | |||
| 83 | listener.accept("Preparing..."); | ||
| 84 | |||
| 85 | CompiledSourceTypeLoader typeLoader = new CompiledSourceTypeLoader(parsedJar); | ||
| 86 | typeLoader.addVisitor(visitor -> new SourceFixVisitor(Opcodes.ASM5, visitor, jarIndex)); | ||
| 87 | |||
| 88 | this.obfSourceProvider = new SourceProvider(SourceProvider.createSettings(), typeLoader); | ||
| 89 | |||
| 90 | // init mappings | ||
| 91 | mapper = new EntryRemapper(jarIndex); | ||
| 92 | } | ||
| 93 | |||
| 94 | public Deobfuscator(JarFile jar, Consumer<String> listener) throws IOException { | ||
| 95 | this(new ParsedJar(jar), listener); | ||
| 96 | } | ||
| 97 | |||
| 98 | public Deobfuscator(ParsedJar jar) { | ||
| 99 | this(jar, (msg) -> { | ||
| 100 | }); | ||
| 101 | } | ||
| 102 | |||
| 103 | public Deobfuscator(JarFile jar) throws IOException { | ||
| 104 | this(jar, (msg) -> { | ||
| 105 | }); | ||
| 106 | } | ||
| 107 | |||
| 108 | public ServiceLoader<EnigmaPlugin> getPlugins() { | ||
| 109 | return plugins; | ||
| 110 | } | ||
| 111 | |||
| 112 | public ParsedJar getJar() { | ||
| 113 | return this.parsedJar; | ||
| 114 | } | ||
| 115 | |||
| 116 | public JarIndex getJarIndex() { | ||
| 117 | return this.jarIndex; | ||
| 118 | } | ||
| 119 | |||
| 120 | public IndexTreeBuilder getIndexTreeBuilder() { | ||
| 121 | return indexTreeBuilder; | ||
| 122 | } | ||
| 123 | |||
| 124 | public EntryRemapper getMapper() { | ||
| 125 | return this.mapper; | ||
| 126 | } | ||
| 127 | |||
| 128 | public void setMappings(EntryTree<EntryMapping> mappings) { | ||
| 129 | setMappings(mappings, ProgressListener.VOID); | ||
| 130 | } | ||
| 131 | |||
| 132 | public void setMappings(EntryTree<EntryMapping> mappings, ProgressListener progress) { | ||
| 133 | if (mappings != null) { | ||
| 134 | Collection<Entry<?>> dropped = dropMappings(mappings, progress); | ||
| 135 | mapper = new EntryRemapper(jarIndex, mappings); | ||
| 136 | |||
| 137 | DeltaTrackingTree<EntryMapping> obfToDeobf = mapper.getObfToDeobf(); | ||
| 138 | for (Entry<?> entry : dropped) { | ||
| 139 | obfToDeobf.trackChange(entry); | ||
| 140 | } | ||
| 141 | } else { | ||
| 142 | mapper = new EntryRemapper(jarIndex); | ||
| 143 | } | ||
| 144 | } | ||
| 145 | |||
| 146 | private Collection<Entry<?>> dropMappings(EntryTree<EntryMapping> mappings, ProgressListener progress) { | ||
| 147 | // drop mappings that don't match the jar | ||
| 148 | MappingsChecker checker = new MappingsChecker(jarIndex, mappings); | ||
| 149 | MappingsChecker.Dropped dropped = checker.dropBrokenMappings(progress); | ||
| 150 | |||
| 151 | Map<Entry<?>, String> droppedMappings = dropped.getDroppedMappings(); | ||
| 152 | for (Map.Entry<Entry<?>, String> mapping : droppedMappings.entrySet()) { | ||
| 153 | System.out.println("WARNING: Couldn't find " + mapping.getKey() + " (" + mapping.getValue() + ") in jar. Mapping was dropped."); | ||
| 154 | } | ||
| 155 | |||
| 156 | return droppedMappings.keySet(); | ||
| 157 | } | ||
| 158 | |||
| 159 | public void getSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> deobfClasses) { | ||
| 160 | for (ClassEntry obfClassEntry : this.jarIndex.getEntryIndex().getClasses()) { | ||
| 161 | // skip inner classes | ||
| 162 | if (obfClassEntry.isInnerClass()) { | ||
| 163 | continue; | ||
| 164 | } | ||
| 165 | |||
| 166 | // separate the classes | ||
| 167 | ClassEntry deobfClassEntry = mapper.deobfuscate(obfClassEntry); | ||
| 168 | if (!deobfClassEntry.equals(obfClassEntry)) { | ||
| 169 | // if the class has a mapping, clearly it's deobfuscated | ||
| 170 | deobfClasses.add(obfClassEntry); | ||
| 171 | } else if (obfClassEntry.getPackageName() != null) { | ||
| 172 | // also call it deobufscated if it's not in the none package | ||
| 173 | deobfClasses.add(obfClassEntry); | ||
| 174 | } else { | ||
| 175 | // otherwise, assume it's still obfuscated | ||
| 176 | obfClasses.add(obfClassEntry); | ||
| 177 | } | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | public SourceProvider getObfSourceProvider() { | ||
| 182 | return obfSourceProvider; | ||
| 183 | } | ||
| 184 | |||
| 185 | public void writeSources(Path outputDirectory, ProgressListener progress) { | ||
| 186 | // get the classes to decompile | ||
| 187 | Collection<ClassEntry> classEntries = jarIndex.getEntryIndex().getClasses(); | ||
| 188 | |||
| 189 | Stopwatch stopwatch = Stopwatch.createStarted(); | ||
| 190 | |||
| 191 | try { | ||
| 192 | Translator deobfuscator = mapper.getDeobfuscator(); | ||
| 193 | |||
| 194 | // deobfuscate everything first | ||
| 195 | Map<String, ClassNode> translatedNodes = deobfuscateClasses(progress, classEntries, deobfuscator); | ||
| 196 | |||
| 197 | decompileClasses(outputDirectory, progress, translatedNodes); | ||
| 198 | } finally { | ||
| 199 | stopwatch.stop(); | ||
| 200 | |||
| 201 | System.out.println("writeSources Done in : " + stopwatch.toString()); | ||
| 202 | } | ||
| 203 | } | ||
| 204 | |||
| 205 | private Map<String, ClassNode> deobfuscateClasses(ProgressListener progress, Collection<ClassEntry> classEntries, Translator translator) { | ||
| 206 | AtomicInteger count = new AtomicInteger(); | ||
| 207 | if (progress != null) { | ||
| 208 | progress.init(classEntries.size(), "Deobfuscating classes..."); | ||
| 209 | } | ||
| 210 | |||
| 211 | return classEntries.parallelStream() | ||
| 212 | .map(entry -> { | ||
| 213 | ClassEntry translatedEntry = translator.translate(entry); | ||
| 214 | if (progress != null) { | ||
| 215 | progress.step(count.getAndIncrement(), translatedEntry.toString()); | ||
| 216 | } | ||
| 217 | |||
| 218 | ClassNode node = parsedJar.getClassNode(entry.getFullName()); | ||
| 219 | if (node != null) { | ||
| 220 | ClassNode translatedNode = new ClassNode(); | ||
| 221 | node.accept(new TranslationClassVisitor(translator, Opcodes.ASM5, translatedNode)); | ||
| 222 | return translatedNode; | ||
| 223 | } | ||
| 224 | |||
| 225 | return null; | ||
| 226 | }) | ||
| 227 | .filter(Objects::nonNull) | ||
| 228 | .collect(Collectors.toMap(n -> n.name, Functions.identity())); | ||
| 229 | } | ||
| 230 | |||
| 231 | private void decompileClasses(Path outputDirectory, ProgressListener progress, Map<String, ClassNode> translatedClasses) { | ||
| 232 | Collection<ClassNode> decompileClasses = translatedClasses.values().stream() | ||
| 233 | .filter(classNode -> classNode.name.indexOf('$') == -1) | ||
| 234 | .collect(Collectors.toList()); | ||
| 235 | |||
| 236 | if (progress != null) { | ||
| 237 | progress.init(decompileClasses.size(), "Decompiling classes..."); | ||
| 238 | } | ||
| 239 | |||
| 240 | //create a common instance outside the loop as mappings shouldn't be changing while this is happening | ||
| 241 | CompiledSourceTypeLoader typeLoader = new CompiledSourceTypeLoader(translatedClasses::get); | ||
| 242 | typeLoader.addVisitor(visitor -> new SourceFixVisitor(Opcodes.ASM5, visitor, jarIndex)); | ||
| 243 | |||
| 244 | //synchronized to make sure the parallelStream doesn't CME with the cache | ||
| 245 | ITypeLoader synchronizedTypeLoader = new SynchronizedTypeLoader(typeLoader); | ||
| 246 | |||
| 247 | MetadataSystem metadataSystem = new Deobfuscator.NoRetryMetadataSystem(synchronizedTypeLoader); | ||
| 248 | |||
| 249 | //ensures methods are loaded on classload and prevents race conditions | ||
| 250 | metadataSystem.setEagerMethodLoadingEnabled(true); | ||
| 251 | |||
| 252 | DecompilerSettings settings = SourceProvider.createSettings(); | ||
| 253 | SourceProvider sourceProvider = new SourceProvider(settings, synchronizedTypeLoader, metadataSystem); | ||
| 254 | |||
| 255 | AtomicInteger count = new AtomicInteger(); | ||
| 256 | |||
| 257 | decompileClasses.parallelStream().forEach(translatedNode -> { | ||
| 258 | if (progress != null) { | ||
| 259 | progress.step(count.getAndIncrement(), translatedNode.name); | ||
| 260 | } | ||
| 261 | |||
| 262 | decompileClass(outputDirectory, translatedNode, sourceProvider); | ||
| 263 | }); | ||
| 264 | } | ||
| 265 | |||
| 266 | private void decompileClass(Path outputDirectory, ClassNode translatedNode, SourceProvider sourceProvider) { | ||
| 267 | try { | ||
| 268 | // get the source | ||
| 269 | CompilationUnit sourceTree = sourceProvider.getSources(translatedNode.name); | ||
| 270 | |||
| 271 | Path path = outputDirectory.resolve(translatedNode.name.replace('.', '/') + ".java"); | ||
| 272 | Files.createDirectories(path.getParent()); | ||
| 273 | |||
| 274 | try (Writer writer = Files.newBufferedWriter(path)) { | ||
| 275 | sourceProvider.writeSource(writer, sourceTree); | ||
| 276 | } | ||
| 277 | } catch (Throwable t) { | ||
| 278 | // don't crash the whole world here, just log the error and keep going | ||
| 279 | // TODO: set up logback via log4j | ||
| 280 | System.err.println("Unable to decompile class " + translatedNode.name); | ||
| 281 | t.printStackTrace(System.err); | ||
| 282 | } | ||
| 283 | } | ||
| 284 | |||
| 285 | public void writeTransformedJar(File out, ProgressListener progress) { | ||
| 286 | Translator deobfuscator = mapper.getDeobfuscator(); | ||
| 287 | writeTransformedJar(out, progress, (node, visitor) -> { | ||
| 288 | ClassEntry entry = new ClassEntry(node.name); | ||
| 289 | node.accept(new TranslationClassVisitor(deobfuscator, Opcodes.ASM5, visitor)); | ||
| 290 | return deobfuscator.translate(entry).getFullName(); | ||
| 291 | }); | ||
| 292 | } | ||
| 293 | |||
| 294 | public void writeTransformedJar(File out, ProgressListener progress, ClassTransformer transformer) { | ||
| 295 | try (JarOutputStream outJar = new JarOutputStream(new FileOutputStream(out))) { | ||
| 296 | if (progress != null) { | ||
| 297 | progress.init(parsedJar.getClassCount(), "Transforming classes..."); | ||
| 298 | } | ||
| 299 | |||
| 300 | AtomicInteger count = new AtomicInteger(); | ||
| 301 | parsedJar.visitNode(node -> { | ||
| 302 | if (progress != null) { | ||
| 303 | progress.step(count.getAndIncrement(), node.name); | ||
| 304 | } | ||
| 305 | |||
| 306 | try { | ||
| 307 | ClassWriter writer = new ClassWriter(0); | ||
| 308 | String transformedName = transformer.transform(node, writer); | ||
| 309 | outJar.putNextEntry(new JarEntry(transformedName.replace('.', '/') + ".class")); | ||
| 310 | outJar.write(writer.toByteArray()); | ||
| 311 | outJar.closeEntry(); | ||
| 312 | } catch (Throwable t) { | ||
| 313 | throw new Error("Unable to transform class " + node.name, t); | ||
| 314 | } | ||
| 315 | }); | ||
| 316 | } catch (IOException ex) { | ||
| 317 | throw new Error("Unable to write to Jar file!"); | ||
| 318 | } | ||
| 319 | } | ||
| 320 | |||
| 321 | public AccessModifier getModifier(Entry<?> entry) { | ||
| 322 | EntryMapping mapping = mapper.getDeobfMapping(entry); | ||
| 323 | if (mapping == null) { | ||
| 324 | return AccessModifier.UNCHANGED; | ||
| 325 | } | ||
| 326 | return mapping.getAccessModifier(); | ||
| 327 | } | ||
| 328 | |||
| 329 | public void changeModifier(Entry<?> entry, AccessModifier modifier) { | ||
| 330 | EntryMapping mapping = mapper.getDeobfMapping(entry); | ||
| 331 | if (mapping != null) { | ||
| 332 | mapper.mapFromObf(entry, new EntryMapping(mapping.getTargetName(), modifier)); | ||
| 333 | } else { | ||
| 334 | mapper.mapFromObf(entry, new EntryMapping(entry.getName(), modifier)); | ||
| 335 | } | ||
| 336 | } | ||
| 337 | |||
| 338 | public boolean isRenamable(Entry<?> obfEntry) { | ||
| 339 | if (obfEntry instanceof MethodEntry) { | ||
| 340 | // HACKHACK: Object methods are not obfuscated identifiers | ||
| 341 | MethodEntry obfMethodEntry = (MethodEntry) obfEntry; | ||
| 342 | String name = obfMethodEntry.getName(); | ||
| 343 | String sig = obfMethodEntry.getDesc().toString(); | ||
| 344 | if (name.equals("clone") && sig.equals("()Ljava/lang/Object;")) { | ||
| 345 | return false; | ||
| 346 | } else if (name.equals("equals") && sig.equals("(Ljava/lang/Object;)Z")) { | ||
| 347 | return false; | ||
| 348 | } else if (name.equals("finalize") && sig.equals("()V")) { | ||
| 349 | return false; | ||
| 350 | } else if (name.equals("getClass") && sig.equals("()Ljava/lang/Class;")) { | ||
| 351 | return false; | ||
| 352 | } else if (name.equals("hashCode") && sig.equals("()I")) { | ||
| 353 | return false; | ||
| 354 | } else if (name.equals("notify") && sig.equals("()V")) { | ||
| 355 | return false; | ||
| 356 | } else if (name.equals("notifyAll") && sig.equals("()V")) { | ||
| 357 | return false; | ||
| 358 | } else if (name.equals("toString") && sig.equals("()Ljava/lang/String;")) { | ||
| 359 | return false; | ||
| 360 | } else if (name.equals("wait") && sig.equals("()V")) { | ||
| 361 | return false; | ||
| 362 | } else if (name.equals("wait") && sig.equals("(J)V")) { | ||
| 363 | return false; | ||
| 364 | } else if (name.equals("wait") && sig.equals("(JI)V")) { | ||
| 365 | return false; | ||
| 366 | } | ||
| 367 | } else if (obfEntry instanceof LocalVariableEntry && !((LocalVariableEntry) obfEntry).isArgument()) { | ||
| 368 | return false; | ||
| 369 | } | ||
| 370 | |||
| 371 | return this.jarIndex.getEntryIndex().hasEntry(obfEntry); | ||
| 372 | } | ||
| 373 | |||
| 374 | public boolean isRenamable(EntryReference<Entry<?>, Entry<?>> obfReference) { | ||
| 375 | return obfReference.isNamed() && isRenamable(obfReference.getNameableEntry()); | ||
| 376 | } | ||
| 377 | |||
| 378 | public boolean isRemapped(Entry<?> entry) { | ||
| 379 | EntryResolver resolver = mapper.getObfResolver(); | ||
| 380 | DeltaTrackingTree<EntryMapping> mappings = mapper.getObfToDeobf(); | ||
| 381 | return resolver.resolveEntry(entry, ResolutionStrategy.RESOLVE_ROOT).stream() | ||
| 382 | .anyMatch(mappings::contains); | ||
| 383 | } | ||
| 384 | |||
| 385 | public void rename(Entry<?> obfEntry, String newName) { | ||
| 386 | mapper.mapFromObf(obfEntry, new EntryMapping(newName)); | ||
| 387 | } | ||
| 388 | |||
| 389 | public void removeMapping(Entry<?> obfEntry) { | ||
| 390 | mapper.removeByObf(obfEntry); | ||
| 391 | } | ||
| 392 | |||
| 393 | public void markAsDeobfuscated(Entry<?> obfEntry) { | ||
| 394 | mapper.mapFromObf(obfEntry, new EntryMapping(mapper.deobfuscate(obfEntry).getName())); | ||
| 395 | } | ||
| 396 | |||
| 397 | public <T extends Translatable> T deobfuscate(T translatable) { | ||
| 398 | return mapper.deobfuscate(translatable); | ||
| 399 | } | ||
| 400 | |||
| 401 | public interface ClassTransformer { | ||
| 402 | String transform(ClassNode node, ClassVisitor visitor); | ||
| 403 | } | ||
| 404 | |||
| 405 | public static class NoRetryMetadataSystem extends MetadataSystem { | ||
| 406 | private final Set<String> _failedTypes = Collections.newSetFromMap(new ConcurrentHashMap<>()); | ||
| 407 | |||
| 408 | public NoRetryMetadataSystem(final ITypeLoader typeLoader) { | ||
| 409 | super(typeLoader); | ||
| 410 | } | ||
| 411 | |||
| 412 | @Override | ||
| 413 | protected synchronized TypeDefinition resolveType(final String descriptor, final boolean mightBePrimitive) { | ||
| 414 | if (_failedTypes.contains(descriptor)) { | ||
| 415 | return null; | ||
| 416 | } | ||
| 417 | |||
| 418 | final TypeDefinition result = super.resolveType(descriptor, mightBePrimitive); | ||
| 419 | |||
| 420 | if (result == null) { | ||
| 421 | _failedTypes.add(descriptor); | ||
| 422 | } | ||
| 423 | |||
| 424 | return result; | ||
| 425 | } | ||
| 426 | |||
| 427 | @Override | ||
| 428 | public synchronized TypeDefinition resolve(final TypeReference type) { | ||
| 429 | return super.resolve(type); | ||
| 430 | } | ||
| 431 | } | ||
| 432 | } | ||