Skip to content
KitploitKITPLOIT
أدواتالمدونة
إرسال
أدواتالمدونة
إرسال

أدوات الاختراق واختبار الاختراق والأمن السيبراني لترسانتك الأمنية!

Kitploit هو دليل لأدوات الاختراق والأمن السيبراني واختبار الاختراق. اكتشف آخر تحديثات المشاريع للعثور على الثغرات وتحليل الأنظمة وأتمتة الاختبارات وتعزيز أمنك.

··الخلاصات·اتصال·الخصوصية·© 2026 Kitploit

دليل الأدوات

الفئات

عرض جميع الفئات
Loading categories
CVE-2023-6702 — ثغرة RCE من نوع 1day في Chrome Renderer عبر Type Confusion في Async Stack Trace (مشاركة v8ctf) | Kitploit
أدوات/GitHubGitHub/kaist-hacking/cve-2023-6702
تحليل الثغرات الأمنيةالاستغلالاستغلال تطبيقات الويبCTFالتعلم والتعليماستغلال الملفات الثنائية
GitHubkaist-hacking/cve-2023-6702

CVE-2023-6702

ثغرة RCE من نوع 1day في Chrome Renderer عبر Type Confusion في Async Stack Trace (مشاركة v8ctf)

عرض المستودع
8692منذ 2 سنواتتمت المراجعة من قبل Kitploit

الأكثر شعبية

عرض الكل →

اكتشف الأدوات الأكثر استخدامًا من قبل مجتمعنا.

استكشف جميع الأدوات

تصفح مجموعتنا من الأدوات

عرض جميع الأدوات →
مشاركة

ثغرة RCE من نوع 1day في مُقدّم Chrome عبر الخلط بين الأنواع (Type Confusion) في تتبّع حزمة الاستدعاءات غير المتزامنة (CVE-2023-6702)

الملخص

سمحت هذه الثغرة لمهاجم عن بُعد بتنفيذ كود تعسفي داخل عملية عرض Chrome (renderer process).

كان هناك فحص غير كافٍ للنوع في كود معالجة تتبّع حزمة الاستدعاءات غير المتزامنة (async stack trace). يؤدي ذلك إلى خلط بين النوعين FunctionContext وNativeContext، مما يسبّب وصولًا غير قانونيًا إلى قيمة JSGlobalProxy->hash. باستخدام حقن الكومة (heap spraying)، تمكن المهاجم من حقن إطار حزمة استدعاءات غير متزامنة مزيف، وبناء أولية fakeobj. وباستخدام أولية fakeobj، تمكن المهاجم من تحقيق تنفيذ كود تعسفي في عملية عرض Chrome.

يمكنكم الاطلاع على شرائح عرضنا في TyphoonCon 2024.

البائع / المنتج / الإصدار

  • Google Chrome
  • الإصدارات المتأثرة: ما قبل 120.0.6099.109
  • الإصدار المُصحَّح: 120.0.6099.109

الخط الزمني

  • 2020-05-13: إدخال الثغرة - [Promise.any] Implment async stack traces for Promise.any
  • 2023-11-10: الإبلاغ عن الثغرة - Security: V8 Debug check failed: LAST_TYPE >= value
  • 2023-11-15: التصحيح - [promises, async stack traces] Fix the case when the closure has run
  • 2023-12-12: النشرة الأمنية - https://chromereleases.googleblog.com/2023/12/stable-channel-update-for-desktop_12.html
  • 2024-01-12: تقديم v8CTF <-- الوقت الذي عملنا فيه على هذه الثغرة
  • 2024-02-23: الإفصاح عن تقرير الثغرة

الخلفية

تتبّع حزمة الاستدعاءات غير المتزامنة (Async Stack Trace)

تُعد البرمجة غير المتزامنة واحدة من أهم الميزات في JavaScript. في الماضي، كان من الصعب تصحيح أخطاء الكود غير المتزامن باستخدام حزمة الأخطاء لأن الدوال غير المتزامنة لا يتم التقاطها في حزمة الأخطاء. يتم تخزين الدوال غير المتزامنة المعلّقة في قائمة انتظار الاستدعاءات الخاصة بحلقة الأحداث (event loop) وليس في حزمة الاستدعاءات، لذلك لا تحتوي حزمة الأخطاء على الدالة غير المتزامنة. لحل هذه المشكلة، توفر V8 ميزة "async stack trace" (وهي مفعّلة افتراضيًا منذ V8 v7.3) لالتقاط الدوال غير المتزامنة في حزمة الأخطاء. (v8 blog, v8 docs)

دالة Promise.all Resolve Element Closure

"Promise.all Resolve Element Closure" هي دالة مساعدة لحل الوعود المُدخلة في دالة Promise.all. تأخذ دالة Promise.all مصفوفة من الوعود وتُعيد وعدًا يتم حلّه عند حل جميع الوعود المُدخلة. "Promise.all Resolve Element Closure" هي معالج حل (resolve handler) لكل وعد مُدخل في دالة Promise.all. دور الدالة هو حل الوعد المُدخل وتخزين قيمة الإنجاز (fulfillment value) في مصفوفة النتائج.

هناك نقطتان يجب ملاحظتهما حول هذه الدالة:

  1. إنها دالة مدمجة داخلية (intrinsic builtin function) ولا يمكن الوصول إليها مباشرة من كود JavaScript.
  2. يُستخدم سياق (context) الدالة كعلامة للتحقق مما إذا كانت الدالة قد نُفِّذت أم لا. تحتوي على FunctionContext حتى يتم استدعاؤها، ثم تحتوي على NativeContext بعد استدعائها. (كود v8)

الثغرة

فئة الثغرة: خلط بين الأنواع (Type confusion) بين FunctionContext وNativeContext

تفاصيل الثغرة:

يمكن تفعيل الثغرة عن طريق التقاط تتبّع حزمة الاستدعاءات غير المتزامنة مع دالة "Promise.all Resolve Element Closure" المنفّذة بالفعل أو دوال مدمجة داخلية مشابهة. في هذا الاستغلال، استخدمت دالة "Promise.all Resolve Element Closure" كمثال.

عند إلقاء خطأ في كود JavaScript، تلتقط V8 حزمة الأخطاء من المكدس وتُلحق إطارات حزمة الاستدعاءات غير المتزامنة من المهمة الصغيرة الحالية (current microtask) [1].

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

root@kitploit:~
} 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"، فإنه يُلحق إطار المُجمِّع غير المتزامن (async promise combinator frame) بحزمة الأخطاء. ثم ينتقل إلى الوعد التالي عن طريق الوصول إلى function->context->capability->promise.

المشكلة هي أن الدالة تفترض أن دالة "Promise.all Resolve Element Closure" لم يتم تنفيذها بعد. إذا تم تنفيذ دالة "Promise.all Resolve Element Closure" بالفعل، فسيتم تغيير السياق من FunctionContext إلى NativeContext. يؤدي ذلك إلى خلط بين النوعين FunctionContext وNativeContext في دالة CaptureAsyncStackTrace.

إعداد الإثبات المفاهيمي (PoC):

استراتيجية تفعيل الثغرة هي كما يلي:

  1. الحصول على دالة "Promise.all Resolve Element Closure" وهي دالة مدمجة داخلية.
  2. استدعاء دالة "Promise.all Resolve Element Closure" صراحةً لتغيير السياق من FunctionContext إلى NativeContext.
  3. تعيين دالة "Promise.all Resolve Element Closure" كمعالج إنجاز (fulfill handler) لوعد ضمن سلسلة وعود جديدة.
  4. إلقاء خطأ في سلسلة الوعود والتقاط تتبّع حزمة الاستدعاءات غير المتزامنة.

استخدمت نمط الحل المتزامن للوعود الخاص بـ Promise.all للحصول على دالة "Promise.all Resolve Element Closure" على مستوى سكربت JS. استعرت هذا النمط من حالات اختبار test262.

بعد استدعاء الدالة صراحةً، ولتفعيل الثغرة، استخدمت الكود النموذجي في مستند تتبّع حزمة الاستدعاءات غير المتزامنة بدون تكلفة لإعداد سلسلة وعود جديدة وتعيين الدالة المدمجة الداخلية كمعالج إنجاز لأحد الوعود.

أخيرًا، عند إلقاء الخطأ، يتم التقاط تتبّع حزمة الاستدعاءات غير المتزامنة مع دالة "Promise.all Resolve Element Closure" المنفّذة بالفعل كمعالج إنجاز، مما يؤدي إلى خلط بين النوعين FunctionContext وNativeContext.

إليكم كود الإثبات المفاهيمي: poc.js

الاستغلال

(تم تعريف مصطلحات أولية الاستغلال (exploit primitive)، واستراتيجية الاستغلال (exploit strategy)، وتقنية الاستغلال (exploit technique)، وتدفق الاستغلال (exploit flow) هنا.)

أولية الاستغلال: أولية fakeobj

استراتيجية الاستغلال: لبناء أولية fakeobj من ثغرة الخلط بين الأنواع، استخدمت الاستراتيجية التالية:

  1. حقن الكومة (heap spray) بكائنات JSPromise لمطابقة رقم التجزئة العشوائي مع مؤشر كائن JSPromise صالح.
  2. استخدام قيمة التجزئة كمؤشر كائن JSPromise صالح وحقن إطار حزمة الاستدعاءات غير المتزامنة المزيف.
  3. استخدام Error.prepareStackTrace مع طريقة getThis لاسترجاع الكائن المزيف.

تؤدي الثغرة إلى خلط بين النوعين FunctionContext وNativeContext في دالة CaptureAsyncStackTrace. تصل إلى Context->PromiseCapability->JSPromise لبناء إطار حزمة الاستدعاءات غير المتزامنة التالي. عند تفعيل الثغرة، تصل إلى NativeContext->JSGlobalProxy->hash. لاستغلال الثغرة، استخدمت قيمة التجزئة كمؤشر لكائن JSPromise.

يمكننا التحقق من أن قيمة التجزئة تقع في النطاق (0, 0xfffff) من خلال دالة توليد التجزئة التالية:

root@kitploit:~
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;
}
root@kitploit:~
pwndbg> p/x mask
$1 = 0xfffff

قيمة التجزئة موسومة بـ SMI (SMI-tagged)، لذا في الذاكرة سيتم تخزينها بالشكل hash << 1. وبالتالي، ستكون القيمة في الذاكرة ضمن النطاق (0, 0xfffff << 1) وستكون عددًا زوجيًا.

لمطابقة رقم التجزئة العشوائي مع مؤشر كائن JSPromise صالح، لدينا قيدان:

  1. يجب أن يكون عنوان المؤشر المُفسَّر عددًا فرديًا.
  2. يجب حقن الكومة ضمن النطاق (0, 0xfffff << 1).

باتباع هذه القيود، حقنت الكومة بكائنات JSPromise مع إزاحة لليسار بمقدار 8 بتات لجعل العنوان فرديًا، واستخدمت حلقات for صغيرة لتناسب النطاق (0, 0xfffff << 1).

هنا تبدو فرصة مطابقة رقم التجزئة العشوائي مع مؤشر كائن صالح منخفضة إلى حد كبير. لزيادة الموثوقية، استخدمت تقنية iframe. تعمل الصفحات من مواقع مختلفة في عمليات منفصلة بفضل عزل المواقع (site isolation) في Chrome. لذلك، أنشأت iframe بنطاق مختلف، وشغّلت الاستغلال داخل الـ iframe لتجنب تعطّل العملية الرئيسية.

بعد الانتقال إلى الوعد التالي في سلسلة الوعود، يتحقق البرنامج من صحة الوعد ويحاول إلحاق إطار حزمة الاستدعاءات غير المتزامنة وفقًا لنوع الاستدعاء غير المتزامن.

root@kitploit:~
  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). يمكننا التحقق من إطار حزمة الاستدعاءات غير المتزامنة المزيف المُحقون من الطرفية.

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

بعد حقن إطار حزمة الاستدعاءات غير المتزامنة المزيف، استخدمت Error.prepareStackTrace مع طريقة getThis للحصول على receiver لكائن الخطأ (في هذه الحالة، هو JSGeneratorObject). باستخدام receiver، يمكننا استرجاع الكائن المزيف من الكومة (أولية fakeobj).

تدفق الاستغلال: استخدمت تدفق الاستغلال النموذجي لاستغلالات V8.

  1. باستخدام أولية fakeobj، زرعت واسترجعت مصفوفة الوصول خارج الحدود (OOB) المزيفة.
  2. باستخدام مصفوفة OOB المزيفة، بنيت أوليتي caged_read/caged_write.
  3. للوصول إلى تنفيذ الكود عن بُعد (RCE)، رجعت إلى التقنية التي تمت مشاركتها من مسابقة Google CTF 2023. للهروب من صندوق الحماية (sandbox) الخاص بـ V8، أفسدت كائن BytecodeArray لتنفيذ كود بايت (bytecode) تعسفي. باستخدام تعليمات Ldar/Star مع الوصول خارج الحدود، يمكننا القراءة/الكتابة من/إلى المكدس. لتسريب عنوان الأساس للملف التنفيذي لـ Chrome، قرأت عنوان إرجاع من المكدس لتسريب البتات الـ32 السفلية من عنوان الأساس، وقرأت مؤشر كومة libc للحصول على البتات الـ16 العلوية من العنوان. ثم أفسدت مؤشر الإطار (frame pointer) لإعادة توجيه المكدس (stack pivoting) وتنفيذ سلسلة ROP لتحقيق RCE.

إليكم كود الاستغلال الكامل: index.html وexploit.html تم اختباره على Chrome 118.0.5993.70 والذي كان الإصدار المستهدف في v8CTF M118.

الاعتمادات

Haein Lee من KAIST Hacking Lab

تنزيل الأداة