Better Android Emulator Detection with Attestation

basanta sapkota
Most Android emulator detection code is security theater. If an app trusts Build.MODEL, ro.kernel.qemu, sensor counts, or the existence of /system/bin/su, an attacker can simply make those checks lie.

That doesn’t mean emulator detection is pointless. It means we need to move the trust boundary. Instead of asking an untrusted Android process to describe itself, we should verify cryptographic evidence, bind it to a specific operation, validate it on our backend, and combine it with server-observed behavior.

Key Takeaways

  • Local checks such as build properties, file paths, device IDs, and sensor lists are easy to spoof or hook.
  • No Android app can determine with 100% certainty that it runs on a physical phone.
  • Use the Play Integrity API to evaluate app integrity, device integrity, licensing, and supported environmental risks.
  • Bind each integrity token to the protected request using requestHash or a nonce.
  • Decrypt and validate integrity verdicts on a trusted backend, never only inside the app.
  • Use hardware-backed key attestation when you need a device-bound cryptographic identity.
  • Treat integrity as a risk signal, not a universal “allow or block” switch.
  • Measure false positives before enforcing restrictions.

Why Traditional Android Emulator Detection Fails

Classic Android emulator detection usually checks a collection of familiar artifacts:

val suspicious =
    Build.FINGERPRINT.startsWith("generic") ||
    Build.MODEL.contains("Emulator", ignoreCase = true) ||
    Build.HARDWARE.contains("goldfish") ||
    Build.HARDWARE.contains("ranchu")

Other implementations inspect:

  • ro.kernel.qemu and related system properties
  • goldfish, ranchu, or VirtualBox files
  • IMEI, phone number, or network operator values
  • Camera and sensor availability
  • Battery behavior
  • CPU architecture
  • test-keys build tags
  • Known emulator packages and processes
  • Root-management files such as su

These checks can still provide low-confidence telemetry. They should not authorize a payment, issue a valuable reward, or unlock private data.

Why? Because every value is read inside a process the attacker may control.

A published Android testing account demonstrates the problem clearly. The tester found emulator and root checks through static analysis, then used Frida to intercept file and system-property lookups. Checks for goldfish, ro.kernel.qemu, and root-related paths were made to return harmless results at runtime. The APK did not need to be permanently rewritten.

That is the core weakness: the app asks a potentially hostile environment whether the environment is hostile.

Obfuscation makes this work more annoying, but not impossible. OWASP describes obfuscation, anti-debugging, anti-tampering, and runtime protection as defense-in-depth measures. It explicitly warns that these controls are not substitutes for sound security architecture and server-side validation.

Better Android Emulator Detection Starts with a Threat Model

Before adding another detection library, decide what you’re actually protecting.

An emulator is not automatically malicious. Developers, QA engineers, accessibility users, security researchers, Chromebook users, and cloud-device testing services may have legitimate reasons to run your app in an unusual environment.

The real threats are normally more specific:

  • Automated account creation
  • Promo or referral farming
  • Game cheating
  • Credential stuffing
  • Payment fraud
  • API scraping
  • Replaying previously valid requests
  • Running a modified APK
  • Operating many accounts from one environment

This distinction matters. Blocking every emulator may inconvenience real users while doing little to stop a motivated attacker using a compromised physical phone.

A better question is:

Does this request come from my recognized app, in a sufficiently trustworthy environment, and does its behavior make sense?

That gives us something useful to enforce.

Use Play Integrity for Environment Attestation

Google’s Play Integrity API overview describes an attestation service that uses hardware-backed security signals designed to be more resistant to manipulation than local properties.

Its verdict can include several categories:

  • App integrity: Is this a Play-recognized, unmodified version signed as expected?
  • Device integrity: Is it running on a genuine, certified Android device or another recognized environment?
  • Account details: Does the signed-in account have a Play license for the app?
  • App access risk: Are other apps potentially capturing the screen, drawing overlays, or controlling the device?
  • Play Protect status: Is Play Protect enabled, and has it identified dangerous software?
  • Recent device activity: Is the device generating an unusually high request volume?
  • Device recall: Has the backend previously marked this device as abusive?

These signals are harder for the app user to fabricate directly because the final verdict is signed, encrypted, and verified through Google’s service. But “harder” is the right word. Attestation reduces uncertainty; it doesn’t create mathematical proof that a human is holding a normal phone.

There are also product constraints. Play Integrity creates a dependency on Google Play services and certified Android environments. OWASP notes that platform-attestation services can exclude legitimate users on alternative Android distributions. Decide whether that trade-off fits your audience, especially as Android becomes more tightly controlled.

Bind Integrity Evidence to the Protected Request

An integrity token should prove more than “this device looked acceptable recently.” It should be tied to the exact operation being authorized.

For a standard Play Integrity request, construct a stable representation of the operation and hash it:

data class ProtectedAction(
    val accountId: String,
    val action: String,
    val amountMinor: Long,
    val currency: String,
    val serverChallenge: String
)

fun requestHash(action: ProtectedAction): String {
    val canonical = listOf(
        action.accountId,
        action.action,
        action.amountMinor.toString(),
        action.currency.uppercase(),
        action.serverChallenge
    ).joinToString("|")

    val digest = MessageDigest
        .getInstance("SHA-256")
        .digest(canonical.toByteArray(Charsets.UTF_8))

    return Base64.encodeToString(
        digest,
        Base64.URL_SAFE or Base64.NO_WRAP or Base64.NO_PADDING
    )
}

The app passes that hash when requesting the token, then sends both the business request and token to the backend.

On the server, recompute the hash from the received request. Do not trust a hash supplied independently by the client.

expectedHash = SHA256(canonicalize(serverRequest))

verdict = decryptAndVerifyWithGoogle(integrityToken)

reject if verdict.packageName != EXPECTED_PACKAGE
reject if verdict.requestHash != expectedHash
reject if verdict.timestamp is outside allowed window
reject if verdict.appRecognition != PLAY_RECOGNIZED

risk = evaluate(verdict, accountHistory, velocity, networkSignals)
return applyPolicy(risk)

Google recommends checking the package, request hash or nonce, and timestamp before evaluating the remaining verdict. It also advises against caching verdicts because cached evidence can be proxied or reused from a trustworthy device.

Standard requests have a typical latency of a few hundred milliseconds and are intended for on-demand checks. Google documents a default quota of 10,000 requests per day across an app’s installs, with an option to request an increase. Plan for this before attaching attestation to every API call.

Add Hardware-Backed Key Attestation When Needed

Play Integrity answers, “What does Google currently know about this interaction?” Hardware-backed key attestation can answer a different question: “Was this key created inside supported secure hardware, and is this request signed by that key?”

A common enrollment flow looks like this:

  1. The backend generates a fresh challenge.
  2. The app creates a non-exportable Android Keystore key.
  3. The challenge is included in the key’s attestation request.
  4. The app sends the certificate chain to the backend.
  5. The backend validates the chain, security level, challenge, application identity, and revocation status.
  6. Future sensitive requests are signed with the enrolled key.

Google’s hardware-backed key attestation documentation says validation must occur on a separate trusted server. The server should verify the certificate-chain signatures, trusted root, revocation status, and an attestation security level of TrustedEnvironment or StrongBox.

Do not parse and “approve” the chain inside the app. A compromised client could patch that approval logic.

And don’t confuse an attested key with a permanent user identity. Devices are replaced, app data is cleared, keys are rotated, and legitimate recovery happens. Build a secure re-enrollment path.

Build a Tiered Android Emulator Detection Policy

Binary blocking is brittle. A risk-based policy is usually safer and more effective.

SignalsSuggested response
Recognized app, acceptable integrity, normal behaviorAllow
Temporary integrity error, established account, low-risk actionAllow with logging or retry
Missing device integrity, new account, unusual velocityAdd CAPTCHA, rate limits, or step-up authentication
Modified app plus replay mismatchReject the request
High-volume automation across many accountsThrottle, suspend rewards, or queue for review
Alternative Android environment with normal historyLimit only features that truly require stronger assurance

For example, a note-taking app probably should not block an uncertified device from opening local notes. A financial app may reasonably require stronger evidence before registering a new payment destination.

Start in observation mode. Record verdict distributions, device models, OS versions, action types, and support complaints. Google itself recommends gathering telemetry before enforcement.

That measured approach also fits broader security practice. If you’re reviewing the rest of your stack, the same principle applies: client-side barriers are useful friction, but authority belongs on the server. Our discussion of why security architecture matters more than surface-level patches covers a similar trust-boundary problem.

Common Android Emulator Detection Mistakes

Trusting a Boolean from the App

Never send this and treat it as proof:

{
  "isEmulator": false
}

An attacker controls the client request. Send verifiable evidence and let the backend make the decision.

Using Device IDs as Attestation

Android ID, advertising IDs, IP addresses, and generated UUIDs are identifiers, not integrity proofs. They may help correlate behavior, but they can be reset, shared, proxied, or spoofed.

Reusing One Verdict for an Entire Session

A clean verdict at login does not validate a payment made two hours later. Request fresh, operation-bound evidence near sensitive actions.

Blocking on One Missing Signal

Integrity services can fail because of network trouble, outdated services, account state, unsupported distributions, or temporary platform errors. Define separate policies for “known bad,” “unknown,” and “temporarily unavailable.”

Shipping the Entire Policy in the APK

If all thresholds and decisions live in Kotlin, attackers can inspect and patch them. Keep meaningful policy on the backend. Client checks can remain as early warnings and speed bumps.

Can Android Emulator Detection Be 100% Accurate?

No. There is no universal Android API that proves a device is a physical phone and can never be fooled.

Hardware-backed attestation, verified app identity, request binding, and behavioral analysis provide much stronger assurance than property checks. Still, sophisticated abuse can come from genuine compromised phones, device farms, human-operated accounts, or relayed requests.

The goal isn’t perfect emulator detection. It is making each protected operation expensive to forge, difficult to replay, visible to the backend, and subject to a policy that matches its actual value.

Conclusion

Better Android emulator detection means abandoning the hunt for one magical property. Keep local heuristics as low-weight telemetry, but place trust in verified attestation, backend validation, request freshness, hardware-protected keys, and server-observed behavior.

Start with one sensitive endpoint. Bind a Play Integrity standard request to its payload, validate the result on your server, and run the policy in observation mode. Once you understand the real impact, introduce tiered enforcement instead of locking everyone out.

Sources

  1. Google: Overview of the Play Integrity API
  2. Google: Play Integrity verdict formats and validation
  3. Google: Making a standard Play Integrity API request
  4. Google: Verify hardware-backed key pairs with key attestation
  5. OWASP MASVS: Resilience Against Reverse Engineering and Tampering
  6. OWASP MASTG: Emulator Detection
  7. Android developer community discussion about bypassing emulator detection
  8. Android testing account: bypassing local root and emulator checks with dynamic instrumentation

Post a Comment