Merge pull request #401 from ThexXTURBOXx/master

Better approach to CFR decompilation
This commit is contained in:
Konloch 2022-03-14 18:23:51 -07:00 committed by GitHub
commit 7b19597014
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 204 additions and 184 deletions

View file

@ -31,8 +31,8 @@
<darklaf.version>2.7.3</darklaf.version> <darklaf.version>2.7.3</darklaf.version>
<darklaf-extensions-rsta.version>0.3.4</darklaf-extensions-rsta.version> <darklaf-extensions-rsta.version>0.3.4</darklaf-extensions-rsta.version>
<decompiler-fernflower.version>5.2.1.Final</decompiler-fernflower.version> <decompiler-fernflower.version>5.2.1.Final</decompiler-fernflower.version>
<dex2jar.version>v45</dex2jar.version> <dex2jar.version>v46</dex2jar.version>
<fernflower.version>b803ad9</fernflower.version> <fernflower.version>5a2b2cc</fernflower.version>
<gson.version>2.9.0</gson.version> <gson.version>2.9.0</gson.version>
<guava.version>31.0.1-jre</guava.version> <guava.version>31.0.1-jre</guava.version>
<imgscalr-lib.version>4.2</imgscalr-lib.version> <imgscalr-lib.version>4.2</imgscalr-lib.version>
@ -44,7 +44,7 @@
<objenesis.version>3.2</objenesis.version> <objenesis.version>3.2</objenesis.version>
<paged-data.version>0.2.0</paged-data.version> <paged-data.version>0.2.0</paged-data.version>
<procyon.version>0.6.0</procyon.version> <procyon.version>0.6.0</procyon.version>
<rsyntaxtextarea.version>3.1.6</rsyntaxtextarea.version> <rsyntaxtextarea.version>3.2.0</rsyntaxtextarea.version>
<semantic-version.version>2.1.1</semantic-version.version> <semantic-version.version>2.1.1</semantic-version.version>
<slf4j.version>1.7.36</slf4j.version> <slf4j.version>1.7.36</slf4j.version>
<smali.version>2.5.2</smali.version> <smali.version>2.5.2</smali.version>

View file

@ -1,31 +1,46 @@
package the.bytecode.club.bytecodeviewer.decompilers.impl; package the.bytecode.club.bytecodeviewer.decompilers.impl;
import com.strobel.core.StringUtilities; import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.benf.cfr.reader.api.CfrDriver; import org.benf.cfr.reader.api.CfrDriver;
import org.benf.cfr.reader.api.ClassFileSource; import org.benf.cfr.reader.api.ClassFileSource;
import org.benf.cfr.reader.api.OutputSinkFactory; import org.benf.cfr.reader.api.OutputSinkFactory;
import org.benf.cfr.reader.api.SinkReturns; import org.benf.cfr.reader.api.SinkReturns;
import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair; import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair;
import org.benf.cfr.reader.state.ClassFileSourceImpl;
import org.benf.cfr.reader.util.getopt.Options;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
import org.objectweb.asm.tree.ClassNode; import org.objectweb.asm.tree.ClassNode;
import the.bytecode.club.bytecodeviewer.BytecodeViewer; import the.bytecode.club.bytecodeviewer.BytecodeViewer;
import the.bytecode.club.bytecodeviewer.api.ASMUtil;
import the.bytecode.club.bytecodeviewer.api.ExceptionUI; import the.bytecode.club.bytecodeviewer.api.ExceptionUI;
import the.bytecode.club.bytecodeviewer.decompilers.InternalDecompiler; import the.bytecode.club.bytecodeviewer.decompilers.InternalDecompiler;
import the.bytecode.club.bytecodeviewer.resources.ResourceContainer;
import the.bytecode.club.bytecodeviewer.translation.TranslatedStrings; import the.bytecode.club.bytecodeviewer.translation.TranslatedStrings;
import java.io.*;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.zip.ZipException;
import java.util.zip.ZipOutputStream;
import static the.bytecode.club.bytecodeviewer.Constants.nl; import static the.bytecode.club.bytecodeviewer.Constants.nl;
import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.CFR; import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.CFR;
import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERROR; import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERROR;
/*************************************************************************** /***************************************************************************
* Bytecode Viewer (BCV) - Java & Android Reverse Engineering Suite * * Bytecode Viewer (BCV) - Java & Android Reverse Engineering Suite *
* Copyright (C) 2014 Kalen 'Konloch' Kinloch - http://bytecodeviewer.com * * Copyright (C) 2014 Kalen 'Konloch' Kinloch - http://bytecodeviewer.com *
@ -50,99 +65,49 @@ import static the.bytecode.club.bytecodeviewer.translation.TranslatedStrings.ERR
* @author GraxCode * @author GraxCode
* Taken mostly out of Threadtear. * Taken mostly out of Threadtear.
*/ */
public class CFRDecompiler extends InternalDecompiler { public class CFRDecompiler extends InternalDecompiler {
private String result;
private static final String CLASS_SUFFIX = ".class"; private static final String CLASS_SUFFIX = ".class";
@Override @Override
public String decompileClassNode(ClassNode cn, byte[] bytes) { public String decompileClassNode(ClassNode cn, byte[] content) {
String name = cn.name; return decompile(cn, cn.name, content);
return decompile(bytes, name);
} }
private String decompile(byte[] bytes, String name) { private String decompile(ClassNode cn, String name, byte[] content) {
try { try {
this.result = null; String classPath = name + (name.endsWith(CLASS_SUFFIX) ? "" : CLASS_SUFFIX);
OutputSinkFactory mySink = new OutputSinkFactory() {
@Override
public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> collection) {
if (sinkType == SinkType.JAVA && collection.contains(SinkClass.DECOMPILED)) {
return Arrays.asList(SinkClass.DECOMPILED, SinkClass.STRING);
} else {
return Collections.singletonList(SinkClass.STRING);
}
}
@Override StringBuilder builder = new StringBuilder();
public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) { Consumer<SinkReturns.Decompiled> dumpDecompiled = d -> builder.append(d.getJava());
if (sinkType == SinkType.JAVA && sinkClass == SinkClass.DECOMPILED) {
return x -> result = ((SinkReturns.Decompiled) x).getJava();
}
return ignore -> {
};
}
};
ClassFileSource source = new ClassFileSource() {
@Override
public void informAnalysisRelativePathDetail(String a, String b) {
}
@Override Options options = generateOptions();
public String getPossiblyRenamedPath(String path) { ClassFileSource source = new BCVDataSource(options, cn, classPath, content);
return path; CfrDriver driver = new CfrDriver.Builder()
} .withClassFileSource(source)
.withBuiltOptions(options)
.withOutputSink(new BCVOutputSinkFactory(dumpDecompiled))
.build();
driver.analyse(Collections.singletonList(name));
@Override return builder.toString();
public Pair<byte[], String> getClassFileContent(String path) throws IOException {
String clzName = path.substring(0, path.length() - 6);
if (clzName.equals(name)) {
return Pair.make(bytes, clzName);
}
URL url = CFRDecompiler.class.getResource("/" + path);
if (url != null) {
return Pair.make(IOUtils.toByteArray(url), path);
}
// don't load extra classes. we don't care about improper API usage.
ClassNode dummy = new ClassNode();
dummy.name = clzName;
dummy.version = 52;
return Pair.make(ASMUtil.nodeToBytes(dummy), clzName);
}
@Override
public Collection<String> addJar(String arg0) {
throw new RuntimeException("This should not be called");
}
};
CfrDriver cfrDriver = new CfrDriver.Builder().withClassFileSource(source).withOutputSink(mySink).withOptions(generateOptions()).build();
cfrDriver.analyse(Collections.singletonList(name));
} catch (Throwable t) { } catch (Throwable t) {
t.printStackTrace(); t.printStackTrace();
StringWriter sw = new StringWriter(); StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw); PrintWriter pw = new PrintWriter(sw);
t.printStackTrace(pw); t.printStackTrace(pw);
return CFR + " " + ERROR + "! " + ExceptionUI.SEND_STACKTRACE_TO + nl + nl + TranslatedStrings.SUGGESTED_FIX_DECOMPILER_ERROR + nl + nl + sw; return CFR + " " + ERROR + "! " + ExceptionUI.SEND_STACKTRACE_TO +
nl + nl + TranslatedStrings.SUGGESTED_FIX_DECOMPILER_ERROR +
nl + nl + sw;
} }
if (result != null && !result.trim().isEmpty()) {
return result;
}
return "No CFR output received.";
} }
@Override @Override
public void decompileToZip(String sourceJar, String zipName) { public void decompileToZip(String sourceJar, String outJar) {
try { try (JarFile jfile = new JarFile(new File(sourceJar));
doSaveJarDecompiled(new File(sourceJar), new File(zipName)); FileOutputStream dest = new FileOutputStream(outJar);
} catch (StackOverflowError | Exception e) { BufferedOutputStream buffDest = new BufferedOutputStream(dest);
BytecodeViewer.handleException(e); ZipOutputStream out = new ZipOutputStream(buffDest)) {
}
}
private void doSaveJarDecompiled(File inFile, File outFile) throws IOException {
try (JarFile jfile = new JarFile(inFile); FileOutputStream dest = new FileOutputStream(outFile); BufferedOutputStream buffDest = new BufferedOutputStream(dest); ZipOutputStream out = new ZipOutputStream(buffDest)) {
byte[] data = new byte[1024]; byte[] data = new byte[1024];
Enumeration<JarEntry> ent = jfile.entries(); Enumeration<JarEntry> ent = jfile.entries();
@ -154,8 +119,9 @@ public class CFRDecompiler extends InternalDecompiler {
if (history.add(etn)) { if (history.add(etn)) {
out.putNextEntry(etn); out.putNextEntry(etn);
try { try {
String internalName = StringUtilities.removeRight(entry.getName(), CLASS_SUFFIX); IOUtils.write(decompile(null, entry.getName(),
IOUtils.write(decompile(IOUtils.toByteArray(jfile.getInputStream(entry)), internalName), out, StandardCharsets.UTF_8); IOUtils.toByteArray(jfile.getInputStream(entry))),
out, StandardCharsets.UTF_8);
} finally { } finally {
out.closeEntry(); out.closeEntry();
} }
@ -184,10 +150,12 @@ public class CFRDecompiler extends InternalDecompiler {
} }
} }
} }
} catch (StackOverflowError | Exception e) {
BytecodeViewer.handleException(e);
} }
} }
private Map<String, String> generateOptions() { public Options generateOptions() {
Map<String, String> options = new HashMap<>(); Map<String, String> options = new HashMap<>();
options.put("decodeenumswitch", String.valueOf(BytecodeViewer.viewer.decodeEnumSwitch.isSelected())); options.put("decodeenumswitch", String.valueOf(BytecodeViewer.viewer.decodeEnumSwitch.isSelected()));
options.put("sugarenums", String.valueOf(BytecodeViewer.viewer.sugarEnums.isSelected())); options.put("sugarenums", String.valueOf(BytecodeViewer.viewer.sugarEnums.isSelected()));
@ -196,7 +164,8 @@ public class CFRDecompiler extends InternalDecompiler {
options.put("collectioniter", String.valueOf(BytecodeViewer.viewer.collectioniter.isSelected())); options.put("collectioniter", String.valueOf(BytecodeViewer.viewer.collectioniter.isSelected()));
options.put("innerclasses", String.valueOf(BytecodeViewer.viewer.innerClasses.isSelected())); options.put("innerclasses", String.valueOf(BytecodeViewer.viewer.innerClasses.isSelected()));
options.put("removeboilerplate", String.valueOf(BytecodeViewer.viewer.removeBoilerPlate.isSelected())); options.put("removeboilerplate", String.valueOf(BytecodeViewer.viewer.removeBoilerPlate.isSelected()));
options.put("removeinnerclasssynthetics", String.valueOf(BytecodeViewer.viewer.removeInnerClassSynthetics.isSelected())); options.put("removeinnerclasssynthetics",
String.valueOf(BytecodeViewer.viewer.removeInnerClassSynthetics.isSelected()));
options.put("decodelambdas", String.valueOf(BytecodeViewer.viewer.decodeLambdas.isSelected())); options.put("decodelambdas", String.valueOf(BytecodeViewer.viewer.decodeLambdas.isSelected()));
options.put("hidebridgemethods", String.valueOf(BytecodeViewer.viewer.hideBridgeMethods.isSelected())); options.put("hidebridgemethods", String.valueOf(BytecodeViewer.viewer.hideBridgeMethods.isSelected()));
options.put("liftconstructorinit", String.valueOf(BytecodeViewer.viewer.liftConstructorInit.isSelected())); options.put("liftconstructorinit", String.valueOf(BytecodeViewer.viewer.liftConstructorInit.isSelected()));
@ -229,6 +198,57 @@ public class CFRDecompiler extends InternalDecompiler {
options.put("recovertypehints", String.valueOf(BytecodeViewer.viewer.recoveryTypehInts.isSelected())); options.put("recovertypehints", String.valueOf(BytecodeViewer.viewer.recoveryTypehInts.isSelected()));
options.put("forcereturningifs", String.valueOf(BytecodeViewer.viewer.forceTurningIFs.isSelected())); options.put("forcereturningifs", String.valueOf(BytecodeViewer.viewer.forceTurningIFs.isSelected()));
options.put("forloopaggcapture", String.valueOf(BytecodeViewer.viewer.forLoopAGGCapture.isSelected())); options.put("forloopaggcapture", String.valueOf(BytecodeViewer.viewer.forLoopAGGCapture.isSelected()));
return options; return new OptionsImpl(options);
} }
private static class BCVDataSource extends ClassFileSourceImpl {
private final ResourceContainer container;
private final String classFilePath;
private final byte[] content;
private BCVDataSource(Options options, ClassNode cn, String classFilePath, byte[] content) {
super(options);
this.container = BytecodeViewer.getResourceContainers().stream()
.filter(rc -> rc.resourceClasses.containsValue(cn))
.findFirst().orElse(null);
this.classFilePath = classFilePath;
this.content = content;
}
@Override
public Pair<byte[], String> getClassFileContent(String classFilePath) throws IOException {
if (classFilePath.equals(this.classFilePath) && content != null) return Pair.make(content, classFilePath);
if (container == null) return super.getClassFileContent(classFilePath);
byte[] data = container.resourceClassBytes.get(classFilePath);
if (data == null) return super.getClassFileContent(classFilePath);
return Pair.make(data, classFilePath);
}
}
private static class BCVOutputSinkFactory implements OutputSinkFactory {
private final Consumer<SinkReturns.Decompiled> dumpDecompiled;
private BCVOutputSinkFactory(Consumer<SinkReturns.Decompiled> dumpDecompiled) {
this.dumpDecompiled = dumpDecompiled;
}
@Override
public List<SinkClass> getSupportedSinks(SinkType sinkType, Collection<SinkClass> available) {
return Collections.singletonList(SinkClass.DECOMPILED);
}
@Override
public <T> Sink<T> getSink(SinkType sinkType, SinkClass sinkClass) {
if (sinkType == SinkType.JAVA && sinkClass == SinkClass.DECOMPILED) {
return x -> dumpDecompiled.accept((SinkReturns.Decompiled) x);
}
return ignore -> {
};
}
}
} }