
Una PoC per innescare CVE-2023-5217 dall'interfaccia WebCodecs o MediaRecorder del browser.
CVE-2023-5217 è una vulnerabilità di libvpx sfruttata in the wild che è stata trovata da Clément Lecigne del Threat Analysis Group di Google mentre prendeva di mira Chrome.
Questo repository mostra come innescare CVE-2023-5217 nel browser usando le API WebCodecs e MediaRecorder. CVE-2023-5217 consente un heap buffer overflow con una lunghezza dell'overflow controllata e la sovrascrittura di un piccolo valore ripetuto di 4 byte. Al momento non è noto come CVE-2023-5217 sia stata sfruttata in the wild.
Al momento della divulgazione pubblica, c'erano due patch in libvpx e una in Chromium che hanno corretto CVE-2023-5217. Le patch di libvpx includevano la disabilitazione delle modifiche al numero di thread VP8 e un test per la codifica multithread. La patch di Chromium ha disabilitato la regolazione del numero di thread in WebCodecs.
libvpx è una libreria che gestisce la codifica e la decodifica VP8/VP9.
Il problema chiave in CVE-2023-5217 è che ridurre il numero di thread mentre si aumenta l'altezza del frame in una sessione di codifica VP8 di libvpx causa un heap overflow lineare di lunghezza controllata e la sovrascrittura controllata di un piccolo valore ripetuto di 4 byte. La differenza nell'altezza del frame controlla la lunghezza della sovrascrittura, e la nuova larghezza del frame controlla il valore a 4 byte che viene scritto ripetutamente. Questa vulnerabilità può essere sfruttata più volte per scrivere continuamente diversi piccoli valori a 4 byte riducendo l'altezza in ogni configurazione successiva.
L'encoder VP8 di libvpx mantiene un array chiamato mt_current_mb_col che memorizza la colonna corrente su cui sta lavorando un thread dell'encoder. Questo array viene allocato solo se c'è più di un thread, e la sua dimensione è una funzione di mb_rows, dove mb_rows = frame_height >> 4 e frame_height viene arrotondato per eccesso al multiplo di 16 più vicino.
// 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);
}
...
}
Quando libvpx termina la codifica di un frame, memorizza il numero di colonne codificate più mt_sync_range in 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));
}
...
}
Per far overflow di mt_current_mb_col, servono tre configurazioni di codifica:
mt_current_mb_col basandosi su configvuln.height. Questa nuova altezza deve essere minore di configinit.initial_height, altrimenti si ottiene un errore.mt_current_mb_col, lasciandolo in uno stato vulnerabile. libvpx scriverà ripetutamente il valore (configattack.width >> 4) + 1 (dove 1 è la variabile mt_sync_range e la larghezza è arrotondata per eccesso al multiplo di 16 più vicino) oltre i limiti precedentemente allocati, quando vale la seguente condizione:\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)$$

Più concretamente, supponiamo di inizializzare una configurazione di codifica VP8 con configinit con larghezza = 1200, altezza = 1200, thread = 4. L'attacco è il seguente:
mb_rows viene impostata a 704/16=44, e l'array mt_current_mb_col viene allocato a (44)*4 = 176 byte. Il valore scritto e memorizzato in mt_current_mb_col è 512/16 + 1 = 33.mt_current_mb_col viene riallocato solo quando c'è più di un thread, la sua dimensione rimane la stessa, ma mb_rows ora è impostato a 1008/16 = 63. Quando libvpx chiama encode_mb_row, sovrascriverà (63-44)*4 = 68 byte oltre l'allocazione di mt_current_mb_col, scrivendo ripetutamente il valore 32/16 + 1 = 3, dove 32 è la larghezza arrotondata per eccesso e 1 è il valore di mt_sync_range.mb_rows = 992/16 = 62 e mb_cols = 48/16 = 3, scrivendo solo (62-44)*4 = 64 byte oltre l'allocazione originale il valore 4.Per sfruttare questa vulnerabilità, un attaccante deve essere in grado di controllare l'altezza, la larghezza e il numero di thread della codifica. Le prime due sono semplici, mentre l'ultimo richiede di trovare punti in cui il numero di thread di codifica viene riconfigurato.
// 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, possiamo controllare il numero di thread regolando l'area del frame che stiamo codificando in VP8TrackEncoder. Se l'area del frame è maggiore di 307.200 (un frame 640x480) e la macchina ha più di 2 core, verrà usato più di un thread.
// 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
}
...
Abbiamo scoperto che l'API MediaRecorder si basa su VP8TrackEncoder, e possiamo regolare larghezza e altezza cambiando la dimensione della canvas registrata. Vedi la sezione MediaRecorder qui sotto per come chiamare questa funzionalità.
Chrome, analogamente, regola il numero di thread in base all'area del frame da codificare, adattandolo al numero di core.
// 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;
}
Questo percorso viene esercitato dall'API WebCodecs VideoEncoding, dove possiamo modificare direttamente larghezza/altezza di codifica. Vedi la sezione WebCodecs per vedere come funziona.
Il file mediarecorder.html mostra come creare una sessione MediaRecorder da una canvas e regolare larghezza/altezza per attivare una riconfigurazione della codifica VP8 e innescare CVE-2023-5217 in un browser vulnerabile. Quando modifichiamo i parametri di larghezza e altezza della canvas, usiamo un setTimeout per garantire che la sessione di codifica VP8 abbia abbastanza tempo per riconfigurarsi. Il parametro del timeout può essere regolato per affidabilità.
Stato
Per testare su Firefox, puoi usare fuzzfetch per ottenere una build ASAN precedente alla patch per questa CVE con il comando fuzzfetch --build 2023-09-27 -a, quindi aprire direttamente mediarecorder.html.

I file webcodecs.html e webcodecs.js mostrano come usare l'API WebCodecs in un Worker per innescare CVE-2023-5217 in un browser vulnerabile. Abbiamo più controllo sulle chiamate per codificare un frame in WebCodecs rispetto a MediaRecorder, ma facciamo comunque affidamento su un timeout per eseguire ciascuno dei tre passaggi.
Stato
Per testare su Chromium, puoi usare get_asan_chrome.py per ottenere una versione vulnerabile di Chrome con il comando python get_asan_chrome.py --version 117.0.5938.131. Dovrai quindi avviare un server HTTP locale con SSL. Vedi gen_server_key.sh e server.py per generare una chiave del server e avviare un server. Poi puoi semplicemente aprire la pagina nel Chromium vulnerabile per vedere il risultato.

Vedi combined.html, che usa MediaRecorder come fallback quando WebCodecs non è disponibile. Questo file combinato può essere usato per colpire sia Chrome che Firefox con la stessa pagina.
Questa vulnerabilità dimostra le sfide e i pericoli dell'esporre complesse librerie multimediali a un attaccante remoto. Usando strumenti come RLBox, i browser possono isolare potenziali vulnerabilità nelle librerie multimediali. Firefox include già questo in alcune librerie.
Grazie per la lettura! I contributi sono benvenuti. Sentiti libero di aprire una Issue o una PR con qualsiasi altra idea. Ciò che resta da esplorare è capire come questa piccola sovrascrittura di 4 byte possa portare all'esecuzione di codice.
Grazie a Anand Balaji per il feedback su una bozza precedente.