Cisco Japan Blog

脆弱性についての解説:7-Zip の HFS+ に起因する、任意コードが実行される脆弱性(CVE-2016-2334)

2 min read



執筆者:Cisco Talos Marcin Nogapopup_icon

はじめに

Talos は 2016 年に、CVE-2016-2334popup_icon のアドバイザリを公開しました。この中では、特定バージョンの 7-Zip(広く利用されている圧縮ユーティリティ)に発見された、リモートでコードが実行される脆弱性について説明しています。今回の記事では、該当バージョンの 7-Zip(x86 15.05 ベータ版)がインストールされた Windows 7 x86 上で本脆弱性がエクスプロイトされる流れを詳しく解説します。解説のため実際に利用できるエクスプロイトを作成します。

分析

初めに 7-Zip のコードに起因する脆弱性について概要を示します。なお、本脆弱性に関する技術的な詳細は、前述のアドバイザリ レポートに記載されています。

問題の脆弱性は、HFS+ ファイルシステム上で圧縮ファイルを解凍する際に発現します。具体的には、CHandler::ExtractZlibFile 関数に起因しています。ReadStream_FALSE 関数は、読み込みバイト数を「size」パラメータから取得し、ファイルから「buf」バッファにコピーします(図 A の 1575 行目を参照)。「buf」バッファのサイズは固定長の 0x10000 + 0x10 で、CHandler::Extract 関数で定義されています。問題は、「size」パラメータがユーザによる変更が可能であり、サニティ チェックを行うことなくファイルから直接読み取られることです(1573 行目を参照)。

概要:

  • 「size」パラメータ:32 ビット値。攻撃者によって完全に制御可能。
  • 「buf」パラメータ:固定長バッファ(0x10010 バイト)。
  • ReadStream_FALSE:ReadFile 関数のラッパー関数。「buf」バッファからオーバーフローする内容は、ファイルから直接取得されるものであり、使用される文字に制限はありません。

注:ヒープ オーバーフローが read/ReadFile などの関数(一般にカーネル内で最後に実行されるコード部分)によってトリガーされる場合、ページ ヒープを有効にすれば、オーバーフローは発生しません。カーネルが利用できないページ(free/protected/guarded)を認識しても、システム コールは単にエラー コードを返すだけです。ページ ヒープを有効にするときは、この点に留意してください。

今回の脆弱性を再現するには、ベースとなる HFS+ イメージを作成し、後からこれを変更する必要があります。その際に Windows プラットフォームではこちらから入手できる python スクリプトを使用します。Apple OSX を使用する場合、OSX Snow Leopard 10.6 以降では、DiskUtil ユーティリティで –hfsCompression オプションを使用することでベース イメージを作成できます。イメージを変更して脆弱性を再現する方法の技術的な詳細については、別途説明します。ここでは変更後のイメージを下記に示します。

c:\> 7z l PoC.img

Scanning the drive for archives:
1 file, 40960000 bytes (40 MiB)
Listing archive: PoC.img
--

Path = PoC.img
Type = HFS
Physical Size = 40960000
Method = HFS+
Cluster Size = 4096
Free Space = 38789120
Created = 2016-07-09 16:41:15
Modified = 2016-07-09 16:59:06

Date      Time    Attr         Size   Compressed  Name
------------------- ----- ------------ ------------  ------------------------
2016-07-09 16:58:35 D....                            Disk Image
2016-07-09 16:59:06 D....                            Disk Image\.fseventsd
2016-07-09 16:41:15 D....                            Disk Image\.HFS+ Private Directory Data
2016-07-09 16:41:16 .....       524288       524288  Disk Image\.journal
2016-07-09 16:41:15 .....         4096         4096  Disk Image\.journal_info_block
2016-07-09 16:41:15 D....                            Disk Image\.Trashes
2014-03-13 14:01:34 .....       131072       659456  Disk Image\ksh
2014-03-20 16:16:47 .....         1164          900  Disk Image\Web.collection
2016-07-09 16:41:15 D....                            Disk Image\[HFS+ Private Data]
2016-07-09 16:59:06 .....          111         4096  Disk Image\.fseventsd\0000000000f3527a
2016-07-09 16:59:06 .....           71         4096  Disk Image\.fseventsd\0000000000f3527b
2016-07-09 16:59:06 .....           36         4096  Disk Image\.fseventsd\fseventsd-uuid
------------------- ----- ------------ ------------  ------------------------

2016-07-09 16:59:06             660838      1201028  7 files, 5 folders

テスト環境の準備

7-Zip 15.05 ベータ版のビルド

エクスプロイトの分析を容易にするために、ソース コードpopup_iconから 7-Zip をビルドし、デバッグ機能をビルドに追加します。ビルド ファイル(Build.mak)を次のように変更し、デバッグ シンボルを有効にします。

Standard:

- CFLAGS = $(CFLAGS) -nologo -c -Fo$O/ -WX -EHsc -Gy -GR-
- CFLAGS_O1 = $(CFLAGS) -O1
- CFLAGS_O2 = $(CFLAGS) -O2
- LFLAGS = $(LFLAGS) -nologo -OPT:REF -OPT:ICF

With debug:

+ CFLAGS_O1 = $(CFLAGS) -Od
+ CFLAGS_O2 = $(CFLAGS) -Od
+ CFLAGS = $(CFLAGS) -nologo -c -Fo$O/ -W3 -WX -EHsc -Gy -GR- -GF -ZI
+ LFLAGS = $(LFLAGS) -nologo -OPT:REF -DEBUG

7-Zip をソースからコンパイルすると、PoC を使用してテストを実行し、オーバーフローが発生する前のヒープ レイアウトを確認できます。

"C:\Program Files\Windows Kits\10\Debuggers\x86\windbg.exe" -c"!gflag -htc -hfc -hpc" t:\projects\bugs\7zip\src\7z1505-src\CPP\7zip\installed\7z.exe x PoC.hfs

注:デバッグを開始する前に !gflag コマンドを使用して、ヒープ オプションをすべて無効にしてください。

ではここで、「buf」バッファに続くメモリ チャンクを確認してみましょう。

ヒープの一覧に着目します。複数のオブジェクトに vftable が設定されていることから、コードのフロー制御にこれを利用できる可能性があります。vftable を別のデータで上書きすることで、最近の OS が備えるヒープ オーバーフロー対策を回避してコード実行を制御できます。

では、PoC を変更せずにテストしてみましょう。デバッグ セッション内でオブジェクトを上書きし、実行を続行させます。

上書きされたオブジェクトがオーバーフロー後すぐに呼び出されたため、破損したヒープに影響するようなメモリ操作(alloc/free など)が特に行われなかったように見えます。オブジェクトが呼び出される前にそのようなメモリ操作が行われていれば、アプリケーションはクラッシュしていたでしょう。次に、ヒープ レイアウトが標準バージョンの 7-Zip と同じであることを確認します。デバッグ バージョンのヒープ レイアウトは大きく異なる可能性がありますので、注意してください。

ExtractZLibFile 関数の確認

7-Zip の標準ビルドにおけるヒープ レイアウトの構造を確認するには、ReadStream_FALSE 関数を呼び出す ExtractZLibFile 関数を見つける必要があります。

この関数をローカライズするために、関数内で使用されている定数の 1 つを探し、IDA 内で検索します。

0x636D7066

 


*(関数名は IDA で変更されていました)

.text1001D9D9 の位置を確認すれば、探しているものが見つかります。

次に、デバッガで 0x1001D7AB にブレークポイントを設定します。0x1001D7AB には ReadStream_FALSE へのコールが含まれており、「buf」周辺のヒープ レイアウトを解析できます。

ヒント:edx が、「buf」バッファのアドレスを示しています。

ヒープ レイアウトは、次のようになります。

標準的な 7-Zip ビルドを使用する場合は、ヒープ レイアウトが異なるようです。たとえば、「buf’」バッファ(サイズ 0x10010)の後に、vftable を含むオブジェクトがありません。

注:WinDBG では、デバッグ シンボルや RTTI がロードされていない場合でも、!heap -p -h コマンドを使用すれば vftable を持つオブジェクトが表示されます。たとえば、次のようなものです。

013360b0 0009 0007  [00]   013360b8    0003a - (busy)
013360f8 0007 0009  [00]   01336100    00030 - (busy)     ←-- object with vftable

? 7z!GetHashers+246f4

01336130 0002 0007  [00]   01336138    00008 - (free)
01336140 9c01 0002  [00]   01336148    4e000 - (busy)
* 01384148 0100 9c01  [00]   01384150    007f8 - (busy)

今回の目標は、実際に使用できるエクスプロイトを作成することです。つまりこの場合、ヒープを操作してうまく並べ替える方法を確認し、作成しやすくする必要があります。

実施方法の策定

PoC.hfs ファイルの内容とその内部データ構造は、ヒープの構造に大きく影響します。現在のヒープ レイアウトを変更するには、一定の信頼性を備えた HFS+ イメージ ファイル ジェネレータを作成する必要があります。そして、HFS+ のブロックをファイル イメージに追加して、ヒープ配置の並べ替えができるようにします。そうすることで、「buf」バッファの後に vftable を持つオブジェクトが現れるようにできます。

考えられる構造、設定、機能をすべて実装した高度な HFS+ イメージ ファイル ジェネレータを作成する必要はありません。ここでは、ヒープの並び替えと、脆弱性を再現する上で必要な要因に対応していれば十分です。

HFS+ のファイル形式に関する詳細については、こちらpopup_iconのドキュメンテーションを参照してください。HFS+ のファイル形式についてある程度把握しておけば、このデバッグ セッションが理解しやすくなります。

ヒープ レイアウトを変更する要因の特定

まず、ファイルからデータがヒープ上に書き込まれる(サイズが可変長の)場所を特定する必要があります。まず、HFS+ のファイル形式を解析するためのコードから確認します。

注:7-Zip は、特定のファイル形式の解析を始める前に、いくつかの命令を実行することがあります。たとえば、「ダイナミックな」ファイル形式を検出する場合に、関連する動作を実行する場合があります。

PoC.hfs のサンプル コードを段階的にデバッグすることで、ファイルの解析プロセスでデータをヒープに書き込む関数をすべて特定できます。

ソース コード上にマッピングします。次の行から開始します。

別途、次の行を確認します。

何回かテストした結果、次の関数に該当すると思えるものが見つかりました。

LoadName 関数の本体:

各属性は UTF-16 の文字列で名前がつけられ、可変長サイズでヒープに割り当てられていることから、まさに該当する部分だと考えられます。これらの名前をスプレーとして使用すれば、必要な数だけ属性を追加できます。ただし、「attr.ID」は対応する「file.ID」以外のものに設定する必要があります。

HFS+ ジェネレータの作成

生成するファイルは、次のようなものになります。

7-Zip における HFS+ ファイル システムのパーサーは、HFS+ の標準規格に完全に沿って実装されていません。そのため、まず 7-Zip を解析して、HFS+ の解析方法(パーサー)が具体的にどのように実装されているのかを確認する必要があります。なお、Talos ではファイル生成スクリプトを公開しています。このスクリプトを使用すれば、今回の脆弱性を再現できる細工されたファイルが作成できます。スクリプトは、こちらpopup_iconから入手できます。

ファイル形式プロセスの逆解析に使用される 010 Editor のテンプレート

 

前述したとおり、このジェネレータには機能制限があるため、今回の記事で取り上げている脆弱性の発現に必要な構造を生成する以外の機能は備えていません。「OVERFLOW_VALUE」(「buf’」バッファのオーバーフローに使用するバッファのサイズ)を 0x10040 に設定することで、この脆弱性を発現させるファイルを作成できます。生成されたファイルを使用すると、デバッグ セッションは次のような結果になります。

コードをステップ実行して、オーバーフローの発生する場所を解析します。

これで HFS+ ジェネレータが機能することを確認できました。OVERFLOW_VALUE 変数の値を 0x10300 に増やします。これは、サイズが 0x310 バイトのオーバーフローしたフリー チャンクを発生させるのに十分な値です。言い換えれば、このチャンクには vftable を持つオブジェクトが含まれます。以下で詳細を確認します。

まず、vftable を持つオブジェクトをオーバーフローにより生成するのに失敗していることが確認できます。これは「buf」バッファに続くフリー チャンクのサイズが増大したことが原因です。該当ファイルのコンテンツに何らかの形で関連したメモリ割り当てがあったようです。そのような命令が発生した位置を確認するため、次のような条件でブレークポイントを設定します。

bp ntdll!RtlAllocateHeap "r $t0=esp+0xc;.if (poi(@$t0) > 0xffff) {.printf \"RtlAllocateHeap hHEAP 0x%x, \", poi(@esp+4);.printf \"Size: 0x%x, \", poi(@$t0);.echo}.else{g}"

また、この作業を簡略化するために、前に作成したデバッグ シンボル付きの 7-Zip を使用します。

デバッガがブレークポイントで止まります。このブレークポイントには、今回のファイルと同じサイズのバッファが割り当てられています。ブレークポイントを分析した結果、ファイル形式のヒューリスティック検出を担うコードであることが判明しました。

7-Zip は、ファイル コンテンツ全体を処理するのに十分なサイズのバッファを割り当てます。割り当てたバッファを最終的に解放する際は、その前にファイル形式を判別しようとします。ここで解放されたバッファ メモリは、後に「buf」バッファの割り当てで使用されます。このため、ペイロード サイズを増やしたときに、そのチャンクが増大し、チャンクの後にギャップが発生するのです。ただし、エクスプロイトは依然として可能です。生成したファイルを保存する際に使用したファイルの拡張子がカギです。適切なファイル拡張子を使用すれば、7-Zip でヒューリスティック ファイル検出を回避できます(今回のケースでは .hfs)。この手口により 7-Zip のヒューリスティック検出を回避すると、ヒープは以下のようになります。

実施方法の策定

ここでは一旦まとめ、実際に使用できるエクスプロイトを作成するための方法を確認します。

  • 目的とするバッファ(「buf」)のサイズは固定長(0x10010)です。
  • こうすることで、ヒープバックエンドによって常にこのバッファ サイズが割り当てられます。詳細については、こちらpopup_iconを参照ください。
  • オーバーフローが発生する前に、任意の数のオブジェクトを任意のサイズで割り当てることができます。
  • ヒープ上で自由にアクションを実行したり、トリガーしたりすることはできません。
  • オーバーフローに続けて、alloc もしくは free 操作を実行することもできません。

上記の状況を前提とし、可能な操作も上記のように限定される中で、Windows 7 で実装されているヒープ対策もすべて考慮すると、考えられる妥当なアプローチは次のようなものとなります。

  • vftable を持つオブジェクトの位置を特定する必要があります。オーバーフローの後に続くオブジェクトの中で、最も早く呼び出されるものを確認します。これは重要な点です。メモリ内で、オーバーフローさせた vftable の呼び出し位置が、オーバーフローの発生位置から離れている場合、コード内で alloc または free 命令が呼び出され、プログラムがクラッシュする可能性が高くなるからです。
  • 確認した個別のオブジェクトと同じサイズで、属性(名前)をヒープにスプレーします。オブジェクト サイズが 0x10 より大きく、0x4000(Low Fragmentation Heap の最大オブジェクト サイズ)未満であるターゲット オブジェクトと同じサイズのオブジェクトを割り当てることを前提として、LFH を有効化し、同じサイズのオブジェクトに対して空きチャンクを割り当てます。これにより、オーバーフローしたバッファの後に空きスロットが割り当てられ、オブジェクトがその中に格納されます。

対象オブジェクトの特定

エクスプロイト作成方法の確認は以上です。次に、上書きに適切なオブジェクトの位置を特定する必要があります。特定には、WinDBG 用のシンプルな JS スクリプトを使用します。このスクリプトは、vftable を持つオブジェクトとそのスタック トレースを表示します。

これらのアクションを実行するスクリプトは、こちらpopup_iconから入手できます。

結果は次のようになります。

まず、オーバーフローが発生した「ExtractZlibFile」と同じ関数で割り当てられたオブジェクトを探します。このオブジェクトであれば、オーバーフロー後すぐに使用される可能性が高いからです。これまでのスクリーンショットに基づいて、2 つの候補に絞り込むことができます。

該当するオブジェクトは、次の場所で定義されています。

Line 1504  CMyComPtr<ISequentialInStream> inStream;
(...)
Line 1560  CBufInStream *bufInStreamSpec = new CBufInStream;
Line 1561  CMyComPtr<ISequentialInStream> bufInStream = bufInStreamSpec;

関数が終了すると、すぐにデストラクタ(仮想メソッドを解放する)が呼び出されます。これをトリガーする最も簡単な方法は、オーバーフローさせるバッファの最初のバイトを「0xf」に設定することです。

オブジェクトの移動

オーバーフローさせるオブジェクトが特定できました。次に、オブジェクトと同じ長さの「name」文字列を含む属性構造体をヒープにスプレーする必要があります。この長さは、次のとおりです。

0x20 および 0x30

このため、次のコードを使用します。

WinDBG を制御し、バッファオーバーフロー後に対象のオブジェクトが割り当てられるまで属性構造体の数を増やします。スクリプトを作成するか、手動で行います。

今回は手動で行い、単純に数値を 10 単位で増やすことでヒープの状態を確認しました。オブジェクトの位置が「buf」バッファの位置に近づいたところで、数値を 1 単位で増やしてみます。

この作業を何度か繰り返すことで、値が 139 になりました。

139 * (0x20 + 0x30 + 2* 0x18)

この時点でのヒープ レイアウトは、以下のとおりです。

このヒープ構造は使えそうです。0x12efdf8 に位置するオブジェクトの後のアドレスから、「buf]バッファのアドレスを引くと、ターゲット オブジェクトの上書きに必要なバイト数を算出するのに役立ちます。「buf]バッファのアドレスは、呼び出し命令(0x12df9d0)から 8 バイトのオフセット減算した 0x12df9c8 です。ペイロードに利用できるサイズを割り出し、そのサイズを最大限確保するため、ヒープ上で利用できるアドレスから、ほぼ最後に位置するものを選びました(上のスクリーンショットでは表示されていません)。これらの情報により、OVERFLOW_VALUE 変数の値は 0x12618 に更新されます。

ここでファイルを再生成し、アプリケーションを実行して vftable が問題なく上書きされたことを確認します。

これで確認できました。では次に、エクスプロイトを攻撃に使えるようにします。

既存の対策を確認

エクスプロイトの開発は、分析対象の 7-Zip に実装されている対策に依存します。以下は、7-Zip バージョン 10.05 で実装されている対策です。

以下のスクリーンショットで確認できるように、7-Zip はアドレス空間レイアウトのランダム化(ASLR)やデータ実行防止(DEP)をサポートしていません。本脆弱性に関連するアドバイザリは昨年に公開されているため、これらがサポートされることを期待していましたが、修正はまだのようです。

64 ビット版の 7-Zip のでは、DEP が OS によって有効にされます。

ペイロードの検出

ガジェットの検討を始める前に、ペイロードを指定するスタック上のすべてのレジスタとポインタを確認しましょう。

上記のスクリーン ショットでわかるように、ペイロードの異なる部分を指定するレジスタとポインタが複数あります。

  • ESI
  • EDX
  • ESP
  • ESP-C
  • ESP+30
  • EBP+40
  • EBP-2C
  • EBP-68

バッファから vftable オブジェクトまでの正確なオフセットを決定する必要があります。ESI は vftable オブジェクトを参照し、EDX はバッファを参照しているため、ESI から EDX を引くだけでオフセットを決定できます。

0:000> ?esi - edx

Evaluate expression: 66608 = 00010430

オフセットに格納されている値をペイロードに入力すると、次のような結果になります。

「8」が追加されたことで値が変更されています。次に、以上のような内容を念頭に置いて、ガジェットについて検討します。

ポインタの特定

vftable へのポインタを上書きするには、ガジェットとそのガジェットへのポインタを特定する必要があります。

この作業では次のツールを使用します。

この種の分析で検出されるガジェットの数を最大にするため、複数のツールを使用します。

まず RopGadgets を使用して、7z.exe および 7z.dll のガジェットのリストを生成します。

 

次に、このリストを Mona に使用して、これらのガジェットのアドレスへのポインタを見つけます。

DEP がサポートされていないことを利用する

今回のバージョンの 7-Zip は DEP をサポートしていないため、脆弱性をエクスプロイトする最も簡単な方法の 1 つは、コード実行をヒープ上のバッファにリダイレクトすることです。前に列挙したポインタのリストを見直すと、こうした要件を満たす候補が明らかになります(以下を参照)。

上記のように、ポインタの値が同じであるアドレスが複数あります。これらは非常に便利です。ガジェットではコード実行がバッファにリダイレクトされますが、その際、ESP レジスタの指定するアドレスに格納されたポインタが使用されるからです。ESP レジスタの値は ESI の指定するものと同じです。ESI では、ポインタのアドレスが、元のオブジェクトとは別の vftable に設定されます。

以上を踏まえて、逆アセンブルする命令を特定します。

上記にあるように、「POP ES」命令により例外が発生しています。ただ、「ES」に「ポップ」されたスタック上の値には特に影響がありません。追加のガジェットアドレスの 1 つが、(例外発生の点で)問題の少ない命令に逆アセンブルされます。

0x1007c748 - 8  = 0x1007c740

「EDI」は書き込み可能なメモリ領域を指しているので、これらの命令は実行できるはずです。

また、バッファのパディングに使用されているバイト列(「0xcc」)が、この命令で使用されていることにも注意してください。

これらの点を考慮して、バッファ内のシェルコードのオフセットを設定する際に 3 バイトを省略します。

シェルコードの追加

これで、シェルコードを追加する準備が整いました。次のオフセットで配置されます。

fake_vftable_ptr_offset = 0x00010430 + 3 ("0xCC")

シェルコードを生成するには、Metasploit に含まれている msfvenompopup_icon を使用します。

更新されたスクリプト(シェルコードを含む)は次のようになります。

エクスプロイトのテスト

これで、HFS ファイルを生成してエクスプロイトをテストする準備がすべて整いました。

動画を見る

シェルコードが意図したとおりに動作することも確認済みです。

エクスプロイトの安定性

サイズが 0x20 と 0x30 のオブジェクトをヒープにスプレーする方法について、その効果を確認できました。では一方で、安定性はどうでしょうか?

単純に考えると、7-Zip のバージョンが同じで、解析する HFS ファイルも完全に同じであれば、該当箇所におけるヒープ レイアウトは同じになるはずです。しかし、ヒープに割り当てられた中間変数、つまり環境変数、コマンド ライン引数の文字列、ペイロードを含むファイルへのパスなども考慮する必要があります。これらの要因によってヒープ レイアウトがシステム内で異なってくる可能性があるのです。

このような場合、残念ながらこれらの中間変数は(オーバーフローさせたバッファと同様に)同じヒープ上に割り当てられます。少なくとも、エクスプロイトとして作成した CLI 版の 7-Zip ではそうなります。対象となるバッファの割り当てに使用されるヒープ メモリの解析に関しては、以下を参照ください。

ヒープを調査すると、解凍する HFS ファイルのパスである可変長の単一文字列を確認できます。この文字列は、ヒープ上の空きスペースや割り当て済みスペースの容量に大きく影響する可能性があります。その結果、ヒープ スプレーのオブジェク構成に影響を与え、エクスプロイトの失敗につながる場合があります。

ヒープにおける空き領域の相違を計算に入れるのであれば、ファイル パスや環境変数の長さなどのシステム上の制限を考慮して、ヒープ上の空き領域を使い切るだけの値を割り当てることもできます。このような手段によって、GUI 版の 7-Zip でヒープ レイアウトの構造ついて調査することもできますが、今回は詳細を割愛します。

まとめ

アーカイブ ユーティリティや一般的なファイル パーサーなどのアプリケーションにおけるバッファ オーバーフロー(ヒープオーバーフロー)の脆弱性は、最新のシステムでもセキュリティ ホールとなる可能性があります。Web ブラウザに対するエクスプロイトとは異なり、ヒープを自在に扱うことはできませんが、それでもやはり危険です。ヒープのメタデータの破損を利用して脆弱性をエクスプロイトできない場合でも、ユーザにアプリケーション データを上書きさせることで、コードの実行フローが制御できます。一部の製品では現在の標準的な対策がとられていないため、非常に簡単にエクスプロイトできます。

 

本稿は 2017年11月30日に Talos Grouppopup_icon のブログに投稿された「Vulnerability Walkthrough: 7zip CVE-2016-2334 HFS+ Code Execution Vulnerabilitypopup_icon」の抄訳です。

 

コメントを書く