Cas client : bypass anti-root sur une application Android
Lors des missions de test d’intrusion d’application mobile, l’analyse et le contournement des mécanismes anti-root et anti-virtualisation constituent une étape clé pour évaluer la résilience réelle d’une application Android face à un attaquant disposant d’un environnement contrôlé.
L’OWASP fournit plusieurs guides de bonnes pratiques et de méthodologies dédiés à la sécurité des applications mobiles :
- OWASP MASVS (Mobile Application Security Verification Standard): bonnes pratiques dans le développement d’applications mobiles ;
- OWASP MASTG (Mobile Application Security Testing Guide): méthodologie d’analyse technique de l’implémentation décrite dans l’OWASP MASVS ;
- OWASP MASWE (Mobile Application Security Weakness Enumeration): base de connaissances regroupant les faiblesses identifiées sur des applications mobiles.
Au travers d’un cas client, cet article met en lumière deux méthodes de contournement fréquemment rencontrées en audit mobile d’applications Android : le patching statique de l’APK (Android Package Kit) et le hooking dynamique à l’aide de Frida.
Par erwan.robin

Contexte
Lors de nos tests d’intrusion, il arrive que certaines applications embarquent des mécanismes contre la modification et la rétro-ingénierie.
Dans le cadre de l’un de nos audits, nous avons été confrontés à l’utilisation de la librairie RootBeer, recommandée par l’OWASP. L’application, lorsqu’elle est lancée sur un terminal rooté ou virtualisé, affiche le message « Login not possible : rooted phone detected ».

Nous allons voir deux méthodes distinctes pour contourner le mécanisme de détection implémenté dans cette application Android :
- statiquement via la modification de l’APK ;
- dynamiquement via Frida.
La première partie présentera les méthodes de récupération, d’analyse, de modification puis de déploiement de l’application. Dans la seconde, nous verrons l’utilisation de Frida pour s’attacher aux fonctions et en modifier le comportement.
Dans les étapes décrites ci-dessous, nous utiliserons l’outil Genymotion pour émuler un terminal Android disposant d’un environnement rooté.
Bypass statique : modification de l’APK
Récupération
Après avoir installé notre application via le Google Play, nous avons besoin de la récupérer en local pour pouvoir l’analyser et la modifier.
Pour ce faire, nous utilisons Android Debug Bridge (ADB) pour lister les paquets installés sur le téléphone :
![]()
Nous récupérons alors le nom de notre application (« com.sysdream.app »), ce qui nous permet d’interroger l’emplacement des fichiers APK sur le téléphone :

Comme l’indique la capture ci-dessus, 3 fichiers APKs sont identifiés (split APKs). Nous avons donc besoin de télécharger l’ensemble :
![]()
Analyse
Maintenant que nous avons les fichiers en notre possession, nous pouvons analyser le code grâce à l’outil Jadx et la commande suivante : jadx-gui –deobf *.apk
Cet outil de rétro-ingénierie Android permet de convertir le bytecode Dalvik (DEX) en code Java lisible. Du moins, il fait de son mieux car il ne s’agit pas du code source tel qu’il a été écrit mais d’une reconstruction à partir du langage assembleur.
En parcourant le code obtenu, on s’aperçoit que l’application embarque RootBeer, une librairie permettant de détecter un matériel rooté au travers de plusieurs fonctions.

Intéressons-nous à l’implémentation dans l’application. On retrouve une classe Java nommée RootDetection, qui importe la librairie RootBeer et l’utilise dans la fonction isRooted qui renvoie une valeur vraie ou fausse (‘boolean z’).

Cette fonction inscrit la valeur de retour dans les journaux de l’application. Pour consulter les journaux Android, il est possible d’utiliser la commande adb logcat. Ici, on utilise l’outil Pidcat pour s’attacher à notre application à l’aide de la commande pidcat.py com.sysdream.app
Au lancement de l’application, les journaux indiquent que plusieurs fichiers sont présents, indiquant un matériel rooté. À la suite, on retrouve la sortie de la fonction et la valeur true.

Smali
Qu’est-ce que le Smali ? Il s’agit d’un type de langage assembleur pour la machine virtuelle Dalvik, utilisé par Android.
Au travers de Jadx, il est possible d’accéder au code Smali de notre fonction isRooted.
On observe donc, en premier lieu, les instructions invoke-virtual (appel d’une fonction d’une autre classe) et invoke-direct (appel d’une fonction dans la classe actuelle) permettant la détection. Les valeurs de retour booléennes sont alors stockées à l’aide de l’instruction move-result.

Ensuite viennent les vérifications avec if-nez (if not equal to zero) pour les vérifications propres à RootBeer et if-eqz (if equal to zero) pour la fonction définie dans la classe RootDetection. En cas de détection, on assigne la valeur true (0x1) à p1 via l’instruction const/4 (1), sinon, on assigne la valeur false (0x0) à p1 (2). La valeur de p1 est alors retournée par la fonction (3).

Modification
Étant donné qu’il s’agit d’un code reconstruit par Jadx, on ne pourra pas le modifier directement. On va alors s’attaquer au Smali.
L’outil apktool permet la décompilation et la recompilation d’un APK. Ici, on commence par décompiler (`d`) notre APK (`base.apk`), sans décoder les ressources (`-r`), en redirigeant (`-o`) dans le dossier `modified_apk`.

Lorsque l’on accède au dossier, un sous-ensemble de fichiers et dossiers a été créé.
![]()
Pour gagner du temps et accéder au fichier .smali contenant notre détection, on cherche les termes « Device is rooted or VM ? ». Le résultat nous indique alors que nous devons regarder du côté du fichier RootDetection.smali, aux alentours de la ligne 689.
![]()
En ouvrant ce fichier, et en remontant au-dessus de la ligne 689, on retrouve nos deux instructions const/4 p1,0x0 et const/4 p1,0x1.

Nous allons modifier la valeur de p1 ‘vraie’ par ‘faux’.

Nous recompilons (b) le contenu du dossier `modified_apk`, toujours sans les ressources (`-r`) pour former notre nouvel APK `root_bypass.apk` dans le dossier `out` (`-o`).

Dès lors, nous devons signer notre APK afin qu’il puisse être installé. Comme vu au départ, nous étions sur un split APK. Il faudra également signer les deux autres fichiers split_config, copiés eux aussi dans le répertoire out.
L’outil Uber APK Signer permet de resigner l’ensemble (`–allowResign`) des APKs du répertoire `out` (`–apk`). L’ensemble étant envoyé dans le répertoire `signed` (`–out`).

Vérification
Notre nouvelle version est prête pour installation sur notre Android. On utilise ADB pour installer nos APK (`install-multiple`).
![]()
On relance Pidcat pour consulter les journaux de l’application, qu’on lance sur le terminal. On observe que RootBeer détecte toujours un téléphone rooté (‘PRESENT!!!’), ce qui est légitime étant donné que l’on n’a pas modifié chaque valeur de retour pour chaque fonction. En revanche, on constate que la valeur de retour de la fonction isRooted est bien définie à false.

Un tour du côté de l’application nous permet de valider que le contournement est bien en place et que l’on peut désormais commencer à tester l’application.

Bypass dynamique : Frida
L’outil
Frida est une bibliothèque d’instrumentation dynamique de code. L’instrumentation dynamique d’application consiste en un ensemble de mécanismes permettant de rajouter des fonctionnalités à un programme, sans modifier ses instructions initiales. Assez souvent, cela se traduit par l’injection de code machine dans un processus, permettant de créer des hooks (points d’ancrage) sur des fonctions du programme originel.
Avec ces modifications, il devient possible de modifier le comportement du programme lors de son exécution, sans avoir à modifier le programme initial.
Environnement
Pour pouvoir utiliser Frida, il est nécessaire de déployer un programme serveur sur le terminal.
Ici, nous utilisons Frida en version 16.7.19 (note : il existe des versions 17.x.x mais pas de commentaire sur ça, on reste sur une version stable et qui fonctionne).
On récupère la bonne version frida-server pour notre Android (note : attention à l’architecture). On pousse le programme sur le téléphone (`adb push`), on le rend exécutable et on le lance.

En utilisant la commande frida-ps -Uai, on peut lister les applications (`-a`) installées (`-i`) sur le terminal connecté en USB (`-U`).

Hooking
Comme identifié lors de l’analyse de l’APK avec Jadx, la fonction isRooted de la classe RootDetection est responsable de la détection.
Nous allons créer un script hooking.js qui contient, dans un premier temps, les instructions permettant de s’attacher à la classe com.sysdream.app.utils.RootDetection (1), réimplémenter la fonction isRooted (2), afficher la valeur de retour (3) et la renvoyer sans modification (4).

On utilise Frida pour lancer l’application (`-f`) sur le terminal via USB (`-U`) en chargeant notre fichier hooking.js (`-l`). On observe alors les différentes sorties console.

Ces résultats sont confirmés par l’observation des journaux de l’application.

Maintenant que l’on parvient à s’attacher à la fonction souhaitée, il faut modifier la valeur de retour pour qu’elle soit toujours vraie. Rien de plus simple, on reprend notre script et au lieu de retourner la valeur actuelle, on retourne false.

Côté application, plus de message de détection, on arrive sur la page de connexion.

Lorsque l’on regarde les journaux de l’application, ces derniers affichent toujours une détection root.

Ce comportement est normal étant donné que nous avons modifié la valeur de retour de la fonction. Cette étape arrive à la fin de la fonction, juste après la journalisation, qui indique l’état réel du terminal.

Pour supprimer ces informations dans les journaux, il est possible de remplacer function(context) par function() et supprimer les deux lignes récupérant la valeur actuelle de retour de isRooted.
Conclusion
Les techniques employées dans cet article sont connues et sont adaptables en fonction du contexte des applications auditées. Bien que ces détections doivent être mises en place, il faut garder à l’esprit qu’elles ne participent qu’à ralentir un attaquant et ne protégeront pas à 100% de la rétro-ingénierie et pourront être contournées par un attaquant.
Comme pour un réseau interne, cette démarche doit s’inscrire dans un principe de défense en profondeur de la résilience d’une application et être adoptée avec d’autres mécanismes, dont :
- Vérification de la signature d’une application (MASTG-KNOW-0003) ;
- Utilisation de Google Play Integrity API (MASTG-KNOW-0035) ;
- Vérification de l’intégrité d’exécution (MASTG-KNOW-0032) ;
- Mise en place d’anti-débogage (MASTG-KNOW-0028) ;
- Obfuscation du code (MASTG-KNOW-0033).
Chez SysDream, la résilience n’est qu’une partie des points de contrôle adoptés dans notre méthodologie de tests d’intrusion d’applications mobile (Android et iOS). Ces dernières doivent être vérifiées dans leur globalité en prenant en compte le contexte métier des applications. Notre expertise dans le domaine nous permet également de transmettre ce savoir-faire dans notre formation dédiée à la sécurité des applications mobiles.