#!/usr/bin/env python3 """Automated APK triage and analysis tool. Extracts metadata, permissions, embedded URLs/IPs, native library information, and other indicators from Android APK files. Designed for initial triage of suspicious mobile applications. """ from __future__ import annotations import argparse import hashlib import json import re import sys import zipfile from pathlib import Path from typing import Any # Patterns for extracting network indicators from strings _URL_PATTERN = re.compile(r"https?://[^\s\"'<>]+", re.IGNORECASE) _IP_PATTERN = re.compile( r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b" ) _DOMAIN_PATTERN = re.compile( r"\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b", re.IGNORECASE ) def _sha256(path: Path) -> str: """Compute SHA-256 hash of a file.""" h = hashlib.sha256() with path.open("rb") as fh: for chunk in iter(lambda: fh.read(8192), b""): h.update(chunk) return h.hexdigest() def _extract_strings(data: bytes, min_length: int = 4) -> list[str]: """Extract printable ASCII strings from binary data.""" pattern = re.compile(rb"[\x20-\x7e]{%d,}" % min_length) return [m.decode("ascii", errors="replace") for m in pattern.findall(data)] def _list_native_libs(apk_path: Path) -> list[dict[str, Any]]: """List native .so libraries inside the APK.""" libs: list[dict[str, Any]] = [] with zipfile.ZipFile(apk_path, "r") as zf: for info in zf.infolist(): if info.filename.endswith(".so"): parts = info.filename.split("/") arch = parts[1] if len(parts) > 2 else "unknown" libs.append({ "path": info.filename, "architecture": arch, "size": info.file_size, }) return libs def _extract_urls_and_ips(apk_path: Path) -> dict[str, list[str]]: """Scan all text-like entries in the APK for URLs and IPs.""" urls: set[str] = set() ips: set[str] = set() domains: set[str] = set() with zipfile.ZipFile(apk_path, "r") as zf: for info in zf.infolist(): if info.file_size > 10 * 1024 * 1024: continue try: data = zf.read(info.filename) except Exception: continue strings = _extract_strings(data) for s in strings: urls.update(_URL_PATTERN.findall(s)) ips.update(_IP_PATTERN.findall(s)) domains.update(_DOMAIN_PATTERN.findall(s)) noise = {"schemas.android.com", "www.w3.org", "ns.adobe.com", "xml.org", "xmlpull.org", "apache.org"} domains = {d for d in domains if not any(n in d for n in noise)} return { "urls": sorted(urls), "ips": sorted(ips), "domains": sorted(domains), } def _list_apk_entries(apk_path: Path) -> list[str]: """Return a list of all file entries in the APK.""" with zipfile.ZipFile(apk_path, "r") as zf: return [info.filename for info in zf.infolist()] def analyze_apk( apk_path: Path, extract_urls: bool = False, extract_ips: bool = False, check_native: bool = False, verbose: bool = False, ) -> dict[str, Any]: """Perform triage analysis of an APK file.""" result: dict[str, Any] = { "sample": { "filename": apk_path.name, "sha256": _sha256(apk_path), "file_size": apk_path.stat().st_size, }, "entries_count": 0, "dex_files": [], "native_libraries": [], "network_indicators": {}, } entries = _list_apk_entries(apk_path) result["entries_count"] = len(entries) result["dex_files"] = [e for e in entries if e.endswith(".dex")] if check_native: result["native_libraries"] = _list_native_libs(apk_path) if extract_urls or extract_ips: indicators = _extract_urls_and_ips(apk_path) if extract_urls: result["network_indicators"]["urls"] = indicators["urls"] result["network_indicators"]["domains"] = indicators["domains"] if extract_ips: result["network_indicators"]["ips"] = indicators["ips"] if verbose: result["all_entries"] = entries return result def analyze(input_path: Path, output_format: str = "json") -> dict[str, Any]: """Analyze an APK and return results as a dict.""" return analyze_apk(input_path, extract_urls=True, extract_ips=True, check_native=True) def main() -> None: parser = argparse.ArgumentParser(description="Automated APK triage and analysis.") parser.add_argument("--input", type=Path, help="Path to APK file (alias for --apk)") parser.add_argument("--apk", type=Path, help="Path to APK file") 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)") parser.add_argument("--extract-urls", action="store_true", help="Extract URLs and domains from the APK") parser.add_argument("--extract-ips", action="store_true", help="Extract IP addresses from the APK") parser.add_argument("--check-native", action="store_true", help="List native shared libraries (.so files)") parser.add_argument("--verbose", action="store_true", help="Include full file listing in output") args = parser.parse_args() apk_path = args.apk or args.input if not apk_path: parser.error("Provide an APK file via --apk or --input") if not apk_path.exists(): print(f"[error] file not found: {apk_path}", file=sys.stderr) sys.exit(1) result = analyze_apk( apk_path, extract_urls=args.extract_urls, extract_ips=args.extract_ips, check_native=args.check_native, verbose=args.verbose, ) 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"[+] Report written to {args.output}", file=sys.stderr) else: print(rendered) if __name__ == "__main__": main()