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

Implement Android App Bundle support #1131

Merged
merged 7 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
Command line and GUI tools for producing Java source code from Android Dex and Apk files

**Main features:**
- decompile Dalvik bytecode to java classes from APK, dex, aar and zip files
- decompile Dalvik bytecode to java classes from APK, dex, aar, aab and zip files
- decode `AndroidManifest.xml` and other resources from `resources.arsc`
- deobfuscator included

Expand Down Expand Up @@ -65,7 +65,7 @@ and also packed to `build/jadx-<version>.zip`

### Usage
```
jadx[-gui] [options] <input file> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc)
jadx[-gui] [options] <input file> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)
options:
-d, --output-dir - output directory
-ds, --output-dir-src - output directory for sources
Expand Down
2 changes: 1 addition & 1 deletion jadx-cli/src/main/java/jadx/cli/JadxCLIArgs.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

public class JadxCLIArgs {

@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc)")
@Parameter(description = "<input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab)")
protected List<String> files = new ArrayList<>(1);

@Parameter(names = { "-d", "--output-dir" }, description = "output directory")
Expand Down
1 change: 1 addition & 0 deletions jadx-core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ dependencies {
api(project(':jadx-plugins:jadx-plugins-api'))

implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.android.tools.build:aapt2-proto:4.1.2-6503028'

testImplementation 'org.apache.commons:commons-lang3:3.11'

Expand Down
22 changes: 16 additions & 6 deletions jadx-core/src/main/java/jadx/api/JadxDecompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
import jadx.core.utils.Utils;
import jadx.core.utils.exceptions.JadxRuntimeException;
import jadx.core.xmlgen.BinaryXMLParser;
import jadx.core.xmlgen.ProtoXMLParser;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResourcesSaver;

Expand Down Expand Up @@ -79,7 +80,8 @@ public final class JadxDecompiler implements Closeable {
private List<JavaClass> classes;
private List<ResourceFile> resources;

private BinaryXMLParser xmlParser;
private BinaryXMLParser binaryXmlParser;
private ProtoXMLParser protoXmlParser;

private final Map<ClassNode, JavaClass> classesMap = new ConcurrentHashMap<>();
private final Map<MethodNode, JavaMethod> methodsMap = new ConcurrentHashMap<>();
Expand Down Expand Up @@ -122,7 +124,8 @@ private void reset() {
root = null;
classes = null;
resources = null;
xmlParser = null;
binaryXmlParser = null;
protoXmlParser = null;

classesMap.clear();
methodsMap.clear();
Expand Down Expand Up @@ -341,11 +344,18 @@ public RootNode getRoot() {
return root;
}

synchronized BinaryXMLParser getXmlParser() {
if (xmlParser == null) {
xmlParser = new BinaryXMLParser(root);
synchronized BinaryXMLParser getBinaryXmlParser() {
if (binaryXmlParser == null) {
binaryXmlParser = new BinaryXMLParser(root);
}
return xmlParser;
return binaryXmlParser;
}

synchronized ProtoXMLParser getProtoXmlParser() {
if (protoXmlParser == null) {
protoXmlParser = new ProtoXMLParser(root);
}
return protoXmlParser;
}

private void loadJavaClass(JavaClass javaClass) {
Expand Down
3 changes: 3 additions & 0 deletions jadx-core/src/main/java/jadx/api/ResourceType.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ public String[] getExts() {
}

public static ResourceType getFileType(String fileName) {
if (fileName.matches("[^/]+/resources.pb")) {
return ARSC;
}
int dot = fileName.lastIndexOf('.');
if (dot != -1) {
String ext = fileName.substring(dot).toLowerCase(Locale.ROOT);
Expand Down
14 changes: 11 additions & 3 deletions jadx-core/src/main/java/jadx/api/ResourcesLoader.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import jadx.api.ResourceFile.ZipRef;
import jadx.api.impl.SimpleCodeInfo;
import jadx.api.plugins.utils.ZipSecurity;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.Utils;
import jadx.core.utils.android.Res9patchStreamDecoder;
import jadx.core.utils.exceptions.JadxException;
Expand Down Expand Up @@ -91,14 +92,21 @@ static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf) {

private static ResContainer loadContent(JadxDecompiler jadxRef, ResourceFile rf,
InputStream inputStream) throws IOException {
RootNode root = jadxRef.getRoot();
switch (rf.getType()) {
case MANIFEST:
case XML:
ICodeInfo content = jadxRef.getXmlParser().parse(inputStream);
case XML: {
ICodeInfo content;
if (root.isProto()) {
content = jadxRef.getProtoXmlParser().parse(inputStream);
} else {
content = jadxRef.getBinaryXmlParser().parse(inputStream);
}
return ResContainer.textResource(rf.getDeobfName(), content);
}

case ARSC:
return new ResTableParser(jadxRef.getRoot()).decodeFiles(inputStream);
return new ResTableParser(root).decodeFiles(inputStream);

case IMG:
return decodeImage(rf, inputStream);
Expand Down
6 changes: 6 additions & 0 deletions jadx-core/src/main/java/jadx/core/dex/nodes/RootNode.java
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public class RootNode {
private String appPackage;
@Nullable
private ClassNode appResClass;
private boolean isProto;

public RootNode(JadxArgs args) {
this.args = args;
Expand All @@ -82,6 +83,7 @@ public RootNode(JadxArgs args) {
this.codeCache = args.getCodeCache();
this.methodUtils = new MethodUtils(this);
this.typeUtils = new TypeUtils(this);
this.isProto = args.getInputFiles().size() > 0 && args.getInputFiles().get(0).getName().toLowerCase().endsWith(".aab");
}

public void loadClasses(List<ILoadResult> loadedInputs) {
Expand Down Expand Up @@ -502,4 +504,8 @@ public MethodUtils getMethodUtils() {
public TypeUtils getTypeUtils() {
return typeUtils;
}

public boolean isProto() {
return isProto;
}
}
141 changes: 141 additions & 0 deletions jadx-core/src/main/java/jadx/core/xmlgen/ProtoXMLParser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package jadx.core.xmlgen;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;

import com.android.aapt.Resources.XmlAttribute;
import com.android.aapt.Resources.XmlElement;
import com.android.aapt.Resources.XmlNamespace;
import com.android.aapt.Resources.XmlNode;
import com.google.protobuf.InvalidProtocolBufferException;

import jadx.api.ICodeInfo;
import jadx.api.ICodeWriter;
import jadx.core.dex.nodes.RootNode;
import jadx.core.utils.StringUtils;

public class ProtoXMLParser {
private Map<String, String> nsMap;
private final Map<String, String> tagAttrDeobfNames = new HashMap<>();

private ICodeWriter writer;

private final RootNode rootNode;
private String currentTag;
private String appPackageName;

public ProtoXMLParser(RootNode rootNode) {
this.rootNode = rootNode;
}

public synchronized ICodeInfo parse(InputStream inputStream) throws IOException {
nsMap = new HashMap<>();
writer = rootNode.makeCodeWriter();
writer.add("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
decode(decodeProto(inputStream));
nsMap = null;
return writer.finish();
}

private void decode(XmlNode n) throws IOException {
if (n.hasSource()) {
writer.attachSourceLine(n.getSource().getLineNumber());
}
writer.add(StringUtils.escapeXML(n.getText().trim()));
if (n.hasElement()) {
decode(n.getElement());
}
}

private void decode(XmlElement e) throws IOException {
String tag = deobfClassName(e.getName());
tag = getValidTagAttributeName(tag);
currentTag = tag;
writer.startLine('<').add(tag);
for (int i = 0; i < e.getNamespaceDeclarationCount(); i++) {
decode(e.getNamespaceDeclaration(i));
}
for (int i = 0; i < e.getAttributeCount(); i++) {
decode(e.getAttribute(i));
}
if (e.getChildCount() > 0) {
writer.add('>');
writer.incIndent();
for (int i = 0; i < e.getChildCount(); i++) {
Map<String, String> oldNsMap = new HashMap<>(nsMap);
decode(e.getChild(i));
nsMap = oldNsMap;
}
writer.decIndent();
writer.startLine("</").add(tag).add('>');
} else {
writer.add("/>");
}
}

private void decode(XmlAttribute a) {
writer.add(' ');
String namespace = a.getNamespaceUri();
if (!namespace.isEmpty()) {
writer.add(nsMap.get(namespace)).add(':');
}
String name = a.getName();
String value = deobfClassName(a.getValue());
writer.add(name).add("=\"").add(value).add('\"');
memorizePackageName(name, value);
}

private void decode(XmlNamespace n) {
String prefix = n.getPrefix();
String uri = n.getUri();
nsMap.put(uri, prefix);
writer.add(" xmlns:").add(prefix).add("=\"").add(uri).add('"');
}

private void memorizePackageName(String attrName, String attrValue) {
if ("manifest".equals(currentTag) && "package".equals(attrName)) {
appPackageName = attrValue;
}
}

private String deobfClassName(String className) {
String newName = XmlDeobf.deobfClassName(rootNode, className, appPackageName);
if (newName != null) {
return newName;
}
return className;
}

private String getValidTagAttributeName(String originalName) {
if (XMLChar.isValidName(originalName)) {
return originalName;
}
if (tagAttrDeobfNames.containsKey(originalName)) {
return tagAttrDeobfNames.get(originalName);
}
String generated;
do {
generated = generateTagAttrName();
} while (tagAttrDeobfNames.containsValue(generated));
tagAttrDeobfNames.put(originalName, generated);
return generated;
}

private static String generateTagAttrName() {
final int length = 6;
Random r = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= length; i++) {
sb.append((char) (r.nextInt(26) + 'a'));
}
return sb.toString();
}

private XmlNode decodeProto(InputStream inputStream)
throws InvalidProtocolBufferException, IOException {
return XmlNode.parseFrom(ResXmlGen.readData(inputStream));
}
}
13 changes: 13 additions & 0 deletions jadx-core/src/main/java/jadx/core/xmlgen/ResXmlGen.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package jadx.core.xmlgen;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand Down Expand Up @@ -224,4 +227,14 @@ private String getFileName(ResourceEntry ri) {
sb.append(".xml");
return sb.toString();
}

public static byte[] readData(InputStream i) throws IOException {
ByteArrayOutputStream buffer = new ByteArrayOutputStream();
byte[] data = new byte[16384];
int read;
while ((read = i.read(data, 0, data.length)) != -1) {
buffer.write(data, 0, read);
}
return buffer.toByteArray();
}
}
1 change: 1 addition & 0 deletions jadx-core/src/main/java/jadx/core/xmlgen/XmlDeobf.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ private XmlDeobf() {

@Nullable
public static String deobfClassName(RootNode rootNode, String potencialClassName, String packageName) {
potencialClassName = potencialClassName.replace('$', '.');
if (packageName != null && potencialClassName.startsWith(".")) {
potencialClassName = packageName + potencialClassName;
}
Expand Down
6 changes: 4 additions & 2 deletions jadx-gui/src/main/java/jadx/gui/treemodel/ApkSignature.java
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,8 @@ private void writeIssues(StringEscapeUtils.Builder builder, String issueType, Li
builder.append("<blockquote>");
// Unprotected Zip entry issues are very common, handle them separately
List<ApkVerifier.IssueWithParams> unprotIssues = issueList.stream()
.filter(i -> i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList());
.filter(i -> i.getIssue() == ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY)
.collect(Collectors.toList());
if (!unprotIssues.isEmpty()) {
builder.append("<h4>");
builder.escape(NLS.str("apkSignature.unprotectedEntry"));
Expand All @@ -202,7 +203,8 @@ private void writeIssues(StringEscapeUtils.Builder builder, String issueType, Li
builder.append("</blockquote>");
}
List<ApkVerifier.IssueWithParams> remainingIssues = issueList.stream()
.filter(i -> i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY).collect(Collectors.toList());
.filter(i -> i.getIssue() != ApkVerifier.Issue.JAR_SIG_UNPROTECTED_ZIP_ENTRY)
.collect(Collectors.toList());
if (!remainingIssues.isEmpty()) {
builder.append("<pre>\n");
for (ApkVerifier.IssueWithParams issue : remainingIssues) {
Expand Down
7 changes: 4 additions & 3 deletions jadx-gui/src/main/java/jadx/gui/treemodel/JResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
import java.util.List;
import java.util.Map;

import javax.swing.*;
import javax.swing.Icon;
import javax.swing.ImageIcon;

import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.jetbrains.annotations.NotNull;
Expand Down Expand Up @@ -63,8 +64,7 @@ public JResource(ResourceFile resFile, String name, String shortName, JResType t

public final void update() {
if (files.isEmpty()) {
if (type == JResType.DIR
|| type == JResType.ROOT
if (type == JResType.DIR || type == JResType.ROOT
|| resFile.getType() == ResourceType.ARSC) {
// fake leaf to force show expand button
// real sub nodes will load on expand in loadNode() method
Expand Down Expand Up @@ -278,6 +278,7 @@ public static boolean isSupportedForView(ResourceType type) {
case CODE:
case FONT:
case LIB:
case MEDIA:
return false;

case MANIFEST:
Expand Down
2 changes: 1 addition & 1 deletion jadx-gui/src/main/java/jadx/gui/ui/MainWindow.java
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ private JFileChooser buildFileChooser(boolean addFiles, String toolTipText) {
if (addFiles) {
exts = new String[] { "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc" };
} else {
exts = new String[] { JadxProject.PROJECT_EXTENSION, "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc" };
exts = new String[] { JadxProject.PROJECT_EXTENSION, "apk", "dex", "jar", "class", "smali", "zip", "aar", "arsc", "aab" };
}
String description = "Supported files: (" + Utils.arrayToStr(exts) + ')';

Expand Down