7-Zip の脆弱性は Marcin Noga が発見しました。
この記事は、Marcin Noga、Jaeson Schultz が執筆しました。
2016 年 5 月 12 日更新:本ブログで取り上げた 7-Zip 問題に関連するアドバイザリは以下のブログで確認できます。
http://www.talosintel.com/reports/TALOS-2016-0093/
http://www.talosintel.com/reports/TALOS-2016-0094/
7-Zip はオープンソースのファイル アーカイブ アプリケーションです。AES-256 の暗号化方式や大容量ファイルをサポートし、「あらゆる圧縮、変換、暗号化方式」に対応していることを特徴としています。Cisco Talos はこのほど、不正利用の恐れがある複数の脆弱性が 7-Zip に存在することを発見しました。このタイプの脆弱性は注意が必要です。影響を受けるライブラリを、さまざまなベンダーがそうとは知らずに使用している可能性があるためです。また、セキュリティ デバイスやアンチウイルス製品などの場合は特に注意が必要です。7-Zip は主なプラットフォームのすべてでサポートされており、現在、最も一般的に利用されているアーカイブ ユーティリティの 1 つです。どれだけ多くの製品やアプライアンスが影響を受けるかを知れば、きっと驚かれることと思います。
TALOS-CAN-0094、領域外メモリ参照の脆弱性 [CVE-2016-2335]
領域外メモリ参照の脆弱性の原因は、7-Zip によるユニバーサル ディスク フォーマット(UDF)ファイルの処理方法にあります。UDF ファイル システムは ISO-9660 ファイル フォーマットに代わるものとして策定され、最終的に DVD-Video や DVD-Audio の公式のファイル システムとして採用されました。
7-Zip による UDF ファイルの処理の中心となるのは、CInArchive::ReadFileItem メソッドです。ボリュームは複数のパーティション マップを保持できるため、それらのオブジェクトはオブジェクト ベクトルに格納されます。アイテムを探す際、このメソッドはまずパーティション マップのオブジェクト ベクトルと Long Allocation Descriptor の「PartitionRef」フィールドを使用して、適切なオブジェクトを参照しようと試みます。この際、パーティション マップ オブジェクトの数よりも「PartitionRef」フィールドが大きいかどうかの確認が行われないために領域外メモリ参照が発生し、特定の状況下において任意のコード実行が可能となる恐れがあります。
脆弱性を示すコード:
CPP\7zip\Archive\Udf\UdfIn.cpp
Line 898 FOR_VECTOR (fsIndex, vol.FileSets) Line 899 { Line 900 CFileSet &fs = vol.FileSets[fsIndex]; Line 901 unsigned fileIndex = Files.Size(); Line 902 Files.AddNew(); Line 903 RINOK(ReadFileItem(volIndex, fsIndex, fs.RootDirICB, kNumRecursionLevelsMax)); Line 904 RINOK(FillRefs(fs, fileIndex, -1, kNumRecursionLevelsMax)); Line 905 } (...) Line 384 HRESULT CInArchive::ReadFileItem(int volIndex, int fsIndex, const CLongAllocDesc &lad, int numRecurseAllowed) Line 385 { Line 386 if (Files.Size() % 100 == 0) Line 387 RINOK(_progress->SetCompleted(Files.Size(), _processedProgressBytes)); Line 388 if (numRecurseAllowed-- == 0) Line 389 return S_FALSE; Line 390 CFile &file = Files.Back(); Line 391 const CLogVol &vol = LogVols[volIndex]; Line 392 CPartition &partition = Partitions[vol.PartitionMaps[lad.Location.PartitionRef].PartitionIndex];
この脆弱性は、不正な形式の Long Allocation Descriptor を含むエントリによってトリガーされます。上記のコードの Line 898 ~ 905 からわかるように、プログラムは特定のボリュームから要素を探し、RootDirICB の Long Allocation Descriptor に基づいてファイル セットが開始されます。このレコードを故意に不正なものとすることで、悪意のある目的に使用される可能性があります。脆弱性は、Line 392 で PartitionRef フィールドが PartitionMaps ベクトルの要素数を超える場合に発生します。
TALOS-CAN-0093、ヒープ オーバーフローの脆弱性 [CVE-2016-2334]
7-Zip の Archive::NHfs::CHandler::ExtractZlibFile メソッドの機能には、不正利用可能なヒープ オーバーフローによる脆弱性が存在します。HFS+ ファイル システムでは、ファイルは zlib を使用した圧縮形式で格納できます。データを zlib 形式で保存する場合、データのサイズによって 3 つの方法があります。圧縮後のサイズが 3800 バイトを超えるファイルのデータは、複数のブロックに分けられリソース フォークに格納されます。
ブロック サイズ情報とそれらのオフセットはリソース フォーク ヘッダーの直後のテーブルに保持されます。ExtractZlibFile メソッドは展開前に、ブロック サイズとオフセットをファイルから読み取ります。その後、サイズが固定されたバッファ「buf」にブロック データが読み込まれます。ここで、ブロックのサイズがバッファ「buf」のサイズよりも大きいかどうかが確認されないため、「buf」のサイズを超える不正な形式のブロック サイズが読み込まれる可能性があります。これにより、バッファ オーバーフローが発生し、ヒープ破損が発生します。
脆弱性を示すコード:
7zip\src\7z1505-src\CPP\7zip\Archive\HfsHandler.cpp
Line 1496 HRESULT CHandler::ExtractZlibFile( Line 1497 ISequentialOutStream *outStream, Line 1498 const CItem &item, Line 1499 NCompress::NZlib::CDecoder *_zlibDecoderSpec, Line 1500 CByteBuffer &buf, Line 1501 UInt64 progressStart, Line 1502 IArchiveExtractCallback *extractCallback) Line 1503 { Line 1504 CMyComPtr inStream; Line 1505 const CFork &fork = item.ResourceFork; Line 1506 RINOK(GetForkStream(fork, &inStream)); Line 1507 const unsigned kHeaderSize = 0x100 + 8; Line 1508 RINOK(ReadStream_FALSE(inStream, buf, kHeaderSize)); Line 1509 UInt32 dataPos = Get32(buf); Line 1510 UInt32 mapPos = Get32(buf + 4); Line 1511 UInt32 dataSize = Get32(buf + 8); Line 1512 UInt32 mapSize = Get32(buf + 12); (...) Line 1538 RINOK(ReadStream_FALSE(inStream, tableBuf, tableSize)); Line 1539 Line 1540 UInt32 prev = 4 + tableSize; Line 1541 Line 1542 UInt32 i; Line 1543 for (i = 0; i < numBlocks; i++) Line 1544 { Line 1545 UInt32 offset = GetUi32(tableBuf + i * 8); Line 1546 UInt32 size = GetUi32(tableBuf + i * 8 + 4); Line 1547 if (size == 0) Line 1548 return S_FALSE; Line 1549 if (prev != offset) Line 1550 return S_FALSE; Line 1551 if (offset > dataSize2 || Line 1552 size > dataSize2 - offset) Line 1553 return S_FALSE; Line 1554 prev = offset + size; Line 1555 } Line 1556 Line 1557 if (prev != dataSize2) Line 1558 return S_FALSE; Line 1559 Line 1560 CBufInStream *bufInStreamSpec = new CBufInStream; Line 1561 CMyComPtr bufInStream = bufInStreamSpec; Line 1562 Line 1563 UInt64 outPos = 0; Line 1564 for (i = 0; i < numBlocks; i++) Line 1565 { Line 1566 UInt64 rem = item.UnpackSize - outPos; Line 1567 if (rem == 0) Line 1568 return S_FALSE; Line 1569 UInt32 blockSize = kCompressionBlockSize; Line 1570 if (rem < kCompressionBlockSize) Line 1571 blockSize = (UInt32)rem; Line 1572 Line 1573 UInt32 size = GetUi32(tableBuf + i * 8 + 4); Line 1574 Line 1575 RINOK(ReadStream_FALSE(inStream, buf, size)); // !!!HEAP OVERFLOW !!!
HFS+ イメージからの解凍中、圧縮されたファイルには「com.apple.decmpfs」属性が付与され、リソース フォークにデータが格納されます。その後、上記のコードが示す結果となります。
圧縮されたファイル データは複数のブロックに分けられ、展開前の各ブロックは「buf」に読み込まれます(Line 1575)。ReadStream_FALSE(実際の中身は ReadFile API)は、「size」の値に基づいて、データの一部を「buf」バッファに読み込みます。
バッファ「buf」の定義とそのサイズは、ExtractZlibFile の呼び出し元「CHandler::Extract」メソッドでわかります。見てわかるように、サイズは固定で 0x10010 バイトです。
Line 1633 STDMETHODIMP CHandler::Extract(const UInt32 *indices, UInt32 numItems, Line 1634 Int32 testMode, IArchiveExtractCallback *extractCallback) Line 1635 { (...) Line 1652 Line 1653 const size_t kBufSize = kCompressionBlockSize; // 0x10000 Line 1654 CByteBuffer buf(kBufSize + 0x10); // we need 1 additional bytes for uncompressed chunk header (...) Line 1729 HRESULT hres = ExtractZlibFile(realOutStream, item, _zlibDecoderSpec, buf, Line 1730 currentTotalSize, extractCallback);
ExtractZlibFile メソッドに戻ると、Line 1573 で、tableBuf から値を読み込んで、ブロックの「size」を設定しています。tableBuf は、Line 1538 でファイルから読み込まれています。つまり、「size」はファイルのデータの一部であり、値を操作することが可能です。0x10010 よりも大きい値が「size」に設定されると、バッファ オーバーフローが発生し、ヒープが破損します。
Line 1573 より前では、「size」変数の値が Line 1543 ~ 1555 のループで読み込まれます。コードのこのブロックでは、データ ブロックに整合性があるかどうかを確認します。つまり、以下を確認します。
– データ ブロックは tableBuf 直後に始まる(Line 1540)
– 次のデータ ブロックは前のデータ サイズ + オフセットから始まる(Line 1549)
– オフセットは dataSize2(圧縮データのサイズ)より大きい値であってはならない(Line 1551)
– 「size」は残りのデータより大きい値であってはならない(Line 1552)
ここでわかるように、「size」のサイズが「buf」のサイズよりも大きいかどうかに関しては確認されていません。また、上記の制約が、「size」のサイズが「buf」のサイズよりも大きいかどうかに影響を与えることもありません。
まとめ
残念なことに、入力データを適切に検証できていないアプリケーションにより、数多くのセキュリティ脆弱性が発生しています。7-Zip のこれらの脆弱性は両方とも、入力に関する検証の欠如が原因です。信頼できないソースからデータが入力される可能性があるため、データ入力の検証はすべてのアプリケーションのセキュリティにおいて非常に重要な要素です。Talos はこの問題を解決するために、7-Zip と協力し、責任をもってこれらの脆弱性を開示しました。また、対応するパッチを作成しました。脆弱なバージョンから最新バージョンである 16.00 にできるだけ早くアップデートすることをお勧めします。
TALOS-CAN-0093 は SID 38323 ~ 38326 で検出できます。
TALOS-CAN-0094 は SID 38293 ~ 38296 で検出できます。
本稿は 2016年5月11日に Talos Group のブログに投稿された「MULTIPLE 7-ZIP VULNERABILITIES DISCOVERED BY TALOS」の抄訳です。