
Preuve de concept pour CVE-2024-54756, une vulnérabilité que j'ai trouvée dans le moteur de script ZScript de GZDoom.
Une preuve de concept pour une vulnérabilité d'exécution de code arbitraire que j'ai trouvée dans la fonctionnalité ZScript de GZDoom (https://github.com/zdoom/gzdoom). Un attaquant peut partager un fichier PK3 contenant un fichier source ZScript malveillant et accéder à l'ordinateur de la victime.
Un grand merci à Rachael et Agent Ash de l'équipe de développement de GZDoom pour leurs réponses rapides, ainsi qu'à eux et aux autres développeurs de GZDoom pour avoir rapidement résolu ce problème !
Confirmé pour fonctionner avec les versions 4.13.0 et 4.13.1, et cela fonctionne probablement aussi pour les versions antérieures. Méfiez-vous de quiconque vous conseille de rétrograder vers la version 4.13.1 ou inférieure pour pouvoir jouer à leur WAD.
Cette preuve de concept ne fonctionne que sous Linux, mais la vulnérabilité existe probablement aussi sous Windows. Non testé sur ZDoom ou LZDoom, mais la vulnérabilité pourrait également y exister.
La vulnérabilité a été divulguée aux développeurs avant la publication de cette preuve de concept et ne devrait plus être présente dans la version 4.13.2. À ma connaissance, cette version n'inclut aucun changement pratique qui casse la compatibilité.
Cette preuve de concept est réalisée et publiée à des fins éducatives, afin que les développeurs de moteurs de jeu/script puissent comprendre comment les vulnérabilités peuvent survenir et que les joueurs puissent comprendre à quoi peut ressembler un mod de jeu malveillant. Je ne suis pas responsable de toute utilisation abusive de cette preuve de concept. Veuillez ne pas l'utiliser pour compromettre les PC de vos camarades joueurs ; c'est illégal (vous n'avez pas besoin que je vous le dise), et c'est particulièrement un sale coup de prendre le contrôle de l'ordinateur de quelqu'un via un jeu vidéo.
Pour utiliser cette preuve de concept, téléchargez ce dépôt et créez un fichier PK3 (qui est en réalité un fichier zip avec l'extension .pk3) contenant zscript.zs et MAPINFO :
git clone https://github.com/Chainmanner/GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
cd GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC
zip PoC.pk3 zscript.zs MAPINFO
La charge utile par défaut est d'exécuter un reverse shell vers localhost sur le port 1337. Démarrez l'écouteur :
nc -nvlp 1337
Exécutez la preuve de concept comme suit :
gzdoom -iwad <your-doom-or-freedoom-wad> -file PoC.pk3
Si cela a fonctionné, vous devriez maintenant avoir un reverse shell vers vous-même.
Cette preuve de concept est uniquement pour Linux. Il se peut qu'elle ne fonctionne pas du premier coup ; réessayez jusqu'à ce que cela fonctionne.
NOTE : Ceci est ma première analyse d'exploit et je travaille encore sur ma compétence pour faire des analyses de bas niveau. De plus, j'ai fait une grande partie de mon débogage avec GDB, et malheureusement je n'ai pas eu la bonne idée de sauvegarder des vidages mémoire pour mieux illustrer mon explication. Désolé ! Ma prochaine analyse sera meilleure, promis.
GZDoom est un port source de Doom conçu pour les performances et l'extensibilité. Grâce à ses fonctionnalités puissantes, de nombreux WADs, mods et même des conversions totales commerciales ont été réalisés. Malheureusement, là où il y a de la complexité, il y a des opportunités pour des vulnérabilités, et dans ce cas, deux étaient présentes dans le moteur de script ZScript, permettant une chaîne d'exploitation complète.
Cette attaque contourne l'ASLR et évite le besoin de contourner les canaris de pile. Je ne pense pas que la CFI de Clang ou les piles fantômes auraient aidé ici.
La première et plus importante vulnérabilité résidait dans la gestion des très grands tableaux. Si vous allouez un tableau suffisamment petit, la région mémoire allouée a tendance à être remplie de zéros et est correctement séparée des autres objets ; aucune information ne peut être obtenue en lisant de la mémoire non initialisée, et aucun objet ne chevauche le tableau. Cependant, si vous allouez un énorme tableau - disons, 1073741823 mots de 32 bits ou plus - vous pourrez lire et écrire jusqu'à 4 Gio de mémoire potentiellement non initialisée à partir du point de départ du tableau, permettant à l'attaquant de modifier directement d'autres objets et de contourner l'ASLR en trouvant des adresses ayant des décalages connus. De plus, tout autre tableau créé après ce point chevauchera le grand tableau.
La deuxième vulnérabilité concernait les permissions de la carte mémoire. Pour de meilleures performances, le code ZScript est compilé JIT en bytecode x86 ou x86-64 chaque fois que possible. Pour ce faire, le code doit être écrit dans une région mémoire, et cette région mémoire doit être exécutable. Cependant, la règle W^X stipule qu'une région doit être soit accessible en écriture, soit exécutable, mais pas les deux. Si les deux sont appliquées en même temps (au lieu de rendre la région accessible en écriture, écrire le code, puis la rendre exécutable et non accessible en écriture), alors un attaquant disposant d'une primitive d'écriture arbitraire pourra l'escalader en exécution de code arbitraire ; il peut écrire du shellcode et y sauter en modifiant par exemple l'adresse de retour sur la pile (en supposant que l'attaquant n'a pas de primitive d'exécution arbitraire). Si vous regardez les mappages mémoire de GZDoom lorsqu'il tourne, vous pouvez voir plusieurs régions RWX :
7fcd19700000-7fcd19800000 rwxp 00000000 00:00 0
7fcd1a100000-7fcd1a200000 rwxp 00000000 00:00 0
7fcd1eb00000-7fcd1ec00000 rwxp 00000000 00:00 0
Donc si des primitives d'écriture arbitraire et d'exécution arbitraire sont disponibles, et que l'attaquant sait où se trouve une région RWX, il peut écrire du shellcode arbitraire et l'exécuter. Rendre ces régions RW- lors de l'écriture du code compilé JIT puis R-X lorsqu'il est prêt à être exécuté arrêterait cette preuve de concept, mais cela n'empêcherait pas un attaquant d'obtenir une exécution de code en modifiant par exemple des données sur la pile (ROP) ou le tas.
De plus, il existe un gadget utile. Rappelez-vous comment, lors de l'allocation d'un grand tableau, tout autre tableau créé après lui le chevauchera ? Cela inclut les tableaux de pointeurs d'objets. Tout comme les objets C++, les objets ZScript peuvent contenir des variables et des pointeurs de fonction. Supposons que nous ayons cet objet :
class WeirdObject
{
uint one;
uint two;
uint three;
uint four;
Function<clearscope void()> funcptr;
}
Si nous créons un tableau contenant un pointeur vers une instance de WeirdObject, alors l'attaquant peut modifier le pointeur pour le faire pointer où il veut en utilisant le grand tableau et modifier les données pointées en accédant aux champs de l'objet, nous donnant une primitive de lecture/écriture arbitraire allant au-delà du tas. Les pointeurs dans ZScript sont vérifiés pour s'assurer qu'ils ne sont pas nuls, mais pas pour s'assurer qu'ils sont sains.
La présence d'un pointeur de fonction nous donne également une primitive d'exécution arbitraire ; cela est cependant un peu moins direct, nécessitant la création d'un faux VMFunction pour satisfaire la machine virtuelle. Dès qu'un appel à une fonction ZScript est introduit dans le code d'exploitation, ce code n'est plus compilé JIT. Cela fonctionne toujours, mais cela devient un peu plus compliqué à déboguer et à exploiter. Il pourrait y avoir une meilleure façon de faire cette partie, mais je n'ai pas assez étudié les rouages internes de GZDoom pour le savoir.
Une chose à noter : WeirdObject a des variables membres héritées, donc le premier membre commence à l'offset 0x28.