En juillet 2019, je me suis penché sur la sécurité d'un antivirus grand public, connu sous le nom de « Avira ». Lors de cette analyse, j'ai identifié, dans le driver en charge d'authentifier un programme exécutable, une vulnérabilité menant à une élévation de privilèges. Après une brève présentation du composant noyau, nous étudierons en détail la vulnérabilité et préparerons les éléments nécessaires à la réussite d'une exploitation.
La vulnérabilité CVE-2019-18568 affecte un composant noyau essentiel de l'antivirus AVIRA. Si on s'en tient à la description des propriétés du driver avipbb.sys, on y lira la valeur suivante : « Avira Driver for Security Enhancement ». On comprend donc mon intérêt à challenger ce composant. Bien évidemment, une élévation de privilèges est présente et permet d'obtenir les droits NT AUTHORITY\SYSTEM. Une description plus appropriée aurait finalement été : « Avira Driver for Security Weakening». Trêve de plaisanteries, plongeons dans les entrailles de cette vulnérabilité !
1. Présentation du composant vulnérable
1.1 Généralités
« Avira Antivir » est composée, comme tout bon antivirus, d'une multitude de modules (services, DLLs, drivers...) s'exécutant avec des droits élevés et offrant une large surface d'attaque. Le développement de drivers est une partie complexe à mettre en œuvre, tant par la connaissance à avoir de l'OS sous-jacent que par les risques de stabilité et de sécurité inhérents à sa programmation. La moindre erreur menant à un « crash » de l'OS comme à une vulnérabilité exploitable.
Le composant avipbb.sys est en charge d'attribuer un niveau de confiance et de droits dès qu'un processus est démarré sur le système hôte. Partant de ce constat, on comprend que notre point d'entrée pour éprouver ce driver n'est pas l'utilisation des IOCTLs [1], mais d'un exécutable. L'enjeu est donc de comprendre le mécanisme interne pour y trouver une faiblesse.
Dès la création d'un nouveau processus, ce driver positionne des « flags » selon les critères suivants :
- présence de droits administrateur en se basant sur les APIs SeTokenIsAdmin [2] et SeQueryInformationToken [3] ;
- appartenance à une liste blanche de processus Windows, codée en dur dans avipbb.sys ;
- appartenance à la suite « Avira Antivirus ».
Usurper le nom d'un processus Windows a pu traverser votre esprit. Cependant, le driver vérifie le chemin complet de l'exécutable démarré avec le préfixe Device\HarddiskVolume1\Windows\system32.
Alors comment Avira détermine qu'un exécutable fait partie de sa solution ? Une fine connaissance du mode de fonctionnement semble indispensable pour déceler une vulnérabilité.
1.2 La signature Avira
Tout exécutable, issu de l'antivirus, embarque une signature. Le driver avipbb.sys est en charge de faire le traitement associé à sa validation. En fait, cette signature fait parti d'une structure de données bien plus complexe composée de « magic strings », d'une version, d'une taille de bloc, etc.. Un extrait, issu du paragraphe 3.2, est représenté de l'offset 0x455 à 0x5BC.
Le driver récupère la signature à partir de l'emplacement du fichier sur le disque et en cherchant les chaînes de début AVCS4F3A4200C37O et de fin B62F3AB0132FAVCSE.
Le calcul de la signature nécessite l'exclusion de certains champs PE (« checksum », « security directory ») et d'elle-même. Ses exclusions sont insérées dans une liste chaînée et triées par ordre croissant d'adresse de début. La liste est ensuite parcourue pour déterminer les données utiles au calcul.
Nous étudierons plus en détail cet aspect dans la partie dédiée à l'exploitation.
2. La vulnérabilité noyau
2.1 Localisation du bug
Avant tout, quoi de plus parlant qu'une image pour mettre en évidence la vulnérabilité (Figure 1).
Nous remarquons l'assignation dans un registre 32 bits (r8d) du résultat de l'addition de deux registres 64 bits (rcx et r12). En fonction des valeurs contenues dans ces registres, cette instruction peut aboutir à un dépassement d'entier. Suite à une phase de rétro-ingénierie, r8d est utilisé dans une structure en tant qu’offset de fin des données à exclure. Cette structure est un élément d'une liste chaînée comportant trois champs :
- un offset vers le début et la fin des données à exclure ;
- un pointeur « next » vers le prochain élément (NULL pour le dernier).
Le registre r12 (FileSize) est préalablement initialisé avec la taille du fichier exécutable. Le registre rcx (SignatureStructureSize), quant à lui, est issu d'un champ de la signature dans l'exécutable et représente sa longueur. Comme bien souvent, une vulnérabilité est issue d'une absence de vérification des données extérieures, nous sommes libres de modifier ce champ :-)
Au final, ce code assembleur résulte en cette formule :
À notre avantage, nous contrôlons toutes ces valeurs. Mais en quoi manipuler dwOffsetEnd nous permettra-t-il d'exploiter le driver ?
2.2 Conséquences du bug
Pour stocker les données utiles au calcul de la signature, un buffer de la taille de l'exécutable est alloué dans le non-paged pool. Conceptuellement, ce buffer est plus large que nécessaire au vu de l'exclusion de certaines données. Ensuite, une boucle est responsable de parcourir la liste chaînée et de lire les données.
Les commentaires IDA sont issus des valeurs de l'extrait de l'exécutable au paragraphe 3.2. Lors du premier tour de boucle, on copie les données du début de fichier jusqu'au début du « checksum » PE (offset 0x108). On se place ensuite après le champ « checksum » (0x10C) pour y copier les données jusqu'au début de la prochaine section à exclure, i.e. la « Security Directory », située à l'offset 0x148. Et ainsi de suite...
Nous constatons que les données situées entre la fin de la signature et la fin du fichier ne sont pas copiées. Au préalable, une vérification détermine si ce dernier offset de début est bien inférieur à la taille de l'exécutable. Le but étant de définir le nombre d'octets restant à copier.
Cher lecteur, ne perdez pas à l'esprit que nous contrôlons l'offset de fin. Si nous positionnons une valeur inférieure à celle prévue, nous nous retrouverons à copier plus de données pour atteindre la fin du fichier.
Le code assembleur illustrant la dernière recopie est présenté en Figure 3.
3. Exploitation
Partant de la connaissance acquise, nous allons forger un exécutable pour déclencher la vulnérabilité et tirer profit d'une liste chaînée corrompue.
Les éléments suivants doivent être pris en compte :
- l'emplacement de la signature ;
- la valeur du champ définissant la longueur de la signature ;
- la taille du fichier exécutable.
3.1 Générer une liste chaînée corrompue
Maintenant, définissons l'emplacement de la signature et positionnons la valeur du champ lié à sa longueur. L'idée est de déborder avec des données insérées à la fin du fichier.
La liste chaînée utilisée pour le calcul de la signature est un élément clef à prendre à compte dans la réussite de l'exploitation. Nous devons jouer avec l'integer overflow pour que l'offset de fin soit supérieur aux autres sections à exclure, mais inférieur à la taille de l'exécutable.
À partir de la formule décrite dans le paragraphe 2.1, considérons les valeurs suivantes :
- FileSize défini à 0x680 octets ;
- OffsetOfSignatureFromEndOfFile positionné à 0x22b octets ;
- SignatureStructureSize fixé à 0xffffff34 octets.
Nous obtenons une valeur 64 bits pour dwOffsetEnd de 0x100000388 octets. Assigné à un registre 32 bits, le résultat est 0x388 octets. Les joies d'un dépassement d'entier.
Vous me direz que ces valeurs sortent de mon chapeau. Quasiment, elles ont été fixées empiriquement. Au final, il en découle la liste chaînée suivante :
fffffa80`0465fb58 000000000000010b
fffffa80`0465fb60 fffffa8004845e00
| Security Directory (RVA/Size)
|__ fffffa80`04845e00 0000000000000148
fffffa80`04845e08 000000000000014f
fffffa80`04845e10 fffffa8002265350
| Signature Avira
|__ fffffa80`02265350 0000000000000455
fffffa80`02265358 0000000000000388
fffffa80`02265360 0000000000000000
Nous retrouvons l'offset de fin 0x388, issu de l'integer overflow. Ce dernier étant inférieur à son offset de début. OK, on est bien là !
3.2 Récapitulation
Nous avons généré un exécutable de 0x680 octets avec les valeurs précédemment déterminées. Voyons concrètement ce qui se passe dans un comportement nominal et corrompu.
Ci-dessous, un extrait du fichier exécutable :
Le comportement nominal est la copie des données :
- de 0 à 0x108 ;
- de 0x10C à 0x148 ;
- de 0x150 à 0x455 ;
- de 0x5BD jusqu'à la fin du fichier, soit 0xC3 octets.
Au total, le driver copie 0x50C octets. Aucun débordement n'a lieu.
Le comportement corrompu causé par l'integer overflow :
- les premières étapes sont identiques ;
- copie des dernières données de 0x388 jusqu'à la fin du fichier, soit 0x2F8 octets.
Au total, le driver copie 0x741 octets pour un buffer de 0x680 octets. Objectif atteint !
3.3 Un petit massage
Pourquoi avoir choisi 0x680 ? Nous allons répondre ici à cette question et étudier pourquoi l'exploitation est étroitement liée à la taille de l'exécutable.
Comme le débordement a lieu dans le non-paged pool, il est nécessaire de faire du « spraying » afin de morceler la mémoire dans un état propice à l'exploitation. Le but étant de réussir à allouer le buffer dédié aux données utiles juste avant une autre allocation. Évidemment, cette allocation adjacente devra être sous notre contrôle. Pour ceux qui n'ont jamais expérimenté une exploitation locale, effectuer des allocations dans le noyau à partir d'un programme utilisateur est une tâche aisée. Il suffit de créer des securable objects tels qu'un fichier ou un objet de synchronisation interprocessus. Depuis le « userland », on récupère un HANDLE, mais au niveau du noyau, l'object manager alloue un type d'objet dont la taille lui est propre.
Après quelques expérimentations sur l'OS ciblé, j'ai choisi l'objet FILE. Ce dernier fait 0x150 octets et est alloué dans le non-paged pool. Partant de ce postulat, nous pouvons créer un fichier de 0x690 octets, correspondant à 5 fois celle d'un objet FILE. Cependant, à chaque allocation dans le « pool », une structure POOL_HEADER de 0x10 octets préfixe le buffer. En conséquence, notre fichier exécutable fait 0x680 octets.
Le scénario sera de :
- « sprayer » avec des objets FILE ;
- libérer 5 objets FILE adjacents tout en conservant un alloué juste après ;
- exécuter l'exécutable finement conçu qui provoquera l' « overflow ».
3.4 Débordement dans le « non-paged pool »
Observons ce qui se passe en mémoire avant le débordement.
Nous remarquons que le buffer alloué par Avira possède le tag AV0z. Comme le « pool header » fait 0x10 octets, sa taille est bien de 0x680 octets. Nous devrions donc réécrire le « chunk » situé à l'adresse 0xfffffa8003612da0.
Ci-dessous, le résultat de la même commande juste après le débordement :
Le « chunk » adjacent a été réécrit, notamment le POOL_HEADER et l'OBJECT_HEADER. On fait croire que l'allocation fait 0x150 octets et référence un objet FILE.
Pour confirmer la provenance des données, voici un « dump » mémoire à l'adresse 0xfffffa8003612da0.
fffffa80`03612da0 69 00 15 02 46 69 6c e5-00 00 00 00 00 00 00 00 i...Fil.........
fffffa80`03612da0 69 00 15 02 46 69 6c e5-00 00 00 00 00 00 00 00 i...Fil.........
fffffa80`03612dc0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00 ................
fffffa80`03612dd0 a0 92 19 04 80 fa ff ff-01 00 00 00 00 00 00 00 ................
fffffa80`03612de0 01 00 00 00 00 00 00 00-01 00 00 00 00 00 00 00 ................
fffffa80`03612df0 00 00 00 00 00 00 00 00-01 00 0c 40 00 00 00 00 ...........@....
fffffa80`03612e00 41 42 43 44 45 46 47 48-aa aa aa aa aa aa aa aa ABCDEFGH........
Jetez un œil à l'extrait du programme exécutable du paragraphe 3.2 et vous remarquez que c'est exactement les données localisées de l'offset 0x5C0 à la fin du fichier exécutable.
4. Exploitation
4.1 Scénario
Maintenant que nous débordons avec des données contrôlées, l'étape suivante est de développer un exploit. En effet, qui dira le contraire, un POC est toujours plus sexy. Néanmoins, le but étant de démontrer l'impact de la vulnérabilité, l'élévation de privilèges est réalisée sous Windows 7 x64.
L'exploitation nécessite :
- un exécutable conçu pour déclencher la vulnérabilité ;
- un second exécutable pour « sprayer » le pool, préparer l'exécution de code et lancer l'exécutable corrompu.
Avant de conclure, étudions brièvement la technique 0xbad0b0b0 [4] pour obtenir l'exécution de code.
4.2 Technique 0xbad0b0b0
Cette technique, dite DKOHM, s'appuie sur la manipulation directe d'entête d'objet noyau. Ces objets possèdent un OBJECT_HEADER comme suit :
Le champ TypeIndex nous intéresse particulièrement et représente un index dans le tableau nt!ObTypeIndextable. Celui-ci contenant des pointeurs vers des OBJECT_TYPE.
Ci-dessous, un dump partiel de cette variable globale :
Une de ces adresses mémoires sonne bien avec une élévation de privilèges noyau. En effet, 0x00000000bad0b0b0 est une adresse « userland ». Ainsi, après avoir réécrit le champ TypeIndex à 1 grâce à notre débordement, nous allouerons cette adresse depuis un processus utilisateur et y écrirons des valeurs propices à l'exploitation.
La structure OBJECT_TYPE est définie comme suit :
Portons notre attention sur le champ TypeInfo et sa structure associée :
Quelle chance ! On en déduit qu'en construisant une fausse structure OBJECT_TYPE, mappée à l'adresse 0xbad0b0b0, et en y insérant notre stucture OBJECT_TYPE_INITIALIZER, on est capable de définir l'adresse de ces fonctions. Celles-ci étant appelées depuis le noyau, on exécutera notre code en tant que NT AUTHORITY/SYSTEM.
Par exemple, nous mettons l'adresse du shellcode à la place de l'adresse de SecurityProcedure. Il ne reste plus qu'à appeler NtQuerySecurityObject() sur l'objet dont on a réécrit les entêtes pour exécuter la charge finale.
Conclusion
À nouveau, on se rend compte qu'un produit de sécurité peut induire des vulnérabilités. Loin de moi l'idée de ne pas se protéger, mais en aucun cas avoir une confiance aveugle en ceux-ci. J'ai rapidement souligné la large surface d'attaque d'un antivirus. Et pourtant, qui n'a pas remarqué le nombre d'agents et de solutions logicielles déployées sur son poste d'entreprise pour le sécuriser. Les risques de vulnérabilités sont accrus, un faux sentiment de confiance s'installe, un ordinateur rame...
Avira a corrigé la CVE-2019-18568 en août 2019. Seulement un mois après sa divulgation. Une réactivité que l'on ne peut attribuer à tout le monde. Aurais-je eu simplement de la chance...
Remerciements
Je remercie Flavian Dola (@_ceax) pour sa relecture attentive et ses remarques constructives.
Références
[1] https://connect.ed-diamond.com/MISC/MISC-047/Peut-on-faire-confiance-aux-antivirus
[2] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-setokenisadmin
[3] https://docs.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-sequeryinformationtoken