
Углубленный технический анализ CVE-2021-25804, уязвимости парсера AVI в VLC. Включает первопричину, различие патча и примитивы эксплуатации для исследователей безопасности.
AVI-файл может содержать множество дорожек. Например, аудиодорожку и видеодорожку.
Для того чтобы разобрать произвольное количество дорожек, VLC сначала должен определить, завершена ли данная дорожка и готова ли к воспроизведению. С этой целью устанавливаются две переменные. Обе называются i_track.
Переменная p_sys для наших целей представляет AVI-файл. Таким образом, «официальная» переменная i_track, которая будет использоваться для ссылки на дорожки, признанные завершенными и готовыми к воспроизведению, — это p_sys->i_track. Она инициализируется макросом здесь:
```
TAB_INIT(p_sys->i_track, p_sys->track);
//#define TAB_INIT( count, tab ) do {(count) = 0; (tab) = NULL; } while(0)
```
Тем временем создается вторая переменная i_track для подсчета ВСЕХ дорожек независимо от того, завершены/готовы ли они к воспроизведению.
```
i_track = AVI_ChunkCount( p_hdrl, AVIFOURCC_strl, true );
```
Вторая i_track представляет все чанки, начинающиеся с . Она управляет циклом , который устанавливает переменную только в том случае, если она проходит ряд проверок:
'strl'forp_sys->i_track```
for( unsigned i = 0 ; i < i_track; i++ )
{
...
TAB_APPEND( p_sys->i_track, p_sys->track, tk );
//#define TAB_APPEND( count, tab, p ) {TAB_APPEND_CAST( , count, tab, p )}
//TAB_APPEND_CAST( ,p_sys->i_track, p_sys->track, tk)
//if( (count) > 0 ) (tab) = cast realloc( tab, sizeof( *(tab) ) * ( (count) + 1 ) );
//else (tab) = cast malloc( sizeof( *(tab) ) );
//if( !(tab) ) abort();
//(tab)[count] = (p);
//(count)++; //p_sys->i_track incremented here
}
```
В последующем цикле for переменная p_sys->i_track используется как номер чанка в вызове AVI_ChunkFind, который возвращает n-ный чанк, соответствующий заданному значению FOURCC чанка, в нашем случае 'strl':
```
for( unsigned i = 0 ; i < p_sys->i_track; i++ )
{
avi_track_t *tk = p_sys->track[i];
if( tk->fmt.i_cat != AUDIO_ES ||
tk->idx.i_size < 1 ||
tk->i_scale != 1 ||
tk->i_samplesize != 0 )
continue;
//CVE-2021-25804
avi_chunk_list_t *p_strl = AVI_ChunkFind( p_hdrl, AVIFOURCC_strl, i, true );
//subsequently changed to: avi_chunk_list_t *p_strl = AVI_ChunkFind( p_hdrl, AVIFOURCC_strl, tk->fmt.i_id, true );
avi_chunk_strf_t *p_strf = AVI_ChunkFind( p_strl, AVIFOURCC_strf, 0, false );
if( !p_strf || p_strf->i_cat != AUDIO_ES )
continue;
const WAVEFORMATEX *p_wf = p_strf->u.p_wf;
```
Хотя p_sys->i_track подсчитывает правильное количество завершенных дорожек, он не отслеживает, какой номер чанка ей соответствовал. Это означает, что если перед допустимым чанком strl находится недопустимый чанк strl, первый чанк будет правильно проигнорирован при настройке объекта дорожки, но при вызове ChunkFind все равно будет выбран первый, недопустимый чанк.
После создания CVE-2021-25804 проблема была исправлена путем замены переменной номера чанка в ChunkFind с i на tk->fmt.i_id. tk->fmt.i_id впервые устанавливается чуть выше вызова TAB_APPEND(), который мы рассматривали ранее:
```
tk->fmt.i_id = i;
if( p_strn && p_strn->p_str )
tk->fmt.psz_description = FromACP( p_strn->p_str );
//see es_format_truncated.c
tk->p_es = es_out_Add( p_demux->out, &tk->fmt );
TAB_APPEND( p_sys->i_track, p_sys->track, tk );
```
Казалось бы, это исправляет проблему, так как tk->fmt.i_id устанавливается в i, и, следовательно, завершенный чанк должен быть тем, который выбирается ChunkFind(). Однако тестирование показывает, что на самом деле значение tk->fmt.i_id на момент проблемного вызова ChunkFind() равно -1. Это значение инициализации, установленное во время вызова es_format_Init().
```
void es_format_Init( es_format_t *fmt,
int i_cat, vlc_fourcc_t i_codec )
{
memset(fmt, 0, sizeof (*fmt));
fmt->i_cat = i_cat;
fmt->i_codec = i_codec;
fmt->i_profile = -1;
fmt->i_level = -1;
fmt->i_id = -1;
fmt->i_priority = ES_PRIORITY_SELECTABLE_MIN;
fmt->psz_language = NULL;
fmt->psz_description = NULL;
fmt->p_extra_languages = NULL;
if (fmt->i_cat == VIDEO_ES)
video_format_Init(&fmt->video, 0);
fmt->b_packetized = true;
fmt->p_extra = NULL;
}
```
Эта инициализация происходит в том же цикле for, где fmt->i_id устанавливается в i.
```
for( unsigned i = 0 ; i < i_track; i++ )
{
...
switch( p_strh->i_type )
{
case( AVIFOURCC_auds ):
{
es_format_Init( &tk->fmt, AUDIO_ES, 0 );
...
}
...
}
tk->fmt.i_id = i;
if( p_strn && p_strn->p_str )
tk->fmt.psz_description = FromACP( p_strn->p_str );
tk->p_es = es_out_Add( p_demux->out, &tk->fmt );
TAB_APPEND( p_sys->i_track, p_sys->track, tk );
}
```