
PoC لتشغيل CVE-2023-5217 من واجهة WebCodecs أو MediaRecorder في المتصفح.
CVE-2023-5217 هي ثغرة في libvpx تم استغلالها في البرية وتم العثور عليها بواسطة Clément Lecigne من فريق تحليل التهديدات في Google لتستهدف Chrome.
يُظهر هذا المستودع كيفية تشغيل CVE-2023-5217 في المتصفح باستخدام واجهات برمجة التطبيقات WebCodecs و MediaRecorder. تسمح CVE-2023-5217 بتجاوز سعة المخزن المؤقت للكومة بطول تجاوز مسيطر عليه وكتابة فوقية لقيمة صغيرة مكررة من 4 بايتات. ليس معروفًا حاليًا كيف تم استغلال CVE-2023-5217 في البرية.
في وقت الإفصاح العام، كان هناك تصحيحان في libvpx وواحد في Chromium عالجوا CVE-2023-5217. تضمنت تصحيحات libvpx تعطيل تغييرات عدد خيوط VP8 و اختبار للترميز متعدد الخيوط. قام تصحيح Chromium بتعطيل تعديل عدد الخيوط في WebCodecs.
libvpx هي مكتبة تتعامل مع ترميز وفك ترميز VP8/VP9.
المشكلة الرئيسية في CVE-2023-5217 هي أن تقليل عدد الخيوط مع زيادة ارتفاع الإطار في جلسة ترميز VP8 في libvpx يؤدي إلى تجاوز سعة كومة خطي بطول مسيطر عليه وكتابة فوقية مسيطر عليها لقيمة صغيرة مكررة من 4 بايتات. الفرق في ارتفاع الإطار يتحكم في طول الكتابة الفوقية، وعرض الإطار الجديد يتحكم في قيمة الـ 4 بايتات التي تُكتب بشكل متكرر. يمكن استغلال هذه الثغرة عدة مرات لكتابة قيم مختلفة من 4 بايتات بشكل مستمر عن طريق تقليل الارتفاع في كل إعداد لاحق.
يحافظ مُشفر VP8 في libvpx على مصفوفة تسمى mt_current_mb_col تخزن العمود الحالي الذي يعمل عليه خيط الترميز. يتم تخصيص هذه المصفوفة فقط إذا كان هناك أكثر من خيط واحد، وحجمها هو دالة في mb_rows، حيث mb_rows = height_of_frame >> 4 ويتم تقريب frame_height لأعلى إلى أقرب مضاعف للعدد 16.
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/common/alloccommon.c#L85
// The width and height are rounded up to a multiple of 16 and then assigned to `mb_rows` and `mb_cols`
int vp8_alloc_frame_buffers(VP8_COMMON *oci, int width, int height) {
...
// Round up the width/height up to the nearest multiple of 16
if ((width & 0xf) != 0) width += 16 - (width & 0xf);
if ((height & 0xf) != 0) height += 16 - (height & 0xf);
...
oci->mb_rows = height >> 4;
oci->mb_cols = width >> 4;
...
}
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/encoder/onyx_if.c#L1232
// This snippet shows the allocation of `mt_current_mb_col`
void vp8_alloc_compressor_data(VP8_COMP *cpi) {
...
// Only allocate if we have more than 1 thread
if (cpi->oxcf.multi_threaded > 1) {
int i;
vpx_free(cpi->mt_current_mb_col);
// sizeof(*cpi->mt_current_mb_col) is 4
CHECK_MEM_ERROR(&cpi->common.error, cpi->mt_current_mb_col,
vpx_malloc(sizeof(*cpi->mt_current_mb_col) * cm->mb_rows));
for (i = 0; i < cm->mb_rows; ++i)
vpx_atomic_init(&cpi->mt_current_mb_col[i], 0);
}
...
}
بمجرد أن ينتهي libvpx من ترميز إطار، فإنه يخزن عدد الأعمدة المُرمزة بالإضافة إلى mt_sync_range في mt_current_mb_col.
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/encoder/onyx_if.c#L1212
// Snippet where the mt_sync_range value is set, based on the width.
void vp8_alloc_compressor_data(VP8_COMP *cpi) {
...
#if CONFIG_MULTITHREAD
if (width < 640) {
cpi->mt_sync_range = 1;
} else if (width <= 1280) {
cpi->mt_sync_range = 4;
} else if (width <= 2560) {
cpi->mt_sync_range = 8;
} else {
cpi->mt_sync_range = 16;
}
#endif
...
}
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/encoder/encodeframe.c#L560
// Function where the attacker chosen value is written
static void encode_mb_row(...) {
...
const int nsync = cpi->mt_sync_range; // This value is set in vp8_alloc_compressor_data
vpx_atomic_int rightmost_col = VPX_ATOMIC_INIT(cm->mb_cols + nsync);
...
if (vpx_atomic_load_acquire(&cpi->b_multi_threaded) != 0) {
current_mb_col = &cpi->mt_current_mb_col[mb_row];
}
...
if (vpx_atomic_load_acquire(&cpi->b_multi_threaded) != 0) {
// current_mb_col is a reference to mt_current_mb_col
vpx_atomic_store_release(current_mb_col,
vpx_atomic_load_acquire(&rightmost_col));
}
...
}
لتجاوز mt_current_mb_col، نحتاج إلى ثلاثة إعدادات ترميز:
mt_current_mb_col الضعيف بناءً على configvuln.height. يجب أن يكون هذا الارتفاع الجديد أصغر من configinit.initial_height وإلا سنحصل على خطأ.mt_current_mb_col مما يتركه في حالة ضعيفة. سيقوم libvpx بكتابة القيمة (configattack.width >> 4) + 1 (حيث 1 هو المتغير mt_sync_range ويتم تقريب العرض لأعلى إلى أقرب مضاعف للعدد 16) خارج الحدود المخصصة سابقًا عندما يتحقق الشرط التالي:\text{ceil}(\text{config}_{\text{init}}.\text{height}/16) \geq \text{ceil}(\text{config}_{\text{attack}}.\text{height}/16) \gt \text{ceil}(\text{config}_{\text{vuln}}.\text{height}/16)$$

بشكل أكثر تحديدًا، لنفترض أننا قمنا بتهيئة إعداد ترميز VP8 باستخدام configinit مع width = 1200، height = 1200، threads = 4. الهجوم كالتالي:
mb_rows إلى 704/16 = 44، ويتم تخصيص المصفوفة mt_current_mb_col إلى (44)*4 = 176 بايت. القيمة المخزنة في mt_current_mb_col هي 512/16 + 1 = 33.mt_current_mb_col يتم إعادة تخصيصها فقط عندما يكون هناك أكثر من خيط واحد، فإنها تبقى بنفس الحجم، ومع ذلك فإن mb_rows الآن مضبوط على 1008/16 = 63. عندما يستدعي libvpx encode_mb_row، فإنه سيكتب فوق (63-44)*4 = 68 بايت بعد تخصيص mt_current_mb_col، ويكتب بشكل متكرر القيمة 32/16 + 1 = 3، حيث 32 هو العرض بعد التقريب لأعلى، و 1 هو قيمة mt_sync_range.mb_rows = 992/16 = 62 و mb_cols = 48/16 = 3، وبذلك يكتب فقط (62-44)*4 = 64 بايت بعد التخصيص الأصلي بالقيمة 4.لاستغلال هذه الثغرة، يحتاج المهاجم إلى أن يكون قادرًا على التحكم في ارتفاع الترميز وعرضه وعدد الخيوط. الأولين مباشران، لكن الأخير يتطلب العثور على أماكن يتم فيها إعادة تكوين عدد خيوط الترميز.
// https://github.com/webmproject/libvpx/blob/67bfb41ed8598edfb25bd6f245f9c39a68808548/vp8/vp8_cx_iface.c#L301
static vpx_codec_err_t set_vp8e_config(VP8_CONFIG *oxcf,
...
oxcf->multi_threaded = cfg.g_threads;
في فايرفوكس، يمكننا التحكم في عدد الخيوط عن طريق تعديل مساحة الإطار التي نرمزها في VP8TrackEncoder. إذا كانت مساحة الإطار أكبر من 307,200 (إطار 640x480) وكان الجهاز يحتوي على أكثر من نواتين، فسيتم استخدام أكثر من خيط واحد.
// https://searchfox.org/mozilla-central/source/dom/media/encoder/VP8TrackEncoder.cpp#97
nsresult CreateEncoderConfig(...) {
...
int32_t number_of_cores = PR_GetNumberOfProcessors();
if (aWidth * aHeight > 1920 * 1080 && number_of_cores >= 8) {
config->g_threads = 4; // 4 threads for > 1080p.
} else if (aWidth * aHeight > 1280 * 960 && number_of_cores >= 6) {
config->g_threads = 3; // 3 threads for 1080p.
} else if (aWidth * aHeight > 640 * 480 && number_of_cores >= 3) {
config->g_threads = 2; // 2 threads for qHD/HD.
} else {
config->g_threads = 1; // 1 thread for VGA or less
}
...
وجدنا أن واجهة برمجة تطبيقات MediaRecorder تعتمد على VP8TrackEncoder، ويمكننا تعديل العرض والارتفاع عن طريق تغيير حجم اللوحة القماشية التي يتم تسجيلها. انظر قسم MediaRecorder أدناه لمعرفة كيفية استدعاء هذا.
يقوم Chrome أيضًا بتعديل عدد الخيوط بناءً على مساحة الإطار الجاري ترميزها، معدلة حسب عدد النوى.
// https://source.chromium.org/chromium/chromium/src/+/main:media/video/vpx_video_encoder.cc;l=84
EncoderStatus SetUpVpxConfig(...) {
...
// Set the number of threads based on the image width and num of cores.
config->g_threads = GetNumberOfThreadsForSoftwareEncoding(opts.frame_size);
}
// https://source.chromium.org/chromium/chromium/src/+/main:media/base/video_encoder.cc;drc=f5bdc89c7395ed24f1b8d196a3bdd6232d5bf771;l=33
int GetNumberOfThreadsForSoftwareEncoding(gfx::Size frame_size) {
int area = frame_size.GetCheckedArea().ValueOrDefault(1);
// Default to 1 thread for less than VGA.
int desired_threads = 1;
if (area >= 3840 * 2160) {
desired_threads = 16;
} else if (area >= 2560 * 1080) {
desired_threads = 8;
} else if (area >= 1280 * 720) {
desired_threads = 4;
} else if (area >= 640 * 480) {
desired_threads = 2;
}
// Clamp to the number of available logical processors/cores.
desired_threads =
std::min(desired_threads, base::SysInfo::NumberOfProcessors());
return desired_threads;
}
يتم استخدام هذا المسار بواسطة واجهة برمجة تطبيقات WebCodecs VideoEncoding، حيث يمكننا تعديل عرض/ارتفاع الترميز مباشرة. انظر قسم WebCodecs لرؤية كيفية عمل ذلك.
يظهر الملف mediarecorder.html كيفية إنشاء جلسة MediaRecorder من لوحة رسم وتعديل العرض والارتفاع لتشغيل إعادة تكوين ترميز VP8 لتشغيل CVE-2023-5217 في متصفح ضعيف. عند تعديل معلمات عرض وارتفاع اللوحة القماشية، نستخدم setTimeout لضمان أن جلسة ترميز VP8 لديها وقت كافٍ لإعادة التهيئة. يمكن تعديل معلمة timeout لتحسين الموثوقية.
الحالة
لاختبار على فايرفوكس، يمكنك استخدام fuzzfetch للحصول على بناء ASAN قبل أن يتم تصحيح هذه الثغرة باستخدام الأمر fuzzfetch --build 2023-09-27 -a ثم افتح mediarecorder.html مباشرة.

تظهر الملفات webcodecs.html و webcodecs.js كيفية استخدام واجهة برمجة تطبيقات WebCodecs في Worker لتشغيل CVE-2023-5217 في متصفح ضعيف. لدينا تحكم أكبر في استدعاءات ترميز إطار في WebCodecs مقارنة بـ MediaRecorder، لكننا لا نزال نعتمد على مهلة زمنية لتغيير وتنفيذ كل من الخطوات الثلاث.
الحالة
لاختبار على Chromium، يمكنك استخدام get_asan_chrome.py للحصول على نسخة ضعيفة من Chrome باستخدام الأمر python get_asan_chrome.py --version 117.0.5938.131. ستحتاج بعد ذلك إلى بدء خادم HTTP محلي مع SSL. انظر gen_server_key.sh و server.py لإنشاء مفتاح خادم وبدء خادم. ثم يمكنك فقط فتح الصفحة في Chromium الضعيف لرؤية النتيجة.

انظر combined.html الذي يستخدم MediaRecorder كخيار احتياطي عندما لا يتم العثور على WebCodecs. سيتم استخدام هذا الملف المدمج لاستهداف كل من Chrome و Firefox بنفس الصفحة.
توضح هذه الثغرة التحديات والمخاطر الناتجة عن تعريض مكتبات الوسائط المعقدة لمهاجم عن بعد. باستخدام أدوات مثل RLBox، يمكن للمتصفحات عزل الثغرات المحتملة في مكتبات الوسائط. تقوم فايرفوكس بالفعل بتضمين هذا في مكتبات محددة.
شكرًا للقراءة! المساهمات مرحب بها. لا تتردد في فتح Issue أو PR مع أي رؤى أخرى. ما تبقى لاستكشافه هو رؤية كيف يمكن أن تؤدي هذه الكتابة الفوقية الصغيرة المكونة من 4 بايتات إلى تنفيذ كود.
شكرًا لـ Anand Balaji على الملاحظات حول مسودة سابقة.