// HackTricks · Mobile

Play Integrity Attestation Bypass (SafetyNet Replacement)

Play Integrity Attestation Bypass (SafetyNet Replacement)

What Play Integrity Does

Play Integrity is Google’s SafetyNet successor for app attestation. The app requests an integrity token and forwards the encrypted token to its backend. The backend sends it to playintegrity.googleapis.com/v1/PACKAGE_NAME:decodeIntegrityToken (or uses the supported Google API client library), then validates the returned verdict and binds it to the protected request. When Google manages response encryption, the application backend should not attempt to decrypt the token locally.[10][12]

  • appIntegrity: APK build/signature match (no repack/tamper).
  • deviceIntegrity: genuine & certified device, locked bootloader, no root/system tamper.
  • accountDetails: installation via Google Play.
  • environmentDetails (optional): recent integrations can also request signals about risky apps in the environment (overlay, capture, control) and Play Protect state.[10]
  • Other optional signals: some backends also enable recent device activity or device recall to catch hyperactive devices and previously flagged hardware even when a verdict otherwise looks valid.[10]

Key verdict flags commonly enforced:[1]

  • MEETS_BASIC_INTEGRITY: token generated by genuine Play Services (not emulator/tampered transport).
  • MEETS_DEVICE_INTEGRITY: genuine/certified device, bootloader locked, no root/system tamper.
  • MEETS_STRONG_INTEGRITY: on Android 13+ it requires DEVICE plus recent security patches on all partitions (OS + vendor). On Android 12 and lower, it mainly means hardware-backed boot integrity, so it is a weaker signal than many testers assume.[10]

Bypass Model

Instead of forging Google’s JWT, spoof the signals Google evaluates so they correspond to a different, legitimate device profile. The attack chain:[1]

  1. Hide root so local checks and Play Services probes don’t see Magisk/su.
  2. Replace the key attestation certificate chain (keybox.xml) with one from a genuine device so Play Integrity sees a certified/locked device.
  3. Spoof the security patch level to satisfy MEETS_STRONG_INTEGRITY.

Google mitigates by revoking abused keyboxes; rotation is required when a keybox is blocked.[1]

Prerequisites & Tooling

Achieve MEETS_BASIC_INTEGRITY + MEETS_DEVICE_INTEGRITY

  1. Install modules & reboot: Flash TrickyStore and Tricky Addon in Magisk, reboot.[1]
  2. Configure TrickyStore (via KSU Web UI): Select TrickyStoreSelect AllDeselect UnnecessarySave.
  3. Inject a valid keybox: In Keybox, choose Valid to download/apply a new keybox.xml (vendor attestation credentials). This file underpins hardware key attestation and is now spoofed from a certified/locked device.
  4. Verify: Run Play Integrity API CheckerMEETS_BASIC_INTEGRITY and MEETS_DEVICE_INTEGRITY should pass. In Key Attestation the bootloader appears locked because the attestation chain is replaced.

Achieve MEETS_STRONG_INTEGRITY (Patch-Level Spoof)

STRONG fails on outdated patch levels. TrickyStore can spoof a modern security patch date for all partitions:[1]

  1. In TrickyStore, pick Set Security PatchGet Security Patch DateSave.
  2. Re-run Play Integrity API Checker; MEETS_STRONG_INTEGRITY should now pass.

Practical Tester Angles Against Weak Integrations

Even when you cannot permanently recover DEVICE/STRONG, many app backends still misuse the API. During testing, look for:[10]

  • Missing action binding: standard requests should bind the protected action to requestHash; classic requests should bind it to a high-entropy nonce. standard requests have Google-managed replay mitigation, but the backend still needs to validate requestHash (and the request timestamp) against the protected action. classic integrations are especially replay-prone if the server accepts any valid token without matching the original nonce.
  • Weak freshness checks: verify whether the backend enforces timestampMillis and rejects old tokens. Long replay windows are common in rushed integrations.
  • Over-trusting package metadata: Google documents that requestPackageName can be spoofed in the middle of the request, so it should not be the only app-identity check.
  • Legacy policy assumptions: some apps still treat MEETS_STRONG_INTEGRITY as equivalent across Android versions. On pre-13 devices that can lead to weaker trust decisions than the product team expects.

Emerging Technique: Remote Key Attestation (RKA)

A newer evolution is to relay the attestation request to another Android device instead of copying keybox.xml onto every client device. The usual flow is:[11]

  1. Intercept the local Key Attestation / Play Integrity request before it reaches the TEE/KeyStore path.
  2. Forward the nonce / app identity / request details to a remote rooted host.
  3. Let that host generate the attestation response using either an unrevoked legacy keybox or a genuinely vulnerable device whose boot chain still reports a locked state.
  4. Return the attestation blob to the client app/backend.

Why it matters for testers:[11]

  • it avoids burning a public keybox.xml on every device used during an assessment;
  • it makes simple certificate-serial revocation less effective against the operator;
  • it shifts the problem from keybox distribution to relay infrastructure + host-device compromise.

This is where the ecosystem is moving: Remote Key Provisioning (RKP) reduces the long-term value of leaked static keyboxes, but it does not fully remove relay-style attacks when the attacker controls a privileged or exploited host device.[11]

Hardware Attestation Clean-Device Relay

This variant targets applications that treat a valid Android Keystore X.509 attestation chain as a Boolean device-integrity result; it is distinct from forging a Play Integrity token. OID 1.3.6.1.4.1.11129.2.1.17 proves that some acceptable TEE/StrongBox generated the leaf key for attestationChallenge, but it does not bind that hardware to the process or network session presenting the chain. Consequently, even signature validation, Google-root pinning, freshness, revocation, security-level, deviceLocked=true, and verifiedBootState=Verified checks can all pass on relayed evidence because those claims are genuine for the oracle phone.[11][13][15]

A practical clean-device relay works as follows:[13][14]

  1. Instrument the rooted target process and capture the backend’s raw challenge before local key generation.
  2. Re-encode it as unpadded Base64URL (android.util.Base64: NO_PADDING | NO_WRAP | URL_SAFE, or 1 | 2 | 8) and send POST /attest with {"nonce":"..."} to a stock, locked phone.
  3. On that phone, generate an ephemeral secp256r1 AndroidKeyStore signing key with SHA-256 and .setAttestationChallenge(challenge); request StrongBox if policy requires it, then export KeyStore.getCertificateChain(alias) as Base64 DER.
  4. Inject that chain into the target using the exact application-specific Java/Kotlin return type. Do not call the original local method, or the rooted phone will generate its own failing RootOfTrust.

The highest-level method that accepts ByteArray and returns an attestation wrapper is usually the safest seam. The following shortened Frida 17-style pattern synchronously relays the challenge and reconstructs the expected object; the controller must always post a response (even an empty array after an HTTP error) or op.wait() deadlocks the application thread.[13][14]

import Java from 'frida-java-bridge';
Java.perform(() => {
  const K = Java.use('com.target.KeystoreAttestation');
  const Result = Java.use('com.target.AttestedKey');
  const B64 = Java.use('android.util.Base64');
  const ArrayList = Java.use('java.util.ArrayList');
  K.generateAttestedKey.overload('[B').implementation = challenge => {
    send({event: 'attestation_request', nonce: B64.encodeToString(challenge, 1 | 2 | 8)});
    let chain = []; const op = recv('response', m => { chain = m.payload ?? []; });
    op.wait();
    const remote = ArrayList.$new(); chain.forEach(cert => remote.add(cert));
    return Result.$new(remote, 'relayed', null);
  };
});

When the application has no convenient wrapper, hook KeyGenParameterSpec.Builder.setAttestationChallenge(byte[]) on input and java.security.KeyStore.getCertificateChain(String) on output. Track the key alias and per-request context so concurrent generations cannot pair one challenge with another chain.[13]

Boundary and backend checks

A chain-only relay does not copy the oracle’s private key. Requiring a signature over fresh session-, challenge-, and transaction-specific data proves possession of the attested leaf key and forces an attacker to proxy every signing operation to the oracle, rather than reuse the initial certificate result.[13]

Also parse attestationApplicationId (authorization tag [709]) and compare the package name and SHA-256 signing-certificate digest with independently configured expected values. This blocks the simple cross-app oracle when it runs a different helper package; pair it with a hardware-enforced locked/Verified boot policy because application identity is platform-supplied rather than an independent physical-device binding.[13][15]

Operational Notes

  • Revocation risk: Hitting the API repeatedly with the same keybox.xml can flag and block it. If blocked, replace with a fresh valid keybox.[1]
  • Arms race: Publicly shared keyboxes burn fast; keep private copies and track community module updates (XDA/Telegram/GitHub) for new working chains. Newer RKA-style setups reduce direct keybox exposure but increase operational complexity.[1][11]
  • Optional environment / abuse signals: Some newer apps also evaluate environmentDetails (for example risky overlay/capture/control apps), recentDeviceActivity, or deviceRecall. In those cases, passing DEVICE/STRONG alone is not enough if your testing stack leaves visible overlay, accessibility, screen-capture artifacts, or generates obviously abnormal token volume.[10]
  • Scope: This bypass only spoofs attestation inputs; backend signature verification by Google still succeeds because the JWT itself is genuine.[1]

References