サウンドをミュート:脆弱性を組み合わせて Outlook での RCE を実現:第 2 部
はじめに
このブログシリーズの 第 1 部で説明した脆弱性を使用することで、再びターゲット上でカスタム音声ファイルを再生し、Outlook のリマインダー音機能を悪用できるようになりました。この機能を活用して完全なリモートコード実行(RCE)を実現するために、Akamai は Windows 上の音声ファイルの解析に脆弱性がないか調査し始めました。
アタックサーフェス
Outlook で再生される音声ファイルは Waveform Audio File Format (WAV)形式で、 PlaySound 関数によって再生されます。音声ファイルパスを受け取った PlaySound は、そのファイルをロードして解析を行ったうえで、 SoundOpen を呼び出します。SoundOpen は、 waveOutOpenなどのさまざまな wave 関数を呼び出します。
WAV ファイルは、複数のオーディオコーデックのコンテナ(またはラッパー)として機能します。コーデックは、データストリーム(画像、動画、音声など)をエンコードまたはデコードするプログラムまたはコードのことです。通常、コーデックは パルス符号変調 (PCM)エンコーディングです。PCM は、サンプリングされたアナログ信号を表す簡単な方法です。
脆弱性を見つけるにあたっては、次の 3 つの主要なアタックサーフェスを調査します。
WAV 形式の解析
オーディオ圧縮マネージャー
さまざまな音声コーデック
WAV 形式の解析
WAV 形式の解析は、winmm.dll(Windows Multimedia API を実行するライブラリ)の soundInitWavHdr 関数内で実行されます。そこにあるアタックサーフェスは大きくなく、すでに検証済みのようです。Akamai の調査では、脆弱性は見つかりませんでした。
オーディオ圧縮マネージャーとは
オーディオ圧縮マネージャー(ACM)は、WAV ファイルで使用されているコーデックが単純な PCM エンコーディングを使用していないために、カスタムデコーダーによってデコードする必要がある場合に対処するコードです。そのデコーダーは、.acm 拡張子を持つファイルで実行されます。よくある例は MP3 コーデックで、これは l3codeca.acm で実行されます。各コーデックは、ACM を介して登録されるドライバー(カーネルモードドライバーとは異なりますが、機能は似ています)によって処理されます。
変換(MP3 から PCM への変換やその逆の変換など)が必要な場合は、常に ACM が動作し、この変換を管理します。PCM が使用されていない WAV ファイルを使用すると、ACM に対して、ファイル自体に指定されたコーデックが存在し、変換を処理できるかどうかを確認するための問い合わせが行われます(図 1)。
各 ACM ドライバーは、 acmdStreamSize や acmdStreamOpenなどの関数を実行する必要があります。acmdStreamSize は、変換の出力に必要なサイズ(バイト)を返します。acmdStreamOpen は、ストリーム構造体を作成し、適切なフィールド(デコーディングコールバック関数など)を設定します。
ACM のアタックサーフェスはそれほど大きくありません。しかし、このブログ記事で後ほど説明するとおり、Akamai はこのコードに脆弱性があることを突き止めました。
さまざまな音声コーデック
最後のアタックサーフェスは、デフォルトでインストールされているさまざまな音声コーデックです。WAV ファイルには次の 2 つの方法でコーデックが指定されています。
FORMAT チャンク内の wFormatTag
wFormatTag が WAVE_FORMAT_EXTENSIBLE の場合、SubFormat は音声コーデックの GUID を保持します。
使用可能なコーデックのリストについては、 付録をご参照ください。
音声信号処理の基礎
コードについて掘り下げていく前に、音声信号処理の基礎を把握しておきましょう。この概念にすでに精通している場合は、スキップして 次のセクションに進んでください。
音が聞こえるとき、実際に聞こえているのは伝送媒体を介して伝達される振動です。音とは振動波の受信であり、耳で拾われて脳で認識されます。
音声信号は連続波形であり、それをデジタル処理するためには、アナログ信号からデジタル信号に変換する必要があります(ADC コンバーターを使用するなど)。この変換は、アナログ信号の離散化です。これを行うために、アナログ信号を等間隔のデータポイント( サンプルと呼ばれます)で複数回サンプリングします。サンプリング レート (サンプリング頻度とも呼ばれます)によって、1 秒あたりに採取されるサンプル数が決まります。サンプリングレートを高くすると、より詳細な情報が取得されますが、同時により多くのストレージと処理が必要になります。
サンプリングレート以外にも サンプルサイズがあり、これは各サンプルに使用されるビット数を指します。これに関しても同様に、サンプルサイズが大きいほど、サンプルの品質が高く(つまり元の音に近く)なります。サンプルサイズは通常、1 サンプルあたり 16 ビットまたは 24 ビットです。
音声コーデックは心理音響モデルに基づいています。 心理音響学 とは、音の知覚と聴覚学(人の聴覚系がさまざまな音をどのように認識するか)の科学的研究です。たとえば、人間の聴覚範囲は 20 Hz~20,000 Hz です。したがって、ファイルサイズをさらに小さくするため、音声コーデックによって人間には聞こえない 周波数 が削除されることがあります。また、音声コーデックによって人間の耳には聞こえない 音量 の信号が削除されることもあります。たとえば、60 デシベル未満の場合、20 Hz の音は聞こえません。
心理音響モデルには、もっとたくさんの例があります。たとえば、大きい信号に近い周波数またはタイミングの静かな信号がある場合、静かな信号のマスキングが発生します。
信号の解析、解釈、および修正は、信号をサブバンドに分割するフィルターバンクを使用して実行されます。サブバンドとは、信号の特定の部分をより詳細に検査および操作できるようにする個別のコンポーネント群です。一般的に使用されるフィルターバンクは、DCT フィルターバンクやポリフェーズ・フィルター・バンクです。
この基本的な知識を念頭に置いて、さまざまなコーデックについて調べていきましょう。
最初の試み:サンプルバッファーへの境界を越えた書き込み
私たちはまず、MP3 を調べてみました。MP3 は他のコーデックと比較してとても複雑だからです。他のコーデックの大半はやや単純な変換のみを実行しますが、MP3 のデコード処理には複数の段階があります(図 2)。
MP3 音声データは一連のフレームに編成されており、各フレームは小さな音声セグメントを表します。フレームは、ヘッダーと音声データで構成されています。音声データは、ハフマン符号化を使用して圧縮されます。各フレームには、1 チャネル(モノラル/ステレオ)あたりちょうど 1,152 個の周波数ドメインサンプルがあります。これがグラニュールと呼ばれる 2 つのチャンクに分割されており、それぞれに 576 個のサンプルが含まれています。 各フレームは、デコーディングに関連する情報(サイド情報)も保持しています (図 3)。
MP3 デコーディングプロセスの一環として実行されるほとんどの操作は複雑です。理論的には、見つかりづらい脆弱性を探す場所として興味深い(そして魅力的である)ものの、実際には、操作の多く(改変 DCT、ポリフェーズ・フィルター・バンク、エイリアス削減など)が、常に 576 個のサンプルの値を保持するバッファーで実行されます。 したがって、ここで境界を越えた書き込みの脆弱性を探すのは理に適っていません。興味深い点を 1 つあげるとすれば、ハフマン復号化です。これは必然的に、(576 個のサンプルバッファーではなく)よりダイナミックなデータに作用するからです。
MP3 のハフマン復号化
グラニュール(576 個のサンプル)のハフマン復号化は、(バイナリーツリーではなく)コードテーブルを使用して実行されます。0~22,050(ナイキスト周波数)の全周波数範囲が 5 つの領域に分割されます(図 4)。
1/2/3.3 つの「big_values 領域」:値が -8,206~8,206 のサンプル
4.「count1 領域」:-1、0、1 のいずれかの 4 つの値
5.「rzero 領域」:高い周波数値は振幅が小さいと思われるため、コード化する必要はありません。この値は 0 になります。
各領域には独自のハフマンテーブルがあり(0 領域を除く)、周波数の異なるサンプルは別々にコード化されます。
サンプルを領域に分類するためには、次の変数を使用します。
big_values — big_values 領域にあるサンプルの総数を特定します
region0_count および region1_count — big_values をサブ領域に分割します。big_values から region0_count と region1_count の合計を減算すると、region2 のサンプル数が算出されます。
part2_3length — スケールファクターに使用されるビット数(パート 2)と、ハフマン符号化に使用されるビット数(パート 3)を特定します。
576 個のサンプルのデコーディングプロセスは、次のようになります。
big_values 領域のサンプルをデコードします
count1 領域のサンプルをデコードします
処理されるビット数が part2_3length よりも大きい場合、実際にはすべての入力データをデコード済みであり、さらに言えばデータを過剰に読み取っています。 したがって、total_samples_read から 4 を減算します(つまり、これらの入力ビットを破棄します)
サンプルが残っている場合は、0 を入力します(これにより 0 領域が形成されます)
表 1 はこのロジックを擬似コードとして表したものです。
total_samples_read = 0;
// Decode big_values region
[redacted for brevity]
// Decode count1 region [1]
for (int i = 0; i < count1; i++) {
samples[total_samples_read++] = huff_decode(bitstream, count1_huff_table);
}
if (bits_processed > part2_3length) [2] {
// Overread. Throw last 4 samples
total_samples_read -= 4;
}
// Fill rzero region with zeros
for (int i = total_samples_read; i < 576; i++) {
samples[total_samples_read++] = 0; [3]
}
表 1:ハフマン復号化の擬似コード
残念ながら、このコードは特定のエッジケースを見落としており、整数アンダーフローにつながります。
big_values 領域のサイズは 0
count1 領域のサイズは 0
part2_3length は 0
bits_processed は 0 を超える
この特定のケースでは、bits_processed が part2_3length より大きくなります(スケールファクターのデコーディング中に、デコーディングプロセスの前にゼロ以外の値に達します)。したがって、コードは最後の 4 つのサンプルを「スロー」します。コードはサンプルを処理しなかったため、total_samples_read は 0 です。ここにアンダーフローが発生し、このコードはサンプルを -4 個処理したと見なします。続いて、次のように 0 領域を埋めます。
バッファーポインターを &samples[total_samples_read] に設定します。これはサンプルバッファーの 前 の 16 バイトを指します。
書き込みサイズを 576 - total_samples_read = 576 - (-4) = 580 という整数に設定します。
その結果、値が 0 のサンプルバッファーの直前に、境界を越えた書き込みが生じます。なんということでしょう!
なぜこの脆弱性に CVE が割り当てられなかったのか?サンプルバッファーは構造体の一部であり、サンプルバッファーの直前のフィールドはスケールファクター配列です。この配列のフィールドはすでに攻撃者が制御しているため、ここでは関心を引くような影響は生じません。
また、コードがサンプルをデクオンタイズした後も、同じ脆弱性が発生します。この場合も 0 領域が埋められますが、今回はデクオンタイズされたバッファーで埋められます。デクオンタイズされたバッファーの前には何があるでしょうか?ハフマン復号化が行われたサンプルバッファーです。これは前述のサンプルバッファーと同じであり、攻撃者が制御できます。そのため、またしても攻撃者にとっては何の影響もありません。
そのような境界を越えた書き込みは MP3 デコーダー(WAV ファイルや .mp3 ファイルを介してアクセス可能)にも存在し、Microsoft によると今後修正される可能性があります。コーデックの反転中には影響のある脆弱性は発見されませんでしたが、デコーダーが実行するさまざまな複雑な操作に脆弱性が潜んでいる可能性があります。
2 つ目の試み:IMA ADPCM コーデックの整数オーバーフロー
次の試みは、imaadp32.acm で実行される IMA ADPCM コーデックです。既にご存知のとおり、ACM は異なるコーデック間の変換を管理します。コーデックを登録するためには、コードが ACM 関数を実行する必要があります。そのような関数の 1 つが acmStreamSizeです。この関数は、宛先バッファーに必要なバイト数を返します。
IMA ADPCM コーデックは、入力ペイロードのサイズ(cbSrcLength)、アラインメント(nBlockAlign)、ブロックごとのサンプル数(wSamplesPerBlock)に基づいて宛先バッファーサイズを計算します(表 2)。
(cbSrcLength / pwfxSrc->nBlockAlign) *(pwfxSrc->wSamplesPerBlock * pwfxDst->nBlockAlign)
表 2:バッファーサイズの計算
乗算の前に、このコードは計算によって整数オーバーフローが発生しないことを確認します(表 3)。
SrcNumberOfBlocks = cbSrcLength / pwfxSrc->WaveFormat.nBlockAlign;
v14 = pwfxSrc->wSamplesPerBlock * pwfxDst->nBlockAlign;
if ( 0xFFFFFFFF / v14 < SrcNumberOfBlocks )
return ERROR_OVERFLOW;
IsThereRemainder = cbSrcLength % pwfxSrc->WaveFormat.nBlockAlign;
if ( IsThereRemainder )
++SrcNumberOfBlocks;
DstBufferLengthInBytes = v14 * SrcNumberOfBlocks;
表 3:整数オーバーフローを防止するための計算チェック
このチェックはオーバーフローを防ぐためには十分ではないようです。もし、 cbSrcLength / pwfxSrc->nBlockAlignの除算に余りがある場合、このコードは乗算に使用される(cbSrcLength / pwfxSrc->nBlockAlign)の計算結果をインクリメントします。 オーバーフローチェックは、この増分をカバーしません。そのため、カスタム値を指定することで、宛先バッファーの長さをオーバーフローすることができます。
攻撃者は、 pwfxSrc->nBlockAlign で除算すると余りが出る cbSrcLength を 供給する必要があります。
表 4 は整数オーバーフローにつながる値の例を示しています。
cbSrcLength = 0x71c71c72
pwfxSrc->nBlockAlign = 8
pwfxSrc->wSamplesPerBlock = 9
pwfxDst->nBlockAlign = 2
表 4:整数オーバーフローにつながる値の例
これにより、宛先バッファーは 0xE バイトになりますが、宛先バッファーはもっと大きくならなければなりません。
これは境界を越えた書き込みにつながる可能性のある整数オーバーフローのように見えますが、デコード関数は割り当てられたバッファーの後に書き込みが行われないよう 正しく 機能し、バッファーが正しいサイズで割り当てられたとは見なしません。
したがって、複数のサンプルを供給しても、実際には宛先バッファーがいっぱいになるとコードは停止します。AD PCM コーデック( msadp32.acmで実行される)でも同じ挙動が起きます。
3 つ目の試み:ACM の整数オーバーフロー(CVE-2023-36710)
最後に、ACM コードに良い脆弱性が見つかりました。WAV ファイルを再生するプロセスの一環として、ACM マネージャーの mapWavePrepareHeader 関数( msacm32.drvで実行される)が呼び出されます。
この関数に整数オーバーフローの脆弱性があります。msacm32.drv は acmStreamSizeを呼び出し、それがドライバーのコールバックを呼び出します。前述のとおり、この関数は宛先バッファーに必要なサイズを返します。このサイズを受け取った後、 mapWavePrepareHeader がオーバーフローのチェックを行わずに 176 バイト(宛先バッファーを処理するストリームヘッダーのサイズ)を追加します。この追加の結果が GlobalAlloc に渡されます(図 6)。
これは悪用される可能性のある問題です。攻撃者は、 GlobalAlloc が大きなバッファーではなく本当に小さいバッファーを割り当てるようにすることができます。そのためには、 acmStreamSize が 0xffffff50~0xffffffff までの値を返すようにします。この割り当ての後、次の 2 つの、境界を越えた書き込みを発生させることができます。
ストリームヘッダーの値(構造体のサイズ、送信元、宛先バッファーのポインターやサイズなど)。これらの値は部分的に制御可能です。
コーデックのデコードされた値。これらの値は完全に制御可能です。
この脆弱性をトリガーするためには、デコード時に 0xffffff50 以上のサイズの WAV サンプルを供給する必要があります。これは簡単に達成できそうに思えますが、一部のコーデックでは達成できない可能性があることが、私たちのこれまでの試みで明らかになっています。たとえば、MP3 コーデックでは、計算の一部として、1,152 または 576(1 フレームあたりのサンプル数)のいずれかによる乗算が行われます。この計算結果は、望みの範囲内には決して収まりません。
最終的には、IMA ADP コーデックを使用して脆弱性をトリガーすることができました。ファイルサイズは約 1.8 GB です。計算上において数学的な極限操作を行うことで、IMA ADP コーデックで可能な最小ファイルサイズは 1 GB であることがわかります。
このような脆弱性の悪用は、攻撃を動的に作成するためのスクリプティングエンジンがあれば、さらに簡単になります。Windows Media Player にはスクリプティングエンジンがないため、脆弱性の悪用は困難ですが、可能性はあります(Chris Evans 氏のブログ記事「Advancing exploitation: a scriptless 0day exploit against Linux desktops(悪用の高度化:Linux デスクトップに対するスクリプトなしのゼロデイ攻撃)」を参照)。ただ、この脆弱性の悪用が成功する可能性がもっと高いのが、Outlook アプリケーション(またはその他のインスタント・メッセージング・アプリケーション)です。
まとめ
このブログシリーズでは、野放し状態で悪用されていた脆弱性をきっかけに始まった調査について取り上げました。(まだご覧になっていない方は、ぜひ 第 1 部もご覧ください。)その後もバイパスを探す形で調査は続けられ、最終的には、組み合わせることでゼロクリックの RCE チェーンを実現できる、相性の良い脆弱性が見つかりました。その脆弱性は修正されていますが、攻撃者は引き続き、リモートで悪用できる同様のアタックサーフェスや脆弱性に目を光らせています。
現在のところ、Akamai が調査した Outlook のアタックサーフェスはまだ存在しており、新たな脆弱性が見つかって悪用される可能性があります。Microsoft は、PidLidReminderFileParameter プロパティを含むメールを排除するため Exchange にパッチを適用しましたが、この緩和策を回避できる可能性があることは否定できません。
付録
このサイト では、Microsoft の Media Foundation で使用可能なすべてのメディアの種類とコーデックがリストアップされています。
Akamai がテストを行った結果、WAV を通じて実際に使用できるコーデックは次のとおりであることがわかりました。
1 — PCM
2 — ADPCM
6 — A-LAW
7 — U-LAW
11 — IMA ADPCM
31 — GSM 6.10
55 — MPEG-1 Audio Layer III(MP3)
00000003_0000_0010_8000_00aa00389b71 — IEEE 浮動小数点数
00000008-0000-0010-8000-00aa00389b71 — DTS オーディオ
00000092-0000-0010-8000-00aa00389b71 — Dolby Digital
00000164-0000-0010-8000-00aa00389b71 — Microsoft WMA