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