Hackers vs root detection on Android

Since the beginning of time, security researchers and hackers have needed to bypass root detection mechanisms implemented in Android apps. In this article, I will present some bypassing techniques and best practices regarding the implementation of these mechanisms.

Łukasz Bobrek 2023.03.13   –   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 a device only) can be bypassed. 
  • (Now deprecated) SafetyNet Attestation is a solution introduced by Google that implements hardware-backed device verification, which enables backend verification.  
  • Play Integrity API is a solution which implements hardware-backed device verification, which enables backend verification.
  • This solution offers the highest level of security, an effective bypass would require a successful attack on the Secure Element.
  • Always inform users that root has been detected or mark 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 a complete evaluation of application security. For instance, it is essential for bypassing certificate pinning, which is required to intercept the 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 the 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 the above-mentioned checks (plus some more) are verified locally, which means that they can be relatively easily bypassed either by frida or smali modification, or by using @topjoohnwu’s Magisk “Deny List”.  

For instance, RootBeer – one of the most popular root detecting libraries – could be bypassed by 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 “Deny List” (or Magisk Hide on outdated Magisk versions). In both cases, root has not been detected:  

The SafetyNet revolution and Play Integrity API evolution

In March 2020, Google changed the behavior of SafetyNet Attestation API, which was 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, which makes it impossible to bypass SafetyNet Attestation 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.  

But you know how it is with many Google products/technologies – well, to put it mildly, some of them do not live long – https://killedbygoogle.com/. In June 2022, Google announced that SafetyNet will be discontinued, and device attestation will be a part of Play Integrity API from now on. Theoretically, merging those APIs made sense as Play Integrity API had already implemented several attestations (like app binary integrity and user license verification), but it might have been a bit frustrating for developers who needed to adjust their code base in order to verify devices’ security.  

So, what’s actually happening beneath the surface when Hardware-Based Device Attestation is initiated?  

Below, there is a short diagram, which presents an execution flow of the attestation process:  

Figure 1:
High-level representation of Play Integrity API usage  


The first step is nonce generation on the App server side and passing it to the Android App. Nonce is a generated random and unique number, which might be treated as a process identifier. The next step is building an IntegrityTokenResponse class instance and calling a token method which returns encrypted token from Google with an attestation verdict:  

Task<IntegrityTokenResponse> integrityTokenResponse = integrityManager.requestIntegrityToken( 
   IntegrityTokenRequest.builder() 
      .setNonce(nonce.toString()) 
      .setCloudProjectNumber(<GCPprojectID>) 
      .build()); 
integrityTokenResponse.addOnSuccessListener(integritytoken -> { 
  String integrityToken = integritytoken.token(); 
  token.setToken(integrityToken); 
  Log.d("Encrypted Token", integrityToken); 
  Application.apiManager.checkIntegrity(token); 
}); 

Class Builder needs nonce and, optionally, Google Cloud Project ID, which has enabled Play Integrity API. A project ID is not required when an application is deployed to the Google Play store where a developer can provide this value instead of hardcoding it into the app.  

On the method call, Google Play Services are invoked, and a bunch of data is sent to the Google servers. Without breaking TEE’s security, this communication cannot be modified because it is signed with a certificate stored in KeyStore, and you get the following encoded JWT token as a response:  

eyJhbGciOiJBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIn0.Ki6c7-6HDZ6SEUJKA5hjW-tu7Wrmj2H44Cv3kC9OVuiUX9GwCupCyg.4uf-qyyL7[...] 

The decoding of the token proves that it is indeed encrypted, so locally an Android device cannot get access to the attestation verdict which is intentional and prevents developers from making attempts to allow apps to verify on their own whether a device is trusted or not. Such an implementation would be fundamentally flawed and allow manipulation by the attacker. 

In the next step, the token is sent to the App Server, where it should be passed to Play Server to be decrypted. Note that either the App Server must run on the Google Cloud Project, which is linked to Google Play Console, or Google Cloud credentials must be provided to authorize the token decryption request. 

Below, there is an example of implementation of the server-side token decryption written in golang and using google.golang.org/api/playintegrity/v1 library:  

import ( 
[...] 
"google.golang.org/api/playintegrity/v1" 
) 
 
[...] 
 
func checkIntegrity(w http.ResponseWriter, r *http.Request) { 
 
var token Token 
err := json.NewDecoder(r.Body).Decode(&token) 
 
ctx := context.Background() 
playintegrityService, err := playintegrity.NewService(ctx) 
 
decodeIntegrityTokenRequest := playintegrity.DecodeIntegrityTokenRequest{} 
decodeIntegrityTokenRequest.IntegrityToken = token.Token 
decodeIntegrityTokenResponse, err := playintegrityService.V1.DecodeIntegrityToken("biz.securing.playintegrity", &decodeIntegrityTokenRequest).Do() 
 
response := decodeIntegrityTokenResponse.TokenPayloadExternal 
jsonResp, err := json.Marshal(response) 
[...] 

Finally, the App Server gets the attestation verdict converted to the json format:  

{"accountDetails":{"appLicensingVerdict":"UNEVALUATED"},"appIntegrity":
{"appRecognitionVerdict":"UNRECOGNIZED_VERSION","certificateSha256Digest":
["heRi9Yu32bpDblmbY5OsxeILIEKPmtEkihtmxnZm06M"],"packageName":"biz.securing.playintegrity","versionCode":"1"},"deviceIntegrity":{"deviceRecognitionVerdict":
["MEETS_DEVICE_INTEGRITY"]},"requestDetails":
{"nonce":"bizsecuringplayintegrityModelsNonce25380fw==","requestPackageName":"biz.securing.playintegrity","timestampMillis":"1677701517042"}}

The key parameter is deviceRecognitionVerdict, which is a list of strings that can contain the following values (https://developer.android.com/google/play/integrity/verdict):   

  • “UNKNOWN” – Play does not have sufficient information to evaluate. 
  • “MEETS_BASIC_INTEGRITY” – An app is running on a device that passes basic system integrity checks but may not meet the Android platform compatibility requirements and may not be approved to run Google Play services. 
  • “MEETS_DEVICE_INTEGRITY” – An app is running on a GMS Android device with Google Play services. 
  • “MEETS_STRONG_INTEGRITY” – An app is running on a GMS Android device with Google Play services and has a strong guarantee of system integrity such as a hardware-backed keystore. 
  • “MEETS_VIRTUAL_INTEGRITY” – An app is running on an Android emulator with Google Play services, and it meets core Android compatibility requirements.

Note that the decision whether to trust a device or not takes place on the App Server so it cannot be tampered with directly by an attacker. 

For high-risk applications, like banking or cryptocurrencies wallets, it is advised to trust only devices that MEETS_STRONG_INTEGRITY, which is a bulletproof guarantee (at least for the moment) that the device is neither rooted nor tampered with in any other way.  

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 never-ending race. 

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

I would rather recommend informing the users that root has been detected or marking them 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 critical data that potentially compromised device should be blocked. 

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

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