flawopen.com/Teardowns/cve-2015-3824-android-stagefright-tx3g-integer-overflow

● CVE-2015-3824 · CVSS v2 10.0 · 高
セキュリティ研究 · FlawOpen

CVE-2015-3824:Android libstagefright における Stagefright tx3g 整数オーバーフロー

MPEG-4 の字幕アトム tx3g の解析中に、検証のないサイズ加算が桁あふれしてごく小さなヒープバッファを確保し、細工された動画がユーザー操作なしで mediaserver のメモリを破壊できた仕組み。

💡 わかりやすい解説 (ELI5)

横断幕の長さを書く欄が4桁しかない印刷所の注文票を想像してください。すでに 9998 メートルが記録されていて、さらに 5 メートル追加を頼みます。店員が合計を4桁の欄に書くと数字が一周して 0003 になり、印刷所は 3 メートルの紙を切り出します。ところが印刷機は 10003 メートル分の文字をその紙に刷り続け、インクが机の上にある他の客の注文にまであふれ出します。Stagefright はメモリで同じことをしました。サイズの合計が一周して小さなバッファだけが確保され、そこに動画のデータがすべて書き込まれたのです。

主要な概念と専門用語

Stagefright(libstagefright)
特権プロセス mediaserver の中で音声・動画ファイルを解析する Android のネイティブメディアライブラリ。
MPEG-4 アトム
MP4 ファイルを構成する自己記述型のブロック。宣言されたサイズ、4文字の種別、データからなり、パーサーは宣言サイズを信じて読み込む量を決める。
tx3g
テキストトラックの字幕書式を格納する 3GPP のタイムドテキスト用アトム。
整数オーバーフロー
計算結果が整数型に収まらず小さな値に戻ってしまう現象。たとえば SIZE_MAX + 2 が 1 になる。

根本原因の分析 (Root Cause)

MPEG4Extractor::parseChunk() は、保存済みの書式データのサイズを、攻撃者が制御できる tx3g アトムの chunk_size に加算する際にオーバーフローを確認していなかった。一周した合計値でヒープ確保のサイズが決まり、その後 readAt() が chunk_size バイトすべてをそこへコピーした。

ステップ・バイ・ステップの攻撃フロー

Step 1

細工された動画が届く

攻撃者は tx3g アトムの chunk_size に最大値付近の値を宣言した MP4 ファイルを、MMS やメッセージアプリで送りつける。

Step 2

解析が自動で始まる

Android 5.1 以前では、プレビューやサムネイル作成のためにメディアサービスが受信したメディアを自動解析したため、タップは不要だった。

Step 3

サイズ計算が一周する

size + chunk_size がオーバーフローし、new[] はわずか数バイトのバッファしか確保しない。

Step 4

ヒープが上書きされる

readAt() が chunk_size バイトを小さなバッファへコピーして mediaserver のヒープを破壊し、攻撃者はそのプロセスの権限でコードを実行できる。

ソースコード比較:脆弱 vs 堅牢化

脆弱な実装
// Simplified from MPEG4Extractor::parseChunk() in libstagefright, before the fix
case FOURCC('t', 'x', '3', 'g'):
{
    uint32_t type;
    const void *data;
    size_t size = 0;
    if (!mLastTrack->meta->findData(kKeyTextFormatData, &type, &data, &size)) {
        size = 0;
    }

    // chunk_size is read from the file. size + chunk_size can wrap around,
    // so a tiny buffer is allocated for a huge copy.
    uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
    if (buffer == NULL) {
        return ERROR_MALFORMED;
    }
    if (size > 0) {
        memcpy(buffer, data, size);
    }
    // Copies the full chunk_size bytes into the undersized buffer.
    if ((size_t)mDataSource->readAt(*offset, buffer + size, chunk_size) < chunk_size) {
        delete[] buffer;
        return ERROR_IO;
    }
    mLastTrack->meta->setData(kKeyTextFormatData, 0, buffer, size + chunk_size);
    delete[] buffer;
    *offset += chunk_size;
    break;
}
堅牢化されたセキュアパッチ
// Simplified from MPEG4Extractor::parseChunk() in libstagefright, after the fix
case FOURCC('t', 'x', '3', 'g'):
{
    uint32_t type;
    const void *data;
    size_t size = 0;
    if (!mLastTrack->meta->findData(kKeyTextFormatData, &type, &data, &size)) {
        size = 0;
    }

    // Reject any length whose sum would overflow before allocating.
    if (chunk_size < 0 || (uint64_t)chunk_size > SIZE_MAX - size) {
        return ERROR_MALFORMED;
    }

    uint8_t *buffer = new (std::nothrow) uint8_t[size + chunk_size];
    if (buffer == NULL) {
        return ERROR_MALFORMED;
    }
    if (size > 0) {
        memcpy(buffer, data, size);
    }
    if ((size_t)mDataSource->readAt(*offset, buffer + size, chunk_size) < chunk_size) {
        delete[] buffer;
        return ERROR_IO;
    }
    mLastTrack->meta->setData(kKeyTextFormatData, 0, buffer, size + chunk_size);
    delete[] buffer;
    *offset += chunk_size;
    break;
}

エンジニアリング&システム堅牢化チェックリスト

参考資料