Skip to content

Commit

Permalink
feat: implement Android App Bundle support (#1129) (PR #1131)
Browse files Browse the repository at this point in the history
* Implement proto parse
* fix code formatting
* fix tests with empty input
* revert not needed code style changes
* Implement parse of resources.pb for AAB

Co-authored-by: bagipro <bugi@MacBook-Pro-2.local>
Co-authored-by: Skylot <skylot@gmail.com>
  • Loading branch information
3 people authored Mar 8, 2021
1 parent 4e5fac4 commit 9ef99a2
Show file tree
Hide file tree
Showing 19 changed files with 725 additions and 138 deletions.
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
19 changes: 16 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,11 +17,13 @@
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;
import jadx.core.utils.files.FileUtils;
import jadx.core.xmlgen.ResContainer;
import jadx.core.xmlgen.ResProtoParser;
import jadx.core.xmlgen.ResTableParser;

import static jadx.core.utils.files.FileUtils.READ_BUFFER_SIZE;
Expand Down Expand Up @@ -91,14 +93,25 @@ 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);
if (root.isProto()) {
return new ResProtoParser(root).decodeFiles(inputStream);
} else {
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(XmlGenUtils.readData(inputStream));
}
}
Loading

0 comments on commit 9ef99a2

Please sign in to comment.