Skip to content

Instantly share code, notes, and snippets.

@obfusk
Last active April 11, 2024 17:04
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save obfusk/ccfd5f515771a6f422c0659b5f92f982 to your computer and use it in GitHub Desktop.
Save obfusk/ccfd5f515771a6f422c0659b5f92f982 to your computer and use it in GitHub Desktop.
Detect APK files
#!/usr/bin/python3
# encoding: utf-8
# SPDX-FileCopyrightText: 2024 FC (Fay) Stegerman <flx@obfusk.net>
# SPDX-License-Identifier: AGPL-3.0-or-later
import fnmatch
import os
import struct
import sys
from collections import namedtuple
from typing import List, Optional
ZipData = namedtuple("ZipData", ("cd_offset", "eocd_offset", "cd_and_eocd"))
APK_FILES = (
b"AndroidManifest.xml", # required
b"META-INF/com/android/build/gradle/app-metadata.properties",
b"META-INF/MANIFEST.MF", # v1 signature
b"resources.arsc",
b"classes.dex",
b"assets/dexopt/baseline.prof",
b"assets/dexopt/baseline.profm",
b"DebugProbesKt.bin",
)
APK_FNMATCH = (
b"res/*.xml",
b"res/drawable/*.xml",
b"lib/arm64-v8a/*.so",
b"lib/armeabi-v7a/*.so",
b"lib/x86/*.so",
b"lib/x86_64/*.so",
b"META-INF/androidx.*.version",
b"META-INF/*.SF",
)
ZIPFLINGER_VIRTUAL_ENTRY = b"PK\x03\x04\x00\x00\x00\x00\x00\x00\x21\x08\x21\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x66\x00" + b"\x00" * 102
class DetectionError(Exception):
pass
def detect_apk(apkfile: str) -> Optional[List[str]]:
"""
Detect APK files; returns a list of things detected, None if the file does
not seem to be an APK, or raises a DetectionError if processing the file
fails.
"""
zdata = zip_data(apkfile)
results = ["ZIP file"]
seen = set()
with open(apkfile, "rb") as fh:
if fh.read(len(ZIPFLINGER_VIRTUAL_ENTRY)) == ZIPFLINGER_VIRTUAL_ENTRY:
results.append("zipflinger virtual entry")
fh.seek(zdata.cd_offset - 16)
if fh.read(16) == b"APK Sig Block 42":
results.append("APK Signing Block")
while fh.tell() < zdata.eocd_offset:
hdr = fh.read(46)
if hdr[:4] != b"\x50\x4b\x01\x02":
raise DetectionError("Expected central directory file header signature")
n, m, k = struct.unpack("<HHH", hdr[28:34])
if fh.tell() + n + m + k > zdata.eocd_offset:
raise DetectionError("Invalid length(s)")
filename = fh.read(n)
if filename in APK_FILES:
results.append(filename.decode())
for pattern in APK_FNMATCH:
if pattern not in seen and fnmatch.fnmatch(filename, pattern):
results.append(pattern.decode())
seen.add(pattern)
fh.seek(m + k, os.SEEK_CUR)
return None if APK_FILES[0].decode() not in results else results
def zip_data(apkfile: str, count: int = 4096) -> ZipData:
with open(apkfile, "rb") as fh:
fh.seek(-min(os.path.getsize(apkfile), count), os.SEEK_END)
data = fh.read()
pos = data.rfind(b"\x50\x4b\x05\x06")
if pos == -1:
raise DetectionError("Expected end of central directory record (EOCD)")
fh.seek(pos - len(data), os.SEEK_CUR)
eocd_offset = fh.tell()
fh.seek(16, os.SEEK_CUR)
cd_offset = int.from_bytes(fh.read(4), "little")
if cd_offset >= eocd_offset:
raise DetectionError("Expected CD before EOCD")
fh.seek(cd_offset)
cd_and_eocd = fh.read()
return ZipData(cd_offset, eocd_offset, cd_and_eocd)
def main() -> None:
import argparse
desc = "Detect APK files; exits with 0 if all files are detected as APKs, 1 otherwise."
parser = argparse.ArgumentParser(prog="detect-apk.py", description=desc)
parser.add_argument("-v", "--verbose", action="count", default=0,
help="increase verbosity level")
parser.add_argument("apks", metavar="APK", nargs="+")
args = parser.parse_args()
not_an_apk = 0
for apkfile in args.apks:
if not os.path.exists(apkfile):
print(f"Error: file {apkfile!r} does not exist.", file=sys.stderr)
sys.exit(1)
if args.verbose:
print(f"Processing {apkfile!r}...")
try:
results = detect_apk(apkfile)
except DetectionError as e:
not_an_apk += 1
if args.verbose:
print(f" Detection error: {e}.")
continue
if results:
if args.verbose:
print(f" Detected as APK ({len(results)} matches).")
if args.verbose > 1:
for result in results:
print(f" Found: {result}.")
else:
not_an_apk += 1
if args.verbose:
print(" Not detected as APK.")
if not_an_apk:
sys.exit(1)
if __name__ == "__main__":
main()
# vim: set tw=80 sw=4 sts=4 et fdm=marker :
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment