
Un PoC para desencadenar CVE-2023-5217 desde la interfaz WebCodecs o MediaRecorder del navegador.
CVE-2023-5217 es una vulnerabilidad de libvpx explotada in-the-wild que fue encontrada por Clément Lecigne del Grupo de Análisis de Amenazas de Google para estar atacando Chrome.
Este repositorio muestra cómo desencadenar CVE-2023-5217 en el navegador utilizando las API WebCodecs y MediaRecorder. CVE-2023-5217 permite un desbordamiento de búfer de montón con una longitud de desbordamiento controlada y una sobrescritura de un valor pequeño repetido de 4 bytes. Actualmente no se sabe cómo fue explotado CVE-2023-5217 en la naturaleza.
Alrededor del momento de la divulgación pública, hubo dos parches en libvpx y uno en Chromium que corrigieron CVE-2023-5217. Los parches de libvpx incluyeron deshabilitar los cambios en el número de hilos de VP8 y una prueba para la codificación multihilo. El parche de Chromium deshabilitó el ajuste del número de hilos en WebCodecs.
libvpx es una biblioteca que maneja la codificación y decodificación de VP8/VP9.
El problema clave en CVE-2023-5217 es que reducir el número de hilos mientras se aumenta la altura del cuadro en una sesión de codificación VP8 de libvpx provoca un desbordamiento lineal de montón de una longitud controlada y una sobrescritura controlada de un valor pequeño repetido de 4 bytes. La diferencia en la altura del cuadro controla la longitud de la sobrescritura, y el nuevo ancho del cuadro controla el valor de 4 bytes que se escribe repetidamente. Esta vulnerabilidad se puede explotar múltiples veces para escribir continuamente diferentes valores pequeños de 4 bytes reduciendo la altura en cada configuración subsiguiente.
El codificador VP8 de libvpx mantiene un array llamado mt_current_mb_col que almacena la columna actual en la que está trabajando un hilo del codificador. Este array solo se asigna si hay más de un hilo, y su tamaño es una función de mb_rows, donde mb_rows = frame_height >> 4 y frame_height se redondea al múltiplo de 16 más cercano.
// 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);
}
...
}
Una vez que libvpx termina de codificar un cuadro, almacena el número de columnas codificadas más mt_sync_range en 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));
}
...
}
Para desbordar mt_current_mb_col, necesitamos tres configuraciones de codificación:
mt_current_mb_col basada en configvuln.height. Esta nueva altura debe ser menor que configinit.initial_height o de lo contrario obtendremos un error.mt_current_mb_col, dejándolo en un estado vulnerable. libvpx escribirá repetidamente el valor (configattack.width >> 4) + 1 (donde 1 es la variable mt_sync_range y el ancho se redondea al múltiplo de 16 más cercano) fuera de los límites previamente asignados cuando se cumpla la siguiente condición:
$$\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)$$
Más concretamente, supongamos que inicializamos una configuración de codificación VP8 con configinit con ancho = 1200, alto = 1200, hilos = 4. El ataque es el siguiente:
mb_rows se establece en 704/16 = 44, y el array mt_current_mb_col se asigna a (44)*4 = 176 bytes. El valor escrito almacenado en mt_current_mb_col es 512/16 + 1 = 33.mt_current_mb_col solo se reasigna cuando hay más de un hilo, permanece del mismo tamaño, pero mb_rows ahora se establece en 1008/16 = 63. Cuando libvpx llama a encode_mb_row, sobrescribirá (63-44)*4 = 68 bytes más allá de la asignación de mt_current_mb_col, escribiendo repetidamente el valor 32/16 + 1 = 3, donde 32 es el ancho redondeado y 1 es el valor de mt_sync_range.mb_rows = 992/16 = 62 y mb_cols = 48/16 = 3, escribiendo solo (62-44)*4 = 64 bytes más allá de la asignación original el valor 4.Para explotar esta vulnerabilidad, un atacante necesita poder controlar la altura, anchura y número de hilos de codificación. Las dos primeras son sencillas, pero la última requiere encontrar lugares donde se reconfigura el número de hilos de codificación.
// 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;
En Firefox, podemos controlar el número de hilos ajustando el área del cuadro que estamos codificando en el VP8TrackEncoder. Si el área del cuadro es mayor que 307,200 (un cuadro de 640x480) y la máquina tiene más de 2 núcleos, entonces se usará más de un hilo.
// 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
}
...
Descubrimos que la API MediaRecorder depende del VP8TrackEncoder, y podemos ajustar la anchura y altura cambiando el tamaño del lienzo que se está grabando. Consulte la sección MediaRecorder a continuación para ver cómo llamar a esto.
Chrome ajusta de manera similar el número de hilos según el área del cuadro que se está codificando, ajustado por el número de núcleos.
// 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;
}
Esta ruta es ejercida por la API WebCodecs VideoEncoding, donde podemos modificar directamente la anchura/altura de codificación. Consulte la sección WebCodecs para ver cómo funciona.
El archivo mediarecorder.html muestra cómo crear una sesión de MediaRecorder desde un lienzo y ajustar la anchura/altura para desencadenar una reconfiguración de codificación VP8 que active CVE-2023-5217 en un navegador vulnerable. Al ajustar los parámetros de anchura y altura del lienzo, usamos un setTimeout para asegurarnos de que la sesión de codificación VP8 tenga tiempo suficiente para reconfigurarse. El parámetro de tiempo de espera se puede ajustar para mayor fiabilidad.
Estado
Para probar en Firefox, puedes usar fuzzfetch para obtener una compilación ASAN anterior a que este CVE fuera parcheado con el comando fuzzfetch --build 2023-09-27 -a y luego abrir mediarecorder.html directamente.

Los archivos webcodecs.html y webcodecs.js muestran cómo usar la API WebCodecs en un Worker para desencadenar CVE-2023-5217 en un navegador vulnerable. Tenemos más control sobre las llamadas para codificar un cuadro en WebCodecs que en MediaRecorder, pero aún así dependemos de un tiempo de espera para realizar cada uno de los tres pasos.
Estado
Para probar en Chromium, puedes usar get_asan_chrome.py para obtener una versión vulnerable de Chrome con el comando python get_asan_chrome.py --version 117.0.5938.131. Luego necesitarás iniciar un servidor HTTP local con SSL. Consulta gen_server_key.sh y server.py para generar una clave de servidor e iniciar un servidor. Luego solo abre la página en el Chromium vulnerable para ver el resultado.

Consulta combined.html que usa MediaRecorder como respaldo cuando WebCodecs no está disponible. Este archivo combinado se usaría para atacar tanto Chrome como Firefox con la misma página.
Esta vulnerabilidad demuestra los desafíos y peligros de exponer bibliotecas multimedia complejas a un atacante remoto. Usando herramientas como RLBox, los navegadores pueden aislar vulnerabilidades potenciales en bibliotecas multimedia. Firefox ya incluye esto en bibliotecas seleccionadas.
¡Gracias por leer! Las contribuciones son bienvenidas. No dudes en crear un Issue o abrir un PR con cualquier otro conocimiento. Lo que queda por explorar es ver cómo esta pequeña sobrescritura de 4 bytes puede conducir a la ejecución de código.
Gracias a Anand Balaji por sus comentarios en un borrador anterior.