diff mbox

[FFmpeg-devel,2/3] avformat/mov: Fix parsing of saio/siaz atoms in encrypted content.

Message ID CAO7y9i89sikBJX_n+U-nDf9qECr_sp9-F5BSTdOP1fK1moEGWg@mail.gmail.com
State Superseded
Headers show

Commit Message

Jacob Trimble Jan. 5, 2018, 9:29 p.m. UTC
On Fri, Jan 5, 2018 at 12:41 PM, Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:
> 2018-01-05 20:49 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
>
>> +    entry_count = avio_rb32(pb);
>> +    encryption_index->auxiliary_offsets = av_malloc_array(sizeof(size_t), entry_count);
>
> (sizeof(variable) instead of sizeof(type), please.)
>
> But since this could be used for a dos attack, please change this
> to something similar to 1112ba01.
> If it is easy to avoid it, very short files should not allocate
> gigabytes.

Switched to calculating the size based on the number of remaining
bytes and returning an error if it doesn't match what is read.

>
> Carl Eugen
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel

Comments

James Almer Jan. 5, 2018, 9:46 p.m. UTC | #1
On 1/5/2018 6:29 PM, Jacob Trimble wrote:
> diff --git a/libavformat/mov.c b/libavformat/mov.c
> index eb3fb71e2a..9ff4a809b7 100644
> --- a/libavformat/mov.c
> +++ b/libavformat/mov.c
> @@ -5835,6 +5835,177 @@ static int mov_read_senc(MOVContext *c, AVIOContext *pb, MOVAtom atom)
>      return 0;
>  }
>  
> +static int mov_parse_auxiliary_info(MOVContext *c, MOVStreamContext *sc, AVIOContext *pb, MOVEncryptionIndex *encryption_index)
> +{
> +    AVEncryptionInfo **sample;
> +    int64_t prev_pos;
> +    size_t sample_count, sample_info_size, i;
> +    int ret = 0;
> +
> +    if (encryption_index->nb_encrypted_samples)
> +        return 0;
> +    sample_count = encryption_index->auxiliary_info_sample_count;
> +    if (encryption_index->auxiliary_offsets_count != 1) {
> +        av_log(c->fc, AV_LOG_ERROR, "Multiple auxiliary info chunks are not supported\n");
> +        return AVERROR_PATCHWELCOME;
> +    }
> +
> +    encryption_index->encrypted_samples = av_mallocz_array(sizeof(AVEncryptionInfo*), sample_count);

Number of elements should be the first argument, and size of each
element the second argument.
Carl Eugen Hoyos Jan. 5, 2018, 10:01 p.m. UTC | #2
2018-01-05 22:29 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
> On Fri, Jan 5, 2018 at 12:41 PM, Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:
>> 2018-01-05 20:49 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
>>
>>> +    entry_count = avio_rb32(pb);
>>> +    encryption_index->auxiliary_offsets = av_malloc_array(sizeof(size_t), entry_count);
>>
>> (sizeof(variable) instead of sizeof(type), please.)
>>
>> But since this could be used for a dos attack, please change this
>> to something similar to 1112ba01.
>> If it is easy to avoid it, very short files should not allocate
>> gigabytes.
>
> Switched to calculating the size based on the number of remaining
> bytes and returning an error if it doesn't match what is read.

Sorry if I miss something:

> +    entry_count = (atom.size - 8 - (has_saio_type ? 8 : 0)) / (version == 0 ? 4 : 8);
> +    if (avio_rb32(pb) != entry_count) {
> +        av_log(c->fc, AV_LOG_ERROR, "incorrect entry_count in saio\n");
> +        return AVERROR_INVALIDDATA;
> +    }
> +    encryption_index->auxiliary_offsets =
> +        av_malloc_array(sizeof(*encryption_index->auxiliary_offsets), entry_count);

Does this avoid a 1G allocation for a file of a few bytes?

Didn't you simply increase the number of needed bytes to change in a valid
mov file to behave maliciously from one to two?

Thank you, Carl Eugen
Jacob Trimble Jan. 5, 2018, 10:58 p.m. UTC | #3
On Fri, Jan 5, 2018 at 2:01 PM, Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:
> 2018-01-05 22:29 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
>> On Fri, Jan 5, 2018 at 12:41 PM, Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:
>>> 2018-01-05 20:49 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
>>>
>>>> +    entry_count = avio_rb32(pb);
>>>> +    encryption_index->auxiliary_offsets = av_malloc_array(sizeof(size_t), entry_count);
>>>
>>> (sizeof(variable) instead of sizeof(type), please.)
>>>
>>> But since this could be used for a dos attack, please change this
>>> to something similar to 1112ba01.
>>> If it is easy to avoid it, very short files should not allocate
>>> gigabytes.
>>
>> Switched to calculating the size based on the number of remaining
>> bytes and returning an error if it doesn't match what is read.
>
> Sorry if I miss something:
>
>> +    entry_count = (atom.size - 8 - (has_saio_type ? 8 : 0)) / (version == 0 ? 4 : 8);
>> +    if (avio_rb32(pb) != entry_count) {
>> +        av_log(c->fc, AV_LOG_ERROR, "incorrect entry_count in saio\n");
>> +        return AVERROR_INVALIDDATA;
>> +    }
>> +    encryption_index->auxiliary_offsets =
>> +        av_malloc_array(sizeof(*encryption_index->auxiliary_offsets), entry_count);
>
> Does this avoid a 1G allocation for a file of a few bytes?
>
> Didn't you simply increase the number of needed bytes to change in a valid
> mov file to behave maliciously from one to two?
>

From what I can tell, the mov_read_default method (which is what reads
child atoms) will verify "atom.size" to fit inside the parent atom.
This means that for "atom.size" to be excessively large for the file
size, the input would need to be non-seekable (since I think the
top-level atom uses the file size) and all the atoms would need to
have invalid sizes.  But technically I guess it is possible.

But this is basically malloc some number of bytes then read that many
bytes.  The only alternative I can think of (in the face of
non-seekable inputs) is to try-read in chunks until we hit EOF or the
end of the expected size.  This seems like a lot of extra work that is
not mirrored elsewhere.  There are several cases where this isn't
explicitly checked.

Also, how does this attack work?  If the number is way too big, well
get EOM and error out.  If it is large but there is enough memory,
we'll hit EOF and error out.  Would it be better to explicitly check
for EOF to avoid the loop running for longer than needed?  Or how
about freeing the memory on error so we don't keep the useless memory
around on the error case (even though the app will probably just free
the context anyway).

> Thank you, Carl Eugen
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> http://ffmpeg.org/mailman/listinfo/ffmpeg-devel
Carl Eugen Hoyos Jan. 5, 2018, 11:41 p.m. UTC | #4
2018-01-05 23:58 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
> On Fri, Jan 5, 2018 at 2:01 PM, Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:
>> 2018-01-05 22:29 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
>>> On Fri, Jan 5, 2018 at 12:41 PM, Carl Eugen Hoyos <ceffmpeg@gmail.com> wrote:
>>>> 2018-01-05 20:49 GMT+01:00 Jacob Trimble <modmaker-at-google.com@ffmpeg.org>:
>>>>
>>>>> +    entry_count = avio_rb32(pb);
>>>>> +    encryption_index->auxiliary_offsets = av_malloc_array(sizeof(size_t), entry_count);
>>>>
>>>> (sizeof(variable) instead of sizeof(type), please.)
>>>>
>>>> But since this could be used for a dos attack, please change this
>>>> to something similar to 1112ba01.
>>>> If it is easy to avoid it, very short files should not allocate
>>>> gigabytes.
>>>
>>> Switched to calculating the size based on the number of remaining
>>> bytes and returning an error if it doesn't match what is read.
>>
>> Sorry if I miss something:
>>
>>> +    entry_count = (atom.size - 8 - (has_saio_type ? 8 : 0)) / (version == 0 ? 4 : 8);
>>> +    if (avio_rb32(pb) != entry_count) {
>>> +        av_log(c->fc, AV_LOG_ERROR, "incorrect entry_count in saio\n");
>>> +        return AVERROR_INVALIDDATA;
>>> +    }
>>> +    encryption_index->auxiliary_offsets =
>>> +        av_malloc_array(sizeof(*encryption_index->auxiliary_offsets), entry_count);
>>
>> Does this avoid a 1G allocation for a file of a few bytes?
>>
>> Didn't you simply increase the number of needed bytes to change in a valid
>> mov file to behave maliciously from one to two?
>
> From what I can tell, the mov_read_default method (which is what reads
> child atoms) will verify "atom.size" to fit inside the parent atom.
> This means that for "atom.size" to be excessively large for the file
> size, the input would need to be non-seekable (since I think the
> top-level atom uses the file size) and all the atoms would need to
> have invalid sizes.

(I did not check this but I am not convinced the sample I fixed last
week is so sophisticated.)

> But technically I guess it is possible.

Thank you.

> But this is basically malloc some number of bytes then read that many
> bytes.  The only alternative I can think of (in the face of
> non-seekable inputs) is to try-read in chunks until we hit EOF or the
> end of the expected size.  This seems like a lot of extra work that is

> not mirrored elsewhere.

On the contrary.

But you are right, I forgot to write that you have to add an "if (!eof)"
to the reading loops, see the function that above commit changed.

> There are several cases where this isn't explicitly checked.

Yes, and they will be fixed, please don't add another one.

> Also, how does this attack work?  If the number is way too big, well
> get EOM and error out.

Which not only causes dos but also makes checking for other (if you
like: more dangerous) issues more difficult which is bad. We already
know that there are cases where the issue is hard to avoid, I believe
this is not such a case.

It is possible to create (valid) samples that allocate a huge amount
of memory but very small files should not immediately allocate an
amount of several G.

Carl Eugen
diff mbox

Patch

From 3c60b4bf10f8711a8db70bf74cc5e4b8ce3d50e0 Mon Sep 17 00:00:00 2001
From: Jacob Trimble <modmaker@google.com>
Date: Wed, 6 Dec 2017 16:17:54 -0800
Subject: [PATCH] avformat/mov: Fix parsing of saio/siaz atoms in encrypted
 content.

This doesn't support saio atoms with more than one offset.

Signed-off-by: Jacob Trimble <modmaker@google.com>
---
 libavformat/isom.h |   6 ++
 libavformat/mov.c  | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 192 insertions(+)

diff --git a/libavformat/isom.h b/libavformat/isom.h
index 3794b1f0fd..3de8053da2 100644
--- a/libavformat/isom.h
+++ b/libavformat/isom.h
@@ -114,6 +114,12 @@  typedef struct MOVEncryptionIndex {
     // settings will be used.
     unsigned int nb_encrypted_samples;
     AVEncryptionInfo **encrypted_samples;
+
+    uint8_t* auxiliary_info_sizes;
+    size_t auxiliary_info_sample_count;
+    uint8_t auxiliary_info_default_size;
+    size_t* auxiliary_offsets;  ///< Absolute seek position
+    size_t auxiliary_offsets_count;
 } MOVEncryptionIndex;
 
 typedef struct MOVFragmentStreamInfo {
diff --git a/libavformat/mov.c b/libavformat/mov.c
index eb3fb71e2a..9ff4a809b7 100644
--- a/libavformat/mov.c
+++ b/libavformat/mov.c
@@ -5835,6 +5835,177 @@  static int mov_read_senc(MOVContext *c, AVIOContext *pb, MOVAtom atom)
     return 0;
 }
 
+static int mov_parse_auxiliary_info(MOVContext *c, MOVStreamContext *sc, AVIOContext *pb, MOVEncryptionIndex *encryption_index)
+{
+    AVEncryptionInfo **sample;
+    int64_t prev_pos;
+    size_t sample_count, sample_info_size, i;
+    int ret = 0;
+
+    if (encryption_index->nb_encrypted_samples)
+        return 0;
+    sample_count = encryption_index->auxiliary_info_sample_count;
+    if (encryption_index->auxiliary_offsets_count != 1) {
+        av_log(c->fc, AV_LOG_ERROR, "Multiple auxiliary info chunks are not supported\n");
+        return AVERROR_PATCHWELCOME;
+    }
+
+    encryption_index->encrypted_samples = av_mallocz_array(sizeof(AVEncryptionInfo*), sample_count);
+    if (!encryption_index->encrypted_samples)
+        return AVERROR(ENOMEM);
+    encryption_index->nb_encrypted_samples = sample_count;
+
+    prev_pos = avio_tell(pb);
+    if (avio_seek(pb, encryption_index->auxiliary_offsets[0], SEEK_SET) != encryption_index->auxiliary_offsets[i]) {
+        av_log(c->fc, AV_LOG_INFO, "Failed to seek for auxiliary info, will only parse senc atoms for encryption info\n");
+        goto finish;
+    }
+
+    for (i = 0; i < sample_count; i++) {
+        sample = &encryption_index->encrypted_samples[i];
+        sample_info_size = encryption_index->auxiliary_info_default_size
+                               ? encryption_index->auxiliary_info_default_size
+                               : encryption_index->auxiliary_info_sizes[i];
+
+        ret = mov_read_sample_encryption_info(c, pb, sc, sample, sample_info_size > sc->cenc.per_sample_iv_size);
+        if (ret < 0)
+            goto finish;
+    }
+
+finish:
+    avio_seek(pb, prev_pos, SEEK_SET);
+    return ret;
+}
+
+static int mov_read_saiz(MOVContext *c, AVIOContext *pb, MOVAtom atom)
+{
+    MOVEncryptionIndex *encryption_index;
+    MOVStreamContext *sc;
+    int ret;
+    unsigned int sample_count, has_saiz_type;
+
+    ret = get_current_encryption_info(c, &encryption_index, &sc);
+    if (ret != 1)
+      return ret;
+
+    if (encryption_index->nb_encrypted_samples) {
+        // This can happen if we have both saio/saiz and senc atoms.
+        av_log(c->fc, AV_LOG_DEBUG, "ignoring duplicate encryption info in saiz\n");
+        return 0;
+    }
+
+    if (encryption_index->auxiliary_info_sample_count) {
+        av_log(c->fc, AV_LOG_ERROR, "duplicate saiz atom\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    avio_r8(pb); /* version */
+    has_saiz_type = avio_rb24(pb) & 0x01;  /* flags */
+    if (has_saiz_type) {
+        if (avio_rb32(pb) != sc->cenc.default_encrypted_sample->scheme) {
+            av_log(c->fc, AV_LOG_DEBUG, "ignoring saiz box with non-zero aux_info_type\n");
+            return 0;
+        }
+        if (avio_rb32(pb) != 0) {
+            av_log(c->fc, AV_LOG_DEBUG, "ignoring saiz box with non-zero aux_info_type_parameter\n");
+            return 0;
+        }
+    }
+
+    encryption_index->auxiliary_info_default_size = avio_r8(pb);
+    sample_count = avio_rb32(pb);
+    encryption_index->auxiliary_info_sample_count = sample_count;
+
+    if (encryption_index->auxiliary_info_default_size == 0) {
+        if (atom.size - 9 - (has_saiz_type ? 8 : 0) != sample_count) {
+            av_log(c->fc, AV_LOG_ERROR, "incorrect sample_count in saiz\n");
+            return AVERROR_INVALIDDATA;
+        }
+
+        encryption_index->auxiliary_info_sizes = av_malloc(sample_count);
+        if (!encryption_index->auxiliary_info_sizes)
+            return AVERROR(ENOMEM);
+        if (avio_read(pb, encryption_index->auxiliary_info_sizes, sample_count) != sample_count) {
+            av_log(c->fc, AV_LOG_ERROR, "failed to read the auxiliary info\n");
+            return AVERROR_INVALIDDATA;
+        }
+    }
+
+    if (encryption_index->auxiliary_offsets_count) {
+        return mov_parse_auxiliary_info(c, sc, pb, encryption_index);
+    }
+
+    return 0;
+}
+
+static int mov_read_saio(MOVContext *c, AVIOContext *pb, MOVAtom atom)
+{
+    MOVEncryptionIndex *encryption_index;
+    MOVStreamContext *sc;
+    int i, ret;
+    unsigned int version, entry_count, has_saio_type;
+
+    ret = get_current_encryption_info(c, &encryption_index, &sc);
+    if (ret != 1)
+      return ret;
+
+    if (encryption_index->nb_encrypted_samples) {
+        // This can happen if we have both saio/saiz and senc atoms.
+        av_log(c->fc, AV_LOG_DEBUG, "ignoring duplicate encryption info in saio\n");
+        return 0;
+    }
+
+    if (encryption_index->auxiliary_offsets_count) {
+        av_log(c->fc, AV_LOG_ERROR, "duplicate saio atom\n");
+        return AVERROR_INVALIDDATA;
+    }
+
+    version = avio_r8(pb); /* version */
+    has_saio_type = avio_rb24(pb) & 0x01;  /* flags */
+    if (has_saio_type) {
+        if (avio_rb32(pb) != sc->cenc.default_encrypted_sample->scheme) {
+            av_log(c->fc, AV_LOG_DEBUG, "ignoring saio box with non-zero aux_info_type\n");
+            return 0;
+        }
+        if (avio_rb32(pb) != 0) {
+            av_log(c->fc, AV_LOG_DEBUG, "ignoring saio box with non-zero aux_info_type_parameter\n");
+            return 0;
+        }
+    }
+
+    entry_count = (atom.size - 8 - (has_saio_type ? 8 : 0)) / (version == 0 ? 4 : 8);
+    if (avio_rb32(pb) != entry_count) {
+        av_log(c->fc, AV_LOG_ERROR, "incorrect entry_count in saio\n");
+        return AVERROR_INVALIDDATA;
+    }
+    encryption_index->auxiliary_offsets =
+        av_malloc_array(sizeof(*encryption_index->auxiliary_offsets), entry_count);
+    if (!encryption_index->auxiliary_offsets)
+        return AVERROR(ENOMEM);
+    encryption_index->auxiliary_offsets_count = entry_count;
+
+    if (version == 0) {
+        for (i = 0; i < entry_count; i++) {
+          encryption_index->auxiliary_offsets[i] = avio_rb32(pb);
+        }
+    } else {
+        for (i = 0; i < entry_count; i++) {
+          encryption_index->auxiliary_offsets[i] = avio_rb64(pb);
+        }
+    }
+    if (c->frag_index.current >= 0) {
+        for (i = 0; i < entry_count; i++) {
+            encryption_index->auxiliary_offsets[i] += c->fragment.base_data_offset;
+        }
+    }
+
+    if (encryption_index->auxiliary_info_sample_count) {
+        return mov_parse_auxiliary_info(c, sc, pb, encryption_index);
+    }
+
+    return 0;
+}
+
 static int mov_read_schm(MOVContext *c, AVIOContext *pb, MOVAtom atom)
 {
     AVStream *st;
@@ -6050,6 +6221,17 @@  static int cenc_filter(MOVContext *mov, MOVStreamContext *sc, AVPacket *pkt, int
     }
 
     if (encryption_index) {
+        if (encryption_index->auxiliary_info_sample_count &&
+            !encryption_index->nb_encrypted_samples) {
+            av_log(mov->fc, AV_LOG_ERROR, "saiz atom found without saio\n");
+            return AVERROR_INVALIDDATA;
+        }
+        if (encryption_index->auxiliary_offsets_count &&
+            !encryption_index->nb_encrypted_samples) {
+            av_log(mov->fc, AV_LOG_ERROR, "saio atom found without saiz\n");
+            return AVERROR_INVALIDDATA;
+        }
+
         if (!encryption_index->nb_encrypted_samples) {
             // Full-sample encryption with default settings.
             encrypted_sample = sc->cenc.default_encrypted_sample;
@@ -6194,6 +6376,8 @@  static const MOVParseTableEntry mov_default_parse_table[] = {
 { MKTAG('s','i','n','f'), mov_read_default },
 { MKTAG('f','r','m','a'), mov_read_frma },
 { MKTAG('s','e','n','c'), mov_read_senc },
+{ MKTAG('s','a','i','z'), mov_read_saiz },
+{ MKTAG('s','a','i','o'), mov_read_saio },
 { MKTAG('s','c','h','m'), mov_read_schm },
 { MKTAG('s','c','h','i'), mov_read_default },
 { MKTAG('t','e','n','c'), mov_read_tenc },
@@ -6589,6 +6773,8 @@  static void mov_free_encryption_index(MOVEncryptionIndex **index) {
         av_encryption_info_free((*index)->encrypted_samples[i]);
     }
     av_freep(&(*index)->encrypted_samples);
+    av_freep(&(*index)->auxiliary_info_sizes);
+    av_freep(&(*index)->auxiliary_offsets);
     av_freep(index);
 }
 
-- 
2.16.0.rc0.223.g4a4ac83678-goog