From 5d7749d75c6456a561827528f882a187b6af22dd Mon Sep 17 00:00:00 2001 From: joseph Date: Sun, 14 Jun 2026 08:07:57 +0200 Subject: [PATCH] docs: lean README + reflect non-Verified boot -> CRITICAL verdict - README: trim to user touchpoints (install, API, signals, output, permissions, privacy); drop marketing/validation/inline JSON dump (414 -> 193 lines). Full detector detail stays in docs/DETECTORS.md. - DETECTORS.md: tee_integrity_verdict is now CRITICAL (was HIGH) for any non-Verified boot; document os_modified_proof + verified_boot_key_sha256. - CHANGELOG: add [Unreleased] entry for the verdict severity change + proof fields. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 14 ++ README.md | 356 +++++++++------------------------------------- docs/DETECTORS.md | 10 +- 3 files changed, 88 insertions(+), 292 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a5a628e..9f8979e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,20 @@ All notable changes to **DeviceIntelligence** are recorded here. Format follows The wire format (`TelemetryReport` JSON, `Finding.kind` identifiers, detector IDs) carries an independent `schema_version` integer that is **only** bumped on breaking changes. Adding new finding kinds or new detectors is additive and does NOT bump `schema_version`. Backends pin against `schema_version` for correctness; library version pinning is for build-time API stability. +## [Unreleased] + +### Changed + +- **`attestation.key` verdict: a non-Verified boot is now CRITICAL (was HIGH).** `deriveIntegrityVerdict` no longer treats SelfSigned (yellow) as a genuine OS — only a Google-Verified (green) boot qualifies for `MEETS_DEVICE_INTEGRITY`. SelfSigned (yellow), Unverified (orange), and Failed (red) all mean the boot chain was modified (custom AVB key / unlocked / failed verification), so the `tee_integrity_verdict` finding is now emitted at **CRITICAL** severity for all three. A yellow/relocked-with-own-key boot (custom kernel / KernelSU / custom ROM) no longer reaches `MEETS_DEVICE_INTEGRITY` and no longer downgrades to HIGH. + +### Added + +- **Hardware-attested "OS modified" proof on `tee_integrity_verdict`.** When the boot state is non-Verified, the finding's `message` now states *why* the OS is considered modified, and `details` gain two fields: `verified_boot_key_sha256` (the attested boot-signing key hash — Google's on stock, a custom value on a re-signed boot) and `os_modified_proof` (a human-readable, hardware-attested explanation). These come from the secure element's RootOfTrust and cannot be forged on-device. + +### Wire-format impact + +`schema_version` stays at `2` — the new `details` keys are additive and the severity field already existed. Backends that branch on `tee_integrity_verdict.severity` will now see `critical` where they previously saw `high` for non-Verified boots; treat that as a stronger signal, not a new shape. + ## [2.0.0] — 2026-05-28 Stable major release. Removes the native analytics drain entirely: the SDK now performs zero network calls under any configuration, and the entire telemetry pipeline (collection → JSON → upload) lives in the consumer's process and on the consumer's backend. diff --git a/README.md b/README.md index c567818..dcdc5cd 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@

DeviceIntelligence

- Open-source Android telemetry SDK for understanding the device ecosystem of your userbase.
- APK integrity · key attestation · bootloader integrity · runtime tampering · root indicators · emulator probe · cloner detection · runtime DEX-injection · 8-layer native anti-hooking stack.
- Not a RASP. Not a kill-switch. Just structured, deterministic JSON your backend can analyze. + Open-source Android device-integrity SDK.
+ Collects APK integrity, hardware key attestation, root / hook / emulator / cloner signals into one deterministic JSON your backend can act on.
+ Not a RASP — it observes and reports; your backend owns the policy.

@@ -15,40 +15,11 @@ GDPR-friendly

-

- Clean Pixel 9 Pro - Rooted Pixel 6 Pro — Signals card - Rooted Pixel 6 Pro — Findings card expanded -

- -

- Same release APK, three devices. Left — clean Pixel 9 Pro. Middle — Pixel 6 Pro running KernelSU + LSPosed, Signals card lifts findings into product-shaped verdicts. Right — Findings card sorted worst-first, tap to expand the diagnostic details map. -

- --- -## Why DeviceIntelligence - -Most Android apps shipping device-tampering checks reach for one of: - -- **Google Play Integrity API** — black-box, requires Google Play Services, can't run on AOSP/FOSS devices, doesn't tell you *why* a device is suspect. -- **A commercial RASP** — expensive, partly closed-source, opaque detection logic, vendor lock-in. -- **A simple root checker** (RootBeer, SafetyNetHelper) — covers maybe 20% of the threat surface a real attacker uses. - -DeviceIntelligence sits in the gap. It's: - -- **Fully open-source** (Kotlin + native C++) — every detection rule is auditable. No closed binary blobs. -- **Free-of-Google** — no Play Services dependency; works on AOSP, OpenGApps, GrapheneOS, CalyxOS, etc. Hardware key attestation still works on those builds because it's a Keymaster API call, not a Google API call. -- **Detection-rich** — covers the techniques real attackers actually use: Frida agents, Xposed/LSPosed/EdXposed, Pine, SandHook, YAHFA, Cydia Substrate, Magisk, Zygisk, Riru, Taichi, app cloners, and runtime DEX injection (`InMemoryDexClassLoader`/`DexClassLoader` payloads). 8 layers of in-process anti-hooking with circular-bypass design. -- **Backend-agnostic** — emits one deterministic JSON. Send it to your own backend, Datadog, BigQuery, anywhere. The SDK itself performs no network calls and ships no analytics — collection and policy stay entirely in your hands. - -**Use it when** you need device-tampering evidence richer than Play Integrity's pass/fail bit, want server-side control of the policy decision, can't or won't take the Play Services dependency, or want to audit the detection logic rather than trust a vendor's claims. - -**Don't use it when** you want a turnkey "block this user" decision baked into the SDK — that's not what this is. DeviceIntelligence reports facts; *your backend* decides what to do about them. - ## Install -Distributed via [JitPack](https://jitpack.io/#iamjosephmj/DeviceIntelligence). +Distributed via [JitPack](https://jitpack.io/#iamjosephmj/DeviceIntelligence). Apply the Gradle plugin — it auto-wires the runtime AAR and bakes the build-time APK fingerprint. **`settings.gradle.kts`** @@ -80,27 +51,27 @@ plugins { } ``` -Ships native binaries for **`arm64-v8a`**, **`x86_64`**, and **`armeabi-v7a`** (32-bit ARM). On 32-bit devices, all detectors run except `integrity.art`, which reports `INCONCLUSIVE` because the underlying `ArtMethod` field-offset table is currently 64-bit-only. +`minSdk` 28. Ships native binaries for `arm64-v8a`, `x86_64`, and `armeabi-v7a`. `kotlinx-coroutines-android` is the only runtime dependency. ## Quick start -Four entry points, pick the one that matches your use case. +Four entry points — pick the one that fits your use case. -**One-shot collect** — your app starts, you want one structured snapshot, you ship it to your backend. +**One-shot collect** — one structured snapshot at startup. ```kotlin lifecycleScope.launch { - val report = DeviceIntelligence.collect(context) // TelemetryReport - val json = DeviceIntelligence.collectJson(context) // canonical JSON - + val report = DeviceIntelligence.collect(context) // TelemetryReport + val json = DeviceIntelligence.collectJson(context) // canonical JSON val signals = report.toIntegritySignals() + if (IntegritySignal.HOOKING_FRAMEWORK_DETECTED in signals) { - // Send to your backend, gate the action, raise a flag — your call. + // Ship to your backend, gate the action, raise a flag — your call. } } ``` -**Periodic observe** — long-running session, you want a fresh snapshot every N seconds (e.g. catch a Frida agent that attaches mid-flow). +**Periodic observe** — a fresh snapshot every N seconds (e.g. catch a Frida agent that attaches mid-flow). ```kotlin DeviceIntelligence.observe(context, interval = 2.seconds) @@ -108,63 +79,48 @@ DeviceIntelligence.observe(context, interval = 2.seconds) .launchIn(lifecycleScope) ``` -**Cumulative session observe** — same as `observe()` but accumulates findings across emissions. A transient hook that fires once and detaches stays visible with `stillActive=false`. Use this when your UI / backend correlation should never lose sight of a signal the moment it stops appearing. +**Cumulative session observe** — like `observe()`, but accumulates findings across emissions. A transient hook that fires once and detaches stays visible with `stillActive = false`. ```kotlin DeviceIntelligence.observeSession(context, interval = 2.seconds) .onEach { session: SessionFindings -> - render(session.findings) // List - ship(session.toJson()) // canonical wire format + render(session.findings) // List + ship(session.toJson()) // canonical wire format } .launchIn(lifecycleScope) ``` -Each `TrackedFinding` carries `firstSeenAtEpochMs`, `lastSeenAtEpochMs`, `observationCount`, and `stillActive` on top of the underlying `Finding`. +Each `TrackedFinding` adds `firstSeenAtEpochMs`, `lastSeenAtEpochMs`, `observationCount`, and `stillActive` on top of the underlying `Finding`. -**Java / synchronous boundary** — for Java consumers, worker threads, JNI bridges. +**Java / synchronous** — for Java consumers, worker threads, JNI bridges. ```java TelemetryReport report = DeviceIntelligence.collectBlocking(context); String json = DeviceIntelligence.collectJsonBlocking(context); ``` -`kotlinx-coroutines-android` is the only runtime dependency. - -## What it collects - -| Detector | id | What it observes | -|----------------------|--------------------------|-------------------------------------------------------------------------------| -| APK integrity | `integrity.apk` | APK bytes vs. the build-time fingerprint baked by the Gradle plugin | -| Bootloader integrity | `integrity.bootloader` | TEE-spoofing / cached-chain detection on `attestation.key` | -| ART integrity | `integrity.art` | In-process ART tampering across 5 vectors (Frida, Xposed, LSPosed, Pine, …) | -| Key attestation | `attestation.key` | TEE / StrongBox attestation: Verified Boot, bootloader lock, OS patch level | -| Runtime environment | `runtime.environment` | Debugger / ptrace / native integrity stack (text hash, GOT, injected libs) + runtime DEX-injection (InMemoryDexClassLoader / DexClassLoader payloads) + Frida 16+ Gum memfd-JIT attribution | -| Root indicators | `runtime.root` | `su` binary, Magisk artifacts, `test-keys`, root-manager apps, Shamiko-bypass cross-checks (init mount-namespace + `@magisk_daemon` socket), MagiskTrustUserCerts TLS-trust-store MITM | -| Emulator probe | `runtime.emulator` | CPU-instruction-level signals (arm64 MRS / x86_64 CPUID hypervisor bit) | -| App cloner | `runtime.cloner` | Foreign APK mappings, mount-namespace inconsistencies, UID mismatches | - -Each detector emits granular `Finding`s; the `IntegritySignal` mapper collapses ~40 finding kinds into 11 product-shaped verdicts for UI / feature-flag code: - -| `IntegritySignal` | Meaning | -|---------------------------------|------------------------------------------------------------------------------------------------------------------------| -| `APK_TAMPERED` | APK on disk modified, repackaged, signer mismatch, or installer not allowlisted. | -| `APK_FINGERPRINT_UNAVAILABLE` | The build-time fingerprint asset is missing/corrupt — couldn't make a verdict either way. | -| `BOOTLOADER_INTEGRITY_FAILED` | Hardware key-attestation chain has anomalies, or device claims StrongBox but attests at a lower level. | -| `TEE_ATTESTATION_DEGRADED` | Local advisory verdict on the attestation chain came back below `MEETS_STRONG_INTEGRITY`, OR the leaf cert's KeyDescription extension is in CBOR/EAT format (KeyMint 200+) and field-level parsing is deferred to backend re-verification. | -| `HOOKING_FRAMEWORK_DETECTED` | Active code-level hooking — Frida (incl. Frida 16+ Gum memfd-JIT attribution), Xposed/LSPosed/EdXposed, Pine, SandHook, Substrate, ART-internals tampering, runtime DEX injection, RWX trampolines, `.text` drift, GOT rewrites. | -| `INJECTED_NATIVE_CODE` | Unknown post-baseline `.so` or anonymous executable mapping; precondition for hooking but not yet proof of one. | -| `ROOT_INDICATORS_PRESENT` | `su` binary, Magisk artifact, `test-keys` build, `which su` succeeds, root-manager app installed, Magisk visible in PID 1's mount namespace (Shamiko bypass), `@magisk_daemon` abstract socket bound, or a `tmpfs` over `/apex/com.android.conscrypt` (MagiskTrustUserCerts TLS-MITM enablement — treat as hard block). | -| `EMULATOR_DETECTED` | CPU-instruction-level signals — arm64 MRS or x86_64 CPUID hypervisor bit. | -| `APP_CLONED` | Foreign APK mappings, mount-namespace inconsistencies, UID mismatches. | -| `DEBUGGER_ATTACHED` | JVM debugger or ptrace tracer attached. | -| `DEBUG_FLAG_MISMATCH` | App's `FLAG_DEBUGGABLE` disagrees with `ro.debuggable`. | -| `HARDWARE_ATTESTED_USERSPACE_TAMPERED` | **Strongest single signal.** Hardware attestation reports `verifiedBootState=Verified` AND a userspace hook finding fires in the same report. Either TEE compromise or post-attestation injection (Magisk + Shamiko, etc.). Backends should treat as highest-confidence compromise signal. | +## Signals + +Detectors emit granular `Finding`s; the `IntegritySignal` mapper collapses them into product-shaped verdicts you branch on. + +| `IntegritySignal` | Meaning | +|----------------------------------------|------------------------------------------------------------------------------------------| +| `APK_TAMPERED` | APK modified, repackaged, signer mismatch, or installer not allowlisted. | +| `APK_FINGERPRINT_UNAVAILABLE` | Build-time fingerprint asset missing/corrupt — no verdict either way. | +| `BOOTLOADER_INTEGRITY_FAILED` | Attestation chain has anomalies, or device claims StrongBox but attests lower. | +| `TEE_ATTESTATION_DEGRADED` | Attestation verdict below `MEETS_STRONG_INTEGRITY` (or a CBOR/EAT leaf needing re-verify).| +| `HOOKING_FRAMEWORK_DETECTED` | Active code hooking — Frida, Xposed/LSPosed, Pine, SandHook, Substrate, DEX injection, `.text`/GOT tampering. | +| `INJECTED_NATIVE_CODE` | Unknown post-baseline `.so` / anon-exec mapping (precondition for hooking). | +| `ROOT_INDICATORS_PRESENT` | `su`, Magisk artifacts, `test-keys`, root-manager app, Shamiko bypass, or TLS-trust-store MITM. | +| `EMULATOR_DETECTED` | CPU-level signals (arm64 MRS / x86_64 CPUID hypervisor bit). | +| `APP_CLONED` | Foreign APK mappings, mount-namespace inconsistencies, UID mismatches. | +| `DEBUGGER_ATTACHED` | JVM debugger or ptrace tracer attached. | +| `DEBUG_FLAG_MISMATCH` | App's `FLAG_DEBUGGABLE` disagrees with `ro.debuggable`. | +| `HARDWARE_ATTESTED_USERSPACE_TAMPERED` | **Strongest signal.** Verified boot **and** a userspace hook in the same report — treat as a hard block. | ```kotlin val report = DeviceIntelligence.collect(context).toIntegritySignalReport() when { - // Hardware-attested AND userspace-tampered = the highest-confidence - // signal the SDK can produce. Treat as a hard block. IntegritySignal.HARDWARE_ATTESTED_USERSPACE_TAMPERED in report.signals -> hardBlock() IntegritySignal.HOOKING_FRAMEWORK_DETECTED in report.signals -> denyPayment() IntegritySignal.ROOT_INDICATORS_PRESENT in report.signals -> warnUser() @@ -176,200 +132,41 @@ report.evidence[IntegritySignal.HOOKING_FRAMEWORK_DETECTED]?.forEach { finding - } ``` -> **Not a RASP.** It does not block sessions, kill processes, or interrupt any flow. It only observes. Build enforcement on the JSON your backend ingests; keep the policy off-device. - -## Validated against - -DeviceIntelligence ships its own offensive verification harnesses — Frida scripts and a real LSPosed module that intentionally trip each detector. Detection isn't claimed; it's *verified* against the same tools an attacker would use, on real hardware (Pixel 6 Pro running KernelSU + LSPosed; secondary Pixel 9 Pro for clean baseline). - -**Cross-OEM stability.** Beyond the per-detector verification on Pixels, `collect()` / `observe()` / `observeSession()` have been validated for runtime stability across [Sauce Labs](https://saucelabs.com/)' real-device farm — every Android 11+ (API 30–36) device in the farm, spanning the major OEM forks (Samsung One UI, Xiaomi HyperOS / MIUI, Vivo OriginOS, Honor MagicOS, OPPO ColorOS, OnePlus OxygenOS, Motorola, plus AOSP-equivalent Pixels). "Stability" here means: the native lib loads, every detector runs to completion, the JSON parses, no crashes on any tested device. Attack-scenario coverage (LSPosed / Frida actually firing detections) is verified on the Pixel 6 Pro reference rig. - -**Cross-ABI stability.** All three native ABIs the AAR ships have been runtime-validated in the same Sauce Labs sweep: - -| ABI | Status | -| -------------- | --------------------------------------------------------------| -| `arm64-v8a` | full coverage — every detector works | -| `x86_64` | full coverage — every detector works | -| `armeabi-v7a` | runtime-stable since 0.8.0 (validated on Sauce Labs 32-bit ARM devices). Every detector works EXCEPT `integrity.art`, which reports `INCONCLUSIVE` because the underlying `ArtMethod` field-offset table is 64-bit-only. Characterising 32-bit ART struct layouts is tracked as a future minor-version research task. | - -| Surface | Validated with | Status | -|----------------------------------|------------------------------------------------------------------------------------------------------|--------| -| ART method-hook vectors A–F | `tools/red-team/frida-vector-{a,c,d,e,f}.js` — 5 independent JNI-level Frida scripts | shipped | -| Frida-Java's `cls.method.implementation` | `tools/red-team/frida-vector-frida-java.js` | shipped | -| LSPosed Java-side method hooks | `samples/lsposed-tester` — real LSPosed module installs hooks; StackGuard + StackWatchdog catch them | shipped | -| Runtime DEX injection (CTF Flag 1) | LSPosed-driven `InMemoryDexClassLoader` + disk-backed `DexClassLoader` from `/data/local/tmp/` | shipped (0.6.0) | -| Pre-baseline DEX injection (Zygisk timing) | `samples/lsposed-tester` `EarlyDexInjectionHook` — synchronous inject in `handleLoadPackage` | shipped via `unattributable_dex_at_baseline` (0.6.0) | -| Newer hook frameworks (Dobby/Whale/YAHFA/FastHook/il2cpp-dumper) | `tools/red-team/maps-newer-frameworks.js` — Frida `prctl(PR_SET_VMA_ANON_NAME)` page renaming | shipped (0.9.0) | -| Hardware attestation × userspace tampering correlation | composes existing detector findings — JVM unit tests + Pixel 6 Pro live data | shipped (1.0.0) | -| Magisk + Shamiko hide-module bypass | `/proc/1/mountinfo` cross-check (init namespace can't be unshared per-process) + `@magisk_daemon` abstract Unix socket via `/proc/self/net/unix` — JVM unit tests with hand-crafted procfs fixtures. Finding kinds: `magisk_in_init_mountinfo`, `magisk_daemon_socket_present` | shipped (1.x) | -| MagiskTrustUserCerts TLS-trust-store MITM | `tmpfs` bind-mount over `/apex/com.android.conscrypt` in `/proc/self/mountinfo` — JVM unit tests, CRITICAL severity (active TLS interception, not just root presence). Finding kind: `tls_trust_store_tampered` | shipped (1.x) | -| Frida 16+ Gum memfd-backed JIT | `/memfd:jit-cache` + `rwxp` + region size >8 MB pattern in `/proc/self/maps` — JVM unit tests; fires alongside the generic `rwx_memory_mapping` for backend Frida-attribution pivot. Finding kind: `frida_memfd_jit_present` | shipped (1.x) | -| EAT/CBOR attestation format detection | `KeyDescriptionParser` heuristic: when legacy ASN.1 parse fails AND the unwrapped extension starts with a CBOR map byte (`0xA0`–`0xBF`), emits `attestation_eat_format_detected` (LOW) so backends know parsed fields need server-side re-verification. Full CBOR-EAT field-level parsing tracked for a follow-up minor | shipped — format detection only (1.x) | -| Real Zygisk module | TBD — see [`tools/red-team/CTF_ROADMAP.md`](tools/red-team/CTF_ROADMAP.md) | planned | -| Samsung Knox warranty-bit parsing | Samsung Knox attestation extension OID prefix is detected on the leaf, but warranty-bit byte parsing requires on-device Samsung validation tracked for a follow-up minor | planned | - -Full step-by-step validation runbook for the Pixel 6 Pro: [`tools/red-team/FLAG1_RUNBOOK.md`](tools/red-team/FLAG1_RUNBOOK.md). - -## JSON contract - -`DeviceIntelligence.collectJson(context)` returns a single deterministic -document. The shape is stable across releases that share the same -`schema_version` (currently `2`). For every `Finding`, the fields -`kind` / `severity` / `subject` / `message` are stable; `details` is -opaque diagnostic data — useful for forensics, but its keys may change -between releases without a `schema_version` bump, so don't key on them -server-side. - -**`status` vs `findings`** answer different questions. `status` -(`ok` / `inconclusive` / `error`) means "did the detector run?"; -`findings[]` means "what did it see?". A rooted device looks like -`status: "ok"` plus a non-empty `findings[]`. Drive your "device looks -tampered" decision off `summary.detectors_with_findings`, not `status`. - -**Per-collect vs cumulative session.** `collectJson()` and -`TelemetryReport.toJson()` emit one snapshot of the moment the -collect ran. `SessionFindings.toJson()` (from `observeSession`) emits -a cumulative session view — same wire shape per finding plus -`first_seen_at_epoch_ms` / `last_seen_at_epoch_ms` / -`observation_count` / `still_active`, with a `latest_report_summary` -correlation block. Both share `schema_version`; pick whichever -matches your backend's correlation model. - -
-Full clean-device report (click to expand) - -Captured live from a clean Pixel 9 Pro. Locale, timezone, install -timestamps, `vpn_active`, `boot_count`, and APK random suffixes were -swapped for generic values; everything else (StrongBox-backed -attestation, Tensor G4 SoC, Mali GPU, 120Hz panel, GMS signer SHA) is -the unmodified real value. For tripped-detector examples, see -[`docs/DETECTORS.md`](docs/DETECTORS.md). - -```json +The underlying detectors (`integrity.apk`, `integrity.bootloader`, `integrity.art`, `attestation.key`, `runtime.environment`, `runtime.root`, `runtime.emulator`, `runtime.cloner`) and their finding kinds are documented in [`docs/DETECTORS.md`](docs/DETECTORS.md). + +> **Not a RASP.** It never blocks sessions, kills processes, or interrupts a flow. It only observes. Build enforcement on the JSON your backend ingests; keep the policy off-device. + +## Output + +`collectJson(context)` returns one deterministic document with a stable `schema_version` (currently `2`). The envelope: + +```jsonc { "schema_version": 2, - "library_version": "1.0.0", + "library_version": "2.0.0", "collected_at_epoch_ms": 1777400000000, - "collection_duration_ms": 8325, - "device": { - "manufacturer": "Google", - "model": "Pixel 9 Pro", - "sdk_int": 36, - "abi": "arm64-v8a", - "fingerprint": "google/caiman/caiman:16/CP1A.260405.005/15001963:user/release-keys", - "total_ram_mb": 15583, - "cpu_cores": 8, - "screen_density_dpi": 480, - "screen_resolution": "1280x2856", - "has_fingerprint_hw": true, - "has_telephony_hw": true, - "sensor_count": 41, - "boot_count": 142, - "vpn_active": false, - "strongbox_available": true, - "brand": "google", - "board": "caiman", - "hardware": "caiman", - "product": "caiman", - "device": "caiman", - "bootloader_version": "ripcurrentpro-16.4-14791556", - "radio_version": "g5400c-251201-260127-B-14784805,g5400c-251201-260127-B-14784805", - "build_host": "67911e6f684b", - "build_user": "android-build", - "build_type": "user", - "build_tags": "release-keys", - "build_time_epoch_ms": 1773135125000, - "supported_abis_all": ["arm64-v8a"], - "soc_manufacturer": "Google", - "soc_model": "Tensor G4", - "gl_es_version": "3.2", - "egl_implementation": "mali", - "default_locale": "en-US", - "system_locales": ["en-US"], - "timezone_id": "America/Los_Angeles", - "timezone_offset_minutes": -480, - "auto_time_enabled": true, - "auto_time_zone_enabled": true, - "display_refresh_rate_hz": 120.0, - "display_supported_refresh_rates_hz": [1.0, 2.0, 5.0, 10.0, 15.0, 20.0, 24.0, 30.0, 40.0, 60.0, 120.0], - "display_hdr_types": ["HDR10", "HLG", "HDR10_PLUS"], - "device_secure": true, - "biometrics_enrolled": true, - "adb_enabled": false, - "developer_options_enabled": false, - "battery_present": true, - "battery_technology": "Li-ion", - "battery_health": "good", - "battery_plug_type": "none", - "thermal_status": "none", - "boot_epoch_ms": 1776800000000, - "play_services_availability": "success", - "play_services_version_code": 261533035, - "play_store_version_code": 85101930, - "gms_signer_sha256": "5f2391277b1dbd489000467e4c2fa6af802430080457dce2f618992e9dfb5402" - }, - "app": { - "package_name": "io.ssemaj.sample", - "apk_path": "/data/app/.../io.ssemaj.sample-.../base.apk", - "installer_package": null, - "signer_cert_sha256": ["a91535782adbd690b915679d456628153166d35527ea867ab830bccd730065a4"], - "build_variant": "debug", - "library_plugin_version": "1.0.0", - "first_install_epoch_ms": 1775000000000, - "last_update_epoch_ms": 1777300000000, - "target_sdk_version": 36, - "install_source": { - "installing_package": null, - "originating_package": null, - "initiating_package": "com.android.shell" - }, - "signer_cert_validity": [ - { "not_before_epoch_ms": 1771714645000, "not_after_epoch_ms": 2717794645000 } - ], - "attestation": { - "chain_sha256": "dd12ccf2a857860f3712b45bcfebb7b917d4e0b9187cca0d0e50e9b119f5c9b8", - "chain_length": 5, - "attestation_security_level": "StrongBox", - "keymaster_security_level": "StrongBox", - "software_backed": false, - "verified_boot_state": "Verified", - "device_locked": true, - "os_patch_level": 202604, - "attested_package_name": "io.ssemaj.sample", - "attested_signer_cert_sha256": ["a91535782adbd690b915679d456628153166d35527ea867ab830bccd730065a4"], - "verdict_device_recognition": "MEETS_BASIC_INTEGRITY,MEETS_DEVICE_INTEGRITY,MEETS_STRONG_INTEGRITY", - "verdict_app_recognition": "RECOGNIZED", - "verdict_reason": null, - "verdict_authoritative": false, - "unavailable_reason": null - } - }, - "detectors": [ - { "id": "integrity.apk", "status": "ok", "duration_ms": 841, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "integrity.bootloader", "status": "ok", "duration_ms": 243, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "integrity.art", "status": "ok", "duration_ms": 4, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "attestation.key", "status": "ok", "duration_ms": 495, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "runtime.environment", "status": "ok", "duration_ms": 5525, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "runtime.root", "status": "ok", "duration_ms": 458, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "runtime.emulator", "status": "ok", "duration_ms": 0, "inconclusive_reason": null, "error_message": null, "findings": [] }, - { "id": "runtime.cloner", "status": "ok", "duration_ms": 0, "inconclusive_reason": null, "error_message": null, "findings": [] } - ], - "summary": { - "total_findings": 0, - "findings_by_severity": { "low": 0, "medium": 0, "high": 0, "critical": 0 }, - "findings_by_kind": {}, - "detectors_with_findings": [], - "detectors_inconclusive": [], - "detectors_errored": [] - } + "device": { /* model, abi, soc, strongbox_available, ... */ }, + "app": { "package_name": "...", "signer_cert_sha256": ["..."], "attestation": { /* chain + verdict */ } }, + "detectors": [ { "id": "integrity.apk", "status": "ok", "findings": [] }, /* ... */ ], + "summary": { "total_findings": 0, "findings_by_severity": {}, "detectors_with_findings": [] } } ``` -
+- **`status` vs `findings`** answer different questions. `status` (`ok` / `inconclusive` / `error`) = "did the detector run?"; `findings[]` = "what did it see?". A rooted device is `status: "ok"` with a non-empty `findings[]` — drive decisions off `summary.detectors_with_findings`, not `status`. +- For every `Finding`, `kind` / `severity` / `subject` / `message` are stable; `details` is opaque diagnostic data whose keys may change without a `schema_version` bump — don't key on them server-side. +- `SessionFindings.toJson()` (from `observeSession`) adds `first_seen_at_epoch_ms` / `last_seen_at_epoch_ms` / `observation_count` / `still_active` per finding. + +A full clean-device report and tripped-detector examples are in [`docs/DETECTORS.md`](docs/DETECTORS.md). + +## Permissions + +| Permission | Required by | Default | Opt-out / opt-in | +|------------------------|-----------------------------------------------------|---------|---------------------------------------| +| `QUERY_ALL_PACKAGES` | `runtime.root` `root_manager_app_installed` channel | on | Strip via `tools:node="remove"` | +| `ACCESS_NETWORK_STATE` | `DeviceContext.vpnActive` | off | `enableVpnDetection.set(true)` | +| `USE_BIOMETRIC` | `DeviceContext.biometricsEnrolled` | off | `enableBiometricsDetection.set(true)` | -A clean device emits empty `findings[]` everywhere and -`summary.total_findings: 0`. You can alert on `total_findings > 0` -server-side without parsing each detector individually. +When opted out, `vpnActive` / `biometricsEnrolled` report `null` (not `false`). ## Try the sample @@ -380,33 +177,16 @@ cd DeviceIntelligence adb shell am start -n io.ssemaj.sample/.MainActivity ``` -## Permissions - -| Permission | Required by | Default | Opt-out / opt-in | -|------------------------|-----------------------------------------------------|---------|----------------------------------------| -| `QUERY_ALL_PACKAGES` | `runtime.root` `root_manager_app_installed` channel | on | Strip via `tools:node="remove"` | -| `ACCESS_NETWORK_STATE` | `DeviceContext.vpnActive` | off | `enableVpnDetection.set(true)` | -| `USE_BIOMETRIC` | `DeviceContext.biometricsEnrolled` | off | `enableBiometricsDetection.set(true)` | - -When you opt out of `vpnActive` / `biometricsEnrolled`, the field -reports `null` (not `false`). - ## Privacy & GDPR -**Zero data collection.** The SDK does not make any network calls, does not contact any vendor cloud, and does not transmit any telemetry to anyone. The output of `DeviceIntelligence.collectJson(context)` stays in your app's process; what your app chooses to upload — and to where — is entirely up to you. - -**GDPR-friendly by design.** Because the SDK transmits no data, it is neither a data controller nor a data processor under GDPR. Your app remains the sole data controller for any device telemetry it chooses to forward to its own backend, and the consent / retention / DSAR posture is yours to define. The library itself reads no advertising ID (GAID), no `ANDROID_ID`, no IMEI/IMSI, no SIM serial, no account identifiers, and no contact / location / file-system content. Everything available in the `TelemetryReport` is documented end-to-end in the [JSON contract](#json-contract) section above so you can audit exactly what fields exist before deciding what to ship server-side. +The SDK makes **zero network calls** and reads no GAID, `ANDROID_ID`, IMEI/IMSI, SIM serial, account, contact, or location data. The output of `collectJson(context)` stays in your process; what you upload — and where — is entirely your decision. Because the library transmits nothing, it is neither a data controller nor processor under GDPR; your app remains the sole controller for any telemetry it forwards. Every field is documented in the [output contract](#output) so you can audit exactly what exists before shipping it. ## Documentation -- [**`docs/DETECTORS.md`**](docs/DETECTORS.md) — full per-detector reference (threat model, finding kinds, sample tripped JSON, costs, caveats) -- [**`NATIVE_INTEGRITY_DESIGN.md`**](NATIVE_INTEGRITY_DESIGN.md) — design of the 8-layer (G0–G7) anti-hooking stack -- [**`CHANGELOG.md`**](CHANGELOG.md) — version history from 0.5.2 → 1.0.0 with wire-format impact notes per release -- [**`SECURITY.md`**](SECURITY.md) — vulnerability disclosure process, response SLOs, supported-versions policy -- [**`tools/red-team/`**](tools/red-team/README.md) — Frida scripts that intentionally trip each `integrity.art` finding (Vectors A/C/D/E/F + Frida-Java) -- [**`tools/red-team/CTF_ROADMAP.md`**](tools/red-team/CTF_ROADMAP.md) — capture-the-flag roadmap of every detection technique on the backlog (Flag 1 — DEX injection — captured 0.6.0; Flag 2 — newer hook frameworks — captured 0.9.0; Flag 5 — attestation × runtime correlation — captured 1.0.0) -- [**`tools/red-team/FLAG1_RUNBOOK.md`**](tools/red-team/FLAG1_RUNBOOK.md) — Pixel 6 Pro on-device validation runbook for the Flag 1 DEX-injection detector -- [**`samples/lsposed-tester/`**](samples/lsposed-tester/) — real LSPosed module that drives runtime DEX injection against the sample app, used to verify the detector against production attacker tooling rather than just Frida +- [**`docs/DETECTORS.md`**](docs/DETECTORS.md) — per-detector reference: finding kinds, sample tripped JSON, costs, caveats. +- [**`NATIVE_INTEGRITY_DESIGN.md`**](NATIVE_INTEGRITY_DESIGN.md) — design of the native anti-hooking stack. +- [**`CHANGELOG.md`**](CHANGELOG.md) — version history with per-release wire-format impact. +- [**`SECURITY.md`**](SECURITY.md) — vulnerability disclosure process and supported-versions policy. ## License diff --git a/docs/DETECTORS.md b/docs/DETECTORS.md index da7d889..edcde8a 100644 --- a/docs/DETECTORS.md +++ b/docs/DETECTORS.md @@ -606,7 +606,7 @@ Its output lives in **two places**, on purpose: | Finding kind | Severity | Triggered when | | --------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- | -| `tee_integrity_verdict` | varies | The locally derived advisory verdict is degraded (severity > LOW). Severity tracks the verdict's own severity ladder, which combines hardware-backing, verified-boot state, bootloader-locked flag, OS patch age, and app-recognition cross-check. CRITICAL when the chain is software-backed OR app-recognition flagged a mismatch; HIGH when verified-boot is anything other than Verified; MEDIUM for patch-too-old / bootloader-unlocked; LOW (suppressed) otherwise. | +| `tee_integrity_verdict` | varies | The locally derived advisory verdict is degraded (severity > LOW). Severity tracks the verdict's own severity ladder, which combines hardware-backing, verified-boot state, bootloader-locked flag, OS patch age, and app-recognition cross-check. **CRITICAL** when the chain is software-backed, app-recognition flagged a mismatch, **OR verified-boot is anything other than Verified** — i.e. SelfSigned (yellow), Unverified (orange), or Failed (red). A non-Verified boot means the boot chain was modified (custom AVB key / unlocked / failed verification) and the device is most probably rooted, so it must not pass device integrity. MEDIUM for patch-too-old / bootloader-unlocked on an otherwise genuine (Verified) boot; LOW (suppressed) otherwise. When the boot state is non-Verified the finding's `message` carries the hardware-attested `os_modified_proof` string, and `details` add `verified_boot_key_sha256` (the attested boot-signing key hash — Google's on stock, a custom value otherwise) and `os_modified_proof`. | | `attestation_eat_format_detected` | low | The leaf cert's KeyDescription extension (OID `1.3.6.1.4.1.11129.2.1.17`) carries **CBOR-EAT** bytes instead of the legacy ASN.1 `KeyDescription` SEQUENCE. KeyMint 200+ on Android 14+ (RKP-provisioned keys) can emit attestation in this format. Library-side parsed fields will be null on those leaves; backends must re-parse the raw chain bytes (`app.attestation.chain_b64`) server-side for full field-level data. Full on-device CBOR/EAT decoding is tracked as a follow-up minor. Heuristic detection only — checks for a CBOR major-type-5 (map) byte (`0xA0`–`0xBF`) at the start of the unwrapped extension content. | ### Sample tripped JSON @@ -619,16 +619,18 @@ Its output lives in **two places**, on purpose: "findings": [ { "kind": "tee_integrity_verdict", - "severity": "high", + "severity": "critical", "subject": "com.example.app", - "message": "TEE evidence indicates degraded device or app integrity (advisory; verify chain server-side)", + "message": "OS MODIFIED (hardware-attested): bootloader is UNLOCKED (verifiedBootState=Unverified, deviceLocked=false) — boot is not verified and can be arbitrarily replaced => most probably rooted.", "details": { "device_recognition": "MEETS_BASIC_INTEGRITY", "app_recognition": "RECOGNIZED", "bootloader_locked": "false", "verified_boot_state": "Unverified", + "verified_boot_key_sha256": "0000000000000000000000000000000000000000000000000000000000000000", "verdict_authoritative": "false", - "reason": "boot_unverified" + "reason": "boot_unverified", + "os_modified_proof": "OS MODIFIED (hardware-attested): bootloader is UNLOCKED (verifiedBootState=Unverified, deviceLocked=false) — boot is not verified and can be arbitrarily replaced => most probably rooted." } } ]