summaryrefslogtreecommitdiff
path: root/src/main/java/cuchaz/enigma/Deobfuscator.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/java/cuchaz/enigma/Deobfuscator.java')
-rw-r--r--src/main/java/cuchaz/enigma/Deobfuscator.java432
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
12package cuchaz.enigma;
13
14import com.google.common.base.Functions;
15import com.google.common.base.Stopwatch;
16import com.strobel.assembler.metadata.ITypeLoader;
17import com.strobel.assembler.metadata.MetadataSystem;
18import com.strobel.assembler.metadata.TypeDefinition;
19import com.strobel.assembler.metadata.TypeReference;
20import com.strobel.decompiler.DecompilerSettings;
21import com.strobel.decompiler.languages.java.ast.CompilationUnit;
22import cuchaz.enigma.analysis.EntryReference;
23import cuchaz.enigma.analysis.IndexTreeBuilder;
24import cuchaz.enigma.analysis.ParsedJar;
25import cuchaz.enigma.analysis.index.JarIndex;
26import cuchaz.enigma.api.EnigmaPlugin;
27import cuchaz.enigma.bytecode.translators.SourceFixVisitor;
28import cuchaz.enigma.bytecode.translators.TranslationClassVisitor;
29import cuchaz.enigma.translation.Translatable;
30import cuchaz.enigma.translation.Translator;
31import cuchaz.enigma.translation.mapping.*;
32import cuchaz.enigma.translation.mapping.tree.DeltaTrackingTree;
33import cuchaz.enigma.translation.mapping.tree.EntryTree;
34import cuchaz.enigma.translation.representation.entry.ClassEntry;
35import cuchaz.enigma.translation.representation.entry.Entry;
36import cuchaz.enigma.translation.representation.entry.LocalVariableEntry;
37import cuchaz.enigma.translation.representation.entry.MethodEntry;
38import org.objectweb.asm.ClassVisitor;
39import org.objectweb.asm.ClassWriter;
40import org.objectweb.asm.Opcodes;
41import org.objectweb.asm.tree.ClassNode;
42
43import java.io.File;
44import java.io.FileOutputStream;
45import java.io.IOException;
46import java.io.Writer;
47import java.nio.file.Files;
48import java.nio.file.Path;
49import java.util.*;
50import java.util.concurrent.ConcurrentHashMap;
51import java.util.concurrent.atomic.AtomicInteger;
52import java.util.function.Consumer;
53import java.util.jar.JarEntry;
54import java.util.jar.JarFile;
55import java.util.jar.JarOutputStream;
56import java.util.stream.Collectors;
57
58public 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}