
PoC для запуска CVE-2023-5217 через интерфейс WebCodecs или MediaRecorder браузера.
CVE-2023-5217 — это эксплуатируемая в реальных условиях уязвимость libvpx, обнаруженная Клеманом Лесинем из Группы анализа угроз Google (Threat Analysis Group), нацеленная на Chrome.
Этот репозиторий показывает, как вызвать CVE-2023-5217 в браузере с помощью API WebCodecs и MediaRecorder. CVE-2023-5217 позволяет выполнить переполнение буфера кучи с контролируемой длиной переполнения и перезаписью повторяющегося малого 4-байтового значения. В настоящее время неизвестно, как CVE-2023-5217 эксплуатировалась в реальных условиях.
Примерно на момент публичного раскрытия было выпущено два патча для libvpx и один для Chromium, которые устраняли CVE-2023-5217. Патчи для libvpx включали отключение изменений количества потоков VP8 и тест для многопоточного кодирования. Патч Chromium отключил регулировку количества потоков в WebCodecs.
libvpx — это библиотека для кодирования и декодирования VP8/VP9.
Ключевая проблема CVE-2023-5217 заключается в том, что уменьшение количества потоков при одновременном увеличении высоты кадра в сеансе кодирования VP8 в libvpx вызывает линейное переполнение кучи с контролируемой длиной и контролируемой перезаписью повторяющегося малого 4-байтового значения. Разница в высоте кадра контролирует длину перезаписи, а новая ширина кадра контролирует 4-байтовое значение, которое повторно записывается. Эта уязвимость может быть использована несколько раз для последовательной записи различных малых 4-байтовых значений путём уменьшения высоты в каждой последующей конфигурации.
Кодировщик VP8 в libvpx поддерживает массив mt_current_mb_col, который хранит текущий столбец, обрабатываемый потоком кодировщика. Этот массив выделяется только в том случае, если потоков больше одного, и его размер зависит от mb_rows, где mb_rows = frame_height >> 4, а frame_height округляется вверх до ближайшего кратного 16.
// 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);
}
...
}
После завершения кодирования кадра libvpx сохраняет количество закодированных столбцов плюс mt_sync_range в 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));
}
...
}
Для переполнения mt_current_mb_col требуются три конфигурации кодирования:
configinit: Во время инициализации libvpx устанавливает initial_width и initial_height. Это максимально возможные границы для последующих конфигураций. Количество потоков здесь не имеет значения. Нам нужна промежуточная конфигурация, потому что никакие последующие комбинации ширины/высоты не могут быть больше исходных.
configvuln: При перенастройке, если используется более одного потока, libvpx создаёт уязвимое выделение mt_current_mb_col на основе configvuln.height. Эта новая высота должна быть меньше configinit.initial_height, иначе мы получим ошибку.
configattack: При перенастройке, если используется только один поток, libvpx не будет перераспределять mt_current_mb_col, оставляя его в уязвимом состоянии. libvpx будет многократно записывать значение (configattack.width >> 4) + 1 (где 1 — это переменная mt_sync_range, а ширина округляется вверх до ближайшего кратного 16) за пределы ранее выделенной области, если выполняется следующее условие:
\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)$$

Более конкретно: предположим, мы инициализируем конфигурацию кодирования VP8 с configinit с шириной = 1200, высотой = 1200, потоками = 4. Атака выглядит следующим образом:
configvuln перенастраивает кодировщик с шириной = 500 (округлено до 512), высотой = 700 (округлено до 704) и потоками = 2. Переменная mb_rows устанавливается в 704/16=44, массив mt_current_mb_col выделяется размером (44)*4 = 176 байт. Записанное значение, хранящееся в mt_current_mb_col, равно 512/16 + 1 = 33.
configattack перенастраивает кодировщик с шириной = 18 (округлено до 32), высотой = 1000 (округлено до 1008) и потоками = 1. Поскольку mt_current_mb_col перераспределяется только при наличии более одного потока, он остаётся того же размера, но mb_rows теперь устанавливается в 1008/16 = 63. Когда libvpx вызывает encode_mb_row, он перезаписывает (63-44)*4 = 68 байт после выделения mt_current_mb_col, многократно записывая значение 32/16 + 1 = 3, где 32 — округлённая ширина, а 1 — значение mt_sync_range.