
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)$$
