
क्रोम रेंडरर 1day RCE एसिंक स्टैक ट्रेस में टाइप कन्फ्यूज़न के माध्यम से (v8ctf सबमिशन)
यह भेद्यता एक दूरस्थ हमलावर को क्रोम रेंडरर प्रक्रिया के अंदर मनमाना कोड निष्पादित करने की अनुमति देती है।
एशिंक स्टैक ट्रेस हैंडलिंग कोड में अपर्याप्त प्रकार जाँच थी।
यह FunctionContext और NativeContext के बीच टाइप कन्फ्यूज़न की ओर ले जाता है, जिससे JSGlobalProxy->hash मान तक अवैध पहुँच होती है।
हीप स्प्रेइंग के साथ, हमलावर एक नकली एशिंक स्टैक फ्रेम इंजेक्ट करने और fakeobj प्रिमिटिव का निर्माण करने में सक्षम था।
fakeobj प्रिमिटिव का उपयोग करके, हमलावर क्रोम रेंडरर प्रक्रिया में मनमाना कोड निष्पादन प्राप्त करने में सक्षम था।
आप हमारे TyphoonCon 2024 स्लाइड्स देख सकते हैं।
एशिंक्रोनस जावास्क्रिप्ट में सबसे महत्वपूर्ण विशेषताओं में से एक है। अतीत में, एशिंक्रोनस कोड को त्रुटि स्टैक के साथ डीबग करना कठिन था क्योंकि एशिंक फ़ंक्शन त्रुटि स्टैक में कैप्चर नहीं किए जाते थे। निलंबित एशिंक फ़ंक्शन कॉल स्टैक में नहीं बल्कि इवेंट लूप के कॉलबैक कतार में संग्रहीत होते हैं, इसलिए त्रुटि स्टैक में एशिंक फ़ंक्शन शामिल नहीं होता है। इस समस्या को हल करने के लिए, V8 त्रुटि स्टैक में एशिंक फ़ंक्शन को कैप्चर करने के लिए "async stack trace" सुविधा (V8 v7.3 से डिफ़ॉल्ट रूप से) प्रदान करता है। (v8 blog, v8 docs)
"Promise.all Resolve Element Closure" Promise.all फ़ंक्शन में इनपुट प्रॉमिसेज़ को हल करने के लिए एक सहायक फ़ंक्शन है।
Promise.all फ़ंक्शन प्रॉमिसेज़ की एक सरणी लेता है और एक प्रॉमिस लौटाता है जो सभी इनपुट प्रॉमिसेज़ हल होने पर हल होता है।
"Promise.all Resolve Element Closure" Promise.all फ़ंक्शन में प्रत्येक इनपुट प्रॉमिस का एक रिज़ॉल्व हैंडलर है।
फ़ंक्शन की भूमिका इनपुट प्रॉमिस को हल करना और परिणाम सरणी में पूर्ति मान को संग्रहीत करना है।
फ़ंक्शन के बारे में 2 बिंदु ध्यान देने योग्य हैं:
FunctionContext होता है, और इसे कॉल करने के बाद इसमें NativeContext होता है। (v8 कोड)बग वर्ग: FunctionContext और NativeContext के बीच टाइप कन्फ्यूज़न
भेद्यता विवरण:
भेद्यता को पहले से निष्पादित "Promise.all Resolve Element Closure" फ़ंक्शन या समान आंतरिक बिल्ट-इन फ़ंक्शन के साथ एक एशिंक स्टैक ट्रेस कैप्चर करके ट्रिगर किया जा सकता है। इस एक्सप्लॉइट में, मैंने "Promise.all Resolve Element Closure" फ़ंक्शन का उपयोग उदाहरण के रूप में किया।
जब जावास्क्रिप्ट कोड में कोई त्रुटि उत्पन्न होती है, V8 स्टैक से त्रुटि स्टैक कैप्चर करता है और वर्तमान माइक्रोटास्क से एशिंक स्टैक फ्रेम जोड़ता है [1]।
CallSiteBuilder builder(isolate, mode, limit, caller);
VisitStack(isolate, &builder);
// If --async-stack-traces are enabled and the "current microtask" is a
// PromiseReactionJobTask, we try to enrich the stack trace with async
// frames.
if (v8_flags.async_stack_traces) {
CaptureAsyncStackTrace(isolate, &builder);
}
CaptureAsyncStackTrace फ़ंक्शन [2] प्रॉमिस श्रृंखला को देखता है और एशिंक कॉल प्रकार (जैसे, await, Promise.all, Promise.any) के अनुसार एशिंक स्टैक फ्रेम जोड़ता है।
नीचे CaptureAsyncStackTrace फ़ंक्शन का स्निपेट है जो Promise.all मामले को संभालता है:
} else if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kPromiseAllResolveElementClosure)) {
Handle<JSFunction> function(JSFunction::cast(reaction->fulfill_handler()),
isolate);
Handle<Context> context(function->context(), isolate);
Handle<JSFunction> combinator(context->native_context()->promise_all(),
isolate);
builder->AppendPromiseCombinatorFrame(function, combinator);
// Now peak into the Promise.all() resolve element context to
// find the promise capability that's being resolved when all
// the concurrent promises resolve.
int const index =
PromiseBuiltins::kPromiseAllResolveElementCapabilitySlot;
Handle<PromiseCapability> capability(
PromiseCapability::cast(context->get(index)), isolate);
if (!IsJSPromise(capability->promise())) return;
promise = handle(JSPromise::cast(capability->promise()), isolate);
} else if (
प्रॉमिस श्रृंखला को देखते समय, यदि reaction->fulfill_handler "Promise.all Resolve Element Closure" बिल्ट-इन फ़ंक्शन है, तो यह त्रुटि स्टैक में एशिंक प्रॉमिस कॉम्बिनेटर फ्रेम जोड़ता है।
फिर, यह function->context->capability->promise तक पहुँचकर अगले प्रॉमिस पर जाता है।
समस्या यह है कि फ़ंक्शन मान लेता है कि "Promise.all Resolve Element Closure" फ़ंक्शन अभी तक निष्पादित नहीं हुआ है।
यदि "Promise.all Resolve Element Closure" फ़ंक्शन पहले ही निष्पादित हो चुका है, तो संदर्भ FunctionContext से NativeContext में बदल जाता है।
यह CaptureAsyncStackTrace फ़ंक्शन में FunctionContext और NativeContext के बीच टाइप कन्फ्यूज़न की ओर ले जाता है।
PoC बनाना:
भेद्यता को ट्रिगर करने की रणनीति इस प्रकार है:
FunctionContext से NativeContext में बदलने के लिए "Promise.all Resolve Element Closure" फ़ंक्शन को स्पष्ट रूप से कॉल करें।मैंने JS स्क्रिप्ट स्तर पर "Promise.all Resolve Element Closure" फ़ंक्शन प्राप्त करने के लिए Promise.all के लिए सिंक्रोनस प्रॉमिस रिज़ॉल्विंग पैटर्न का उपयोग किया।
मैंने test262 परीक्षण मामलों से पैटर्न उधार लिया।
फ़ंक्शन को स्पष्ट रूप से कॉल करने के बाद, भेद्यता को ट्रिगर करने के लिए, मैंने एक नई प्रॉमिस श्रृंखला तैयार करने और आंतरिक बिल्ट-इन फ़ंक्शन को एक प्रॉमिस के फ़ुलफ़िल हैंडलर के रूप में सेट करने के लिए शून्य-लागत एशिंक स्टैक ट्रेस दस्तावेज़ में नमूना कोड का उपयोग किया।
अंत में, जब त्रुटि उत्पन्न होती है, तो एशिंक स्टैक ट्रेस पहले से निष्पादित "Promise.all Resolve Element Closure" फ़ंक्शन के साथ फ़ुलफ़िल हैंडलर के रूप में कैप्चर किया जाता है, जिससे FunctionContext और NativeContext के बीच टाइप कन्फ्यूज़न होता है।
यहाँ PoC कोड है: poc.js
(एक्सप्लॉइट प्रिमिटिव, एक्सप्लॉइट रणनीति, एक्सप्लॉइट तकनीक और एक्सप्लॉइट प्रवाह की शर्तें यहाँ परिभाषित हैं।)
एक्सप्लॉइट प्रिमिटिव: fakeobj प्रिमिटिव
एक्सप्लॉइट रणनीति: टाइप कन्फ्यूज़न बग से fakeobj प्रिमिटिव बनाने के लिए, मैंने निम्नलिखित रणनीति का उपयोग किया:
getThis विधि के साथ Error.prepareStackTrace का उपयोग करें।बग CaptureAsyncStackTrace फ़ंक्शन में FunctionContext और NativeContext के बीच टाइप कन्फ्यूज़न की ओर ले जाता है।
यह अगले एशिंक स्टैक फ्रेम के निर्माण के लिए Context->PromiseCapability->JSPromise तक पहुँचता है।
जब बग ट्रिगर होता है, तो यह NativeContext->JSGlobalProxy->hash तक पहुँचता है।
बग का शोषण करने के लिए, मैंने हैश मान को JSPromise ऑब्जेक्ट पॉइंटर के रूप में उपयोग किया।
हम निम्नलिखित हैश उत्पन्न करने वाले फ़ंक्शन से जाँच सकते हैं कि हैश मान की सीमा (0, 0xfffff) है:
int Isolate::GenerateIdentityHash(uint32_t mask) {
int hash;
int attempts = 0;
do {
hash = random_number_generator()->NextInt() & mask;
} while (hash == 0 && attempts++ < 30);
return hash != 0 ? hash : 1;
}
pwndbg> p/x mask
$1 = 0xfffff
हैश मान SMI-टैग किया जाता है, इसलिए मेमोरी में, इसे hash << 1 के रूप में संग्रहीत किया जाएगा।
इसलिए, मेमोरी में मान सम संख्या के साथ (0, 0xfffff << 1) की सीमा में होगा।
यादृच्छिक हैश संख्या को एक वैध JSPromise ऑब्जेक्ट पॉइंटर से मिलाने के लिए, हमें 2 बाधाएँ मिलीं:
बाधाओं का पालन करते हुए, मैंने पते को विषम बनाने के लिए JSPromise ऑब्जेक्ट्स को 8 बिट्स शिफ्ट-लेफ्ट करके हीप स्प्रे किया, और सीमा (0, 0xfffff << 1) में फिट होने के लिए छोटे फॉर-लूप का उपयोग किया।
यहाँ यादृच्छिक हैश संख्या को एक वैध ऑब्जेक्ट पॉइंटर से मिलाना काफी कम संभावना लगता है। विश्वसनीयता बढ़ाने के लिए, मैंने iframe तकनीक का उपयोग किया। क्रोम में साइट आइसोलेशन के कारण विभिन्न वेबसाइटों के पेज विभिन्न प्रक्रियाओं में चल रहे हैं। इसलिए, मैंने एक अलग डोमेन के साथ एक iframe बनाया, और मुख्य प्रक्रिया की क्रैश से बचने के लिए iframe में एक्सप्लॉइट चलाया।
प्रॉमिस श्रृंखला में अगले प्रॉमिस पर जाने के बाद, प्रोग्राम प्रॉमिस की वैधता की जाँच करता है और एशिंक कॉल प्रकार के अनुसार एशिंक स्टैक फ्रेम जोड़ने का प्रयास करता है।
while (!builder->Full()) {
// Check that the {promise} is not settled.
if (promise->status() != Promise::kPending) return;
// Check that we have exactly one PromiseReaction on the {promise}.
if (!IsPromiseReaction(promise->reactions())) return;
Handle<PromiseReaction> reaction(
PromiseReaction::cast(promise->reactions()), isolate);
if (!IsSmi(reaction->next())) return;
// Check if the {reaction} has one of the known async function or
// async generator continuations as its fulfill handler.
if (IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncFunctionAwaitResolveClosure) ||
IsBuiltinFunction(isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorAwaitResolveClosure) ||
IsBuiltinFunction(
isolate, reaction->fulfill_handler(),
Builtin::kAsyncGeneratorYieldWithAwaitResolveClosure)) {
// Now peek into the handlers' AwaitContext to get to
// the JSGeneratorObject for the async function.
Handle<Context> context(
JSFunction::cast(reaction->fulfill_handler())->context(), isolate);
Handle<JSGeneratorObject> generator_object(
JSGeneratorObject::cast(context->extension()), isolate);
CHECK(generator_object->is_suspended());
// Append async frame corresponding to the {generator_object}.
builder->AppendAsyncFrame(generator_object);
हमने kAsyncFunctionAwaitResolveClosure मामला चुना क्योंकि AppendAsyncFrame फ़ंक्शन का पैरामीटर, generator_object, पूरी तरह से नियंत्रणीय है।
स्थितियों को पारित करने के लिए PromiseReaction, Function, Context, JSGeneratorObject जैसी उपयुक्त नकली वस्तुएँ स्थापित करके, हम builder->AppendAsyncFrame(generator_object) को कॉल करके अपना नकली एशिंक फ्रेम इंजेक्ट कर सकते हैं।
हम टर्मिनल से इंजेक्ट किए गए नकली एशिंक फ्रेम की जाँच कर सकते हैं।
Error: Let's have a look...
at bar (../../../../fake_frame.js:168:15)
at async foo (../../../../fake_frame.js:163:9)
at async Promise.all (index 0)
at async Array.sloppy_func (../../../../fake_frame.js:1:1)
यहाँ fake_frame.js कोड है।
नकली एशिंक फ्रेम इंजेक्ट करने के बाद, मैंने त्रुटि ऑब्जेक्ट का receiver प्राप्त करने के लिए getThis विधि के साथ Error.prepareStackTrace का उपयोग किया (इस मामले में, यह JSGeneratorObject है)।
receiver के साथ, हम हीप से नकली ऑब्जेक्ट प्राप्त कर सकते हैं (fakeobj प्रिमिटिव)।
एक्सप्लॉइट प्रवाह: मैंने V8 एक्सप्लॉइट्स के लिए विशिष्ट शोषण प्रवाह का उपयोग किया।
यहाँ पूरा एक्सप्लॉइट कोड है: index.html और exploit.html इसे क्रोम 118.0.5993.70 पर परीक्षण किया गया है जो v8CTF M118 का लक्ष्य संस्करण था।
कैस्ट हैकिंग लैब के हेन ली