Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
AndroidKernelVulnerability — Déclenchement et analyse de la vulnérabilité CVE-2019-2215 du noyau Android | Kitploit
Outils/GitHubGitHub/sharif-dev/androidkernelvulnerability
Sécurité AndroidEscalade de PrivilègesAnalyse StatiqueAnalyse Dynamique (Sandboxing)ExploitationApprentissage et ÉducationExploitation de BinairesLabs et Pratique

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager
GitHub
sharif-dev/androidkernelvulnerability

AndroidKernelVulnerability

Déclenchement et analyse de la vulnérabilité CVE-2019-2215 du noyau Android

Voir le dépôt
7219il y a 3 ansVérifié par Kitploit

Vulnérabilité du noyau Android

Aperçu

En novembre 2017, un bug de use-after-free dans le noyau Linux a été détecté par le système syzkaller . En février 2018, ce bug a été corrigé dans certains noyaux Linux et versions d'Android.

Ce correctif n'a jamais été inclus dans les bulletins de sécurité mensuels d'Android, il n'a donc pas été appliqué à de nombreux appareils récemment commercialisés comme les Pixel et Pixel2.

En septembre 2019, Android a été informé des implications de sécurité de ce bug par Project Zero. Android a ensuite attribué CVE-2019-2215 à cette vulnérabilité pour la rendre plus formelle et plus connue.

CVE-2019-2215 est un use-after-free dans binder.c qui permet l'élévation de privilège (obtention d'un accès root) à partir d'une application Android. Aucune interaction de l'utilisateur n'est nécessaire pour exploiter cette vulnérabilité. Elle nécessite uniquement l'installation d'une application locale malveillante.

Nous allons ici présenter cette vulnérabilité du noyau Android plus en détail et utiliser cette vulnérabilité pour obtenir un accès root (élévation de privilèges) sur l'ensemble de l'appareil Android.

Nous utiliserons cette preuve de concept (PoC) :

https://github.com/cloudfuzz/android-kernel-exploitation

Déclenchement de la vulnérabilité

Nous allons d'abord vous montrer comment déclencher cette vulnérabilité sur un émulateur Android et provoquer un crash du noyau. Ensuite, pour voir à quel point cela peut être dangereux, nous continuons en utilisant la PoC pour obtenir un accès root sur l'appareil Android simulé. Puis nous analyserons le code du noyau pour voir quelle en est la raison (analyse statique et analyse dynamique).

Après l'analyse, nous verrons comment nous avons obtenu l'accès root. Enfin, nous verrons comment cette vulnérabilité est atténuée à l'aide de correctifs.

Pour provoquer un crash du noyau en déclenchant cette vulnérabilité, nous utilisons les étapes suivantes :

  1. Tout d'abord, vous avez besoin d'un système d'exploitation Linux avec 'gdb' et 'python' installés.
  2. Clonez le dépôt PoC. (https://github.com/cloudfuzz/android-kernel-exploitation)
  3. Installez l'émulateur Android et le NDK Android (en installant Android Studio)
  4. Clonez le code source du noyau Android. (La branche 'q-goldfish-android-goldfish-4.14-dev' sera utilisée)
  5. Ce noyau est déjà corrigé ; nous le modifions pour réintroduire la vulnérabilité dans ce code du noyau.
  6. Nous devons maintenant compiler le noyau à partir du code source. Nous compilons notre noyau avec KASan.
  7. Nous démarrons le noyau compilé et lançons notre émulateur avec celui-ci.
  8. Ensuite, à l'aide de 'trigger.cpp' dans le dépôt PoC, nous déclenchons un crash (en utilisant la commande 'adb').
  9. Nous utiliserons 'root-me.py' dans la PoC et la commande 'gdb' pour obtenir un accès root sur l'appareil émulé.

Regardez la vidéo suivante :

[video]

Analyse statique

Dans cette section (et la suivante), nous allons comprendre pourquoi le crash se produit en utilisant l'analyse statique et dynamique.

Ici, nous analyserons le code du noyau (analyse statique) pour comprendre le problème. Dans crash_report.txt se trouve le rapport de KASan qui indique qu'il s'agit d'un bug de use-after-free. Cela signifie qu'un objet est alloué dans le tas (nous en avons une référence), que nous avons ensuite libéré l'objet du tas, puis que nous l'avons appelé par erreur via une référence. Ce rapport affiche la trace de pile de ces trois étapes.

Si vous vous souvenez de ce qui précède, nous avons utilisé 'trigger.cpp' dans la PoC.

Voici le code principal de trigger.cpp:``` int main() { //1 int fd, epfd; //2 struct epoll_event event = {.events = EPOLLIN}; //3 fd = open("/dev/binder", O_RDONLY); //4 epfd = epoll_create(1000); //5 epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event); //6 ioctl(fd, BINDER_THREAD_EXIT, NULL); }

root@kitploit:~
Voyons ce que fait 'trigger.cpp'.

Sous Android (comme sous les autres systèmes d'exploitation de type Unix), nous avons des processus. Chaque programme que nous exécutons crée un (ou plusieurs) processus, et ces processus sont gérés par l'OS. L'OS peut basculer entre eux (multitâche) ou terminer un processus, etc. Pour des raisons de sécurité, les processus sont isolés les uns des autres par défaut.

Dans certains cas, un processus peut avoir besoin d'échanger des données avec un autre processus. Cela s'appelle la communication interprocessus (IPC). Il existe plusieurs moyens pour les processus de communiquer sous Linux. Android a introduit un mécanisme d'IPC spécifique appelé **'Binder'**. Binder est un pilote du noyau qui facilite la communication interprocessus.

Sous Android, l'IPC peut être réalisé en appelant directement certaines méthodes du noyau (la plupart se trouvent dans drivers/binder.c) ou en utilisant des implémentations de haut niveau (par exemple en Java).






![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/binder.jpg)


Pour utiliser **binder**, nous devons ouvrir le module binder du noyau. Cela se fait à la ligne 3 de trigger.cpp. Ensuite, nous avons un pointeur de descripteur de fichier. En utilisant ce fd, l'initiateur et les destinataires de l'IPC peuvent être identifiés dans le noyau.

Toutes les interactions avec le pilote se feront via un petit ensemble de commandes **'ioctl'** (BINDER_THREAD_EXIT, BINDER_WRITE_READ, ...).

En savoir plus sur binder : [link1](https://www.nds.ruhr-uni-bochum.de/media/attachments/files/2012/03/binder.pdf), [link2](http://rts.lab.asu.edu/web_438/project_final/CSE_598_Android_Architecture_Binder.pdf)

Sous Linux, il existe un concept appelé « **event polling** ». L'API « **epoll** » est utilisée lorsque nous voulons surveiller plusieurs descripteurs de fichier (les descripteurs de fichier sont ce que nous obtenons en ouvrant un pilote ou en travaillant avec des E/S, etc.).

**epoll** est une structure du noyau qui possède deux champs importants.



*   interest list = liste des descripteurs de fichier que nous voulons surveiller.
*   ready list = liste des descripteurs de fichier prêts pour les E/S.

Afin d'utiliser l'event polling, nous créons d'abord un epoll (ligne 4), puis nous ajoutons ou supprimons (EPOLL_CTL_ADD) un événement (&event) associé à un descripteur de fichier (**fd**) à notre epoll créé (**epfd**) en appelant la méthode **epoll_ctl** du noyau.

=> `epoll_event event` est un événement déclenché lorsque le fichier associé (fd) est disponible pour une opération de lecture.

Maintenant, nous comprenons ce que fait **trigger.cpp** (pas besoin d'aller plus loin !). Il ouvre le module **binder**, crée un **epoll** pour l'écouter lorsqu'il est prêt. Puis, à la ligne 6, nous sortons du binder que nous avons démarré à la ligne 3.


#### Allocation :

En appelant open(), nous appelons en réalité **open_binder()** (l'implémentation de open() dans binder.c). Dans open_binder(), une nouvelle structure **'binder_proc'** sera créée et :

   ` fd->pricate_data = binder_proc`

En appelant **epoll_create(),** une nouvelle structure epoll sera créée et ajoutée à une structure de file d'attente.






![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/event_poll.jpg)


En appelant **epoll_ctrl(epdf, ADD, fd, event)**, un nouveau **ep_item** est créé, associe **fd** (le descripteur de fichier à écouter) à ce **ep_item**, et il est inséré dans l'arbre rouge-noir de event_poll (une structure de données dans ep pour stocker les ep_items). Il appelle également **ep_item_poll(),** cette méthode gère l'association de la fonction de rappel à ep_item.

Il crée une nouvelle structure **binder_thread** (**l'allocation se produit ici**), la lie à la **binder_proc** (créée ci-dessus), puis une structure **epoll_entry** est créée. Elle possède deux listes, **epoll_entry->wait** et **epoll_entry->whead**, et ces deux listes ont un pointeur vers le **binder_thread** créé précédemment.

Ensuite, **epoll_entry** est lié à ep_item (**ep_item->pwqlist** est une liste qui contient cet epoll_entry).





![alt_text](https://raw.githubusercontent.com/sharif-dev/AndroidKernelVulnerability/master/images/even_poll.jpg)



#### Libération :

En appelant **ioctl(fd, ...)**, **binder_proc** est accédé via fd->private_data, puis la structure **binder_thread** sera **libérée** de la mémoire.


#### Utilisation :

Lorsque notre processus actuel se termine, **epoll_ctl(epfd, DEL, fd, event)** sera appelé.

Il appelle **ep_remove(event_poll, ep_item)**. Cette méthode récupère le **epoll_entry** à partir de **ep_item->pwqlist**, puis obtient la liste d'attente de **ep_item (ep_tem->wait)**, c'est une liste chaînée, et elle veut retirer l'un des éléments de cette liste d'attente.

Il utilise le code suivant (un pseudo-code est utilisé) :```
entry = wait->entry;
entry.next.prev = entry.prev;
entry.prev.next = entry.next;

Ici wait->entry est un pointeur vers binder_thread qui a été supprimé de la mémoire ! Il s'agit donc d'une utilisation après libération (use after free) qui provoque un bug !!

  • Ci-dessus, nous avons utilisé des codes similaires aux codes réels du noyau, ils peuvent différer dans certains détails. (dans certains cas, binder_thread est utilisé à la place de binder_thread->wait)

Résumé :

Nous avons créé un event_poll qui possède un red_black_tree ; chaque nœud est un ep_item qui a un champ correspondant à une liste de epoll_entry ; chaque epoll_entry possède deux pointeurs vers la struct binder_thread (wait, whead).

En appelant ioctl(), nous avons libéré le binder_thread de la mémoire. Puis, lors de la sortie, cette structure est accédée via un pointeur qui était toujours disponible !

Analyse dynamique

L'analyse dynamique consiste à tester et évaluer un programme en exécutant des données en temps réel ; pour trouver des erreurs dans un programme pendant son exécution.

étapes :

  1. Compiler le noyau Android sans KASan

    Nous le compilons sans KASAN pour surveiller les opérations d'écriture et d'unlink et voir ce qui se passe réellement après l'opération d'unlink.

  2. Démarrer l'émulateur avec le noyau fraîchement compilé

  3. Lancer l'émulateur

  4. Utiliser GDB pour se connecter à l'instance QEMU

  5. Compiler le déclencheur de vulnérabilité et le pousser sur l'appareil virtuel

  6. Interrompre dans GDB

    Charger le script Python personnalisé(dynamic-analysis.py du dépôt) : Pour tracer les appels de fonction et vider le bloc de la structure binder_thread avant et après sa libération. Vider également la même structure binder_thread avant et après l'opération d'unlink.

    Dans ce fichier, on commence par supprimer tous les points d'arrêt puis on place 2 points d'arrêt (BP) ; Le premier symbole est “binder_free_thread” (tracera la fonction binder_free_thread) avant que binder_thread soit libéré, la fonction stop sera appelée ; ainsi les paramètres et le symbole seront affichés avec (gb.write(....) ) puis la méthode de rappel (que nous avons définie avec set_dump_binder_thread ) sera appelée ; Dans cette fonction, binder_thread_address sera défini dans notre variable globale et gdb.execute envoie toute sortie produite par la commande vers la sortie standard de GDB.

    Le second symbole est “remove_wait_queue” (tracera la fonction remove_wait_queue) ; les paramètres que nous voulons observer sont "wq_head", "wq_entry" et un point d'arrêt sera placé à wait.c:52 pour la sortie. Leur fonction de rappel est dump_binder_thread . Ces points d'arrêt montreront ce qui se passe avant et après l'opération d'unlink.

résultat :

  • première partie du résultat :
root@kitploit:~
binder_free_thread(thread=0xffff88800c18f200)(enter)
0xffff88800c18f200:	0xffff88806793c000	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

dans notre code python, nous avions les codes ci-dessous :``` gdb.write ( "{function}({param})(enter)\n".format( function=self.function_name, param=params ) )

root@kitploit:~
et c'est la fonction binder_free_thread donc le paramètre de cette fonction est un pointeur vers binder_thread:```
static void binder_free_thread(struct binder_thread *thread)
{
        [...]
        kfree(thread);
}

dans le résultat, nous avons obtenu (binder_free_thread(thread=0xffff88800c18f200)(enter)).

les lignes suivantes montrent le résultat de l'exécution, avec la commande ci-dessous nous obtenons l'offset de binder_thread.wait :``` p offsetof(struct binder_thread, wait)

root@kitploit:~
le résultat est 0xa0 et si nous mettons wait.head au lieu de wait dans la commande , le résultat sera 0xa8 et il contient `0xffff88805c05cae0````
0xffff88805c05cae0 is pointer to eppoll_entry->wait.entry which is of type struct list_head
  • deuxième partie du résultat
root@kitploit:~
remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88805c05cae0
0xffff88800c18f2b0:	0xffff88805c05cae0	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

Dans remove_wait_queue(wq_head=0xffff88800c18f2a0, wq_entry=0xffff88805c05cac8)(enter) , wq_head est l'adresse de binder_thread.wait et wq_entry est les données de wait.head .

Après cela, l'opération unlink se produira.

  • Troisième partie du résultat :
root@kitploit:~
Breakpoint 3 at 0xffffffff802aa5be: file /home/ashfaq/workshop/android-4.14-dev/goldfish/kernel/sched/wait.c, line 53.
remove_wait_queue_wait.c:52(exit)
0xffff88800c18f200:	0xffff88800c18f600	 0x0000000000000001
0xffff88800c18f210:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f220:	0xffff88800c18f220	 0xffff88800c18f220
0xffff88800c18f230:	0x0000002000001b35	 0x0000000000000001
0xffff88800c18f240:	0x0000000000000000	 0xffff88800c18f248
0xffff88800c18f250:	0xffff88800c18f248	 0x0000000000000000
0xffff88800c18f260:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f270:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f280:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f290:	0x0000000000000003	 0x0000000000007201
0xffff88800c18f2a0:	0x0000000000000000	 0xffff88800c18f2a8
0xffff88800c18f2b0:	0xffff88800c18f2a8	 0x0000000000000000
0xffff88800c18f2c0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2d0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2e0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f2f0:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f300:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f310:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f320:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f330:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f340:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f350:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f360:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f370:	0x0000000000000000	 0x0000000000000000
0xffff88800c18f380:	0x0000000000000000	 0x0000000000000001
0xffff88800c18f390:	0xffff88806d4bb200

La partie en surbrillance est due à ceci .Le résultat est après l'opération unlink et nous pouvons voir que pour délier, écrire l'adresse (0xffff88800c18f2a0 + 0x8) dans next et previous.

cela signifie :

(pointeur vers binder_thread->wait.head) = binder_thread->wait.head.next = `binder_thread->wait.head.prev

Exploiter la vulnérabilité

Dans cette partie, nous allons montrer comment utiliser ce bug pour obtenir un accès root.

Rappelez-vous de ce qui précède : nous avions une structure binder_thread, elle a été libérée puis réutilisée à l'aide d'un pointeur. Voici le code de la structure binder_thread :``` struct binder_thread { struct binder_proc proc; struct rb_node rb_node; struct list_head waiting_thread_node; int pid; int looper; / only modified by this thread*/ bool looper_need_return; /* can be written by other thread*/ struct binder_transaction *transaction_stack; struct list_head todo; bool process_todo; struct binder_error return_error; struct binder_error reply_error; wait_queue_head_t wait; struct binder_stats stats; atomic_t tmp_ref; bool is_dead; struct task_struct *task; };

root@kitploit:~
L'un des champs est un pointeur **task_struct**, cette structure possède un champ appelé **addr_limit**.

Lorsque nous voulons accéder à une adresse dans un processus, il est vérifié que l'adresse se trouve dans l'**espace utilisateur** ou non ; si elle se trouve dans l'**espace noyau**, cet accès doit être bloqué. Cette vérification est effectuée en comparant notre adresse avec **addr_limit** ; si notre adresse est inférieure à **addr_limit**, nous avons un accès valide.

**addr_limit** sépare en réalité l'espace utilisateur de l'espace noyau ; ainsi, en modifiant ce champ dans **task_struct**, nous avons un accès complet à l'espace noyau et nous pouvons tout faire !

Nous avons donc deux étapes à exploiter :

1. trouver l'adresse de task_struct dans l'espace noyau
2. modifier addr_limit dans task_struct

#### Trouver l'adresse de task_struct

Dans le noyau, nous pouvons effectuer des E/S vectorisées, ce qui signifie que nous pouvons écrire ou lire plus d'un bloc de données vers ou depuis un descripteur de fichier (fichier, socket, etc.).

Les E/S vectorisées sont réalisées à l'aide des méthodes **writev**, **readv**, **recvmsg** et  la structure **iovec**.```
struct iovec 
{ 
     void __user *iov_base; /* BSD uses caddr_t (1003.1g requires void *) */ \
     __kernel_size_t iov_len; /* Must be size_t (1003.1g) */ \
};

En utilisant les E/S vectorisées, nous effectuons des opérations d'E/S sur un tableau de tampons (iovec). Chaque iovec possède un pointeur vers un tampon (iov_base) et la taille du tampon (iov_len).

E/S vectorisées

Par exemple, si nous voulons écrire un tableau de tampons dans un fichier (fd), nous appelons

writev(fd, iovecStack, count)

Cette méthode (comme readv et recvmsg) copie tout d'abord le tableau iovec (iovecStack) dans l'espace noyau, puis lit depuis ces tampons et écrit dans fd.

  • la première partie (copie du tableau iovec dans l'espace noyau) est similaire dans les trois méthodes.

Nous pouvons écrire et lire des tampons en utilisant pipe ; pipe est une structure qui nous donne deux descripteurs de fichier, un pour la lecture et un pour l'écriture. Un pipe a une longueur en octets ; lorsqu'un processus écrit dans un pipe plus que sa longueur, le pipe bloque ce processus et attend qu'un autre processus lise depuis ce pipe (en utilisant le descripteur de lecture de ce pipe).

pipe sous Linux

Le noyau essaie d'allouer de la mémoire (pour une structure) en fonction de sa taille. Par exemple, lorsque nous avons libéré la structure binder_thread de la mémoire (voir l'analyse statique), et qu'ensuite nous avons une structure de taille similaire à binder_thread, il y a de bonnes chances qu'elle soit allouée au même emplacement que le binder_thread libéré.

D'abord, nous créons un iovecStack (tableau de structures iovec) avec une taille similaire à celle de la structure binder_thread.

Ensuite, nous libérons binder_thread de la mémoire (voir la partie 'free' de l'analyse statique)

Puis nous appelons writev() sur cet iovecStack. La première partie de cette méthode copie iovecStack dans l'espace noyau,

Il est plus probable que notre iovecStack soit alloué au même endroit que le binder_thread libéré.

Si nous avons assez d'iovec dans iovecStack, la mémoire à l'emplacement de binder_thread ressemble à ceci :

alt_text

Vous voyez que iovecStack[10].iov_base, iovecStack[10].iov_len et iovecStack[11].iov_base seront au même emplacement que les champs wait.lock, wait.head.next et wait.head.prev de binder_thread.

Dans la partie 'use' de l'analyse statique, nous avons vu que le crash se produisait à cause d'un accès à la partie wait de binder_thread pendant le processus de unlinking (suppression d'un élément d'une liste chaînée).

Pendant le processus d'unlinking, wait.head est délié et ses champs next et prev pointeraient vers le champ wait de binder_thread (unlinking).

alt_text

Avant que la deuxième partie de writev ne se produise (écriture depuis les iovecs vers le fichier), si nous exécutons le processus d'unlink, iovecStack[10].iov_len et iovecStack[11].iov_base seront écrasés par une adresse noyau. Ensuite, après avoir exécuté le reste de writev, lorsqu'il veut traiter iovecStack[11], il lit depuis iovecStack[11].iov_base (= adresse de wait dans binder_thread) une longueur de données correspondant à iovecStack[11].iov_len.

Si iovecStack[11].iov_len est suffisant, nous lisons depuis le champ wait jusqu'au champ task_struct de binder_thread, ce qui nous donne le pointeur task_struct.

  • pendant la partie 'free' de l'analyse statique, tout binder_thread n'est pas libéré et des parties comme task_struct restent (nous ne l'avons pas mentionné pour simplifier).

Ainsi, pour obtenir le pointeur task_struct, nous procédons comme suit :

  1. nous créons un pipe et une pile d'iovecs
  2. nous créons binder_thread et event_poll et les lions (comme nous l'avons fait pour l'analyse statique)
  3. nous forkkons un processus enfant (nous avons maintenant un processus parent et un processus enfant)
  4. dans le processus parent, appelons writev : il importera iovecStack dans l'espace noyau (avant de continuer, bloquons writev en écrivant des données factices dans le pipe, par exemple)
  5. dans le processus enfant, effectuons l'unlink, puis lisons les données factices pour que le processus parent soit notifié.
  6. dans le processus parent, continuons le reste de writev() et lisons depuis les iovecs dans l'espace noyau, puis écrivons dans le fichier.
  7. Maintenant, le fichier contient la lecture depuis wait et en dessous, du binder_thread dans l'espace noyau (task_struct se trouve 0xe8 octets après le champ wait).

voir exploit.cpp dans le dépôt.

Modification de addr_limit dans task_struct

Nous avons maintenant l'adresse de task_struct dans l'espace noyau (task_ptr).

Ici, nous utilisons socket_pair au lieu de pipe. Et nous utilisons recvmsg() pour lire depuis le socket et écrire dans les iovecs.

Étapes :

  1. nous faisons d'abord des initialisations comme ci-dessus
  2. nous forkkons un processus enfant
  3. nous écrivons quelques données factices dans socket_pair
  4. dans le processus parent, appelons recvmsg() :
    1. il importe les iovecs dans l'espace noyau
    2. il lit ensuite les données factices et attend (se bloque) pour recevoir d'autres données du socket.
  5. dans le processus enfant, effectuons l'opération unlink
  6. dans le processus enfant, écrivons ces données dans le socket :``` static uint64_t finalSocketData[] = { 0x1, // iovecStack[10].iov_len 0x41414141, // iovecStack[11].iov_base 0x8 + 0x8 + 0x8 + 0x8, // iovecStack[11].iov_len (uint64_t) ((uint8_t *) task_ptr + OFFSET_OF_ADDR_LIMIT_IN_TASK_STRUCT), // iovecStack[12].iov_base 0xFFFFFFFFFFFFFFFE // addr_limit value };
root@kitploit:~
Après l'écriture, **recvmsg**() commence à lire depuis la socket et à écrire dans **iovecStack**. À cause des déchets, il a écrit jusqu'à **iovecStack**[10].

Il commence donc à écrire **finalSocketData** dans **iovecStack**[12], obtient l'adresse depuis **iovecStack[12].iov_base**, qui est l'adresse de** wait dans binder_thread **en raison de l'opération unlink, **iovecStack[12].iov_len**  est défini à 4 octets, donc écrit :



*   0x1 dans iovecStack[10].iov_len
*   0x41414141 dans iovecStack[11].iov_base
*   0x8 + 0x8 + 0x8 + 0x8 dans iovecStack[11].iov_len
*   **pointer_to_addr_limit** dans iovecStack[12].iov_base

maintenant 4 octets ont été écrits dans **iovecStack**[11] donc **recvmsg**() passe à **iovecStack**[12] pour écrire le reste de **finalSocketData** :

écrit **0xFFFFFFFFFFFFFFFE **dans l'adresse contenue dans** iovecStack[12].iov_base** qui est défini sur **pointer_to_addr_limit**, cela signifie que recvmsg() fait passer addr_limit à 0xFFFFFFFFFFFFFFFE (pas 0xFFFFFFFFFFFFFFFF à cause de certains problèmes avec arm64).

Maintenant l'espace utilisateur s'étend approximativement à tout l'espace noyau! (et peut tout faire!!)


# Correctif

binder_poll() passe la waitqueue thread->wait sur laquelle on peut se mettre en sommeil pour attendre du travail. Lorsqu'un thread qui utilise epoll se termine explicitement via BINDER_THREAD_EXIT, la waitqueue est libérée, mais elle n'est <span style="text-decoration:underline;">jamais retirée de la structure de données epoll correspondante</span>. Lorsque le processus se termine ensuite, le code de nettoyage d'epoll tente d'accéder à la liste d'attente, ce qui entraîne une use-after-free.

Empêchez cela en utilisant POLLFREE lorsque le thread se termine.

nous avions ce code :```
static int binder_thread_release(struct binder_proc *proc, struct binder_thread *thread)
{
        .
        .
        .
        int active_transactions = 0;
        .
        .
        .
        binder_thread_dec_tmpref(thread);
        return active_transactions;
}

Ces lignes ont été ajoutées au code source :

root@kitploit:~
static int binder_thread_release(struct binder_proc *proc,
binder_thread *thread)
{
	.
	.
	.
	/*
	 * If this thread used poll, make sure we remove the waitqueue
	 * from any epoll data structures holding it with POLLFREE.
	 * waitqueue_active() is safe to use here because we're holding
	 * the inner lock.
	*/
	  if ((thread->looper & BINDER_LOOPER_STATE_POLL) && waitqueue_active(&thread->wait)) {
		wake_up_poll(&thread->wait, EPOLLHUP | POLLFREE);
	  }
	  binder_inner_proc_unlock(thread->proc);
	  /*
	   * This is needed to avoid races between wake_up_poll() above and
	   * and ep_remove_waitqueue() called for other reasons (eg the epoll file
	   * descriptor being closed); ep_remove_waitqueue() holds an RCU read
	   * lock, so we can be sure it's done after calling synchronize_rcu().
	  */
	  if (thread->looper & BINDER_LOOPER_STATE_POLL)
		synchronize_rcu();
	  .
	  .
	  .
}

voir le code complet ici

Définitions

Noyau

Le système d'exploitation est une couche logicielle chargée de faire fonctionner tout le matériel plus efficacement et de bâtir une infrastructure sur laquelle les applications que vous utilisez peuvent fonctionner ; son cœur est le noyau.

Exploitation

L'idée derrière l'exploitation est simple : les logiciels ont des bugs, et les bugs font que le logiciel se comporte mal ou exécute incorrectement une tâche qu'il était censé effectuer correctement ; exploiter un bug signifie transformer ce mauvais comportement en un avantage pour les attaquants.

Les bugs exploitables sont appelés vulnérabilités.

Privilégié et non privilégié

Un utilisateur ou un processus privilégié est celui qui a un accès complet à l'appareil.

La plupart des architectures de jeux d'instructions offrent au moins deux modes d'exécution :

privilégié : toutes les instructions au niveau machine sont accessibles.

non privilégié : seul un sous-ensemble des instructions est accessible.

Vulnérabilité UAF

Les vulnérabilités Use-After-Free sont un type de faille de corruption de mémoire qui peut être exploitée par des hackers pour exécuter du code arbitraire.

Use-After-Free fait spécifiquement référence à la tentative d'accéder à la mémoire après qu'elle a été libérée, ce qui peut faire planter un programme ou, dans le cas d'un défaut Use-After-Free, peut potentiellement aboutir à l'exécution de code arbitraire ou même permettre des capacités complètes d'exécution de code à distance.

CVE-2019-2215

Il s'agit d'une use-after-free dans Binder dans le noyau Android. Ce bug est une vulnérabilité d'élévation de privilèges locale qui permet de compromettre entièrement un appareil vulnérable. S'il est chaîné avec une exploitation du moteur de rendu du navigateur, ce bug pourrait compromettre entièrement un appareil via un site Web malveillant. Il est accessible depuis l'intérieur du sandbox Chrome.

Remarque : cela fonctionne sur Pixel 1 et 2, mais pas sur Pixel 3 et 3a. Voir cette attaque.

Bulletins de sécurité Android

Les bulletins de sécurité Android sont une liste publiée par Google (mensuellement). Cette liste contient les vulnérabilités de sécurité corrigées qui affectent le framework Android, le noyau Linux, etc.

Syzkaller

Syzkaller est un fuzzer de noyau. Le fuzzing est une technique de test dans laquelle un programme automatisé produit des entrées semi-aléatoires pour un programme cible afin de vérifier si un bug est déclenché. Le fuzzing est particulièrement utile pour trouver des bugs de corruption mémoire dans les programmes C ou C++.

Project Zero

Project Zero est une équipe d'analystes de sécurité employés par Google chargée de trouver des vulnérabilités zero-day. Une vulnérabilité zero-day est une vulnérabilité inconnue de ceux qui devraient la corriger.

GDB

GDB signifie GNU Project Debugger, le débogueur le plus populaire pour les systèmes UNIX afin de déboguer les programmes C et C++. GDB vous permet d'exécuter le programme jusqu'à un certain point, puis de s'arrêter et d'afficher les valeurs de certaines variables à ce point, ou de parcourir le programme ligne par ligne et d'afficher les valeurs de chaque variable après l'exécution de chaque ligne.

Vous pouvez connecter votre émulateur Android avec un gdbserver pour le débogage.

Le Kernel Address Sanitizer

Kernel Address SANitizer (KASAN) est un détecteur dynamique d'erreurs mémoire conçu pour trouver les bugs hors limites et use-after-free. KASAN utilise une instrumentation à la compilation pour insérer des contrôles de validité avant chaque accès mémoire, et nécessite donc une version de compilateur qui le prend en charge. Les accès mémoire du noyau peuvent être vérifiés par rapport à la shadow map pour voir s'ils sont valides.

QEMU

QEMU (Quick Emulator) est un émulateur gratuit et open source qui effectue de la virtualisation matérielle. L'émulateur Android est dérivé de l'émulateur QEMU ; il ajoute la prise en charge du démarrage des appareils Android, émule le matériel Android typique (OpenGL, GPS, GSM, capteurs) et une interface graphique. L'émulateur Android étend QEMU de diverses manières.

Analyse statique

Dans l'analyse statique, on utilise le code source d'un programme pour trouver un bug ou tout problème. On n'exécute pas le programme.

Analyse dynamique

Dans l'analyse dynamique, on analyse le comportement d'un programme pendant son exécution. Par exemple en fournissant des entrées spéciales.

Android NDK

Android NDK est un ensemble d'outils qui permet d'exécuter du code natif comme C et C++ sur un appareil Android.

Android Goldfish

Le noyau Android Goldfish est utilisé pour exécuter le code du noyau dans un émulateur Android. Il peut être cloné, modifié puis compilé pour être utilisé dans un émulateur.

Android Debug Bridge

ADB est un outil en ligne de commande. Il permet de communiquer avec un appareil Android en cours d'exécution et d'obtenir un shell à partir de celui-ci. Il peut être utilisé à des fins de débogage.

Références

https://googleprojectzero.blogspot.com/2019/11/bad-binder-android-in-wild-exploit.html

https://nvd.nist.gov/vuln/detail/CVE-2019-2215

https://www.tutorialspoint.com/gnu_debugger

https://www.kernel.org/doc/html/latest/dev-tools/kasan.html

https://android.googlesource.com/kernel/goldfish/

https://man7.org/linux/man-pages/man7/epoll.7.html

https://www.scaler.com/topics/c/debugging-c-program/

Télécharger l’outil
  • lancer adb shell et exécuter le PoC de déclenchement