Skip to the content.

10 · APK harvest — reproduce from scratch

A standalone, end-to-end guide so an independent third party can reproduce the APK harvest (07), the integration matrix (08) and the live km-range + kill-switch audit (09) from nothing. It lists prerequisites with the exact versions used, the precise commands (one-shot or step-by-step), the expected outputs, and how to verify the result against the published checksums. Where output can legitimately vary, it says so.

This complements the deeper techniques in ../methodology-and-reproduction.md (decompile, GPS spoofing, HTTPS MITM); here we focus only on harvest → identify → scan → read config → verify.


1. Prerequisites

Need Why Version used here
Rooted Android device, USB debugging on Root is required only to read each app’s private Crowd Connected DB/prefs (step 5). APK pulling (steps 2–4) needs no root. Pixel 10a, Android 16, Magisk root
The Appmiral apps installed on it You can only harvest what’s installed; only launched apps have a live config. 140 apps
Android platform-tools (adb) Enumerate packages, pull APKs, pull DBs. adb 1.0.41
Android build-tools (aapt2) — optional Cross-check badging/labels. build-tools 36.1.0
Python 3.11+ Runs the scan + config reader. Python 3.11.9
androguard Robust APK manifest/DEX analysis. androguard 4.1.4 (pip install androguard==4.1.4)
sqlite3 Decode the Crowd Connected Room DB. Python stdlib, SQLite 3.45.1
PowerShell 7 (pwsh) Runs the .ps1 harvester/orchestrator. Windows 11

The exact versions for this capture are recorded programmatically in ../../evidence/appmiral-apks_2026-06-28_154246/logs/tool-versions.txt and ../../tools/apk-harvest/logs/tool-versions.txt. Confirm the device is connected:

adb devices            # must list exactly one "<serial>   device"
adb shell su -c id     # must print uid=0(root) — needed for step 5 only

The reusable, documented scripts live in ../../tools/apk-harvest/: harvest-apks.ps1, scan-integrations.py, read-cc-config.py, and the one-command run-all.ps1. None hard-codes a device serial — it is resolved from adb devices (override with -Serial / --serial).


2. One command (does everything)

pwsh tools/apk-harvest/run-all.ps1 `
  -PrivateDir <big/private location for APK + DB bytes> `
  -DataDir    docs/appmiral-comparison/data

This runs harvest → scan → config-read, then generates the private SHA256SUMS.txt byte manifests. Heavy bytes go to -PrivateDir (apks_<stamp>/, cc_dbs_<stamp>/); the lightweight CSV/JSON tables go to -DataDir. To run the phases individually (or re-run just one), use the steps below.


3. Step by step

Step 1 — Enumerate & reconcile

adb shell pm list packages -3        # third-party (user-installed) packages

Filter to the Appmiral namespace (appmiral, appstrakt, mysticfestival, triple.verstappen). The harvester does this for you and writes the raw list to logs/appmiral_packages_<date>.txt. Expected: ~140 packages.

Step 2 — Harvest the APKs (no root)

pwsh tools/apk-harvest/harvest-apks.ps1 `
  -OutDir      <private>/apks_<stamp> `
  -ManifestCsv docs/appmiral-comparison/data/apk_files_manifest_<date>.csv
# optional: -Serial <id>  -Filter <regex>  -Force (re-pull)

For each package it resolves every split with adb shell pm path and adb pulls the exact installed bytes into apks_<stamp>/<pkg>/. Idempotent: already-pulled packages are skipped (use -Force to re-pull).

Expected output — apk_files_manifest_<date>.csv, one row per file:

package,versionName,versionCode,file,size_bytes,sha256,on_device_path,captured_ts
com.appmiral.ttfestival,"2.x.x",N,base.apk,<bytes>,<sha256>,/data/app/~~…/base.apk,2026-…

~375 files / ~7.08 GB for 140 apps (varies with your app set/versions).

Step 3 — Identify + integration scan

python tools/apk-harvest/scan-integrations.py `
  --apks <private>/apks_<stamp> `
  --out  docs/appmiral-comparison/data/integration_matrix_<date>

Writes three files: …csv (app × integration matrix), ….identify.csv (label, versions, minSdk/targetSdk, ABIs, location perms), ….json (every hit with the matched byte-pattern, for audit).

Expected: baseline integrations (Instagram, Firebase, Fused Location) at ~140/140; the egress stack (Cognito, Kinesis Firehose) at ~139/140; variable-adoption ones lower (Permutive ~36, Mapbox ~16); ~19 advertised partners at 0. All 140 request fine+coarse location, ~30 add background.

Silence androguard’s logging if you adapt the script: it floods loguru DEBUG. The scanner already calls logger.remove() + logging.getLogger("androguard").setLevel(ERROR) before importing any APK.

Step 4 — Read the live km-range + kill-switch (root)

python tools/apk-harvest/read-cc-config.py `
  --serial  <id> `
  --workdir <private>/cc_dbs_<stamp> `
  --out     docs/appmiral-comparison/data/cc_live_config_<date>

For every installed app it checks (root) for databases/CrowdConnected-RoomDatabase, copies it (+ -wal/-shm) and shared_prefs/CrowdConnected.xml off the device, then decodes — tolerant of both SDK schemas:

Identifiers are redacted (deviceId → salted prefix; alias values dropped). The raw DBs/prefs stay in -workdir (private — they contain identifiers).

Expected: a row per app with a populated config. In this capture, 6 apps — com.appmiral.ttfestival ON @ 60 km, the rest OFF but armed at 40–80 km. See 09.


4. Verify

The public snapshot folder ../../evidence/appmiral-apks_2026-06-28_154246/ carries the checksums:

# 1. the public artifacts (tables, logs, the APK manifest itself)
cd evidence/appmiral-apks_2026-06-28_154246 && sha256sum -c SHA256SUMS.txt

# 2. the actual APK bytes (in the private repo) against the public manifest
cd <private>/apks_2026-06-28_154246 && sha256sum -c /path/to/apk-manifest.sha256

apk-manifest.sha256 (public) is byte-for-byte identical to apks_2026-06-28_154246/SHA256SUMS.txt (private), so the public repo attests exactly which APK bytes exist without re-hosting them. Regenerate any manifest with ../../tools/gen-sha256sums.ps1.


5. What should — and shouldn’t — match

Output Should match byte-for-byte? Why
APK SHA-256s ✅ yes, if the same app versions are installed APKs are the exact Play-delivered bytes; a Play update changes versionCode → new hash.
Integration matrix ✅ largely Same APKs → same static signatures. Differences mean a different app/version set.
Live km range / kill-switch ❌ not necessarily Config is server-pushed and changes over time; the operator can flip the switch or change radii at any moment.
Which apps have a config ❌ no Only launched apps have a cached DB; your launch history differs.

A different live switch state is not a failed reproduction — it confirms the key finding that activation is a remote, server-side decision. Capture your own evidence header (device, build, wall-clock) so your run is independently dated, the same way this one is.


6. Determinism caveats (summary)