
Detaillierte technische Analyse von CVE-2021-25804, einer Schwachstelle im VLC-AVI-Parser. Enthält Grundursache, Patch-Diff und Ausnutzungsprimitive für Sicherheitsforscher.
Eine AVI-Datei kann mehrere Spuren enthalten. Zum Beispiel eine Audiospur und eine Videospur.
Um eine beliebige Anzahl von Spuren zu parsen, muss VLC zunächst feststellen, ob eine bestimmte Spur abgeschlossen und abspielbar ist. Zu diesem Zweck werden zwei Variablen eingerichtet. Beide heißen i_track.
Die Variable p_sys repräsentiert für unsere Zwecke die AVI-Datei. Die "offizielle" Variable i_track, mit der die Spuren referenziert werden, die als abgeschlossen und abspielbar eingestuft wurden, ist p_sys->i_track. Sie wird hier mit einem Makro initialisiert:
```
TAB_INIT(p_sys->i_track, p_sys->track);
//#define TAB_INIT( count, tab ) do {(count) = 0; (tab) = NULL; } while(0)
```
Währenddessen wird eine zweite Variable i_track eingerichtet, um ALLE Spuren zu zählen, unabhängig davon, ob sie abgeschlossen/abspielbar sind oder nicht.
```
i_track = AVI_ChunkCount( p_hdrl, AVIFOURCC_strl, true );
```
Die zweite Variable i_track repräsentiert alle Chunks, die mit 'strl' beginnen. Sie steuert eine 'for'-Schleife, die die Variable p_sys->i_track nur dann setzt, wenn der jeweilige Track eine Reihe von Prüfungen besteht:
```
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
}
```
Während einer nachfolgenden 'for'-Schleife wird die Variable p_sys->i_track als Chunk-Nummer in einem Aufruf von AVI_ChunkFind verwendet, die den n-ten Chunk zurückgibt, der mit einem gegebenen FOURCC-Chunk-Wert übereinstimmt, in unserem Fall '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;
```
Obwohl p_sys->i_track die korrekte Anzahl der abgeschlossenen Spuren zählt, wird nicht nachverfolgt, auf welche Chunk-Nummer es gesetzt wurde. Das bedeutet: Wenn ein ungültiger strl-Chunk vor dem gültigen strl-Chunk vorhanden ist, wird der erste Chunk beim Einrichten des Track-Objekts korrekt ignoriert, aber wenn ChunkFind aufgerufen wird, wird immer noch der erste, ungültige Chunk ausgewählt.
Nachdem CVE-2021-25804 erstellt wurde, wurde das Problem behoben, indem die Chunk-Nummer-Variable von ChunkFind von i auf tk->fmt.i_id geändert wurde. tk->fmt.i_id wird zuerst direkt über dem TAB_APPEND()-Aufruf gesetzt, den wir uns zuvor angesehen haben:
```
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 );
```
Dies scheint das Problem zu beheben, da tk->fmt.i_id auf i gesetzt wird und der abgeschlossene Chunk daher der von ChunkFind() ausgewählte sein sollte. Tests zeigen jedoch, dass der Wert von tk->fmt.i_id zum Zeitpunkt des fehlerhaften Aufrufs von ChunkFind() tatsächlich auf -1 gesetzt ist. Dies ist der Initialisierungswert, der während eines Aufrufs von es_format_Init() gesetzt wird.
```
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;
}
```
Diese Initialisierung erfolgt in derselben 'for'-Schleife, in der fmt->i_id auf i gesetzt wird.
```
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 );
}
```