Antivirus Avira (CVE-2019-18568) : quand l'authentification d'un PE mène à une LPE

Magazine
Marque
MISC
Numéro
108
Mois de parution
mars 2020
Spécialité(s)


Résumé

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.


Body

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).

Avira CVE-2019-XXXX-integer overflow

Fig. 1 : Localisation de l'« integer overflow ».

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 :

# dwOffsetEnd = SignatureStructureSize + (FileSize – OffsetOfSignatureFromEndOfFile) - 1

À 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.

Avira CVE-2019-XXXX-loop copy useful data 2

Fig. 2 : Lecture des données utiles au calcul de la signature.

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.

Avira CVE-2019-XXXX-loop copy useful data ReadFile overflow 0x388

Fig. 3 : Lecture du dernier bloc de données utiles.

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 :

Checksum PE
ffffa80`0465fb50 0000000000000108
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 :

Offset    0 1 2 3 4 5 6 7 8 9 A B C D E F   Ascii
00000000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ.#...#...ÿÿ..
[......]
00000100 00 20 00 00 00 02 00 00 00 00 00 00 02 00 00 04 .....#......#..#
00000110 00 00 10 00 00 10 00 00 00 00 10 00 00 10 00 00 ..#..#....#..#..
00000120 00 00 00 00 10 00 00 00 00 00 00 00 00 00 00 00 ....#...........
00000130 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000140 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000150 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
[......]
00000440 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
00000450 00 00 00 00 00 41 56 43 53 34 46 33 41 34 32 30 .....AVCS4F3A420
00000460 30 43 33 37 4F 03 00 00 00 00 03 34 FF FF FF 01 0C37O#....#4ÿÿÿ#
00000470 00 00 00 00 00 00 00 00 00 00 00 00 03 03 3B 00 ............##;.
00000480 00 00 03 00 01 00 00 50 00 00 00 00 00 00 00 00 ..#.#..P........
00000490 82 F0 80 9C ED 1F B2 F2 68 90 C2 86 CC 51 E9 73 ‚ð€œí#²òhÂ†ÌQés
000004A0 6A 5F F7 F4 78 78 E2 1B 29 1C 78 E6 80 C9 A3 4B j_÷ôxxâ#)#xæ€É£K
[......]
00000570 04 58 2E 92 C7 4E 48 5A 0F C3 48 F8 8B 76 AA A8 #X.’ÇNHZ#ÃHø‹vª¨
00000580 D9 6E B5 AF C2 5D 3C 15 8F 91 B5 92 BF CB E7 B8 Ùnµ¯Â]<#‘µ’¿Ëç¸
00000590 00 00 00 00 00 00 00 00 00 00 00 00 52 01 00 00 ............R#..
000005A0 03 06 00 00 00 03 42 36 32 46 33 41 42 30 31 33 ##...#B62F3AB013
000005B0 32 46 41 56 43 53 45 00 00 00 00 00 00 00 00 00 2FAVCSE.........
000005C0 69 00 15 02 46 69 6C E5 00 00 00 00 00 00 00 00 i.##Filå........
000005D0 00 04 00 00 80 01 00 00 00 00 00 00 00 00 00 00 .#..€#..........
000005E0 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
000005F0 A0 92 19 04 80 FA FF FF 01 00 00 00 00 00 00 00  ’##€úÿÿ#.......
00000600 01 00 00 00 00 00 00 00 01 00 00 00 00 00 00 00 #.......#.......
00000610 00 00 00 00 00 00 00 00 01 00 0C 40 00 00 00 00 ........#.@....
00000620 41 42 43 44 45 46 47 48 AA AA AA AA AA AA AA AA ABCDEFGHªªªªªªªª
[......]
00000680 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................

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.

1: kd> !pool fffffa8003612720
Pool page fffffa8003612720 region is Nonpaged pool 
fffffa8003612000 size:  150 previous size: 0    File 
[...]
*fffffa8003612710 size:  690 previous size:  20  *AV0z
      Owning component : Unknown (update pooltag.txt) 
fffffa8003612da0 size:  160 previous size:  660 Ntfx

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 :

1: kd> !pool fffffa8003612720
Pool page fffffa8003612720 region is Nonpaged pool
fffffa8003612000 size: 150 previous size:      0   (Allocated) File (Protected)
[...]
*fffffa8003612710 size: 690 previous size:   20    (Allocated) *AV0z
              Owning component : Unknown (update pooltag.txt)
fffffa8003612da0 size: 150 previous size: 690 (Allocated) File (Protected)

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.

1: kd> db fffffa8003612da0

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 :

1: kd> dt nt!_OBJECT_HEADER
   +0x000 PointerCount    : Int8B
   +0x008 HandleCount     : Int8B
   +0x008 NextToFree      : Ptr64 Void
   +0x010 Lock            : _EX_PUSH_LOCK
   +0x018 TypeIndex      : Uchar

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 :

1: kd> dq nt!ObTypeIndexTable L40
fffff800`02c76100 00000000`00000000 00000000`bad0b0b0
fffff800`02c76110 fffffa80`01846f30 fffffa80`01846de0

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 :

1: kd> dt nt!_OBJECT_TYPE
   +0x000 TypeList    : _LIST_ENTRY
   [...]
   +0x040 TypeInfo    : _OBJECT_TYPE_INITIALIZER

Portons notre attention sur le champ TypeInfo et sa structure associée :

1: kd> dq nt!_OBJECT_TYPE_INITIALIZER
   +0x000 Length                :    Uint2B
   […]
   +0x030 DumpProcedure         :    Ptr64 void
   +0x038 OpenProcedure         :    Ptr64 long
   +0x040 CloseProcedure        :    Ptr64 void
   +0x048 DeleteProcedure       :    Ptr64 void
   +0x050 ParseProcedure        :    Ptr64 long
   +0x058 SecurityProcedure     :    Ptr64 long
   +0x060 QueryNameProcedure    :    Ptr64 long
   +0x068 OkayToCloseProcedure  :    Ptr64 unsigned char

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

[4] https://www.blackhat.com/docs/us-14/materials/us-14-Tarakanov-Data-Only-Pwning-Microsoft-Windows-Kernel-Exploitation-Of-Kernel-Pool-Overflows-On-Microsoft-Windows-8.1.pdf



Article rédigé par

Les derniers articles Premiums

Les derniers articles Premium

Du graphisme dans un terminal ? Oui, avec sixel

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

On le voit de plus en plus, les outils en ligne de commandes s'étoffent peu à peu d'éléments graphiques sous la forme d'émojis UTF8. Plus qu'une simple décoration, cette pointe de « graphisme » dans un monde de texte apporte réellement un plus en termes d'expérience utilisateur et véhicule, de façon condensée, des informations utiles. Pour autant, cette façon de sortir du cadre purement textuel d'un terminal n'est en rien une nouveauté. Pour preuve, fin des années 80 DEC introduisait le VT340 supportant des graphismes en couleurs, et cette compatibilité existe toujours...

Game & Watch : utilisons judicieusement la mémoire

Magazine
Marque
Contenu Premium
Spécialité(s)
Résumé

Au terme de l'article précédent [1] concernant la transformation de la console Nintendo Game & Watch en plateforme de développement, nous nous sommes heurtés à un problème : les 128 Ko de flash intégrés au microcontrôleur STM32 sont une ressource précieuse, car en quantité réduite. Mais heureusement pour nous, le STM32H7B0 dispose d'une mémoire vive de taille conséquente (~ 1,2 Mo) et se trouve être connecté à une flash externe QSPI offrant autant d'espace. Pour pouvoir développer des codes plus étoffés, nous devons apprendre à utiliser ces deux ressources.

Les listes de lecture

11 article(s) - ajoutée le 01/07/2020
Clé de voûte d'une infrastructure Windows, Active Directory est l'une des cibles les plus appréciées des attaquants. Les articles regroupés dans cette liste vous permettront de découvrir l'état de la menace, les attaques et, bien sûr, les contre-mesures.
8 article(s) - ajoutée le 13/10/2020
Découvrez les méthodologies d'analyse de la sécurité des terminaux mobiles au travers d'exemples concrets sur Android et iOS.
10 article(s) - ajoutée le 13/10/2020
Vous retrouverez ici un ensemble d'articles sur les usages contemporains de la cryptographie (whitebox, courbes elliptiques, embarqué, post-quantique), qu'il s'agisse de rechercher des vulnérabilités ou simplement comprendre les fondamentaux du domaine.
Voir les 55 listes de lecture

Abonnez-vous maintenant

et profitez de tous les contenus en illimité

Je découvre les offres

Déjà abonné ? Connectez-vous