Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for exporting to proper deobfuscation mapping file formats #1505

Merged
merged 24 commits into from
Jun 11, 2022
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
e222abb
Add option to export mappings as Tiny v2 file
NebelNidas May 30, 2022
d0fc80d
Comply with JADX's import order conventions
NebelNidas May 30, 2022
b5c7ad9
Only use Java 8 features
NebelNidas May 30, 2022
21cccb4
Only use Java 8 features (2)
NebelNidas May 30, 2022
d14c59a
Export comments to mappings file
NebelNidas May 30, 2022
fe104a1
Method args test (doesn't work)
NebelNidas May 31, 2022
aee948d
Make method arg mapping exports work now
NebelNidas Jun 2, 2022
ce78a62
Use `getTopParentClass()` instead of `getParentClass()`
NebelNidas Jun 2, 2022
235991f
Remove unneeded method load call
NebelNidas Jun 2, 2022
faaa99a
Merge remote-tracking branch 'upstream/master'
NebelNidas Jun 3, 2022
9ad0f93
Small code cleanup; initial (broken) support for method vars
NebelNidas Jun 4, 2022
d0ddba5
Fixes regarding inner classes
NebelNidas Jun 8, 2022
a6235f3
Add option to export mappings as Enigma directory
NebelNidas Jun 8, 2022
dee78a6
Add option to export mappings as Enigma file/directory
NebelNidas Jun 9, 2022
7907865
Merge branch 'skylot:master' into master
NebelNidas Jun 9, 2022
414b5be
Fix method vars' lv-indices
NebelNidas Jun 9, 2022
8cb764c
Use correct offset value for method var mappings
NebelNidas Jun 10, 2022
ed7b091
Also supply lvt-index for method var mappings
NebelNidas Jun 11, 2022
d29d631
Clarify why we're using local mapping-io fork; comment out Fabric Mav…
NebelNidas Jun 11, 2022
f537352
Remove unnecessary `public` modifier
NebelNidas Jun 11, 2022
782a56c
Make an `if` condition less complicated
NebelNidas Jun 11, 2022
71766d2
Move mapping export code into jadx-gui (for now)
NebelNidas Jun 11, 2022
1a4d693
Make mapping export async; make export menu only clickable when every…
NebelNidas Jun 11, 2022
86a6a50
Fix export mappings menu field declaration position
NebelNidas Jun 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ allprojects {
mavenLocal()
mavenCentral()
google()
// Commented out for now since we're using a local mapping-io fork atm.
// maven {
// name 'FabricMC'
// url 'https://maven.fabricmc.net/'
// }
}
}

Expand Down
6 changes: 3 additions & 3 deletions jadx-core/src/main/java/jadx/core/dex/info/ClassInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,12 @@ public String makeRawFullName() {
return makeFullClsName(pkg, name, parentClass, false, true);
}

private String makeAliasFullName() {
public String makeAliasFullName() {
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, false);
}

private String makeAliasRawFullName() {
return makeFullClsName(pkg, name, parentClass, true, true);
public String makeAliasRawFullName() {
return makeFullClsName(getAliasPkg(), getAliasShortName(), parentClass, true, true);
}

public String getAliasFullPath() {
Expand Down
4 changes: 4 additions & 0 deletions jadx-core/src/main/java/jadx/core/dex/nodes/MethodNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,10 @@ public int getRegsCount() {
return regsCount;
}

public int getArgsStartReg() {
return argsStartReg;
}

public SSAVar makeNewSVar(@NotNull RegisterArg assignArg) {
int regNum = assignArg.getRegNum();
return makeNewSVar(regNum, getNextSVarVersion(regNum), assignArg);
Expand Down
6 changes: 5 additions & 1 deletion jadx-gui/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ plugins {

dependencies {
implementation(project(':jadx-core'))

implementation(project(":jadx-cli"))
implementation 'com.beust:jcommander:1.82'
implementation 'ch.qos.logback:logback-classic:1.2.11'
Expand All @@ -30,6 +29,11 @@ dependencies {
implementation 'com.android.tools.build:apksig:4.2.1'
implementation 'io.github.hqktech:jdwp:1.0'

// TODO: Switch back to upstream once this PR gets merged:
// https://github.com/FabricMC/mapping-io/pull/19
// implementation 'net.fabricmc:mapping-io:0.3.0'
implementation files('libs/mapping-io-0.4.0-SNAPSHOT.jar')

testImplementation project(":jadx-core").sourceSets.test.output
}

Expand Down
Binary file added jadx-gui/libs/mapping-io-0.4.0-SNAPSHOT.jar
Binary file not shown.
285 changes: 285 additions & 0 deletions jadx-gui/src/main/java/jadx/gui/plugins/mappings/MappingExporter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,285 @@
package jadx.gui.plugins.mappings;

import java.io.IOException;
import java.nio.file.Path;
import java.util.AbstractMap.SimpleEntry;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import net.fabricmc.mappingio.MappedElementKind;
import net.fabricmc.mappingio.MappingWriter;
import net.fabricmc.mappingio.format.MappingFormat;
import net.fabricmc.mappingio.tree.MemoryMappingTree;

import jadx.api.ICodeInfo;
import jadx.api.data.ICodeComment;
import jadx.api.data.ICodeRename;
import jadx.api.data.IJavaNodeRef.RefType;
import jadx.api.data.impl.JadxCodeData;
import jadx.api.data.impl.JadxCodeRef;
import jadx.api.metadata.ICodeNodeRef;
import jadx.api.metadata.annotations.InsnCodeOffset;
import jadx.api.metadata.annotations.NodeDeclareRef;
import jadx.api.metadata.annotations.VarNode;
import jadx.api.utils.CodeUtils;
import jadx.core.Consts;
import jadx.core.codegen.TypeGen;
import jadx.core.dex.info.ClassInfo;
import jadx.core.dex.info.FieldInfo;
import jadx.core.dex.info.MethodInfo;
import jadx.core.dex.nodes.ClassNode;
import jadx.core.dex.nodes.FieldNode;
import jadx.core.dex.nodes.MethodNode;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.files.FileUtils;

public class MappingExporter {
private static final Logger LOG = LoggerFactory.getLogger(MappingExporter.class);
private final RootNode root;

public MappingExporter(RootNode rootNode) {
this.root = rootNode;
}

private List<VarNode> collectMethodArgs(MethodNode methodNode) {
ICodeInfo codeInfo = methodNode.getTopParentClass().getCode();
int mthDefPos = methodNode.getDefPosition();
int lineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);
List<VarNode> args = new ArrayList<>();
codeInfo.getCodeMetadata().searchDown(mthDefPos, (pos, ann) -> {
if (pos > lineEndPos) {
// Stop at line end
return Boolean.TRUE;
}
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode();
if (declRef instanceof VarNode) {
VarNode varNode = (VarNode) declRef;
if (!varNode.getMth().equals(methodNode)) {
// Stop if we've gone too far and have entered a different method
return Boolean.TRUE;
}
args.add(varNode);
}
}
return null;
});
return args;
}

private List<SimpleEntry<VarNode, Integer>> collectMethodVars(MethodNode methodNode) {
ICodeInfo codeInfo = methodNode.getTopParentClass().getCode();
int mthDefPos = methodNode.getDefPosition();
int mthLineEndPos = CodeUtils.getLineEndForPos(codeInfo.getCodeStr(), mthDefPos);

List<SimpleEntry<VarNode, Integer>> vars = new ArrayList<>();
AtomicInteger lastOffset = new AtomicInteger(-1);
codeInfo.getCodeMetadata().searchDown(mthLineEndPos, (pos, ann) -> {
if (ann instanceof InsnCodeOffset) {
lastOffset.set(((InsnCodeOffset) ann).getOffset());
}
if (ann instanceof NodeDeclareRef) {
ICodeNodeRef declRef = ((NodeDeclareRef) ann).getNode();
if (declRef instanceof VarNode) {
VarNode varNode = (VarNode) declRef;
if (!varNode.getMth().equals(methodNode)) {
// Stop if we've gone too far and have entered a different method
return Boolean.TRUE;
}
if (lastOffset.get() != -1) {
vars.add(new SimpleEntry<VarNode, Integer>(varNode, lastOffset.get()));
} else {
LOG.warn("Local variable not present in bytecode, skipping: "
+ methodNode.getMethodInfo().getRawFullId() + "#" + varNode.getName());
}
lastOffset.set(-1);
}
}
return null;
});
return vars;
}

public void exportMappings(Path path, JadxCodeData codeData, MappingFormat mappingFormat) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
// Map < SrcName >
Set<String> mappedClasses = new HashSet<>();
// Map < DeclClass + ShortId >
Set<String> mappedFields = new HashSet<>();
Set<String> mappedMethods = new HashSet<>();
Set<String> methodsWithMappedElements = new HashSet<>();
// Map < DeclClass + MethodShortId + CodeRef, NewName >
Map<String, String> mappedMethodArgsAndVars = new HashMap<>();
// Map < DeclClass + *ShortId + *CodeRef, Comment >
Map<String, String> comments = new HashMap<>();

// We have to do this so we know for sure which elements are *manually* renamed
for (ICodeRename codeRename : codeData.getRenames()) {
if (codeRename.getNodeRef().getType().equals(RefType.CLASS)) {
mappedClasses.add(codeRename.getNodeRef().getDeclaringClass());
} else if (codeRename.getNodeRef().getType().equals(RefType.FIELD)) {
mappedFields.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
} else if (codeRename.getNodeRef().getType().equals(RefType.METHOD)) {
if (codeRename.getCodeRef() == null) {
mappedMethods.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
} else {
methodsWithMappedElements.add(codeRename.getNodeRef().getDeclaringClass() + codeRename.getNodeRef().getShortId());
mappedMethodArgsAndVars.put(codeRename.getNodeRef().getDeclaringClass()
+ codeRename.getNodeRef().getShortId()
+ codeRename.getCodeRef(),
codeRename.getNewName());
}
}
}
for (ICodeComment codeComment : codeData.getComments()) {
comments.put(codeComment.getNodeRef().getDeclaringClass()
+ (codeComment.getNodeRef().getShortId() == null ? "" : codeComment.getNodeRef().getShortId())
+ (codeComment.getCodeRef() == null ? "" : codeComment.getCodeRef()),
codeComment.getComment());
if (codeComment.getCodeRef() != null) {
methodsWithMappedElements.add(codeComment.getNodeRef().getDeclaringClass() + codeComment.getNodeRef().getShortId());
}
}

try {
if (mappingFormat.hasSingleFile()) {
if (path.toFile().exists()) {
path.toFile().delete();
}
path.toFile().createNewFile();
} else {
FileUtils.makeDirs(path);
}

mappingTree.visitHeader();
mappingTree.visitNamespaces("official", Arrays.asList("named"));
mappingTree.visitContent();

for (ClassNode cls : root.getClasses()) {
ClassInfo classInfo = cls.getClassInfo();
String classPath = classInfo.makeRawFullName().replace('.', '/');
String rawClassName = classInfo.getRawName();

if (classInfo.hasAlias()
&& !classInfo.getAliasShortName().equals(classInfo.getShortName())
&& mappedClasses.contains(rawClassName)) {
mappingTree.visitClass(classPath);
String alias = classInfo.makeAliasRawFullName().replace('.', '/');

if (alias.startsWith(Consts.DEFAULT_PACKAGE_NAME)) {
alias = alias.substring(Consts.DEFAULT_PACKAGE_NAME.length() + 1);
}
mappingTree.visitDstName(MappedElementKind.CLASS, 0, alias);
}
if (comments.containsKey(rawClassName)) {
mappingTree.visitClass(classPath);
mappingTree.visitComment(MappedElementKind.CLASS, comments.get(rawClassName));
}

for (FieldNode fld : cls.getFields()) {
FieldInfo fieldInfo = fld.getFieldInfo();
if (fieldInfo.hasAlias() && mappedFields.contains(rawClassName + fieldInfo.getShortId())) {
visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType()));
mappingTree.visitDstName(MappedElementKind.FIELD, 0, fieldInfo.getAlias());
}
if (comments.containsKey(rawClassName + fieldInfo.getShortId())) {
visitField(mappingTree, classPath, fieldInfo.getName(), TypeGen.signature(fieldInfo.getType()));
mappingTree.visitComment(MappedElementKind.FIELD, comments.get(rawClassName + fieldInfo.getShortId()));
}
}

for (MethodNode mth : cls.getMethods()) {
MethodInfo methodInfo = mth.getMethodInfo();
String methodName = methodInfo.getName();
String methodDesc = methodInfo.getShortId().substring(methodName.length());
if (methodInfo.hasAlias() && mappedMethods.contains(rawClassName + methodInfo.getShortId())) {
visitMethod(mappingTree, classPath, methodName, methodDesc);
mappingTree.visitDstName(MappedElementKind.METHOD, 0, methodInfo.getAlias());
}
if (comments.containsKey(rawClassName + methodInfo.getShortId())) {
visitMethod(mappingTree, classPath, methodName, methodDesc);
mappingTree.visitComment(MappedElementKind.METHOD, comments.get(rawClassName + methodInfo.getShortId()));
}

if (!methodsWithMappedElements.contains(rawClassName + methodInfo.getShortId())) {
continue;
}
// Method args
int lvtIndex = mth.getAccessFlags().isStatic() ? 0 : 1;
int lastArgLvIndex = lvtIndex - 1;
List<VarNode> args = collectMethodArgs(mth);
for (VarNode arg : args) {
int lvIndex = arg.getReg() - args.get(0).getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1);
String key = rawClassName + methodInfo.getShortId()
+ JadxCodeRef.forVar(arg.getReg(), arg.getSsa());
if (mappedMethodArgsAndVars.containsKey(key)) {
visitMethodArg(mappingTree, classPath, methodName, methodDesc, args.indexOf(arg), lvIndex);
mappingTree.visitDstName(MappedElementKind.METHOD_ARG, 0, mappedMethodArgsAndVars.get(key));
mappedMethodArgsAndVars.remove(key);
}
lastArgLvIndex = lvIndex;
lvtIndex++;
// Not checking for comments since method args can't have any
}
// Method vars
List<SimpleEntry<VarNode, Integer>> vars = collectMethodVars(mth);
for (SimpleEntry<VarNode, Integer> entry : vars) {
VarNode var = entry.getKey();
int offset = entry.getValue();
int lvIndex = lastArgLvIndex + var.getReg() + (mth.getAccessFlags().isStatic() ? 0 : 1);
String key = rawClassName + methodInfo.getShortId()
+ JadxCodeRef.forVar(var.getReg(), var.getSsa());
if (mappedMethodArgsAndVars.containsKey(key)) {
visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, offset);
mappingTree.visitDstName(MappedElementKind.METHOD_VAR, 0, mappedMethodArgsAndVars.get(key));
}
key = rawClassName + methodInfo.getShortId() + JadxCodeRef.forInsn(offset);
if (comments.containsKey(key)) {
visitMethodVar(mappingTree, classPath, methodName, methodDesc, lvtIndex, lvIndex, offset);
mappingTree.visitComment(MappedElementKind.METHOD_VAR, comments.get(key));
}
lvtIndex++;
}
}
}

MappingWriter writer = MappingWriter.create(path, mappingFormat);
mappingTree.accept(writer);
mappingTree.visitEnd();
writer.close();
} catch (IOException e) {
LOG.error("Failed to save deobfuscation map file '{}'", path.toAbsolutePath(), e);
}
}

private void visitField(MemoryMappingTree tree, String classPath, String srcName, String srcDesc) {
tree.visitClass(classPath);
tree.visitField(srcName, srcDesc);
}

private void visitMethod(MemoryMappingTree tree, String classPath, String srcName, String srcDesc) {
tree.visitClass(classPath);
tree.visitMethod(srcName, srcDesc);
}

private void visitMethodArg(MemoryMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int argPosition,
int lvIndex) {
visitMethod(tree, classPath, methodSrcName, methodSrcDesc);
tree.visitMethodArg(argPosition, lvIndex, null);
}

private void visitMethodVar(MemoryMappingTree tree, String classPath, String methodSrcName, String methodSrcDesc, int lvtIndex,
int lvIndex, int startOpIdx) {
visitMethod(tree, classPath, methodSrcName, methodSrcDesc);
tree.visitMethodVar(lvtIndex, lvIndex, startOpIdx, null);
}
}
Loading