
CVE-2021-25804, VLC AVI 파서 취약점에 대한 심층 기술 분석. 근본 원인, 패치 차이(diff), 그리고 보안 연구자를 위한 익스플로잇 기본 요소를 포함합니다.
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'로 시작하는 모든 청크를 나타냅니다. 이 변수는 일련의 검사를 통과한 경우에만 p_sys->i_track 변수를 설정하는 'for' 루프를 제어합니다:
```
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 호출 시 청크 번호로 사용됩니다. 이 함수는 주어진 FOURCC 청크 값과 일치하는 n번째 청크를 반환하며, 여기서는 '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()에서 선택되어야 하므로 이 문제가 해결된 것처럼 보입니다. 그러나 테스트 결과 문제가 되는 ChunkFind() 호출 시점에 tk->fmt.i_id 값이 실제로 -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;
}
```
이 초기화는 fmt->i_id가 i로 설정되는 것과 동일한 'for' 루프에서 발생합니다.
```
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 );
}
```