Detects Play Integrity Fix, PlayIntegrityFork, TrickyStore, and the newer wave of bypass modules (inject-s companion streaming, autopif4 Canary fingerprints, TS-Enhancer-Extreme, PIF-Hybrid Rust edition, Treat Wheel root hider) on Android, plus passive and active hardware key-attestation validation that counters the 2026 STRONG-integrity keybox-spoofing stack. Native C++ detection engine with runtime string obfuscation and a Kotlin UI.
The load-bearing signal is key attestation. Every Zygisk PIF fork gates on the target process and unloads itself everywhere else, and the keybox spoofers hook keystore2 rather than the calling app, so from an unprivileged untrusted_app there is nothing of them in our own address space to find. Checks that look for module names and paths are kept for privileged runs but are labelled honestly in the UI rather than counted as coverage. See SELinux and the privilege boundary.
- Play Integrity Fix: maps scan for PIF classes &
InMemoryDexClassLoaderDEX regions,custom.pif.prop/custom.pif.json, module dirs, known props - PIF Companion Streaming (inject-s v4.5): Zygisk companion IPC sockets, memfd-backed dex regions, module installed without
pif.json - Pixel Canary Fingerprint (autopif4): monthly Canary build IDs, vendor partition /
ro.build.idmismatch, brand vs fingerprint cross-check - PIF Pure Rust (PIF-Hybrid): module.prop markers ("Pure Rust Edition", "zero DobbyHook", "Enginex0"), Rust crate libs in maps
- TrickyStore / KeyboxHub:
keybox.xml,target.txt,security_patch.txtunder/data/adb/tricky_store/, KeyboxHub auto-rotation paths, maps artifacts - TS-Enhancer-Extreme: anti-detection module that masquerades the bootloader as locked; module deploy path, config dir,
tseed/tsees/tseevRust binaries - Zygisk / Magisk / KernelSU / APatch: maps scan for zygisk libs (incl. ReZygisk, ZygiskNext, Shamiko, NoHello), env vars,
rwxpanomalies, root-manager packages viaPackageManager(Magisk + KernelSU + APatch + classic SuperSU/Koush/Kingo + cloakers), su binary paths (incl./system_ext/bin/suon A11+), busybox, legacy SuperUser/SuperSU APKs in/system - Root Hiders: mount namespace divergence, OverlayFS on
/system, SELinux context anomalies, elevated rwxp anonymous mapping count - Treat Wheel: closed-source "Shamiko for ReZygisk" root hider (written in C to dodge
__cxa_atexitunload detection);/proc/self/mapsscan for thetreat_wheel/zygisk/module mapping - Key Attestation Anomaly: generates an attested EC key in
AndroidKeyStoreand runs several low-false-positive checks. (1a) Cryptographically validates the certificate chain: leaf-hacking spoof modes (TrickyStore/TrickyStoreOSS/TEESimulator?mode) mutate the TEE leaf's attestation extension, which breaks the issuer's signature. (anti-replay) Verifies the attestation echoes the random challenge nonce we passed, defeating cached/replayed certs. (1b) Parses the attestationRootOfTrust(deviceLocked/verifiedBootState) and flags a contradiction when it claims a locked/verified device while the engine's own root-hider signals prove tampering. (1c) Anchors the chain to Google's hardware-attestation roots (RSA valid through 2042 and ECDSA P-384 valid through 2035, SHA-256-pinned offline): a self-consistent chain that does not terminate at a genuine Google root is a software/fake-keybox spoof. (1d, opt-in) For a Google-anchored chain, checks each cert serial against Google's revocation list to catch generation-mode spoofers (a leaked-but-revoked real keybox). 1d is the only check that touches the network and is OFF by default behind a user toggle, so the app stays network-silent otherwise. Fails safe throughout: absence of attestation (old/AOSP devices), parse failure, an unrecognised root, or an offline revocation fetch never flags. - Attestation Forgery (active): rather than inspecting whatever chain a spoofer chooses to hand back, this asks for keys that are awkward to forge and catches the answer contradicting itself. It requests a
PURPOSE_ATTEST_KEYkey, which TrickyStoreOSS forges for any caller because that arm of its force-forge condition carries no uid gate, so the forgery is provoked even when the detector is not in the module'starget.txt. It also requests an auth-bound key with a SHA-512 digest, which surfaces two more contradictions: an authorization list assertingNO_AUTH_REQUIRED(tag 503) for a key that plainly requires authentication and that omits tags 504/505, and a leaf signed with the digest we asked for rather than the batch key's fixed SHA-256. A single self-signed certificate carrying an attestation extension is caught too. When the passive probe has already flagged, the anchoring half is suppressed so a device with no hardware attestation is reported once rather than twice. - Frida / Xposed: TCP connect probe to
127.0.0.1:27042and:27043, maps scan for gadget libs,gmain/gum-js-loopthread names, parent process cmdline, Objection. The TCP probe replaces the legacy/proc/net/tcpscan, which Android 10+ filters to empty foruntrusted_app. - Property Spoofing: cross-validation of
ro.build.fingerprintvsro.product.*, board (Pixel boards must run on Tensor SoCs), property read timing, and build-identity self-contradiction. PIFork, inject-s and FS-Enhancer-Extreme all globally resetro.build.typetouserand everyro.*.build.tagstorelease-keys, but none of them rewrites a fingerprint property globally, because fingerprint spoofing lives in their Zygisk layer and that only loads inside GMS and the Play Store. A genuine fingerprint always ends in:<build_type>/<tags>, so the scalars disagreeing with it proves something rewrote them after the image was built. The check is directional and only fires when the scalars claim a clean production build, which makes it inert on both genuine user builds and genuine userdebug builds. It therefore only catches this on a device whose ROM is really userdebug or eng. - Bootloader:
ro.boot.verifiedbootstate,ro.boot.flash.locked,ro.boot.veritymode,vbmeta.device_state,ro.debuggable,ro.secure,sys.oem_unlock_allowed - Debuggers:
TracerPidfrom/proc/self/status,FLAG_DEBUGGABLE(release builds only) - APK Tampering: SHA-256 of the signing cert obtained via
PackageManager.GET_SIGNING_CERTIFICATES(API 28+), compared against a digest injected at build time. Skipped in debug builds, and in release builds when no digest is supplied. See Build.
PIF runs as a Zygisk module and:
- Hooks
__system_property_read_callbackto spoof build properties (ro.build.fingerprint,ro.build.version.security_patch, etc.) - Injects
classes.dexat runtime intocom.google.android.gms.unstableviaInMemoryDexClassLoader - Uses reflection to modify
android.os.Buildfields and inject a customKeyStoreSpiprovider - TrickyStore extends this by modifying key attestation certificate chains using stolen/leaked keybox files
Newer forks add: streaming the dex payload over a Zygisk companion IPC channel (no on-disk JSON, defeats config-file scans); rotating monthly Pixel Canary fingerprints from automation; pure-Rust rewrites that ditch DobbyHook and so escape libdobby signature scans; and TS-Enhancer-Extreme, which actively patches VerifiedBootHash and security-patch props to make the bootloader look locked.
MainActivity is UI-only. DetectionRunner owns the worker executor and the JNI binding to the native engine. The native engine runs in two phases:
- Debug/instrumentation checks run first (fail-fast):
isTraced(), Frida TCP-connect probe and thread scan, parent process check, debuggable flag (release only) - Tampering checks run in randomized order each time: Zygisk maps scan plus PackageManager root-app probe and su-binary scan, PIF side-effect probe, companion-streaming probe, mount namespace, OverlayFS, SELinux, bootloader props, APK signature, TrickyStore paths, property consistency, Pixel Canary fingerprint, TS-Enhancer-Extreme, PIF Pure Rust, Treat Wheel
The native engine needs a Context for the three checks that touch the framework (root-manager package probe, debuggable flag, APK signature), so isIntegrityTampered takes one explicitly. It previously received the DetectionRunner instance instead, which under CheckJNI aborts the process and without it is undefined behaviour that made all three silently fail open.
After the native call returns, DetectionRunner runs the passive key-attestation probe (KeyAttestationProbe, written in Kotlin because the AndroidKeyStore attestation API lives in Java land) and ORs ATTEST_ANOMALY into the bitmask, then the active probe (ActiveAttestationProbe) which ORs ATTEST_FORGERY. The active probe is told whether the passive one already flagged, so a device with no hardware attestation at all is not reported twice for one cause. Its contradiction check reuses the root-hider bit the native engine already computed; its chain-anchor check uses the SHA-256-pinned Google roots in AttestationRoots. The optional online revocation check (1d) runs only when the user enables the toggle (SharedPreferences, default off), so the app stays network-silent by default. The flag bit is still registered in nativeAllFlagsMask() so the SSOT assertion holds; the pure validation/parse/anchor logic is isolated in AttestationAnalysis and unit-tested (including against real Google attestation bytes and the pinned root fingerprints). This is the only detection signal produced outside the native engine.
Sensitive strings are base64+XOR encoded and only decoded at runtime. An earlier obfuscation VM was removed in v2.4: its rolling-key decoder desynced on every taken jump, so all three VM-backed checks always returned false. Its unique needles were folded into the direct scanners before the VM was deleted.
JNI helpers use a LocalRef<T> RAII wrapper for ref hygiene, and security checks (signature, debuggable) fail closed: any JNI lookup error fires the detection bit so a hooked-JNI environment can't suppress it.
untrusted_app on Android 10+ cannot read /proc/net/tcp (filtered to empty) or access() paths under /data/adb/* (denied). Detectors that target those signals use alternatives: TCP connect probes for ports, PackageManager for root managers, and in-process /proc/self/maps for module presence.
The boundary is wider than the filesystem, and it decides what this app can honestly claim:
- The PIF forks are not in our process. Both PIFork and inject-s gate on
app_data_dirending in/com.google.android.gmsor/com.android.vendingand callDLCLOSE_MODULE_LIBRARYin every other process. FS-Enhancer-Extreme has no Zygisk component at all. A maps scan for them cannot match. - The keybox spoofers are not in our process either. TrickyStoreOSS and ForgeStore ptrace-inject into
keystore2and hookioctlthere. Their effect reaches us only through the attestation chain. - The kernel's own copy of boot state is unreadable.
ro.boot.*derives fromandroidboot.*, and no module touches/proc/cmdlineor/proc/bootconfig, so the contradiction is real but out of reach: AOSP sepolicy grantsproc_cmdlineandproc_bootconfigto no app domain./sys/fs/selinux/enforcecarries an explicitneverallowfor untrusted apps,/proc/versionis denied, and/system/build.propis mode 0600./proc/cpuinfois the one kernel-exposed source apps may still read, which is why the SoC cross-check works.
Five flags therefore cannot fire unprivileged: PIF, TRICKYSTORE, PIF_STREAM, TSEE and PIF_RUST. They are retained because they do fire when the app runs with root or adb, but the UI renders them as NOT OBSERVABLE in grey rather than a green pass, and excludes them from the summary count, so the result list never implies coverage the sandbox forbids. TREAT_WHEEL is deliberately not in that set: it is a ReZygisk root hider that loads into every app process including ours, so its in-process scan genuinely fires.
The native function returns a bitmask: 0 means clean, any set bit indicates a detection.
0x0001 DEBUGGER 0x0040 TRICKYSTORE 0x0400 CANARY_FP
0x0002 FRIDA 0x0080 PROP_SPOOF 0x0800 TSEE
0x0004 ZYGISK 0x0100 ROOT_HIDER 0x1000 PIF_RUST
0x0008 PIF 0x0200 PIF_STREAM 0x2000 TREAT_WHEEL
0x0010 BOOTLOADER 0x4000 ATTEST_ANOMALY
0x0020 SIGNATURE 0x8000 ATTEST_FORGERY
Flag values are owned by the native side; DetectionRunner.verifyFlagsInSync() asserts at startup that the Kotlin mirrors match nativeAllFlagsMask().
./gradlew assembleDebug # debug build (signature check skipped)
./gradlew assembleRelease # release build (requires signing config)
./gradlew test # run unit tests
./gradlew lintDebug # run lint checksRequires NDK r25+, Android Studio Hedgehog or later, SDK 35, Kotlin 2.0+. The manifest declares INTERNET (used by the Frida TCP probe and the opt-in attestation revocation check) and a <queries> block listing root-manager package names (Android 11+ visibility requirement).
The APK signature check pins the release signing certificate. Supply its digest through the environment at build time:
export RELEASE_CERT_SHA256=$(keytool -list -v -keystore <ks> -alias <alias> \
| grep SHA256: | sed 's/.*SHA256: //' | tr -d ':' | tr 'A-Z' 'a-z')
./gradlew assembleReleaseLeave it unset and the check compiles out, exactly as it does for debug builds. It used to be a constant in native-lib.cpp, which silently pinned every release to whichever machine last edited the file; because the check is fail-closed, a build signed with any other key then reported its own signature as tampered on every device. Absent is safer than wrong.
- Android 7.0+ (API 24)
See CONTRIBUTING.md.
| Name | GitHub |
|---|---|
| Ir0nByte | @IR0NBYTE |
GPL-3.0. Derivative works must stay open source.
This tool is for defensive security research. Use it only on devices you own or have permission to test.

