package com.googlecode.d2j.ai; import com.googlecode.dex2jar.tools.BaseCmd; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import java.util.zip.ZipInputStream; /** * Inspect an APK's binary {@code AndroidManifest.xml} and report the attributes * that matter for triage: package identity, SDK levels, requested permissions, * and every exported component (the app's attack surface). * *
Accepts either an {@code .apk} (the manifest is extracted from the zip) or a
* standalone binary {@code AndroidManifest.xml}. The parser is self-contained
* ({@link AxmlParser}); no aapt or Android SDK is required.
*/
public class ManifestInspectCmd extends BaseCmd {
@Opt(opt = "o", longOpt = "output", description = "Output file path (default: stdout)")
String output;
@Opt(opt = "e", longOpt = "exported-only", hasArg = false,
description = "Only report exported components (the attack surface)")
boolean exportedOnly;
private static final String ANDROID_NS = "http://schemas.android.com/apk/res/android";
@Override
protected void doCommandLine() throws Exception {
if (remainingArgs == null || remainingArgs.length == 0) {
throw new HelpException("No input file specified");
}
Path input = Paths.get(remainingArgs[0]);
if (!Files.exists(input)) {
System.err.println("File not found: " + input);
return;
}
byte[] manifest = readManifestBytes(input);
if (manifest == null) {
System.err.println("No AndroidManifest.xml found in " + input);
return;
}
ManifestModel model = new ManifestModel();
new AxmlParser(manifest).parse(model);
emit(model.toJson(exportedOnly));
}
/** Read AndroidManifest.xml from an APK zip, or treat the input as a raw AXML file. */
private static byte[] readManifestBytes(Path input) throws Exception {
// Probe the first bytes: AXML files start with chunk type 0x0003 (LE: 03 00).
byte[] head = new byte[2];
try (InputStream in = Files.newInputStream(input)) {
int n = in.read(head);
if (n == 2 && (head[0] & 0xFF) == 0x03 && (head[1] & 0xFF) == 0x00) {
return Files.readAllBytes(input);
}
}
// Otherwise treat as a zip/APK.
try (ZipFile zf = new ZipFile(input.toFile())) {
ZipEntry e = zf.getEntry("AndroidManifest.xml");
if (e != null) {
try (InputStream in = zf.getInputStream(e)) {
return drain(in);
}
}
} catch (Exception zipErr) {
// Fall through to a streaming scan for odd zips.
try (ZipInputStream zis = new ZipInputStream(Files.newInputStream(input))) {
ZipEntry e;
while ((e = zis.getNextEntry()) != null) {
if ("AndroidManifest.xml".equals(e.getName())) {
return drain(zis);
}
}
}
}
return null;
}
private static byte[] drain(InputStream in) throws Exception {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[8192];
int r;
while ((r = in.read(buf)) != -1) {
bos.write(buf, 0, r);
}
return bos.toByteArray();
}
private void emit(String result) throws Exception {
if (output != null && !output.isEmpty()) {
Files.write(Paths.get(output), result.getBytes("UTF-8"));
System.out.println("Written to " + output);
} else {
System.out.println(result);
}
}
/** Accumulates manifest facts via AxmlParser events. */
private static final class ManifestModel implements AxmlParser.Handler {
String pkg;
String versionCode;
String versionName;
String minSdk;
String targetSdk;
String compileSdk;
Boolean debuggable;
Boolean allowBackup;
String usesCleartext;
final List