Rooting, Flashing, & Bootloader Exploits

Reverse Engineering SafetyNet Attestation: A Practical Lab for Exploit Development

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction to SafetyNet Attestation

Google’s SafetyNet Attestation API is a crucial security mechanism within the Android ecosystem, designed to help application developers determine if a device has been tampered with. It acts as a gatekeeper, protecting apps from running on devices that might pose a security risk, such as those that are rooted, running custom ROMs, or infected with malware. For exploit developers and security researchers, understanding and potentially bypassing SafetyNet is a significant challenge and a key area of study.

SafetyNet Attestation primarily focuses on two integrity checks:

  • Basic Integrity: Checks for common signs of device compromise, such as rooting, an unlocked bootloader, or a custom ROM. If any of these are detected, basic integrity fails.
  • CTS Profile Match: Verifies if the device is running a ROM that has passed Android’s Compatibility Test Suite (CTS) and has been approved by Google. Failure here usually indicates a modified system image or unofficial software.

The core challenge for those developing exploits or seeking to run applications on modified devices is to present a ‘clean’ attestation response to the target application, even when the underlying device is compromised.

The Attestation Process Explained

Client-Side Request

When an Android application wants to verify device integrity, it initiates a SafetyNet request. The typical flow involves:

  1. The app generates a unique nonce (number used once) for the request.
  2. It then calls the SafetyNetApi.attest() method, passing the nonce.
  3. Google Play services on the device collects various device data (software, hardware, security patches, etc.).

Server-Side Verification

Google Play services sends the collected device data to Google’s SafetyNet servers. The servers then process this data, perform the integrity checks, and generate a signed JSON Web Signature (JWS) response. This JWS is sent back to the device’s Google Play services, which then delivers it to the requesting application. The application is then responsible for sending this JWS to its *own* backend server for verification with Google’s API. This server-side verification is critical as it prevents client-side tampering with the JWS itself.

Setting Up Your Reverse Engineering Lab

To practically explore SafetyNet, we’ll set up a lab environment. This lab will allow us to observe, hook, and manipulate the attestation process.

Prerequisites

  • Android Device: A physical Android phone (preferably rooted with Magisk for full control) or an Android emulator. We will assume a rooted device for this lab.
  • Magisk: For root access and potentially hiding root from apps (MagiskHide/DenyList).
  • ADB (Android Debug Bridge): For interacting with the device from your computer.
  • Frida: A dynamic instrumentation toolkit for hooking into applications.
  • Jadx-GUI or apktool: For decompiling Android application packages (APKs).
  • A Target Application: An Android application that uses SafetyNet Attestation. For this lab, you can create a simple app that calls SafetyNetApi.attest(), or use a publicly available app known to use it (e.g., a simple banking app or a game that enforces integrity).

Initial Setup Steps

Ensure your device is connected via ADB and Frida server is running:

# Check ADB connectionadbos devices# Push and install Frida server (replace with correct architecture)adb push frida-server /data/local/tmp/frida-serveradb shell 'chmod 755 /data/local/tmp/frida-server'adb shell '/data/local/tmp/frida-server &'

Install your target application on the device. Make sure you have the APK for decompilation.

Decompiling and Identifying SafetyNet Calls

The first step in reverse engineering is to understand how the target app integrates SafetyNet. We’ll use Jadx-GUI (or apktool followed by a Java decompiler) to inspect the application’s source code.

  1. Open your target app’s APK in Jadx-GUI.
  2. Search for keywords like SafetyNetApi, attest, GoogleApi, SafetyNetClient.
  3. Locate the method where SafetyNetApi.attest() is called. This will often be within a `VerificationManager` or `SecurityCheck` class.
// Example of a decompiled Java snippet showing SafetyNet usageimport com.google.android.gms.safetynet.SafetyNet;import com.google.android.gms.safetynet.SafetyNetApi;public class MySecurityChecker {    public void performSafetyNetCheck(Context context, byte[] nonce) {        SafetyNet.getClient(context).attest(nonce)            .addOnSuccessListener(response -> {                String jwsResult = response.getJwsResult();                // Send jwsResult to backend for verification            })            .addOnFailureListener(e -> {                // Handle failure            });    }}

Dynamic Analysis with Frida

With static analysis providing insights into the call points, we now turn to Frida for dynamic analysis. Frida allows us to hook methods at runtime, inspect arguments, and even modify return values.

Hooking SafetyNetApi.attest()

Our primary goal is to observe the parameters passed to attest() and, more importantly, the JWS response received by the application. We’ll write a Frida script to intercept these calls.

// frida_safetynet_hook.jsJava.perform(function() {    var SafetyNetApi = Java.use('com.google.android.gms.safetynet.SafetyNetApi');    var SafetyNetClientImpl = Java.use('com.google.android.gms.safetynet.internal.zzh'); // Internal implementation    console.log('[*] Hooking SafetyNet API...');    // Hook the attest method    SafetyNetClientImpl.attest.implementation = function(nonce) {        console.log('[+] SafetyNetClient.attest() called with nonce: ' + bytesToHex(nonce));        var result = this.attest(nonce); // Call original method        result.addOnSuccessListener(new Java.use('com.google.android.gms.tasks.OnSuccessListener')({            onSuccess: function(response) {                var jwsResult = response.getJwsResult();                console.log('[+] SafetyNet Attestation Success. JWS Result: ' + jwsResult);                // You can further parse/decode the JWS here                // Example: send to python script for verification                send(jwsResult);            }        }));        result.addOnFailureListener(new Java.use('com.google.android.gms.tasks.OnFailureListener')({            onFailure: function(e) {                console.error('[-] SafetyNet Attestation Failed: ' + e.getMessage());            }        }));        return result;    };    function bytesToHex(bytes) {        var hex = [];        for (var i = 0; i < bytes.length; i++) {            hex.push((bytes[i] >>> 4).toString(16));            hex.push((bytes[i] & 0xF).toString(16));        }        return hex.join('');    }});

Run this script using Frida:

frida -U -l frida_safetynet_hook.js -f <YOUR_PACKAGE_NAME> --no-pause

Replace <YOUR_PACKAGE_NAME> with the package name of your target app (e.g., com.example.myapp). Interact with the app to trigger the SafetyNet check. You will see the JWS result logged in your Frida console.

Analyzing the JWS Response

The JWS (JSON Web Signature) is a base64url-encoded string consisting of three parts, separated by dots: Header, Payload, and Signature. You can decode the Header and Payload using online tools like jwt.io. The payload typically contains:

  • nonce: The nonce provided in the request.
  • timestampMs: When the request was processed.
  • apkPackageName, apkDigestSha256, apkCertificateDigestSha256: Information about the requesting app.
  • basicIntegrity: Boolean indicating basic integrity check status.
  • ctsProfileMatch: Boolean indicating CTS profile match status.
  • advice: Optional advice for the app developer.

By observing these values, you can confirm whether your device is failing basic integrity or CTS profile match.

Exploring Bypass Techniques (Lab Focus)

Bypassing SafetyNet is a cat-and-mouse game, with Google constantly improving its detection mechanisms. The techniques below are for lab understanding, not for guaranteed real-world exploits.

MagiskHide / DenyList

Magisk has historically been a powerful tool for hiding root. Its ‘MagiskHide’ feature attempted to prevent apps from detecting common signs of root. While MagiskHide has evolved into ‘DenyList’ and its effectiveness varies, the principle remains: obfuscate or remove indicators of compromise that apps check for.

To experiment:

  1. Enable Magisk’s DenyList.
  2. Add your target app to the DenyList.
  3. Clear the app’s data and force close it.
  4. Rerun the app and observe the SafetyNet results in your Frida console.

You might find that for many apps, Magisk alone is insufficient to pass SafetyNet if the device truly fails CTS profile match or has a deeply embedded root signature that Google’s servers detect.

Frida-Based Client-Side Spoofing

A more advanced lab technique involves using Frida to intercept the JWS response *before* it reaches the application and manipulate it. The goal is to change basicIntegrity and ctsProfileMatch to true. However, this is challenging because the app’s backend is supposed to verify the *signature* of the JWS with Google’s servers, which means a client-side modified JWS would have an invalid signature.

For a purely client-side

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner