
ثغرة RCE من نوع 1day في Chrome Renderer عبر Type Confusion في Async Stack Trace (مشاركة v8ctf)
سمحت هذه الثغرة لمهاجم عن بُعد بتنفيذ كود تعسفي داخل عملية عرض Chrome (renderer process).
كان هناك فحص غير كافٍ للنوع في كود معالجة تتبّع حزمة الاستدعاءات غير المتزامنة (async stack trace).
يؤدي ذلك إلى خلط بين النوعين FunctionContext وNativeContext، مما يسبّب وصولًا غير قانونيًا إلى قيمة JSGlobalProxy->hash.
باستخدام حقن الكومة (heap spraying)، تمكن المهاجم من حقن إطار حزمة استدعاءات غير متزامنة مزيف، وبناء أولية fakeobj.
وباستخدام أولية fakeobj، تمكن المهاجم من تحقيق تنفيذ كود تعسفي في عملية عرض Chrome.
يمكنكم الاطلاع على شرائح عرضنا في TyphoonCon 2024.
تُعد البرمجة غير المتزامنة واحدة من أهم الميزات في JavaScript. في الماضي، كان من الصعب تصحيح أخطاء الكود غير المتزامن باستخدام حزمة الأخطاء لأن الدوال غير المتزامنة لا يتم التقاطها في حزمة الأخطاء. يتم تخزين الدوال غير المتزامنة المعلّقة في قائمة انتظار الاستدعاءات الخاصة بحلقة الأحداث (event loop) وليس في حزمة الاستدعاءات، لذلك لا تحتوي حزمة الأخطاء على الدالة غير المتزامنة. لحل هذه المشكلة، توفر 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" هي معالج حل (resolve handler) لكل وعد مُدخل في دالة Promise.all.
دور الدالة هو حل الوعد المُدخل وتخزين قيمة الإنجاز (fulfillment value) في مصفوفة النتائج.
هناك نقطتان يجب ملاحظتهما حول هذه الدالة:
FunctionContext حتى يتم استدعاؤها، ثم تحتوي على NativeContext بعد استدعائها. (كود v8)فئة الثغرة: خلط بين الأنواع (Type confusion) بين FunctionContext وNativeContext
تفاصيل الثغرة:
يمكن تفعيل الثغرة عن طريق التقاط تتبّع حزمة الاستدعاءات غير المتزامنة مع دالة "Promise.all Resolve Element Closure" المنفّذة بالفعل أو دوال مدمجة داخلية مشابهة. في هذا الاستغلال، استخدمت دالة "Promise.all Resolve Element Closure" كمثال.
عند إلقاء خطأ في كود JavaScript، تلتقط V8 حزمة الأخطاء من المكدس وتُلحق إطارات حزمة الاستدعاءات غير المتزامنة من المهمة الصغيرة الحالية (current microtask) [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"، فإنه يُلحق إطار المُجمِّع غير المتزامن (async promise combinator frame) بحزمة الأخطاء.
ثم ينتقل إلى الوعد التالي عن طريق الوصول إلى function->context->capability->promise.
المشكلة هي أن الدالة تفترض أن دالة "Promise.all Resolve Element Closure" لم يتم تنفيذها بعد.
إذا تم تنفيذ دالة "Promise.all Resolve Element Closure" بالفعل، فسيتم تغيير السياق من FunctionContext إلى NativeContext.
يؤدي ذلك إلى خلط بين النوعين FunctionContext وNativeContext في دالة CaptureAsyncStackTrace.
إعداد الإثبات المفاهيمي (PoC):
استراتيجية تفعيل الثغرة هي كما يلي:
FunctionContext إلى NativeContext.استخدمت نمط الحل المتزامن للوعود الخاص بـ Promise.all للحصول على دالة "Promise.all Resolve Element Closure" على مستوى سكربت JS.
استعرت هذا النمط من حالات اختبار test262.
بعد استدعاء الدالة صراحةً، ولتفعيل الثغرة، استخدمت الكود النموذجي في [مستند تتبّع حزمة الاستدعاءات غير المتزامنة بدون تكلفة][v8 docs] لإعداد سلسلة وعود جديدة وتعيين الدالة المدمجة الداخلية كمعالج إنجاز لأحد الوعود.
أخيرًا، عند إلقاء الخطأ، يتم التقاط تتبّع حزمة الاستدعاءات غير المتزامنة مع دالة "Promise.all Resolve Element Closure" المنفّذة بالفعل كمعالج إنجاز، مما يؤدي إلى خلط بين النوعين FunctionContext وNativeContext.
إليكم كود الإثبات المفاهيمي: poc.js
(تم تعريف مصطلحات أولية الاستغلال (exploit primitive)، واستراتيجية الاستغلال (exploit strategy)، وتقنية الاستغلال (exploit technique)، وتدفق الاستغلال (exploit flow) هنا.)
أولية الاستغلال: أولية fakeobj
استراتيجية الاستغلال: لبناء أولية fakeobj من ثغرة الخلط بين الأنواع، استخدمت الاستراتيجية التالية:
Error.prepareStackTrace مع طريقة getThis لاسترجاع الكائن المزيف.