Cisco Japan Blog

有害なアーカイブ

4 min read



脆弱性発見者:Marcin “Icewall” Nogapopup_icon ブログ記事執筆者:Marcin Nogapopup_iconJaeson Schultzpopup_icon

libarchivepopup_icon は、多様なファイル アーカイブ フォーマットへのアクセスを提供するオープンソース ライブラリで、さまざまな場面popup_iconで使用されています。Cisco Talos はこの程、libarchive のメンテナンス担当者と協力して、このライブラリに潜む 3 つの重大なバグを修正するパッチを開発しました。libarchive は、多くの製品で圧縮ファイルの処理に使用されています。関連する脆弱性が存在するソフトウェアに、パッチまたはアップグレードを適用することをすべてのユーザに強く推奨します。

TALOS-2016-0152 [CVE-2016-4300]:7-Zip READ_SUBSTREAMSINFO 整数オーバーフローpopup_icon

7-Zip に、コードの実行につながる新たな脆弱性が見つかりました(7-Zip の脆弱性に関する以前のブログ記事はこちら)。 この脆弱性は、特別な細工が施された 7-Zip ファイルによって整数オーバーフローが引き起こされ、その結果メモリが破壊されてコードが実行される可能性があるというものです。攻撃者は、libarchive で処理される有害な 7-Zip ファイルを被害者に送信するだけで、この脆弱性を悪用できます。

脆弱性のあるコードは、libarchive\archive_read_support_format_7zip.c という 7-Zip サポート フォーマット モジュールに存在します。

(...)
#define UMAX_ENTRY        ARCHIVE_LITERAL_ULL(100000000)
(...)
Line 2129        static int
Line 2130        read_SubStreamsInfo(struct archive_read *a, struct _7z_substream_info *ss,
Line 2131                struct _7z_folder *f, size_t numFolders)
Line 2132        {
Line 2133                const unsigned char *p;
Line 2134                uint64_t *usizes;
Line 2135                size_t unpack_streams;
Line 2136                int type;
Line 2137                unsigned i;
Line 2138                uint32_t numDigests;
(...)
Line 2149        if (type == kNumUnPackStream) {
Line 2150                unpack_streams = 0;
Line 2151                for (i = 0; i < numFolders; i++) {
Line 2152                        if (parse_7zip_uint64(a, &(f[i].numUnpackStreams)) < 0)
Line 2153                                return (-1);
Line 2154                        if (UMAX_ENTRY < f[i].numUnpackStreams)
Line 2155                                return (-1);
Line 2156                        unpack_streams += (size_t)f[i].numUnpackStreams;    
^^^^^^^^^  ---- INTEGER OVERFLOW
Line 2157                }
Line 2158                if ((p = header_bytes(a, 1)) == NULL)
Line 2159                        return (-1);
Line 2160                type = *p;
Line 2161        } else
Line 2162                unpack_streams = numFolders;
Line 2163
Line 2164        ss->unpack_streams = unpack_streams;
Line 2165        if (unpack_streams) {
Line 2166                ss->unpackSizes = calloc(unpack_streams,                                
^^^^^^^^^  ---- ALLOCATION BASED ON OVERFLOWED INT
Line 2167                    sizeof(*ss->unpackSizes));
Line 2168                ss->digestsDefined = calloc(unpack_streams,
Line 2169                    sizeof(*ss->digestsDefined));
Line 2170                ss->digests = calloc(unpack_streams,
Line 2171                    sizeof(*ss->digests));
Line 2172                if (ss->unpackSizes == NULL || ss->digestsDefined == NULL ||
Line 2173                    ss->digests == NULL)
Line 2174                        return (-1);
Line 2175        }
Line 2176
Line 2177        usizes = ss->unpackSizes;
Line 2178        for (i = 0; i < numFolders; i++) {
Line 2179                unsigned pack;
Line 2180                uint64_t sum;
Line 2181
Line 2182                if (f[i].numUnpackStreams == 0)
Line 2183                        continue;
Line 2184
Line 2185                sum = 0;
Line 2186                if (type == kSize) {
Line 2187                        for (pack = 1; pack < f[i].numUnpackStreams; pack++) {
Line 2188                                if (parse_7zip_uint64(a, usizes) < 0)                         ^^^^^^^^^  ---- BUFFER OVERFLOW
Line 2189                                        return (-1);
Line 2190                                sum += *usizes++;
Line 2191                        }
Line 2192                }
Line 2193                *usizes++ = folder_uncompressed_size(&f[i]) - sum;
Line 2194        }

 

コードの Line 2149 ~ 2157 を見ると、すべての「フォルダ」に対して「numUnpackStreams」の合計を計算し、その結果が「unpack_streams」変数に格納されることがわかります。

この変数は「size_t」なので、x86 プラットフォームでは 32 ビット符号なし整数です。しかし、このコードで許可されている「numUnpackStreams」の最大値は次のようになります。

UMAX_ENTRY        100000000

つまり、「unpack_streams」変数をオーバーフローさせるには、フォルダ数「numFolders」が 42 を上回る 7-Zip ファイルを 1 つ用意し、「numUnpackStreams」に十分な値を投入するだけでいいのです。

このオーバーフロー値は、Line 2166 ~ 2171 の calloc の size パラメータで使用されます。ここで割り当てられているバッファで、最も興味深いのは「ss->unpackSizes」です。その後に続く Line 2187 ~ 2194 で、各フォルダの「numFolders」と「numUnpackStreams」に基づいてファイルから 64 ビット符号なし整数(最大)が読み出され、バッファ「usizes」に格納されます。このバッファが実は「ss->unpackSizes」(Line 2177)であり、(オーバーフロー値に応じて)ヒープ ベースのバッファ オーバーフローを引き起こします。これが何度か繰り返された後、攻撃者はバッファをオーバーフローさせるためのバイト数とその値も完全に制御するようになります。

TALOS-2016-0153 [CVE-2016-4301]:MTREE PARSE_DEVICE スタックベースのバッファ オーバーフローpopup_icon

この脆弱性は、コードがバッファ オーバーフローを防ぐために最善を尽くすものの、その方法が正しくない、というものです。まず、最大 3 つの符号なし長整数を格納するための配列が作成されます。その後、このコードは引数の数が最大の 3 より少ないことを検証しようとしますが、その引数がサイズ長よりも大きいかどうかをチェックしません。

脆弱なコードは、libarchive\archive_read_support_format_mtree.c という mtree サポート フォーマット モジュールに存在しています。

Line 1353        static int
Line 1354        parse_device(dev_t *pdev, struct archive *a, char *val)
Line 1355        {
Line 1356        #define MAX_PACK_ARGS 3
Line 1357                unsigned long numbers[MAX_PACK_ARGS];
Line 1358                char *p, *dev;
Line 1359                int argc;
Line 1360                pack_t *pack;
Line 1361                dev_t result;
Line 1362                const char *error = NULL;
(...)
Line 1377                while ((p = la_strsep(&dev, ",")) != NULL) {
Line 1378                        if (*p == '\0') {
Line 1379                                archive_set_error(a, ARCHIVE_ERRNO_FILE_FORMAT,
Line 1380                                    "Missing number");
Line 1381                                return ARCHIVE_WARN;
Line 1382                        }
Line 1383                        numbers[argc++] = (unsigned long)mtree_atol(&p);
Line 1384                        if (argc > MAX_PACK_ARGS) {
Line 1385                                archive_set_error(a, ARCHIVE_ERRNO_FILE_FORMAT,
Line 1386                                    "Too many arguments");
Line 1387                                return ARCHIVE_WARN;
Line 1388                        }
Line 1389                }

 

Line 1357 に、3 つの要素を格納するために準備された静的バッファの定義があります。Line 1377 ~ 1389 の while ループには、「numbers」バッファのオーバーフローを防ぐはずの条件(Line 1384)が存在します。しかし、この条件は正しくコーディングされていないため、攻撃者は符号なし長整数よりも値が大きい単一の要素を利用して、バッファをオーバーフローさせることができます。プラットフォームとアーキテクチャに応じて、上書きは 4 バイトまたは 8 バイトになり、その内容が完全に制御されてしまいます。

TALOS-2016-0154 [CVE-2016-4302]:LIBARCHIVE RAR RESTARTMODEL ヒープ オーバーフローpopup_icon

コンテキスト設定のため、以下にヒープ破壊につながる実行フローを示します。

archive_read_next_header
           archive_read_format_rar_read_header
        head_type : 0x72
        head_type : 0x73
        head_type : 0x74
        read_header
        rar->packed_size : 0x1
        rar->dictionary_size = 0;
        archive_format_name : RAR
archive_read_extract
        rar->compression_method : 0x31
        read_data_compressed
           archive_read_format_rar_read_header
        head_type : 0x7a
        read_header
        parse_codes
                if (ppmd_flags & 0x20)
                   archive_read_format_rar_read_header
                head_type : 0x7b
                   archive_read_format_rar_read_header
                head_type : 0x74
                read_header
                rar->packed_size : 0x1
                rar->dictionary_size = 0;
                   archive_read_format_rar_read_header
                head_type : 0x7a
                read_header
                rar->dictionary_size : 0x10000000
                   archive_read_format_rar_read_header
                head_type : 0x7b
                   archive_read_format_rar_read_header
                head_type : 0x74
                read_header
                rar->packed_size : 0x1
                rar->dictionary_size = 0;
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                __archive_ppmd7_functions.PpmdRAR_RangeDec_CreateVTable(&rar->range_dec);
                ppmd_alloc : 0
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                   archive_read_format_rar_read_header
                Ppmd7_Init
                RestartModel
        *** Heap corruption ***

 

抽出フェーズ(archive_read_extract 以下のすべて)に注目してみましょう。ここで鍵となる変数またはフィールドは rar->dictionary_size です。まず、値が次のように設定されます。

rar->dictionary_size : 0x10000000

libarchive\archive_read_support_format_rar.c

Line 2073    /* Memory is allocated in MB */
Line 2074    if (ppmd_flags & 0x20)
Line 2075    {
Line 2076      if (!rar_br_read_ahead(a, br, 8))
Line 2077        goto truncated_data;
Line 2078      rar->dictionary_size = (rar_br_bits(br, 8) + 1) << 20;
Line 2079      rar_br_consume(br, 8);
Line 2080    }

 

次に、以下の要素に注目してみましょう。

rar->packed_size : 0x1

この値が小さいために、ファイルからデータの別の部分が読み取られ、archive_read_next_header の「読み取りフェーズ」中に archive_read_format_rar_read_header および read_header 関数をコールすることによって解析されます。これらのコールのうちの 1 つで、dictionary_size の値が 0 に設定されます。次に、dictionary_size の値に応じて、Ppmd コンテキストに対する割り当てが行われます。

libarchive\archive_read_support_format_rar.c:

Line 2115      if ( !__archive_ppmd7_functions.Ppmd7_Alloc(&rar->ppmd7_context,rar->dictionary_size, &g_szalloc) )

 

libarchive\archive_ppmd7.c

Line 125        static Bool Ppmd7_Alloc(CPpmd7 *p, UInt32 size, ISzAlloc *alloc)
Line 126        {
Line 127          if (p->Base == 0 || p->Size != size)
Line 128          {
Line 129                Ppmd7_Free(p, alloc);
Line 130                p->AlignOffset =
Line 131                  #ifdef PPMD_32BIT
Line 132                        (4 - size) & 3;
Line 133                  #else
Line 134                        4 - (size & 3);
Line 135                  #endif
Line 136                if ((p->Base = (Byte *)alloc->Alloc(alloc, p->AlignOffset + size
Line 137                        #ifndef PPMD_32BIT
Line 138                        + UNIT_SIZE
Line 139                        #endif
Line 140                        )) == 0)
Line 141                  return False;
Line 142                p->Size = size;
Line 143          }
Line 144          return True;
Line 145        }

 

dictionary_size が 0 に等しいので割り当ては 0 バイトとなり、最小限のチャンクが割り当てられます。

python import gdbheap
p p->Base
$5 = (Byte *) 0x80e2480 " \b\r\brn"
heap select size==16
Reading in symbols for malloc.c...done.
    Start         End         Domain         Kind    Detail                                                                             Hexdump
----------  ----------  -------------  -----------  --------  ----------------------------------------------------------------------------------
0x080d0798  0x080d07a7  uncategorized               16 bytes  a8 07 0d 08 03 00 00 00 57 4d 54 00 19 00 00 00 c0 07 0d 08 |........WMT.........|
0x080d07c0  0x080d07cf  uncategorized               16 bytes  d0 07 0d 08 03 00 00 00 43 45 54 00 19 00 00 00 e8 07 0d 08 |........CET.........|
0x080d07e8  0x080d07f7  uncategorized               16 bytes  00 00 00 00 03 00 00 00 45 45 54 00 21 00 00 00 72 61 72 2d |........EET.!...rar-|
0x080d0818  0x080d0827  uncategorized               16 bytes  00 00 00 00 50 a4 d8 f7 00 00 00 00 11 00 00 00 50 70 6d 64 |....P...........Ppmd|
0x080d0828  0x080d0837              C  string data            50 70 6d 64 37 5f 49 6e 69 74 0a 00 21 00 00 00 44 00 00 00 |Ppmd7_Init..!...D...|
0x080d0cd8  0x080d0ce7  uncategorized               16 bytes  e8 0c 0d 08 00 00 00 00 00 00 00 00 f9 01 00 00 c5 b0 01 c0 |....................|
0x080e2470  0x080e247f  uncategorized               16 bytes  00 69 62 61 32 2e 68 74 6d 6c c0 cc 11 00 00 00 20 08 0d 08 |.iba2.html...... ...|
0x080e2480  0x080e248f              C  string data            20 08 0d 08 72 6e 00 00 00 00 00 00 21 00 00 00 72 61 72 2d | ...rn......!...rar-|

 

最後に、RestartModel 関数について検討します。

libarchive\archive_ppmd7.c

Line 314        static void RestartModel(CPpmd7 *p)
Line 315        {
Line 316          unsigned i, k, m;
Line 317
Line 318          memset(p->FreeList, 0, sizeof(p->FreeList));
Line 319          p->Text = p->Base + p->AlignOffset;
Line 320          p->HiUnit = p->Text + p->Size;
Line 321          p->LoUnit = p->UnitsStart = p->HiUnit - p->Size / 8 / UNIT_SIZE * 7 * UNIT_SIZE;
Line 322          p->GlueCount = 0;
Line 323
Line 324          p->OrderFall = p->MaxOrder;
Line 325          p->RunLength = p->InitRL = -(Int32)((p->MaxOrder < 12) ? p->MaxOrder : 12) - 1;
Line 326          p->PrevSuccess = 0;
Line 327
Line 328          p->MinContext = p->MaxContext = (CTX_PTR)(p->HiUnit -= UNIT_SIZE); /* AllocContext(p); */
Line 329          p->MinContext->Suffix = 0;
Line 330          p->MinContext->NumStats = 256;
Line 331          p->MinContext->SummFreq = 256 + 1;
Line 332          p->FoundState = (CPpmd_State *)p->LoUnit; /* AllocUnits(p, PPMD_NUM_INDEXES - 1); */
Line 333          p->LoUnit += U2B(256 / 2);
Line 334          p->MinContext->Stats = REF(p->FoundState);

 

ここで重要なのは以下の値です。

p p->AlignOffset
$6 = 0
p p->Size
$7 = 0

上記からわかるように、両方が 0 に等しくなっています。これが、Line 328 に影響します。

UNIT_SIZE 値が p->HiUnit から差し引かれます。

Line 30 #define UNIT_SIZE 12

 

そして、p->MinContext に割り当てられます。

すべてが正しく処理された場合、まず p->HiUnit が Ppmd コンテキストの割り当てスペースの末尾に設定されますが、AlignOffset と Size が 0 に等しいため、Ppmd の割り当てスペースの先頭をポイントしたままです。この減算の結果、p->MinContext は前のヒープ チャンク スペース内の値に設定されます。

p p->MinContext
$8 = (CPpmd7_Context *) 0x80e2474

 

該当のヒープ [a][b] を示します。

(...)
0x080e2470  0x080e247f  uncategorized               16 bytes  00 69 62 61 32 2e 68 74 6d 6c c0 cc 11 00 00 00 20 08 0d 08 |.iba2.html...... ...|
0x080e2480  0x080e248f              C  string data            20 08 0d 08 72 6e 00 00 00 00 00 00 21 00 00 00 72 61 72 2d | ...rn......!...rar-|
(...)

 

この構造に対してさらに書き込みが行われると、ヒープ チャンクが上書きされます。Line 329 の実行前に、ヒープ チェックが行われます。

python from heap.glibc import *
python print MChunkPtr(gdb.Value(0x080e2480-8).cast(MChunkPtr.gdb_type()))
<MChunkPtr chunk=0x80e2478 mem=0x80e2480 PREV_INUSE inuse chunksize=16 memsize=8>

 

実行後は、次のようになります。

python print MChunkPtr(gdb.Value(0x080e2480-8).cast(MChunkPtr.gdb_type()))
<MChunkPtr chunk=0x80e2478 mem=0x80e2480 prev_size=3435162733 free chunksize=0 memsize=-8&gt

まとめ

安全なコードを記述することは、容易ではありません。このような libarchive の脆弱性の根本的な原因は、入力値、つまり圧縮ファイルから読み出されるデータの検証の不備です。残念ながら、このタイプのプログラミング エラーは幾度となく繰り返されています。libarchive のようなソフトウェアで脆弱性が発見されると、libarchive を活用およびバンドルしている多くのサードパーティ プログラムに影響が及びます。共通モード故障と呼ばれるこうした状況が発生すると、攻撃者は、単一の攻撃を利用して多くの異なるプログラムやシステムを侵害できるようになります。できるだけ早急に、すべての関連プログラムにパッチを適用することを推奨します。

TALOS-CAN-0152 には、ClamAV を利用して対処します(検出にファイル全体が必要になるため)。TALOS-CAN-0153 は SID 39034、39035 で検出できます。TALOS-CAN-0154 は SID 39045、39046 で検出できます。

 

本稿は 2016年6月21日に Talos Grouppopup_icon のブログに投稿された「The Poisoned Archivespopup_icon」の抄訳です。

コメントを書く