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

Mais concretamente, suponha que inicializamos uma configuração de encoding VP8 com configinit com width = 1200, height = 1200 e threads = 4. O ataque é o seguinte:
mb_rows é definida como 704/16=44, e o array mt_current_mb_col é alocado com (44)*4 = 176 bytes. O valor gravado em mt_current_mb_col é 512/16 + 1 = 33.mt_current_mb_col só é realocado quando há mais de uma thread, ele permanece com o mesmo tamanho, mas mb_rows agora é definido como 1008/16 = 63. Quando o libvpx chama encode_mb_row, ele sobrescreverá (63-44)*4 = 68 bytes além da alocação de mt_current_mb_col, gravando repetidamente o valor 32/16 + 1 = 3, onde 32 é a largura arredondada para cima e 1 é o valor de mt_sync_range.mb_rows = 992/16 = 62 e mb_cols = 48/16 = 3, gravando apenas (62-44)*4 = 64 bytes além da alocação original o valor 4.Para explorar essa vulnerabilidade, um atacante precisa ser capaz de controlar a altura, a largura e o número de threads do encoding. Os dois primeiros são simples, mas o último exige encontrar lugares onde o número de threads de encoding é reconfigurado.
// 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;
No Firefox, podemos controlar o número de threads ajustando a área do quadro que estamos codificando no VP8TrackEncoder. Se a área do quadro for maior que 307.200 (um quadro 640x480) e a máquina tiver mais de 2 núcleos, mais de uma thread será usada.
// 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 para > 1080p.
} else if (aWidth * aHeight > 1280 * 960 && number_of_cores >= 6) {
config->g_threads = 3; // 3 threads para 1080p.
} else if (aWidth * aHeight > 640 * 480 && number_of_cores >= 3) {
config->g_threads = 2; // 2 threads para qHD/HD.
} else {
config->g_threads = 1; // 1 thread para VGA ou menos
}
...
Descobrimos que a API MediaRecorder depende do VP8TrackEncoder, e podemos ajustar a largura e a altura alterando o tamanho do canvas que está sendo gravado. Veja a seção MediaRecorder abaixo sobre como chamar isso.
O Chrome também ajusta o número de threads com base na área do quadro sendo codificada, ajustado pelo número de núcleos.
// https://source.chromium.org/chromium/chromium/src/+/main:media/video/vpx_video_encoder.cc;l=84
EncoderStatus SetUpVpxConfig(...) {
...
// Define o número de threads com base na largura da imagem e no número de núcleos.
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);
// Padrão de 1 thread para menos que 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;
}
// Limita ao número de processadores/núcleos lógicos disponíveis.
desired_threads =
std::min(desired_threads, base::SysInfo::NumberOfProcessors());
return desired_threads;
}
Esse caminho é exercitado pela API WebCodecs VideoEncoding, onde podemos modificar diretamente a largura/altura do encoding. Veja a seção WebCodecs para entender como isso funciona.
O arquivo mediarecorder.html mostra como criar uma sessão MediaRecorder a partir de um canvas e ajustar a largura/altura para acionar uma reconfiguração de encoding VP8 e disparar a CVE-2023-5217 em um navegador vulnerável. Ao ajustar os parâmetros de largura e altura do canvas, usamos um setTimeout para garantir que a sessão de encoding VP8 tenha tempo suficiente para reconfigurar. O parâmetro de timeout pode ser ajustado para maior confiabilidade.
Status
Para testar no Firefox, você pode usar o fuzzfetch para obter uma build ASAN anterior ao patch dessa CVE com o comando fuzzfetch --build 2023-09-27 -a e depois abrir mediarecorder.html diretamente.

Os arquivos webcodecs.html e webcodecs.js mostram como usar a API WebCodecs em um Worker para acionar a CVE-2023-5217 em um navegador vulnerável. Temos mais controle sobre as chamadas para codificar um quadro no WebCodecs do que no MediaRecorder, mas ainda dependemos de um timeout para executar cada uma das três etapas.
Status
Para testar no Chromium, você pode usar o get_asan_chrome.py para obter uma versão vulnerável do Chrome com o comando python get_asan_chrome.py --version 117.0.5938.131. Depois, você precisará iniciar um servidor HTTP local com SSL. Veja gen_server_key.sh e server.py para gerar uma chave de servidor e iniciar um servidor. Então, basta abrir a página no Chromium vulnerável para ver o resultado.

Veja combined.html, que usa o MediaRecorder como fallback quando o WebCodecs não está disponível. Esse arquivo combinado seria usado para mirar tanto no Chrome quanto no Firefox com a mesma página.
Essa vulnerabilidade demonstra os desafios e perigos de expor bibliotecas de mídia complexas a um atacante remoto. Usando ferramentas como o RLBox, os navegadores podem isolar possíveis vulnerabilidades em bibliotecas de mídia. O Firefox já distribui isso em bibliotecas selecionadas.
Obrigado por ler! Contribuições são bem-vindas. Sinta-se à vontade para abrir uma Issue ou um PR com quaisquer outros insights. O que resta explorar é ver como essa pequena sobrescrita de 4 bytes pode levar à execução de código.
Agradecimentos ao Anand Balaji pelo feedback em um rascunho anterior.