
Um PoC para acionar o CVE-2023-5217 a partir da interface WebCodecs ou MediaRecorder do navegador.
A CVE-2023-5217 é uma vulnerabilidade do libvpx explorada ativamente que foi encontrada por Clément Lecigne do Threat Analysis Group do Google e estava sendo usada contra o Chrome.
Este repositório mostra como acionar a CVE-2023-5217 no navegador usando as APIs WebCodecs e MediaRecorder. A CVE-2023-5217 permite um estouro de buffer na pilha (heap) com comprimento controlado do estouro e sobrescrita de um pequeno valor repetido de 4 bytes. Atualmente não se sabe como a CVE-2023-5217 foi explorada em ataques reais.
Na época da divulgação pública, havia dois patches no libvpx e um no Chromium que corrigiam a CVE-2023-5217. Os patches do libvpx incluíam a desativação de alterações no número de threads do VP8 e um teste para encoding multithread. O patch do Chromium desativou o ajuste do número de threads no WebCodecs.
O libvpx é uma biblioteca que lida com encoding e decoding de VP8/VP9.
A questão central da CVE-2023-5217 é que reduzir o número de threads enquanto se aumenta a altura do quadro em uma sessão de encoding VP8 do libvpx causa um estouro linear na pilha (heap) com comprimento controlado e sobrescrita controlada de um pequeno valor repetido de 4 bytes. A diferença na altura do quadro controla o comprimento da sobrescrita, e a nova largura do quadro controla o valor de 4 bytes que é escrito repetidamente. Essa vulnerabilidade pode ser explorada várias vezes para gravar continuamente diferentes valores pequenos de 4 bytes, reduzindo a altura em cada configuração subsequente.
O encoder VP8 do libvpx mantém um array chamado mt_current_mb_col que armazena a coluna atual em que uma thread do encoder está trabalhando. Esse array só é alocado se houver mais de uma thread, e seu tamanho é uma função de mb_rows, onde mb_rows = frame_height >> 4 e o frame_height é arredondado para cima até o múltiplo de 16 mais próximo.
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/common/alloccommon.c#L85
// A largura e a altura são arredondadas para cima até um múltiplo de 16 e depois atribuídas a `mb_rows` e `mb_cols`
int vp8_alloc_frame_buffers(VP8_COMMON *oci, int width, int height) {
...
// Arredonda a largura/altura para cima até o múltiplo de 16 mais próximo
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
// Este trecho mostra a alocação de `mt_current_mb_col`
void vp8_alloc_compressor_data(VP8_COMP *cpi) {
...
// Só aloca se tivermos mais de 1 thread
if (cpi->oxcf.multi_threaded > 1) {
int i;
vpx_free(cpi->mt_current_mb_col);
// sizeof(*cpi->mt_current_mb_col) é 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 o libvpx termina de codificar um quadro, ele armazena o número de colunas codificadas mais mt_sync_range em mt_current_mb_col.
// https://github.com/webmproject/libvpx/blob/6512f994da13e2f27e6a7bd449efee0a374b55b7/vp8/encoder/onyx_if.c#L1212
// Trecho onde o valor de mt_sync_range é definido, com base na largura.
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
// Função onde o valor escolhido pelo atacante é gravado
static void encode_mb_row(...) {
...
const int nsync = cpi->mt_sync_range; // Este valor é definido em 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 é uma referência a mt_current_mb_col
vpx_atomic_store_release(current_mb_col,
vpx_atomic_load_acquire(&rightmost_col));
}
...
}
Para estourar mt_current_mb_col, precisamos de três configurações de encoding:
mt_current_mb_col com base em configvuln.height. Essa nova altura deve ser menor que configinit.initial_height, caso contrário receberemos um erro.mt_current_mb_col, deixando-o em um estado vulnerável. O libvpx gravará repetidamente o valor (configattack.width >> 4) + 1 (onde 1 é a variável mt_sync_range e a largura é arredondada para cima até o múltiplo de 16 mais próximo) fora dos limites previamente alocados quando a seguinte condição for válida:\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)$$
