脆弱性発見者:Marcin “Icewall” Noga ブログ記事執筆者:Marcin Noga、Jaeson Schultz
libarchive は、多様なファイル アーカイブ フォーマットへのアクセスを提供するオープンソース ライブラリで、さまざまな場面で使用されています。Cisco Talos はこの程、libarchive のメンテナンス担当者と協力して、このライブラリに潜む 3 つの重大なバグを修正するパッチを開発しました。libarchive は、多くの製品で圧縮ファイルの処理に使用されています。関連する脆弱性が存在するソフトウェアに、パッチまたはアップグレードを適用することをすべてのユーザに強く推奨します。
TALOS-2016-0152 [CVE-2016-4300]:7-Zip READ_SUBSTREAMSINFO 整数オーバーフロー
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 スタックベースのバッファ オーバーフロー
この脆弱性は、コードがバッファ オーバーフローを防ぐために最善を尽くすものの、その方法が正しくない、というものです。まず、最大 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 ヒープ オーバーフロー
コンテキスト設定のため、以下にヒープ破壊につながる実行フローを示します。
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>
まとめ
安全なコードを記述することは、容易ではありません。このような libarchive の脆弱性の根本的な原因は、入力値、つまり圧縮ファイルから読み出されるデータの検証の不備です。残念ながら、このタイプのプログラミング エラーは幾度となく繰り返されています。libarchive のようなソフトウェアで脆弱性が発見されると、libarchive を活用およびバンドルしている多くのサードパーティ プログラムに影響が及びます。共通モード故障と呼ばれるこうした状況が発生すると、攻撃者は、単一の攻撃を利用して多くの異なるプログラムやシステムを侵害できるようになります。できるだけ早急に、すべての関連プログラムにパッチを適用することを推奨します。
TALOS-CAN-0152 には、ClamAV を利用して対処します(検出にファイル全体が必要になるため)。TALOS-CAN-0153 は SID 39034、39035 で検出できます。TALOS-CAN-0154 は SID 39045、39046 で検出できます。
本稿は 2016年6月21日に Talos Group のブログに投稿された「The Poisoned Archives」の抄訳です。