前回のブログ記事で、公開されたPoCを使い、実際に脆弱な Windows10 でシステムがクラッシュすることを確認しました。
今回は、その原因についてです。
と、言っても、既に原因は公開されており、検証で用いたPoCもその原因を突くものでした。
以下の記事に原因が書かれています(前回のブログの記事のPoCは別のものを使っています)。
CoronaBlue / SMBGhost Microsoft Windows 10 SMB 3.1.1 Proof Of Concept
This script connects to the target host, and compresses the authentication request with a bad offset field set in the transformation header, causing the decompresser to buffer overflow and crash the target.
「不正なオフセットフィールドを使用して認証要求を圧縮する」とあり、この通信時のオフセット値に異常値を入れることで、解凍プログラムがバッファオーバーフローし、ターゲットがクラッシュするという理由です。
答えだけなら、既に解析をされた方々が書いてくれていましたので、そちらを参照されるといいと思います。
私は相変わらずナナメな人なので、「今回はpcapのデータから原因を推測する」といことを書いてみたいと思います。
通信データから攻撃の原因を推測する
この方法をあえて出すのは、実際にパケットを見て問題がどこにあるかを確認することで理解が深まるというのもありますが、「実際にエクスプロイトとみられる通信を分析する場合」にも、方法として参考になるのでは、と考えたためです。
実際に、ワームの分析で使ってみたことがあります。WannaCryっていうんだけどね。コイツがEternalBlue脆弱性を狙って攻撃したり、DoublePulsarバックドアの通信で実際にやってみると、色々得るところがありました。
今回のPoCで、通信をWiresharkでモニタしていました。
この通信内容を見てみます。
問題となっているオフセット値に0xFFFFFFFFが入っています。これが、クラッシュの直接的な原因と言われています。
解凍処理でこの値を信じてしまい、境界値を超えているかどうかをチェックしていなかったのがその理由と言われています。
では、この値はどのような値が正しいのでしょうか?
異常を知るには、どのような値が入るべきかを知る必要があります。
また、値の意味を知ることで、どのような動作をする可能性があるか、も推測できるではと思います。
この通信のデータの内容を確認し、問題点を分析しましょう。
仕様の確認
仕様を確認するのに最も簡単かつ確度が高いのは、公開されているドキュメントを確認することです。
SMBは公開されたプロトコルであるため、その仕様はMicrosoftのHPに公開されています。
今回はSMB3ということで、以下のドキュメントを参考にします。
[MS-SMB2]: Server Message Block (SMB) Protocol Versions 2 and 3
結構紛らわしいのですが、今回は「[MS-SMB]: Server Message Block (SMB) Protocol」のほうは関係ありません。
SMB2と3は別文書かつ別のページになっているので、見落とさないように注意・・・っていうか、前回に引き続き今回も私は最初間違えてましたよ!どうしてもマニュアルと合わなくて、「アレ?」と思ったものです(汗)。
全てのSMBの通信内容を確認していると非常に長くなる(といっても今回は5つしかないんだが)ので、問題のあったフレームだけ見ていきます。
マニュアルの「2.2 Message Syntax」が名前のとおりメッセージの構文であり、その構文内の規約が書いてあります。
今回は、「2.2.42 SMB2 COMPRESSION_TRANSFORM_HEADER」が問題のフレームの対象になります。
なお、2019年3月13日のプロトコルバージョン57.0でこの項が追加されており、新しい機能はこのように項がどんどん追加されていくようです。
当該項目の内容は以下のとおりです。
(Microsoft Corporation "[MS-SMB2]: Server Message Block (SMB) Protocol Versions 2 and 3" より引用)
問題が発生した通信のデータと仕様の検証
では、今回問題になった通信データと仕様に書かれたパラメータを比較します。
問題のフレームのうち、該当するメッセージ構文の部分の図を再掲します。
マニュアルに出ているパラメータは以下のとおり。
- ProtocolId = FC 53 4d 42 (\0xFC + "SMB")
- OriginalCompressedSegmentSize = B4 01 00 00 = 436 *注
- CompressionAlgorithm = 01 00 = 1 *注
- Flags = FF FF
- Offset/Length = FF FF FF FF = 4,294,967,295 *注
*注:リトルエンディアン
それぞれのパラメータを確認してみましょう。
まず、ProtocolIdは仕様どおりで問題ありません。
OriginalCompressedSegmentSizeは解凍後(圧縮前)のデータのサイズのようです。これは、パケットデータからでは正しい数値の確認は難しいですが、飛びぬけておかしい数値にはみえません。
CompressionAlgorithmは、「2.2.3.1.3 SMB2_COMPRESSION_CAPABILITIES」に掲載されている表を参照するようになっています。
(Microsoft Corporation "[MS-SMB2]: Server Message Block (SMB) Protocol Versions 2 and 3" より引用)
この表から、CompressionAlgorithmには「LZNT1」の圧縮アルゴリズムが指定されていることが分かります。
Flagは FF FF ですが、これは変です。
仕様書通りであれば、 00 00 か 00 01 のいずれかしか値をとらないはずですが、FF FF となっています。
このため、フラグの値が異常なために誤動作を起こす可能性は考慮する必要があります。
このケースでは、SMBの実装プログラムが、このフラグの値をリテラル値で判定しているか、ビット単位でANDで判定しているかで動作が変わってしまいそうです。
リテラル値での判定の場合、if文で 00 01 と合致すれば「SMB2_COMPRESSION_FLAG_CHAINED」と判定するでしょう。
問題は「それ以外の値」の判定で、00 01 以外は全て 「SMB2_COMPRESSION_FLAG_NONE」とみなすか、ちゃんと 00 00 も if文 でチェックし、合致しなければ異常を検知してアボートするかは実装次第です。今回は、恐らく前者だったのではないかと私は思います。なぜなら、フラグの値のチェックで異常でアボートしていたら、その後のオフセット値を使うことなく通信エラーで終了していたと考えられるためです。
一方、ビット単位で判定している場合、最下位ビットが立っているため、「SMB2_COMPRESSION_FLAG_CHAINED」と判定してしまう可能性があります。
ここは、フラグの値の使い方次第なので、どちらかを判定するのは割と難しかったりします。
(「This field MUST be set to one of the following values」となっていて、いずれかのうちの1つの値をとる、というような表現なので、リテラルには見えるんですけどね。一方、Flagになっているパラメータはビット判定しているものも多いので・・・)
ただし、今回はオチだけ言ってしまえば、別のPoCではこの値を 00 00 にしていても脆弱性を突けるようなので、このパラメータが正常でも今回の脆弱性を突くことができることが分かっています。
正確な検証をする場合、この値を正しくしても再現するか、を確認するのも重要な観点になるでしょう。
そして、問題のOffset/Length。
これは FF FF FF FF となっており、オフセットとしてもサイズとしても、明らかにおかしいと考えられます。
ここで、「Offset/Length」になっている理由は把握する必要があります。
ここのパラメータは、一つ前の Flag の値によって意味が変わります。
マニュアルには、フラグが「SMB2_COMPRESSION_FLAG_CHAINED」の場合Length、それ以外の場合はOffsetであると書かれています。
今回の問題は「SMB2_COMPRESSION_FLAG_NONE」の場合と考えられ、この値はOffsetとして扱われたと考えられます。
結果、Unsignedで扱われたオフセット値のアドレスの参照する処理が異常を起こし、カーネルがクラッシュしたのが、今回のPoCの動作だったのだろう、と考えています。
この脆弱性でリモートコードは実行できる?
この点が非常に重要なのですが、ここの判断が今のところ悩ましいところです。
一見すると、EternalBlueのように絶妙なペイロードのサイズでバッファをオーバーランさせ、シェルコードを実行できる穴を作ったわけではなく、異常なオフセット値から不正なメモリ領域を参照したことによるクラッシュのようには見えます。
リモートコードの実行のためにシェルコードを実行させるためには、どうしてもEIPをシェルコードのあるアドレスへ飛ばす必要があります。
異常なオフセット値による不正なメモリ領域の参照が原因であれば、EIPが変更されることはないのでリモートコードは実行されないと思います。
しかし、今回は不具合の発生したプログラムが、異常なオフセット値を受け取った際にどのような処理をしていたかは分かりません。
もしかすると、オフセット値を絶妙な値にすることで、例えばcallのリターンアドレスが上手く書き換わるような手があるのかもしれません。
また、Microsoftも、「リモートコード実行が実行される脆弱性」と書いているため、実際のところはオフセット値が異常な場合にEIPに変な値が入って、変なアドレスの値を命令としてフェッチしようとしてぶっ飛んだんじゃないか、ということも十分考えられます。
まあ、私のような凡人はそんな想像くらいしかできないので、あとはハッカーさんに任せましょう。
いや、こんな妄想で終わらせず、異常動作を分析してリモートコード実行できるとか、マジハッカーさんスゲーっす。
(技術的な検証と言っておいて、このオチで果たしていいのだろうか・・・)
おわりに
今回は、キャプチャした通信データをもとに、攻撃の可能性が無いか、あるとしたらどこを狙ってきたかを分析してみる、という観点で記事にしてみました。
今回のケースで言えば、原因はもう圧縮の場合のオフセット値の問題という情報がでていたので、こういった分析をする必要性はあまりないのですが。
ただ、実際に脆弱性を突く通信の内訳を詳しく知っておけば、この攻撃に対する理解も深まると思います。
また、この方法は、攻撃方法が未知な場合に、通信データが仕様外になっていることが原因の場合に、その理由を突き止めるにはある程度有効だと思ったということもあります。
もっとも、今では通信量は膨大なので、ある程度プロトコルを絞って、かつ目星をつけながら一つずつ潰していくという、面倒な作業でもあります。
また、今回の内容は、セキュリティ技術者だけでなく、プログラムを作成する人にも見てほしい内容です。
プログラムの実装で、「このような実装がこういった大きな問題になる」という、非常に良い実例でもあります。
プログラムの作成では、境界値チェックやエラー処理はたくさん入れるはずです。
ただ、振り返ってみて、「そのチェック、本当に足りていますか?」と考える時があると思います。
その時に、チェックが不足しているとこのような脆弱性になる、ということを知っていれば、それを回避するためにどのようなチェックをどのような判定方法で実装するか、ということを考えるトリガーになると思います。
最近は通信が非常に安定しているため、アプリケーション層でのコーディングでは受信データの誤りを気にしなくて良くなっているため、「仕様どおりならこの範囲の値しか来ない」や、「正しい値を入れるのは送信側の役目」と考えがちですが、攻撃者はあえて「仕様を外した値を入れて誤作動を起こさせる」ことを仕組んでくるので、それを想定したコーディングが必要、ということを感じ取ってもらえれば幸いです。
まあ、私が掲載しているサンプルコード、チェック甘々だけどな!!