
Ein PoC zur Auslösung von CVE-2023-5217 aus der Browser-WebCodecs- oder MediaRecorder-Schnittstelle.
CVE-2023-5217 ist eine in-the-wild ausgenutzte libvpx-Schwachstelle, die von Clément Lecigne von Googles Threat Analysis Group entdeckt wurde und auf Chrome abzielt.
Dieses Repository zeigt, wie man CVE-2023-5217 im Browser mit den WebCodecs- und MediaRecorder-APIs auslöst. CVE-2023-5217 ermöglicht einen Heap-Pufferüberlauf mit kontrollierter Überlauflänge und Überschreiben eines wiederholten kleinen 4-Byte-Werts. Es ist derzeit nicht bekannt, wie CVE-2023-5217 in freier Wildbahn ausgenutzt wurde.
Zum Zeitpunkt der öffentlichen Offenlegung gab es zwei Patches in libvpx und einen in Chromium, die CVE-2023-5217 behoben. Die libvpx-Patches umfassten das Deaktivieren von VP8-Thread-Nummern-Änderungen und einen Test für Multithread-Kodierung. Der Chromium-Patch deaktivierte das Anpassen der Thread-Anzahl in WebCodecs.
libvpx ist eine Bibliothek, die VP8/VP9-Kodierung und -Dekodierung übernimmt.
Das Hauptproblem bei CVE-2023-5217 ist, dass die Reduzierung der Thread-Anzahl bei gleichzeitiger Vergrößerung der Bildhöhe in einer libvpx-VP8-Kodierungssitzung einen linearen Heap-Überlauf mit kontrollierter Länge und kontrolliertem Überschreiben eines wiederholten kleinen 4-Byte-Werts verursacht. Der Unterschied in der Bildhöhe steuert die Länge des Überschreibens, und die neue Bildbreite steuert den 4-Byte-Wert, der wiederholt geschrieben wird. Diese Schwachstelle kann mehrfach ausgenutzt werden, um durch Verringern der Höhe in jeder nachfolgenden Konfiguration kontinuierlich verschiedene kleine 4-Byte-Werte zu schreiben.
Der libvpx-VP8-Kodierer verwaltet ein Array namens mt_current_mb_col, das die aktuelle Spalte speichert, an der ein Codierungs-Thread arbeitet. Dieses Array wird nur zugewiesen, wenn mehr als ein Thread vorhanden ist, und seine Größe ist eine Funktion von mb_rows, wobei mb_rows = frame_height >> 4 und die frame_height auf das nächste Vielfache von 16 aufgerundet wird.
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/common/alloccommon.c#L85
// Die Breite und Höhe werden auf ein Vielfaches von 16 aufgerundet und dann `mb_rows` und `mb_cols` zugewiesen.
int vp8_alloc_frame_buffers(VP8_COMMON *oci, int width, int height) {
...
// Runde die Breite/Höhe auf das nächste Vielfache von 16 auf
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
// Dieser Ausschnitt zeigt die Zuweisung von `mt_current_mb_col`
void vp8_alloc_compressor_data(VP8_COMP *cpi) {
...
// Nur zuweisen, wenn wir mehr als 1 Thread haben
if (cpi->oxcf.multi_threaded > 1) {
int i;
vpx_free(cpi->mt_current_mb_col);
// sizeof(*cpi->mt_current_mb_col) ist 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);
}
...
}
Sobald libvpx die Kodierung eines Bildes abgeschlossen hat, speichert es die Anzahl der kodierten Spalten plus mt_sync_range in mt_current_mb_col.
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/encoder/onyx_if.c#L1212
// Ausschnitt, in dem der Wert von mt_sync_range basierend auf der Breite gesetzt wird.
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
// Funktion, in der der vom Angreifer gewählte Wert geschrieben wird
static void encode_mb_row(...) {
...
const int nsync = cpi->mt_sync_range; // Dieser Wert wird in vp8_alloc_compressor_data gesetzt
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 ist eine Referenz auf mt_current_mb_col
vpx_atomic_store_release(current_mb_col,
vpx_atomic_load_acquire(&rightmost_col));
}
...
}
Um mt_current_mb_col zum Überlaufen zu bringen, benötigen wir drei Codierungskonfigurationen:
mt_current_mb_col-Zuweisung basierend auf configvuln.height erstellen. Diese neue Höhe muss kleiner sein als configinit.initial_height, sonst erhalten wir einen Fehler.mt_current_mb_col nicht neu zuweisen und sie in einem anfälligen Zustand belassen. libvpx wird wiederholt den Wert (configattack.width >> 4) + 1 (wobei 1 die Variable mt_sync_range ist und die Breite auf das nächste Vielfache von 16 aufgerundet wird) außerhalb der zuvor zugewiesenen Grenzen schreiben, wenn die folgende Bedingung erfüllt ist:\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)$$

Konkreter: Angenommen, wir initialisieren eine VP8-Kodierungskonfiguration mit configinit mit width = 1200, height = 1200, threads = 4. Der Angriff läuft wie folgt ab:
mb_rows wird auf 704/16 = 44 gesetzt, und das Array mt_current_mb_col wird mit (44)*4 = 176 Bytes zugewiesen. Der geschriebene Wert in mt_current_mb_col ist 512/16 + 1 = 33.mt_current_mb_col nur neu zugewiesen wird, wenn mehr als ein Thread vorhanden ist, bleibt es gleich groß, aber mb_rows wird nun auf 1008/16 = 63 gesetzt. Wenn libvpx encode_mb_row aufruft, werden (63-44)*4 = 68 Bytes über die mt_current_mb_col-Zuweisung hinaus überschrieben, wobei wiederholt der Wert 32/16 + 1 = 3 geschrieben wird (32 ist die aufgerundete Breite, 1 der mt_sync_range-Wert).mb_rows = 992/16 = 62 und mb_cols = 48/16 = 3 setzen und (62-44)*4 = 64 Bytes über die ursprüngliche Zuweisung hinaus den Wert 4 schreiben.Um diese Schwachstelle auszunutzen, muss ein Angreifer in der Lage sein, die Kodierungshöhe, -breite und die Anzahl der Threads zu kontrollieren. Die ersten beiden sind einfach, aber letzteres erfordert das Auffinden von Stellen, an denen die Anzahl der Kodierungs-Threads neu konfiguriert wird.
// 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;
In Firefox können wir die Anzahl der Threads steuern, indem wir den Bildbereich anpassen, den wir im VP8TrackEncoder kodieren. Wenn der Bildbereich größer als 307.200 (ein 640x480-Bild) ist und der Rechner mehr als 2 Kerne hat, wird mehr als ein Thread verwendet.
// 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 für > 1080p.
} else if (aWidth * aHeight > 1280 * 960 && number_of_cores >= 6) {
config->g_threads = 3; // 3 threads für 1080p.
} else if (aWidth * aHeight > 640 * 480 && number_of_cores >= 3) {
config->g_threads = 2; // 2 threads für qHD/HD.
} else {
config->g_threads = 1; // 1 thread für VGA oder weniger.
}
...
Wir haben festgestellt, dass die MediaRecorder-API auf dem VP8TrackEncoder basiert, und wir können Breite und Höhe anpassen, indem wir die Größe der aufgenommenen Canvas ändern. Siehe Abschnitt MediaRecorder unten, um zu erfahren, wie man dies aufruft.
Chrome passt die Anzahl der Threads ebenfalls basierend auf dem zu kodierenden Bildbereich an, korrigiert um die Anzahl der Kerne.
// 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);
// Standardmäßig 1 Thread für weniger als 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;
}
// Begrenzung auf die Anzahl der verfügbaren logischen Prozessoren/Kerne.
desired_threads =
std::min(desired_threads, base::SysInfo::NumberOfProcessors());
return desired_threads;
}
Dieser Pfad wird von der WebCodecs VideoEncoding-API genutzt, bei der wir die Kodierungsbreite/-höhe direkt ändern können. Siehe Abschnitt WebCodecs, um zu sehen, wie dies funktioniert.
Die Datei mediarecorder.html zeigt, wie man eine MediaRecorder-Sitzung aus einer Canvas erstellt und die Breite/Höhe anpasst, um eine VP8-Kodierungsneukonfiguration auszulösen, die CVE-2023-5217 in einem anfälligen Browser auslöst. Beim Anpassen der Canvas-Breiten- und -Höhenparameter verwenden wir ein setTimeout, um sicherzustellen, dass die VP8-Kodierungssitzung genügend Zeit für die Neukonfiguration hat. Der Timeout-Parameter kann für die Zuverlässigkeit angepasst werden.
Status
Um auf Firefox zu testen, kannst du fuzzfetch verwenden, um einen ASAN-Build vor der Behebung dieser CVE zu erhalten, mit dem Befehl fuzzfetch --build 2023-09-27 -a und dann mediarecorder.html direkt öffnen.

Die Dateien webcodecs.html und webcodecs.js zeigen, wie man die WebCodecs-API in einem Worker verwendet, um CVE-2023-5217 in einem anfälligen Browser auszulösen. Wir haben mehr Kontrolle über die Aufrufe zum Kodieren eines Bildes in WebCodecs als bei MediaRecorder, verlassen uns aber dennoch auf einen Timeout, um die drei Schritte durchzuführen.
Status
Um auf Chromium zu testen, kannst du get_asan_chrome.py verwenden, um eine anfällige Version von Chrome mit dem Befehl python get_asan_chrome.py --version 117.0.5938.131 zu erhalten. Du musst dann einen lokalen HTTP-Server mit SSL starten. Siehe gen_server_key.sh und server.py um einen Serverschlüssel zu generieren und einen Server zu starten. Dann öffne einfach die Seite im anfälligen Chromium, um das Ergebnis zu sehen.

Siehe combined.html, das MediaRecorder als Fallback verwendet, wenn WebCodecs nicht gefunden wird. Diese kombinierte Datei würde verwendet, um sowohl Chrome als auch Firefox mit derselben Seite anzugreifen.
Diese Schwachstelle zeigt die Herausforderungen und Gefahren, die mit der Offenlegung komplexer Medienbibliotheken für einen entfernten Angreifer verbunden sind. Mit Tools wie RLBox können Browser potenzielle Schwachstellen in Medienbibliotheken isolieren. Firefox liefert dies bereits in ausgewählten Bibliotheken aus.
Danke fürs Lesen! Beiträge sind willkommen. Sie können gerne ein Issue einreichen oder einen PR mit weiteren Erkenntnissen öffnen. Es bleibt zu erforschen, wie diese kleine 4-Byte-Überschreibung zu Codeausführung führen kann.
Dank an Anand Balaji für Feedback zu einem früheren Entwurf.