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
Pixel_GPU_Exploit — Android 14 kernel exploit for Pixel7/8 Pro | Kitploit
Outils/GitHubGitHub/0x36/pixel_gpu_exploit
Android SecurityPrivilege EscalationMemory ForensicsVulnerability AnalysisExploitationLearning & EducationBinary Exploitation
GitHub0x36/pixel_gpu_exploit

Pixel_GPU_Exploit

Android 14 kernel exploit for Pixel7/8 Pro

Voir le dépôt
55788il y a 2 ansVérifié par Kitploit

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

Mali GPU Kernel LPE (Élévation de privilèges locale du noyau Mali GPU)

Cet article fournit une analyse approfondie de deux vulnérabilités du noyau dans le GPU Mali, accessibles depuis le sandbox applicatif par défaut, que j'ai identifiées et signalées indépendamment à Google. Il inclut un exploit noyau qui permet des capacités de lecture/écriture arbitraires dans le noyau. Par conséquent, il désactive SELinux et élève les privilèges vers root sur les modèles Google Pixel 7 et 8 Pro exécutant les versions Android 14 suivantes :

  • Pixel 8 Pro : google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys
  • Pixel 7 Pro : google/cheetah/cheetah:14/UP1A.231105.003/11010452:user/release-keys
  • Pixel 7 Pro : google/cheetah/cheetah:14/UP1A.231005.007/10754064:user/release-keys
  • Pixel 7 : google/panther/panther:14/UP1A.231105.003/11010452:user/release-keys (par m4b4 (Marcel))

Vulnérabilités

Cet exploit exploite deux vulnérabilités : un dépassement d'entier résultant d'un correctif incomplet dans la commande ioctl gpu_pixel_handle_buffer_liveness_update_ioctl, et une fuite d'informations dans les tampons de messages du flux de timeline.

Sous-dépassement de tampon dans gpu_pixel_handle_buffer_liveness_update_ioctl() dû à un correctif incorrect du dépassement d'entier

Google a corrigé un dépassement d'entier dans la commande ioctl gpu_pixel_handle_buffer_liveness_update_ioctl dans ce commit. Au départ, lorsque j'ai signalé ce problème, je pensais que le bogue était causé par un problème dans le correctif décrit précédemment. Après avoir examiné le rapport, j'ai réalisé que mon analyse de la vulnérabilité était inexacte. Malgré ma première hypothèse selon laquelle le correctif était incomplet, il résout et prévient effectivement un sous-dépassement dans le calcul. Cela m'a conduit à soupçonner que la modification n'avait pas été appliquée dans les versions de production. Cependant, bien que je puisse provoquer un sous-dépassement dans le calcul, il n'est pas possible de provoquer un dépassement. Cela suggère que la commande ioctl a été partiellement corrigée, mais pas avec le correctif montré ci-dessus. En regardant IDA, il s'est avéré qu'un autre correctif incomplet a été livré dans les versions de production, et ce correctif n'est présent dans aucune branche git du module noyau mali gpu.

Cette vulnérabilité a été découverte pour la première fois dans la dernière version d'Android et signalée le 19 novembre 2023. Google m'a ensuite informé qu'ils l'avaient déjà identifiée en interne et lui avaient attribué CVE-2023-48409 dans le bulletin de sécurité Android de décembre, la qualifiant de problème en double. Bien que j'aie pu vérifier que le bogue avait été identifié en interne plusieurs mois avant mon signalement (basé sur la date du commit vers le 30 août), il subsiste une confusion. Plus précisément, il est étrange que les niveaux de correctifs de sécurité (SPL) d'octobre et novembre des appareils les plus récents aient toujours été affectés par cette vulnérabilité — je n'ai pas examiné les versions antérieures à celles-ci. Par conséquent, je ne peux pas déterminer de manière concluante s'il s'agissait vraiment d'un problème en double et si le correctif approprié était effectivement prévu pour décembre avant ma soumission, ou s'il y a eu un oubli dans la correction de cette vulnérabilité.

Quoi qu'il en soit, ce qui rend ce bogue puissant est ce qui suit :

  • Le tampon info.live_ranges est entièrement contrôlé par l'utilisateur.
  • Les valeurs de dépassement sont une entrée contrôlée par l'utilisateur, de sorte que nous pouvons faire dépasser le calcul pour que le pointeur info.live_ranges puisse être à un décalage arbitraire avant le début de l'adresse noyau buff.
  • La taille d'allocation est également une entrée contrôlée par l'utilisateur, ce qui permet de demander une allocation mémoire à partir de n'importe quel allocateur de slabs à usage général.

Cette vulnérabilité partage des similitudes avec la vulnérabilité de sous-dépassement de tampon DeCxt::RasterizeScaleBiasData() que j'ai trouvée et exploitée dans le noyau iOS 15 en 2022.

Fuite de pointeurs noyau dans les tampons de messages du flux de timeline

Le GPU Mali implémente un timeline stream personnalisé conçu pour collecter des informations, les sérialiser, puis les écrire dans un tampon circulaire selon un format spécifique. Les utilisateurs peuvent invoquer la commande ioctl kbase_api_tlstream_acquire pour obtenir un descripteur de fichier, leur permettant de lire ce tampon circulaire. Le format des messages est le suivant :

  • Un en-tête de paquet

  • Un identifiant de message

  • Un tampon de message sérialisé, dont le contenu spécifique dépend de l'ID du message. Par exemple, la fonction __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait sérialise les pointeurs noyau kbase_kcpu_command_queue et dma_fence dans le tampon de message, ce qui entraîne une fuite de pointeurs noyau vers l'espace utilisateur.```c void __kbase_tlstream_tl_kbase_kcpuqueue_enqueue_fence_wait( struct kbase_tlstream *stream, const void *kcpu_queue, const void *fence ) { const u32 msg_id = KBASE_TL_KBASE_KCPUQUEUE_ENQUEUE_FENCE_WAIT; const size_t msg_size = sizeof(msg_id) + sizeof(u64) + sizeof(kcpu_queue) + sizeof(fence) ; char *buffer; unsigned long acq_flags; size_t pos = 0;

    buffer = kbase_tlstream_msgbuf_acquire(stream, msg_size, &acq_flags);

    pos = kbasep_serialize_bytes(buffer, pos, &msg_id, sizeof(msg_id)); pos = kbasep_serialize_timestamp(buffer, pos); pos = kbasep_serialize_bytes(buffer, pos, &kcpu_queue, sizeof(kcpu_queue)); pos = kbasep_serialize_bytes(buffer, pos, &fence, sizeof(fence));

    kbase_tlstream_msgbuf_release(stream, acq_flags); }

root@kitploit:~
La preuve de concept de l'exploitation divulgue l'adresse de l'objet `kbase_kcpu_command_queue` en surveillant l'ID de message `KBASE_TL_KBASE_NEW_KCPUQUEUE` qui est dispatché par la fonction `kbasep_kcpu_queue_new` chaque fois qu'un nouvel objet de file d'attente kcpu est alloué.

Google m'a informé que la vulnérabilité a été signalée en mars 2023 et a été assignée [CVE-2023-26083](https://source.android.com/docs/security/bulletin/2023-07-01) dans leur bulletin de sécurité. Néanmoins, j'ai pu reproduire le problème sur les derniers appareils Pixel livrés avec les niveaux de correctif de sécurité (SPL) d'octobre et novembre, indiquant que le correctif n'avait pas été appliqué correctement ou pas du tout. Par la suite, Google a rapidement résolu le problème dans le bulletin de mise à jour de sécurité de décembre sans offrir de crédit, et m'a plus tard informé que le problème était considéré comme un doublon. La justification derrière le fait de qualifier ce problème de doublon reste toutefois discutable.

## Exploitation
---
J'ai donc deux vulnérabilités intéressantes. La première offre une capacité puissante à modifier le contenu de n'importe quelle adresse mémoire noyau alignée sur 16 octets située avant l'adresse du ~buff~ alloué. La seconde vulnérabilité fournit des indications sur les emplacements potentiels des objets dans la mémoire du noyau.

### Remarques sur les valeurs de buffer_count et live_ranges_count
Avec un contrôle total sur les champs `buffer_count` et `live_ranges_count`, j'ai la flexibilité de sélectionner le slab cible et le décalage précis auquel j'ai l'intention d'écrire. Cependant, la sélection des valeurs pour `buffer_count` et `live_ranges_count` nécessite une réflexion minutieuse en raison de plusieurs contraintes et facteurs :
- Les deux valeurs sont liées, et le débordement ne se produira que si tous les contrôles nouvellement introduits sont contournés.
- L'exigence que le décalage négatif soit aligné sur 16 octets restreint la capacité d'écrire à un emplacement choisi. Cependant, ce n'est généralement pas un obstacle majeur.
- Opter pour un décalage plus important entraîne l'écriture d'une grande quantité de données dans des zones de mémoire qui ne sont peut-être pas des cibles intentionnelles. Par exemple, si la taille d'allocation déborde à `0x3004`, le pointeur `live_ranges` serait défini à `-0x4000` octets de l'espace alloué de l'objet `buff`. La fonction `copy_from_user` écrirait alors `0x7004` octets, selon le calcul de `update->live_ranges_count` multiplié par 4. Par conséquent, cette opération entraînerait une réécriture de la zone mémoire entre le pointeur `live_ranges` et l'allocation `buff` par des données contrôlées par l'utilisateur. Il est donc essentiel de veiller soigneusement à ce qu'aucun objet système critique dans cette plage ne soit accidentellement écrasé. Étant donné que l'opération implique un appel `copy_from_user`, on pourrait envisager de déclencher une erreur `EFAULT` en dé-mappant délibérément la région mémoire indésirable après le tampon source utilisateur pour empêcher l'écriture de données à des emplacements sensibles. Cependant, cette approche est inefficace, car si la fonction `raw_copy_from_user` échoue, elle mettra à zéro les octets restants dans le tampon de destination noyau. Ce comportement est implémenté pour garantir qu'en cas de copie partielle due à une erreur, le reste du tampon noyau ne contienne pas de données non initialisées.```c
static inline __must_check unsigned long
_copy_from_user(void *to, const void __user *from, unsigned long n)
{
	unsigned long res = n;
	might_fault();
	if (!should_fail_usercopy() && likely(access_ok(from, n))) {
		instrument_copy_from_user(to, from, n);
		res = raw_copy_from_user(to, from, n);
	}
	if (unlikely(res))
		memset(to + (n - res), 0, res);
	return res;
}

Compte tenu de cela, nous devons soigneusement sélectionner l'objet à écraser et les données à écrire.

Choisir le bon objet à écraser

Étant donné que je suis coincé avec cette malheureuse vérification, ma stratégie consiste à identifier un objet qui, s'il est mis à zéro, ne produira aucun résultat indésirable. Mais, avant d'en arriver là, il y a un autre problème à traiter. Vous souvenez-vous quand j'ai dit dans la dernière partie que je pouvais choisir n'importe quelle taille d'allocation et donc n'importe quel allocateur de cache slab à usage général pour servir mon tampon d'allocation ? Ce n'est pas correct, car c'est à cause de copy_from_user encore une fois ! C'est dû à la mitigation CONFIG_HARDENED_USERCOPY. Elle interdit de spécifier une taille qui ne correspond pas à la taille du cache slab correspondant où se trouve le tampon de destination du noyau (dans ce cas, un objet du tas). Elle détermine si la page du tampon est une page slab, et si c'est le cas, elle récupère le kmem_cache->size correspondant et détermine si la taille fournie par l'utilisateur ne le dépasse pas ; sinon, le noyau se plante en raison de la non-correspondance de taille. Donc, en d'autres termes, je ne peux pas cibler des objets qui appartiennent à l'allocateur à usage général, MAIS je peux toujours cibler des objets qui ont de grandes tailles (c'est-à-dire ceux servis directement par l'allocateur de pages).

La première pensée qui m'est venue à l'esprit a été d'utiliser la technique pipe_buffer, qui est une technique très élégante pour obtenir des primitives de lecture/écriture arbitraires. Je n'entrerai pas dans les détails de la technique, mais les lecteurs sont invités à lire ce fantastique blog d'Interrupt Labs. Lors de la construction d'un objet pipe, l'objet pipe_buffer est initialement créé dans un tableau de 16 éléments ; cependant, la taille du tableau peut être ajustée en utilisant fcntl(F_SETPIPE_SZ). Par conséquent, l'allocation du tableau pipe_buffer peut être ajustée de manière à être servie par l'allocateur de pages, ce qui en fait un objet cible parfait pour l'attaque. Après avoir sélectionné l'objet pipe_buffer comme candidat cible, l'étape suivante pour atteindre la r/w du noyau est d'écraser son contenu avec la vulnérabilité de débordement négatif, ce qui me permettra de lire/écrire depuis/vers n'importe quel emplacement mémoire dont la page écrase le champ pipe_buffer->page. Parce que la vulnérabilité me permet d'écrire des données arbitraires, je peux contrôler tout le contenu de 'pipe_buffer', y compris son champ page, et pour ce faire, je dois allouer le tableau pipe_buffer avant l'objet vulnérable kbuff et ils doivent être adjacents.

Positionner les objets pipe_buffer et buff de manière adjacente

J'ai aspergé la mémoire du noyau avec beaucoup d'objets kbase_kcpu_command_queue puis suivi par un ensemble de tableaux pipe_buffer. Je ne peux pas utiliser uniquement les tableaux pipe_buffer comme source principale de pulvérisation en raison de la limitation imposée par pipe_max_size. Par conséquent, j'ai décidé de commencer la pulvérisation avec l'objet kbase_kcpu_command_queue. Le choix de l'objet kbase_kcpu_command_queue était pour deux raisons : sa taille d'allocation est 0x38C8 donc gérée par l'allocateur de pages, et je peux obtenir de manière déterministe son adresse noyau en utilisant le bug de fuite d'informations du noyau, ce qui en fait un bon objet pour la pulvérisation ainsi qu'un bon objet à cibler (comme nous le verrons dans la section suivante).

Comme mentionné précédemment, j'ai utilisé fcntl(F_SETPIPE_SZ) pour augmenter la taille de l'allocation du tableau pipe_buffer afin qu'il puisse être servi par l'allocateur de pages. Pour être plus précis, j'ai choisi la taille d'allocation à ==0x4000 octets (4 * PAGE_SIZE)== afin d'être cohérent avec les allocations de kbase_kcpu_command_queue.

Obtenir une adresse de struct page

Afin d'utiliser correctement le pipe_buffer, une adresse de page est nécessaire. Pouvoir identifier l'adresse noyau d'un objet kbase_kcpu_command_queue que je peux délibérément créer et détruire en fait un bon candidat à utiliser et trouver son struct page correspondant peut être réalisé en utilisant virt_to_page .

Contenu à écrire dans le pipe_buffer

Ainsi, l'objet pipe_buffer est le suivant :```c struct pipe_buffer { struct page *page; unsigned int offset, len; const struct pipe_buf_operations *ops; unsigned int flags; unsigned long private; };

root@kitploit:~
Comme mentionné précédemment, le champ `page` doit inclure une adresse de page valide. Les champs `offset` et `len` ne doivent pas dépasser `PAGE_SIZE`, sinon le tube augmentera les compteurs head/tail, ce qui entraînera l'utilisation d'un nouvel objet `pipe_buffer` et la perte de contrôle sur le faux tampon du tube. 
De plus, le champ `flags` doit être `PIPE_BUF_FLAG_CAN_MERGE` afin que les appels suivants à `pipe_write`, au lieu d'incrémenter aveuglément le compteur head et d'utiliser le tampon suivant, vérifient d'abord s'il y a de l'espace dans le `pipe_buffer` actuel qui peut accueillir la demande d'écriture ou non, et si c'est le cas, ils ajouteront simplement les données au même tampon à partir de la valeur stockée dans le champ `len`. 
Afin d'éviter un crash du périphérique lors de l'appel à `pipe_buf_confirm`, qui est appelé par `pipe_write` et `pipe_read`’, le pointeur `ops` doit également être une adresse noyau valide avec un champ `ops->confirm` défini à _NULL_. Je peux simplement utiliser un décalage dans l'objet `kbase_kcpu_command_queue` divulgué qui est NULL et qui ne changera en aucune circonstance.

### Choisir la valeur de décalage optimale pour le sous-dépassement
Alors que les tailles d'allocation de `buff`, `kbase_kcpu_command_queue` et `pipe_buffer` sont ~0x4000~ octets, j'ai choisi de sous-dépasser le tampon avec **0x8000** octets. Pourquoi ?

Jetons un bref coup d'œil à la façon dont les `pipe_buffers` sont mis à jour lors des opérations de lecture et d'écriture. Supposons que nous puissions façonner le `pipe_buffer` pour qu'il ressemble à ceci :```c
struct pipe_buffer {
	.page = virt_to_page(addr),
	.offset =  0,
	.len = 0x40,
	.ops = kcpu_addr + 0x50,
	.flags = PIPE_BUF_FLAG_CAN_MERGE,
	unsigned long private = 0
};

Bien que le bug donne la capacité de contrôler arbitrairement le contenu de cet objet, il ne le fait qu'une seule fois car l'objet sous-dépassé est libéré immédiatement après la fin de l'appel ioctl. Cela pose en fait un problème car je dois mettre à jour manuellement l'objet pipe_buffer pour le rendre à nouveau utilisable, puisque chaque opération de lecture/écriture de pipe :

  • Le champ .page n'est pas mis à jour ; il reste le même, et lorsque le buffer est vide, il est libéré, ce que je ne souhaite pas qu'il se produise car le champ .ops n'est pas correctement défini.
  • Parce que le pipe_buffer met à jour le champ .offset lors d'une opération de lecture, je ne peux donc pas relire la même région mémoire.
  • Les données écrites dans le pipe_buffer seront ajoutées au buffer à partir de la valeur .len (en supposant que le flag PIPE_BUF_FLAG_CAN_MERGE soit défini) et .len est mis à jour en conséquence. Autrement dit, nous ne pouvons pas écrire des données deux fois à la même adresse.

Par conséquent, à moins que je ne mette correctement à jour le pipe_buffer après chaque opération de lecture ou d'écriture, je ne peux pas lire et écrire depuis/vers le même pipe simultanément. C'est pourquoi le sous-dépassement avec 0x8000 octets est beaucoup plus pratique, car au lieu d'écraser un seul pipe_buffer, je vais écraser deux instances distinctes de pipe_buffer appartenant à deux objets pipes distincts : l'une sera considérée pour les opérations de lecture et l'autre pour les opérations d'écriture.```c #define PIPE_BUF_FLAG_CAN_MERGE 0x10 /* can merge buffers */

pipe_read = (struct pipe_buffer *)( ptr); pipe_read->page = virt_to_page(ta->kcpu_kaddr); pipe_read->offset = 0; pipe_read->len = 0xfff; pipe_read->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_read->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_read->private = 0;

pipe_write = (struct pipe_buffer )( ptr + 0x4000); pipe_write->page = virt_to_page(ta->kcpu_kaddr); pipe_write->offset = 0; pipe_write->len = 0; / This is the starting position of the pipe_write */ pipe_write->ops = (const void *)(ta->kcpu_kaddr + 0x50); pipe_write->flags = PIPE_BUF_FLAG_CAN_MERGE; pipe_write->private = 0;

root@kitploit:~
Le `pipe_read` est un tampon de pipe factice qui sera utilisé pour lire les données de la page cible en commençant à `.offset = 0` jusqu'à `0xfff` octets, tandis que `pipe_write` est un `pipe_buffer` factice qui sera utilisé pour écrire des données en commençant à `.len = 0` jusqu'à `0xfff` octets.
Il est également très important de mentionner à nouveau que l'écriture de plus de `PAGE_SIZE` octets poussera le pipe à incrémenter le compteur de tête, utilisant ainsi un nouveau `pipe_buffer` alloué fraîchement et perdant le contrôle sur notre `pipe_write` factice. D'un autre côté, vider (lire `0xfff` données du) tampon `fake_read` indique au noyau de libérer la page réelle en appelant `ops→release`, ce qui provoque un crash du noyau car je n'ai toujours pas d'adresse de texte du noyau.
Bien que j'aie réussi à séparer les opérations de lecture et d'écriture du pipe de sorte qu'une écriture dans une extrémité du pipe n'interfère pas avec l'autre tampon du pipe et vice versa, je n'ai toujours pas résolu le problème principal : comment mettre à jour de manière fiable le tampon du pipe ? La réponse évidente qui m'est venue à l'esprit était simplement de répéter le processus de spray encore et encore après chaque appel de lecture ou d'écriture du pipe. Et cela n'a aucun sens car cela aurait un impact significatif sur la fiabilité de l'exploit. Dans la section suivante, je vais diviser l'objectif en deux sous-objectifs : d'abord, je me concentrerai uniquement sur le champ `.page`, puis sur les champs `.len/.offset` par la suite.

### Modification du champ pipe_buffer→page
À ma grande surprise, je n'ai pas besoin de mettre à jour `.page` du tout, car je peux écraser `pipe_buffer→page` pour qu'il pointe vers l'adresse de la page du `kbase_kcpu_command_queue` divulgué. Par conséquent, **tout ce que j'ai à faire est de libérer l'objet `kbase_kcpu_command_queue` et de le chevaucher avec un nouvel objet `pipe_buffer`. Oui ! Maintenant, j'ai un `pipe_buffer→page` qui pointe vers un objet `pipe_buffer` légitime !**
Remplacer `kbase_kcpu_command_queue` par `pipe_buffer` nous donne la capacité de manipuler un tampon de pipe légitime sans avoir à mettre à jour régulièrement le champ `.page`. Cependant, je dois encore gérer les champs `.len` et `.offset`.

### Modification des champs pipe_buffer→len/offset
Comme je l'ai mentionné plus tôt, effectuer une lecture/écriture du pipe met à jour les champs `.len` et `.offset`, rendant inutilisables les opérations de lecture/écriture ultérieures sur la même page, même si elles sont effectuées sur les deux pipes distincts. Voici une autre astuce : **il existe une technique pour lire/écrire des données sans même toucher aux champs `.len/.offset` !**. Et il est possible d'y parvenir en provoquant une erreur (fault) sur les appels `copy_page_from_iter` et `copy_page_to_iter` dans `pipe_read/write` ! Oui, tout comme `copy_to/from_user`, `copy_page_to/from_iter` copie des données depuis/vers l'espace utilisateur qui est passé via la structure `iov_iter`, et il peut être mis en défaut.

Pour continuer avec l'exemple précédent, si nous souhaitons écrire 8 octets de données à une adresse, la taille du tampon de l'espace utilisateur fourni doit être de 8, suivie d'une zone mémoire non mappée ou non lisible, puis passer `9` comme argument de taille à l'appel système `write`, indiquant la quantité de données que nous voulons écrire. Cette opération écrira 8 octets et échouera sur le _neuvième_ car il rencontre un emplacement mémoire non mappé/non lisible. Par conséquent, les données ont été effectivement écrites dans le tampon du noyau de destination et le champ `.len` n'a pas été modifié. La fonction du noyau `pipe_write` retournera simplement sans mettre à jour le champ `buf->len`.```c
		if ((buf->flags & PIPE_BUF_FLAG_CAN_MERGE) &&
		    offset + chars <= PAGE_SIZE) {
			ret = pipe_buf_confirm(pipe, buf);
			if (ret)
				goto out;

			ret = copy_page_from_iter(buf->page, offset, chars, from);
			if (unlikely(ret < chars)) {
				ret = -EFAULT;
				goto out;
			}

			buf->len += ret;
			if (!iov_iter_count(from))
				goto out;
		}

Il en va de même pour les opérations de lecture ; si nous souhaitons lire 8 octets, rendons le neuvième octet du tampon illisible puis prétendons vouloir lire 9 octets, les données seront copiées dans le tampon utilisateur sans modifier le champ .offset.
Par conséquent, nous sommes capables d'effectuer des opérations de lecture/écriture illimitées sur n'importe quelle adresse mémoire du noyau sans avoir à répéter le processus de spray.

Obtenir les droits root

Maintenant que je dispose d'une primitive de lecture/écriture arbitraire robuste, j'ai simplement parcouru toutes les struct page dans le tableau VMEMMAP_START pour déterminer l'adresse de début du texte du noyau en utilisant la technique décrite dans l'article du blog Interrupt Labs. J'ai ensuite réalisé que init_task est mis à zéro dans les Android November Security Updates, j'ai donc utilisé kthreadd_task à la place. Disposer de l'adresse noyau de kthreadd_task m'a permis de parcourir la liste task->tasks et d'obtenir l'adresse noyau de ma propre tâche current, puis de mettre à zéro la structure cred pour obtenir les privilèges root.

Plus tard, j'ai réalisé qu'il n'était pas nécessaire de scanner toutes les adresses de pages car j'avais déjà l'adresse texte noyau de anon_pipe_buf_ops à partir d'un objet pipe_buffer. Avec cette information, je pouvais en déduire l'adresse de base du texte du noyau, contournant ainsi efficacement KASLR.

Désactiver SELinux

L'exploit désactive également SELinux ; avec l'adresse de base du texte du noyau, il me suffit de trouver l'emplacement de la structure globale selinux_state puis de mettre à zéro la valeur .enforcing.

Preuve de concept

La preuve de concept accompagnant le rapport a été testée sur des appareils Pixel 7 et 8 Pro fonctionnant sous Android 14 avec les ASB d'octobre et novembre, atteignant un taux de réussite de près de 100 %. Il est également important de mentionner que l'exploit ne fonctionnera pas directement sur d'autres appareils en raison de l'utilisation de certains décalages codés en dur. Pour ajouter la prise en charge d'un nouvel appareil, il faut fournir les éléments suivants :

  • Le décalage de kthreadd_task par rapport à l'adresse de base du noyau.
  • Le décalage de selinux_state par rapport à l'adresse de base du noyau.
  • Les décalages de structure task_struct->cred, task_struct->pid et task_struct->tasks.
  • Le décalage de anon_pipe_buf_ops par rapport à l'adresse de base du noyau.

Compilation

Pour compiler l'exploit en tant que binaire autonome, utilisez la commande suivante, puis utilisez adb shell pour l'exécuter :```sh $ aarch64-linux-androidXX-clang++ -static-libstdc++ -w -Wno-c++11-narrowing -DUSE_STANDALONE -o poc poc.cpp -llog $ adb push poc /data/local/tmp/ $ adb shell /data/local/tmp/poc

root@kitploit:~
Vous pouvez également exécuter l'exploit via une application Android Studio en intégrant ce répertoire avec celle-ci et assurez-vous de désactiver les avertissements C++ inutiles en ajoutant `-w -Wno-c++11-narrowing` au fichier cmake.

### Démo```shell
$ adb logcat  |grep -i EXPLOIT
11-28 16:04:12.500  7989  7989 E EXPLOIT : [+] Target device: 'google/husky/husky:14/UD1A.231105.004/11010374:user/release-keys' 0xa9027bfdd10203ff 0xa90467faa9036ffc
11-28 16:04:15.563  7989  7989 E EXPLOIT : [+] Got the kcpu_id (0) kernel address = 0xffffff8901390000  from context (0x0)
11-28 16:04:18.441  7989  7989 E EXPLOIT : [+] Got the kcpu_id (255) kernel address = 0xffffff89b0bf8000  from context (0xff)
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] Found corrupted pipe with size 0xfff
11-28 16:04:18.442  7989  7989 E EXPLOIT : [+] SUCCESS! we have a fake pipe_buffer (0)!
11-28 16:04:18.444  7989  7989 E EXPLOIT : 10 00 39 01 89 FF FF FF  10 00 39 01 89 FF FF FF  | ..9.......9.....
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 B0 CD 12 C0 FF FF FF  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.444  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  | ................
11-28 16:04:18.445  7989  7989 E EXPLOIT : [+] Freeing kcpu_id = 0 (0xffffff8901390000)
11-28 16:04:18.446  7989  7989 E EXPLOIT : [+] Allocating 61 pipes with 256 slots
11-28 16:04:18.462  7989  7989 E EXPLOIT : [+] Successfully overlapped the kcpuqueue object with a pipe buffer
11-28 16:04:18.463  7989  7989 E EXPLOIT : 40 AB BA 26 FE FF FF FF  00 00 00 00 30 00 00 00  | @..&........0...
11-28 16:04:18.463  7989  7989 E EXPLOIT : 70 37 8D F1 DA FF FF FF  10 00 00 00 00 00 00 00  | p7..............
11-28 16:04:18.463  7989  7989 E EXPLOIT : 00 00 00 00 00 00 00 00                           | ........
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] pipe_buffer {.page = 0xfffffffe26baab40, .offset = 0x0, .len = 0x30, ops = 0xffffffdaf18d3770}
11-28 16:04:18.463  7989  7989 E EXPLOIT : [+] kernel base = 0xffffffdaf0010000, kthreadd_task = 0xffffff8002da3780 selinux_state = 0xffffffdaf28a3168
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Found our own task struct 0xffffff88416c5c80
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully got root: getuid() = 0 getgid() = 0
11-28 16:04:20.097  7989  7989 E EXPLOIT : [+] Successfully disabled SELinux
11-28 16:04:20.102  7989  7989 E EXPLOIT : [+] Cleanup  ... OK
Télécharger l’outil