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