CVE-2016-2414の分析 - Android Minikinライブラリに存在する領域外書き込みのDoS脆弱性

GoogleはMinikinライブラリに存在するDoS脆弱性(CVE-2016-2414)を今月公開のAndroid用パッチで修正しました。筆者がこの脆弱性をGoogleに報告したのは2016年3月上旬のことで、その後Googleは2015年11月に別の研究者によって報告された不具合「26413177」と重複する報告であることを確認しました。

本ブログでは、この脆弱性を詳しく分析していきます。この脆弱性の原因は、Minikinライブラリが.TTFフォントファイルを適切に解析できないことにあります。その結果、ローカルの攻撃者は当該のAndroidデバイスへのアクセスを一時的にブロックできてしまいます。攻撃者は不正なフォントファイルを読み込ませて、クラッシュの原因となるMinikinコンポーネントでのオーバーフローを引き起こすことが可能です。

クラッシュが発生するとAndroidが再起動を繰り返すため、Googleはこの脆弱性の深刻度を「高」と評価しています。

影響を受ける製品

Android 5.0.2、5.1.1、6.0、6.0.1

PoC(概念実証)

以下のコードスニペットを使用すると、この問題を発生させ検証することができます。関数setTypefaceは、外部からTextViewにカスタムフォントを読み込んで設定するために使用されます。

public class MyActivity extends Activity
    {
        public void onCreate(Bundle savedInstanceState)
        {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.main);
            TextView textView2 = (TextView) findViewById(R.id.textView2);
           Typeface typeface2 = Typeface.createFromFile(Environment.getExternalStorageDirectory() + "/fuzzed.ttf");
            textView2.setTypeface(typeface2);

        }
    }

以下はクラッシュログです。

--------- beginning of crash
04-04 16:44:43.230  5021  5021 F libc    : Fatal signal 11 (SIGSEGV), code 1, fault addr 0x36587 in tid 5021 (example.TTFFont)
04-04 16:44:43.248 32383 28153 I Icing   : Indexing done 215BF78BC46028A346E51FB3E9502079034D8D40
04-04 16:44:43.249 32383 28153 I Icing   : Indexing 4400EDF81586FA899DD2C6CB98D8E1B8279596F4 from com.google.android.gms
04-04 16:44:43.250 32383 28153 I Icing   : Indexing done 4400EDF81586FA899DD2C6CB98D8E1B8279596F4
04-04 16:44:43.332 10881 10881 F DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** 
04-04 16:44:43.332 10881 10881 F DEBUG   : Build fingerprint: 'google/hammerhead/hammerhead:6.0.1/MMB29V/2554798:user/release-keys'
04-04 16:44:43.332 10881 10881 F DEBUG   : Revision: '0' 04-04 16:44:43.332 10881 10881 F DEBUG   : ABI: 'arm'
04-04 16:44:43.332 10881 10881 F DEBUG   : pid: 5021, tid: 5021, name: example.TTFFont  >>> com.example.TTFFont <<<
04-04 16:44:43.332 10881 10881 F DEBUG   : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 0x36587
04-04 16:44:43.344 10881 10881 F DEBUG   :     r0 0000d962  r1 0000d938  r2 0000001e  r3 00000006
04-04 16:44:43.344 10881 10881 F DEBUG   :     r4 acb4ef7c  r5 0000001e  r6 ffffffff  r7 00000043
04-04 16:44:43.344 10881 10881 F DEBUG   :     r8 ab173400  r9 ffffffff  sl 0000002b  fp 07fffffe
04-04 16:44:43.345 10881 10881 F DEBUG   :     ip 07fffffd  sp bef6ce20  lr 00001e0c  pc b5db01e0  cpsr 00030030
04-04 16:44:43.347 10881 10881 F DEBUG   : 
04-04 16:44:43.347 10881 10881 F DEBUG   : backtrace:
04-04 16:44:43.347 10881 10881 F DEBUG   :     #00 pc 0000b1e0  /system/lib/libminikin.so (android::SparseBitSet::initFromRanges(unsigned int const*, unsigned int)+311)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #01 pc 0000652b  /system/lib/libminikin.so (android::CmapCoverage::getCoverage(android::SparseBitSet&, unsigned char const*, unsigned int)+498)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #02 pc 00006ee1  /system/lib/libminikin.so (android::FontFamily::getCoverage()+116)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #03 pc 0000685b  /system/lib/libminikin.so (android::FontCollection::FontCollection(std::__1::vector<android::FontFamily*, std::__1::allocator<android::FontFamily*> > const&)+150)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #04 pc 0009784b  /system/lib/libandroid_runtime.so (android::TypefaceImpl_createFromFamilies(long long const*, unsigned int)+94)
04-04 16:44:43.347 10881 10881 F DEBUG   :     #05 pc 0009752b  /system/lib/libandroid_runtime.so
04-04 16:44:43.347 10881 10881 F DEBUG   :     #06 pc 72e8db21  /data/dalvik-cache/arm/system@framework@boot.oat (offset 0x1ec9000)
04-04 16:44:43.649 10881 10881 F DEBUG   : 
04-04 16:44:43.649 10881 10881 F DEBUG   : Tombstone written to: /data/tombstones/tombstone_09
04-04 16:44:43.649 10881 10881 E DEBUG   : AM write failed: Broken pipe 

分析結果

この脆弱性がMinikinライブラリに存在している原因は、.TTFフォントファイルを適切に解析できないことにあります。これにより、領域外書き込みが発生し、DoSを引き起こす可能性があります。

まずは細工された.TTFフォントファイルを見てみましょう。最小版のPoCでは、オフセット0x60、0x58D、0x5D6、0xAD60でそれぞれ4つの相違点があります。通常のTTFファイルと最小化されたPoCファイルの比較を以下に示します。


図1. 通常のTTFファイルと最小版PoCファイルの比較(オフセット0x60の場合)


図2. 通常のTTFファイルと最小版PoCファイルの比較(オフセット0x58D、0x5D6の場合)


図3. 通常のTTFファイルと最小版PoCファイルの比較(オフセット0xAD60の場合)

図1と3では、TTFTemplateで010 Editorを使用して.TTFフォントファイルを解析しています。オフセット0x60の4バイトは、「cmap」テーブルの「checksum」フィールドです。オフセット0xAD60の4バイトは、「thead」ヘッド構造体の「checkSumAdjustment」フィールドです。上記2つのchecksumフィールドは、ファジング時に再計算され修正されます。これらによって脆弱性が引き起こされることはないので、無視しても問題ありません。以下は、最小版PoCファイルをTTFTemplateを使用して解析した結果です。


図4. 最小版PoCファイルの解析結果(オフセット0x58D、0x5D6の場合)

上記の図4を見ると、オフセット0x58Dと0x5D6の修正済みバイトは「cmap」テーブルに含まれていることが分かります。「cmap」テーブルに関する仕様は、https://www.microsoft.com/typography/otspec/cmap.htmに記載されています。「Format 4」に関する仕様は以下のとおりです。

「startCount[]」と「endCount[]」の比較を以下に示します。


図6. 「startCount[]」と「endCount[]」の比較

通常、配列startCount[]とendCount[]の各要素の値は順に増加します。一方、特定のインデックスiの場合、endCount[i]はstartCount[i]以上になります。しかし、最小版のPoCファイルではstartCount[30]を0x1E78に修正しているので、startCount[30] > endCount[30]となります。また、startCount[67]を0xE0FFに修正しているので、startCount[67] < startCount[66]になります。startCount[67]は最後の要素であり、仕様に従って0xFFFFになるはずです。

スタックバックトレースからは、SIGSEGVが関数android::SparseBitSet::initFromRangesで発生し、クラッシュのコードの場所を取得することが分かります。関数SparseBitSet::initFromRangesを以下に示します。

void SparseBitSet::initFromRanges(const uint32_t* ranges, size_t nRanges) {
   ...
   ...
   ...
        size_t index = ((currentPage - 1) << (kLogValuesPerPage - kLogBitsPerEl)) +
            ((start & kPageMask) >> kLogBitsPerEl);
        size_t nElements = (end - (start & ~kElMask) + kElMask) >> kLogBitsPerEl;
        if (nElements == 1) {
           mBitmaps[index] |= (kElAllOnes >> (start & kElMask)) &
                (kElAllOnes << ((-end) & kElMask));
        } else {
            mBitmaps[index] |= kElAllOnes >> (start & kElMask);
            for (size_t j = 1; j < nElements - 1; j++) {
                mBitmaps[index + j] = kElAllOnes;                      
 //crash here, it’s an out-of-bound write.
            }
           mBitmaps[index + nElements - 1] |= kElAllOnes << ((-end) & kElMask);    
       }
    ...
    ...
    ...
}

次に、IDA Proを使用して動的な分析をします。関数SparseBitSet::initFromRangesのエントリにブレークポイントを設定します。次に、デバッガーを実行して、以下のコードを確認します。


図7. 関数SparseBitSet::initFromRanges

上記の図7を見ると、R1が最初のパラメーター(const uint32_t* ranges)であり、R2が2番目のパラメーター(size_t nRanges)であり、0x43に等しくなることが分かります。R1によってポイントされているメモリを以下に示します。


図8. const uint32_t* rangesのメモリレイアウト

オフセット0xAB1734F0の4つのバイト |78 1E 00 00| にはstartCount[30] (|1E 78|) の値が格納され、それに続く4つのバイト |0C 1E 00 00| にはendCount[30]+1 (|1E 0B|+1) の値が格納されます。その後、プログラムがranges[30] (|78 1E 00 00 0C 1E 00 00|) を処理する際にトレースを継続します。


図9. loc_B5DB0128(1) の分析

図9を見ると、R2が0x1Eと等しい場合、レジスタR10はranges[30] (0xAB1734F0) をポイントすることが分かります。さらに分析を続けます。


図10. loc_B5DB0128(2) の分析

次に、loc_B5DB0176にジャンプします。


図11. loc_B5DB0176の分析

次に、loc_B5DB01D2から始まるループに入ります。レジスタR12はループの条件であり、数値が大きすぎます。[R4+8]には変数「mBitmaps」が格納され、R9は(ソースコードの)変数「mBitmaps」をポイントします。これを以下に示します。


図12. loc_B5DB01D2(1)の分析

続いて、アドレスB5DB01E0において、ループごとにSTR命令が実行され、0xFFFFFFFFがメモリ[R9+R0<<2]に書き込まれます(下図参照)。


図13. loc_B5DB01D2(2)の分析

次に、デバッガーを実行すると(F9キーを押すと)、次のようなポップアップダイアログが表示されます。

セグメンテーション違反がアドレス0xB5DB01E0で発生します。レジストリとメモリの情報を確認しましょう。


図14. セグメンテーション違反発生時のレジスタとメモリの情報

R4 (0xACB4F0FC) によってポイントされたメモリには、ループloc_B5DB01D2で0xFFFFFFFFが書き込まれます。さらに、[R4+8]も0xFFFFFFFFで上書きされたため、R9が0xFFFFFFFFになっています。R9+R0<<2は無効なメモリアドレスであるため、STR命令の実行時にセグメンテーション違反が発生します。

ソースコードの分析結果を以下に示します。

void SparseBitSet::initFromRanges(const uint32_t* ranges, size_t nRanges) {
   if (nRanges == 0) {
        mMaxVal = 0;
        mIndices.reset();
        mBitmaps.reset();
        return;
    }
    mMaxVal = ranges[nRanges * 2 - 1];
    size_t indexSize = (mMaxVal + kPageMask) >> kLogValuesPerPage;
    mIndices.reset(new uint32_t[indexSize]);
    uint32_t nPages = calcNumPages(ranges, nRanges);
    mBitmaps.reset(new element[nPages << (kLogValuesPerPage - kLogBitsPerEl)]);
    memset(mBitmaps.get(), 0, nPages << (kLogValuesPerPage - 3));
    mZeroPageIndex = noZeroPage;
    uint32_t nonzeroPageEnd = 0;
    uint32_t currentPage = 0;
    for (size_t i = 0; i < nRanges; i++) {      //when the variable i is eqaul to 0x1E, the program handles the modified data in 'startCount' array in PoC file.
        uint32_t start = ranges[i * 2];         //the variable start is eqaul to 0x1E78
        uint32_t end = ranges[i * 2 + 1];       //the variable end is eqaul to 0x1E0C
        uint32_t startPage = start >> kLogValuesPerPage;
        uint32_t endPage = (end - 1) >> kLogValuesPerPage;
       if (startPage >= nonzeroPageEnd) {
            if (startPage > nonzeroPageEnd) {
               if (mZeroPageIndex == noZeroPage) {
                    mZeroPageIndex = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl);
                }
                for (uint32_t j = nonzeroPageEnd; j < startPage; j++) {
                    mIndices[j] = mZeroPageIndex;
                }
            }
            mIndices[startPage] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl);
        }
 
        size_t index = ((currentPage - 1) << (kLogValuesPerPage - kLogBitsPerEl)) +
            ((start & kPageMask) >>
kLogBitsPerEl);                          // the variable index is equal to 0x2B from dynamic debugging.
        size_t nElements = (end - (start & ~kElMask) + kElMask) >> kLogBitsPerEl;  // nElements = (0x1E0C - (0x1E78 & ~0x1F) + 0x1F) >> 5 = 0x07FFFFFE, because start is greater than end, it causes a overflow when doing subtraction.
        if (nElements == 1) {
           mBitmaps[index] |= (kElAllOnes >> (start & kElMask)) &
                (kElAllOnes << ((-end) & kElMask));
        } else {
            mBitmaps[index] |= kElAllOnes >> (start & kElMask);
            for (size_t j = 1; j < nElements - 1; j++)
{        // the loop condition is j < 0x07FFFFFD, it's a too large value.
                mBitmaps[index + j] = kElAllOnes;               //crash here, it's out-of-bound write.
            }
           mBitmaps[index + nElements - 1] |= kElAllOnes << ((-end) & kElMask);
       }
        for (size_t j = startPage + 1; j < endPage + 1; j++) {
            mIndices[j] = (currentPage++) << (kLogValuesPerPage - kLogBitsPerEl);
        }
        nonzeroPageEnd = endPage + 1;
    }
}

まとめ

この脆弱性は、Minikinが「cmap」テーブルで無効な範囲をチェックしていないために存在しています。破損したフォントファイルや悪意のあるフォントファイルに含まれる範囲のサイズがマイナスであることがあり、その場合メモリが破損する可能性があります。攻撃者は破損したフォントファイルを読み込んで、Minikinコンポーネントでオーバーフローを引き起こすことができます。クラッシュが発生すると、Androidが再起動を繰り返す可能性があります。