Hackers vs root detection on Android

Since the beginning of time, security researchers and hackers are in need of bypassing root detection mechanisms implemented in Android apps. In this article, I will present some bypassing techniques and best practices regarding the implementation of this mechanism.

Łukasz Bobrek 2021.03.03   –   13 MIN read

TL;DR & Quick Recommendations 

  • Devices with unlocked root access are obviously more vulnerable, so attackers will always try to exploit them. 
  • Critical applications should evaluate device security, i.e. by root detection.
  • Each local check (performed on the device only) can be bypassed. 
  • SafetyNet Attestation is a solution introduced by Google, which implements hardware-backed device verification supported with backend verification. 
  • This solution offers the highest level of security, effective bypass would require successful attack on the Secure Element.
  • Always inform users that root was detected or marked them as potentially compromised on a backend and monitor their behavior before blocking access to the application.
  • Follow good mobile application development practices -> see our “Guidelines on mobile application security Android edition”.


Root detection & bypass

Security researchers need root access in order to perform complete evaluation of application security. For instance, it is essential for bypassing certificate pinning, thus interception of HTTP communication between the app and a remote server. Root access is also mandatory to examine and evaluate data storage and secrets handling. 

On the other hand, hackers need root access to manipulate target application logic or to increase impact of malware campaigns. 

Hackers on the other hand need root access to manipulate target application logic or to increase impact of malware campaigns. 

In the simple old days, root detection was based on local checks, some of them are listed below:

  • detection of su binary,
  • detection of root management apps,
  • detection of rw access to system files,
  • checking if selinux is in enforcing mode.

All of the above-mentioned checks (plus some more) are verified locally, which means that they can be relatively easily bypassed, either with frida, smali modification or using @topjoohnwu’s Magisk Hide. 

For instance, RootBeer, one of the most popular root detecting library could be bypassed with the following smali modification:

Original smali code invoking rootbeer checks:

.method public final a()V
    .locals 2
    new-instance v0, Lcom/scottyab/rootbeer/b;
    (...)
    invoke-virtual {v0}, Lcom/scottyab/rootbeer/b;->a()Z
    move-result v0
    if-eqz v0, :cond_0
    iget-object v0, p0, Lcom/softwarehouse/bank/dz/e$e;->a:Lcom/softwarehouse/bank/dz/e;
    invoke-static {v0}, Lcom/softwarehouse/bank/dz/e;->b(Lcom/softwarehouse/bank/dz/e;)V
    :cond_0
    return-void
.end method

Modified smali code, which ignores all the checks, and returns with null, which is an expected response when all the checks are passed:

.method public final a()V
    .locals 0
    return-void
.end method

The easiest method though is using Magisk Hide. In both cases, root is not detected: 

The SafetyNet revolution

In march 2020, Google changed the behaviour of SafetyNet Attestation API, which is a Google’s method of device security verification. Up to this date, SafetyNet Attestation could be bypassed locally, like any other root detection mechanism. 

The change made by Google introduced hardware-based key attestation into SafetyNet, which makes it impossible to bypass without breaking TEE (Trusted Execution Environment), which could be either a dedicated chip (ex. Google Titan) or ARM Trustzone. The presence of TEE is required for any device to be “Google Play Certified”, thus almost all modern devices worldwide have this feature. 

So, what’s actually happening beneath the surface, when SafetyNet Attestation API is called? 

According to Google’s documentation, Attestation API is called with attest() method from SafetyNetClient class:

SafetyNet.getClient(this).attest(nonce, API_KEY)
    .addOnSuccessListener(this,
        new OnSuccessListener<SafetyNetApi.AttestationResponse>() {
            @Override
            public void onSuccess(SafetyNetApi.AttestationResponse response) {
                // Indicates communication with the service was successful.
                // Use response.getJwsResult() to get the result data.
            }
        })
    .addOnFailureListener(this, new OnFailureListener() {
        @Override
        public void onFailure(@NonNull Exception e) {
            // An error occurred while communicating with the service.
            if (e instanceof ApiException) {
                // An error with the Google Play services API contains some
                // additional details.
                ApiException apiException = (ApiException) e;
                // You can retrieve the status code using the
                // apiException.getStatusCode() method.
            } else {
                // A different, unknown type of error occurred.
                Log.d(TAG, "Error: " + e.getMessage());
            }
        }
    });

 

nonce is a random number generated by the application, and API_KEY is used to authenticate developers in Google APIs Console. On the method call, Google Play Services are invoked and a bunch of data is sent to Google servers. Without breaking TEE’s security, this communication cannot be modified, because it is signed with a certificate stored in KeyStore:
 

Figure 1: SafetyNet Attestation API

Among the transmitted data there is the Google Hardware Attestation Root Certificate, which contains extension data. It contains information about bootloader status, which must be unlocked (unverified) in all rooted devices:

RootOfTrust ::= SEQUENCE {
       verifiedBootKey  OCTET_STRING,
       deviceLocked  BOOLEAN,
       verifiedBootState  VerifiedBootState,
       verifiedBootHash OCTET_STRING,
}
      
VerifiedBootState ::= ENUMERATED {
       Verified  (0),
       SelfSigned  (1),
       Unverified  (2),
       Failed  (3),
}

Next, Google verifies the data and responds to the application with ctsProfile and basicIntegrity boolean values. An example of Google’s response is shown below:

{"nonce":"<redacted>","timestampMs":1596023040552,"apkPackageName":"com.lbobrek.safetynetchecker","apkDigestSha256":"<redacted>","ctsProfileMatch":false,"apkCertificateDigestSha256":["<redacted>"],"basicIntegrity":true,"evaluationType":"BASIC,HARDWARE_BACKED"}

If both are true, the device is considered secure. In any other cases, it is not. Google response is (obviously) signed, and at this stage the application backend should be waiting for the data from the application. If it doesn’t come – device will be denied. If the data is modified, Google signature won’t match and the device will be denied. 

As of now (early 2021) hardware-backed attestation is in a rolling phase, which means it is enforced to each SafetyNet call. SafetyNet response contains information about evaluation type – it is either BASIC or HARDWARE_BACKED. Older devices, which do not have hardware-backed certificate store can still pass SafetyNet attestation and only BASIC evaluation is applied. Furthermore, rooted devices can modify response from Keymaster to “Not implemented” error code, which tricks Google Services to use only BASIC attestation also on modern devices.

Having that said, it is expected that Google will enforce hardware-backed attestation in all cases in the foreseeable future.   

Summary 

Root detection has always been an important part of security sensitive Android applications. Nowadays, for the first time ever, developers of security sensitive applications are equipped with a tool which allows them to stay ahead of the hackers in this neverending race.

Keep in mind that blocking completely an access to the application for all the devices, which did not pass SafetyNet might not be the best solution – that would cut off from your application all the users that consciously unlocked their devices. 

I would rather recommend to inform users that root was detected or marked as potentially compromised on a backend and monitor their behavior before taking any actions.  Unless, of course you are 100% sure that the application processes such a critical data, that potentially compromised devices should be blocked.

Last but not least, always remember, to follow good practices in development of secure mobile applications. To make it easier, we had prepared a guide which gathers in one place the most significant challenges and recommendations:

Feel free to reach me out. You can find me on Twitter on LinkedIn.

Łukasz Bobrek
Łukasz Bobrek Principal IT Security Consultant
Head of Cloud Security