#!/usr/bin/env python3 """Analyze AndroidManifest.xml for security issues and suspicious permissions. Parses a decoded AndroidManifest.xml (from jadx or apktool output) and flags dangerous permissions, exported components, debug settings, and other security concerns commonly exploited by mobile malware. """ from __future__ import annotations import argparse import json import re import sys import zipfile from pathlib import Path from typing import Any from xml.etree import ElementTree as ET # Android namespace used in manifest attributes _ANDROID_NS = "http://schemas.android.com/apk/res/android" # Permissions considered dangerous or commonly abused by malware _DANGEROUS_PERMISSIONS: set[str] = { "android.permission.SEND_SMS", "android.permission.RECEIVE_SMS", "android.permission.READ_SMS", "android.permission.RECEIVE_MMS", "android.permission.ACCESS_FINE_LOCATION", "android.permission.ACCESS_COARSE_LOCATION", "android.permission.ACCESS_BACKGROUND_LOCATION", "android.permission.CAMERA", "android.permission.RECORD_AUDIO", "android.permission.READ_CONTACTS", "android.permission.WRITE_CONTACTS", "android.permission.READ_CALL_LOG", "android.permission.WRITE_CALL_LOG", "android.permission.CALL_PHONE", "android.permission.READ_PHONE_STATE", "android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE", "android.permission.MANAGE_EXTERNAL_STORAGE", "android.permission.BIND_ACCESSIBILITY_SERVICE", "android.permission.BIND_DEVICE_ADMIN", "android.permission.BIND_NOTIFICATION_LISTENER_SERVICE", "android.permission.SYSTEM_ALERT_WINDOW", "android.permission.REQUEST_INSTALL_PACKAGES", "android.permission.PACKAGE_USAGE_STATS", } def _attr(element: ET.Element, name: str) -> str | None: """Get an android: namespaced attribute from an element.""" return element.get(f"{{{_ANDROID_NS}}}{name}") def _parse_manifest_xml(xml_path: Path) -> ET.Element: """Parse an AndroidManifest.xml file and return the root element.""" tree = ET.parse(xml_path) return tree.getroot() def _extract_permissions(root: ET.Element) -> dict[str, Any]: """Extract requested permissions and classify them.""" all_perms: list[str] = [] dangerous: list[str] = [] for elem in root.iter("uses-permission"): perm = _attr(elem, "name") if perm: all_perms.append(perm) if perm in _DANGEROUS_PERMISSIONS: dangerous.append(perm) risk = "low" if len(dangerous) >= 5: risk = "high" elif len(dangerous) >= 2: risk = "medium" return { "total": len(all_perms), "all": sorted(all_perms), "dangerous": sorted(dangerous), "risk_assessment": risk, } def _extract_components(root: ET.Element) -> dict[str, list[dict[str, Any]]]: """Extract exported activities, services, receivers, and providers.""" component_types = ["activity", "service", "receiver", "provider"] components: dict[str, list[dict[str, Any]]] = {} for ctype in component_types: exported: list[dict[str, Any]] = [] for elem in root.iter(ctype): name = _attr(elem, "name") or "unknown" is_exported = _attr(elem, "exported") has_intent_filter = any(True for _ in elem.iter("intent-filter")) # Components with intent-filters are implicitly exported unless # exported="false" is explicitly set. if is_exported == "true" or (has_intent_filter and is_exported != "false"): perm = _attr(elem, "permission") exported.append({ "name": name, "permission": perm, "has_intent_filter": has_intent_filter, }) components[f"exported_{ctype}s" if ctype != "activity" else "exported_activities"] = exported return components def _check_security_flags(root: ET.Element) -> list[dict[str, str]]: """Check application-level security flags.""" findings: list[dict[str, str]] = [] app_elem = root.find("application") if app_elem is None: return findings debuggable = _attr(app_elem, "debuggable") if debuggable == "true": findings.append({ "severity": "high", "finding": "Application is debuggable", "detail": "android:debuggable=\"true\" allows attaching a debugger.", }) allow_backup = _attr(app_elem, "allowBackup") if allow_backup == "true": findings.append({ "severity": "medium", "finding": "Application allows backup", "detail": "android:allowBackup=\"true\" allows data extraction via adb backup.", }) cleartext = _attr(app_elem, "usesCleartextTraffic") if cleartext == "true": findings.append({ "severity": "medium", "finding": "Cleartext traffic allowed", "detail": "android:usesCleartextTraffic=\"true\" permits unencrypted HTTP.", }) return findings def analyze_manifest(manifest_path: Path) -> dict[str, Any]: """Analyze an AndroidManifest.xml and return structured findings.""" root = _parse_manifest_xml(manifest_path) package = root.get("package", "unknown") version_code = _attr(root, "versionCode") version_name = _attr(root, "versionName") sdk_elem = root.find("uses-sdk") min_sdk = _attr(sdk_elem, "minSdkVersion") if sdk_elem is not None else None target_sdk = _attr(sdk_elem, "targetSdkVersion") if sdk_elem is not None else None permissions = _extract_permissions(root) components = _extract_components(root) security_findings = _check_security_flags(root) return { "package_name": package, "version_code": version_code, "version_name": version_name, "min_sdk": min_sdk, "target_sdk": target_sdk, "permissions": permissions, "components": components, "security_findings": security_findings, } def analyze(input_path: Path, output_format: str = "json") -> dict[str, Any]: """Analyze a manifest file and return results.""" return analyze_manifest(input_path) def main() -> None: parser = argparse.ArgumentParser( description="Analyze AndroidManifest.xml for security issues." ) parser.add_argument("--input", type=Path, help="Path to AndroidManifest.xml or APK file") parser.add_argument("--manifest", type=Path, help="Path to decoded AndroidManifest.xml") parser.add_argument("--apk", type=Path, help="Path to APK (will extract manifest automatically)") parser.add_argument("--output", type=Path, help="Path to output file (stdout if omitted)") parser.add_argument("--format", default="json", choices=["json", "text", "csv"], help="Output format (default: json)") args = parser.parse_args() manifest_path = args.manifest or args.input if not manifest_path and not args.apk: parser.error("Provide a manifest via --manifest, --input, or an APK via --apk") if args.apk and not manifest_path: # Attempt to extract the binary AndroidManifest.xml from the APK. # Note: the binary XML needs apktool/jadx to decode; this stub reads # an already-decoded XML if available. print("[error] Direct APK manifest extraction requires apktool or jadx. " "Decode the APK first, then pass the decoded manifest with --manifest.", file=sys.stderr) sys.exit(1) if manifest_path and not manifest_path.exists(): print(f"[error] file not found: {manifest_path}", file=sys.stderr) sys.exit(1) result = analyze_manifest(manifest_path) rendered = json.dumps(result, indent=2) if args.output: args.output.parent.mkdir(parents=True, exist_ok=True) args.output.write_text(rendered, encoding="utf-8") print(f"[+] Findings written to {args.output}", file=sys.stderr) else: print(rendered) if __name__ == "__main__": main()