バッファオーバーランによるリターンアドレスの書き換え | reverse-eg-mal-memoのブログ

reverse-eg-mal-memoのブログ

サイバーセキュリティに関して、あれこれとメモするという、チラシの裏的存在。
medium(英語):https://sachiel-archangel.medium.com/

SMBv3の脆弱性でいくつか記事を書きましたが。

そもそも、バッファオーバーランで、なんでリモートコード実行ができるの?」っていう疑問があるんじゃなかろうか、と思います。

そこで、今回はバッファオーバーランの脆弱性のある関数を用いて、実際にバッファオーバーランするところを、IDAを使って観測してみます。

プログラムの内部でこういうことが起こっているから、リモートコード実行の可能性が発生しうる、だからバッファオーバランには気を付けなければいけない、ということの理解が目的です。

そのため、今回のサンプルでは、あえてリモートコードの実行まではせず、自身のプログラムがクラッシュする(例外が発生する)程度にとどめています。例示するソースコードのような単純なバグは少ないですが、それでもリモートコード実行の悪用をされないようにするための措置です。

どうしてもという方は、ハッカーさんの書いた本でも参照してください。

(私も詳しくは知らないともいう。)

 

今回は32ビットでやっており、IDAの無償版でも確認できると思いますので、興味がある方はやってみてもいいでしょう。

 

 

サンプルプログラムの準備

今回のサンプルプログラムは非常に単純なもので、main処理から一つの関数を、文字列を引数として呼び出しています。

関数の中ではローカル変数の16バイトのバッファを用意しており、こちらに一度データをコピーしてから printf して関数を終了するという単純なものです。

この関数を呼び出すときの文字列を、16バイト未満のものと、16バイト以上の「適当な長さ」に調整したものの2つを用いています。

 

ソースコードも記事に入れると長くなるので、これもおまけ記事に掲載しておきます。

 

作成は VisualStudio 2017 で、32ビットでコンパイルしたものを使っています。

(特に32ビットというところは割と重要。)

 

 

正常な処理の流れの確認

まず、正常な流れから確認します。おまけ記事の OverRunFunc に、"Test" というオーバーランしない長さの文字列を与えます。

この文字列は、実際は「Test\0」となるため、5バイトということになります。

IDAでの OverRunFunc の呼び出しは以下のようになりました。

 

 

 

Windowsのx86の場合、関数を呼び出すと、スタックにまずリターンアドレスが引数の上に積まれます

さらに、関数終了時にスタックポインタを戻すために、ベースポインタ(EBP)のアドレスが積まれます

さらにローカル変数分の領域を確保した先にスタックポインタ(ESP)が移動します。

以下の図は、関数が呼ばれた直後で、リターンアドレス(0x012E10BD)がスタックに積まれた状況です。

0x012E1000 の push  ebp の実行でベースポインタ(EBP)のアドレスが積まれ、0x012E1003 の sub  esp, 10h の実行でローカル変数分の領域を確保した先にスタックポインタ(ESP)が移動する、という流れです。

x86 の関数の場合、概ねこのような流れになっています。

(今回は本筋と違うところで話がこじれるので stdcall と cdecl の違いは割愛しよう・・・)

 

 

 

このあと、ローカル変数の buf[16] にあたる領域を0クリアし、ループ処理でのデータのコピーを実施します。

ループ完了すると、以下の図のようになります。

スタックの領域に文字列とNULL止め(0x00)がコピーされています

 

 

 

そして、printf を実行する関数(sub_12E10E0)を呼び出したあと、関数を終了しリターンします。

retn 命令時にスタックポインタが示しているアドレスの値がリターンアドレスです。

この場合、 0x012E10BD が格納されており、 call した次のアドレスへ正しく戻ることがわかります。

これが、正常な場合の関数の実行の流れです。

 

 

 

バッファオーバーラン発生時の流れ

次は、バッファオーバランする場合の処理の流れです。おまけ記事の OverRunFunc に、"Buffer over run.        "(末尾にスペース8つ) というオーバーランする長さの文字列を与えます。

この文字列は、「Buffer over run.        \0」となるため、25バイトということになります。

IDAでの OverRunFunc の呼び出しは以下のようになりました。

 

 

 

関数開始時のスタックの状態は、正常時と同様にリターンアドレスが正しく格納されています。

下図では、スタックポインタの示すアドレス 0x008FFBD4 に、リターンアドレス 0x012E10CA が格納されていることが確認できます。

 

 

 

このあと、ローカル変数の buf[16] にあたる領域を0クリアし、ループ処理でのデータのコピーを実施します。

ループ完了すると、以下の図のようになります。

スタックの領域に文字列とNULL止め(0x00)がコピーされています

・・・が、ここで正常時との違いが出ます。

buf[16]で確保した領域をはみ出して文字列が書き出されています

そのため、元あった別のパラメータが上書きされています。

さっきリターンアドレスをいれたアドレス 0x008FFBD4 のデータも、0x20202020 に書き換わってる!?

 

 

 

そして、printf を実行する関数(sub_12E10E0)を呼び出したあと、関数を終了しリターンします。

この printf 自体は、あくまで関数内で使っているスタックポインタの範囲内を参照しているので、メモリ参照エラーにならず正常に出力されます

retn 命令時にスタックポインタが示しているアドレスの値がリターンアドレスです。

この場合、 0x20202020 が格納されている!? call した次のアドレスとは違うアドレスに飛んでしまう!?

 

 

 

 

このリターンアドレスでさらに処理を続けると、IDAにメモリ参照エラーで怒られました・・・。

なぜなら、このプログラムでは、 0x20202020 というアドレスは有効な領域ではないからです。

しかし、もしそれが有効なアドレスで実行可能領域であったなら、エラーにならないのではないか?といえば、Yes です。

そして、もしそのアドレスが攻撃者によって用意されたコードであれば、リモートコード実行が成立する、という理屈です。

これが、バッファオーバーランでリモートコード実行になり得る、という一例です。

 

まあ、そのアドレスにどうやって飛ばすかってのが、実際にリモートコードを実行するにあたっては問題なのですが。

(この例だとEBPもおかしくなってるしね。)

そういうのはハッカーさんに聞いてくださいw

私はタダのおっさんなのですw

 

 

対 策

では、このようなバッファオーバーランを起こさないような方法はあるのでしょうか?

いくつかの方法を紹介します。

 

コーディング上での対策

まずはコーディングでバッファ長を必ず入れる癖をつけて、サイズをチェックする処理を心掛けることです。

・・・って、それができてればバッファオーバーランは起きないし、ポカミスでやらかすことも多い、ってことですよね。

ただ、使う関数によっては、必然的にそれを成すことになるものがあります。

Windows の例になりますが、今回の文字列のコピーでは関数を使う場合に strcpy() という関数を使わず、strcpy_s() を用いることを推奨しています。というか、 strcpy() を使うと、デフォルトではWarning が出ますね。

strcpy_s() では、strcpy() に引数としてバッファサイズを与えます。これにより、意図しないバッファオーバーランを防ぐことができます。

もちろん、バッファサイズに正しい値を設定することが前提となります。

他にも _s 付でバッファサイズを引数に追加している関数があるので、Warning レベルを変えるのではなく、そちらを使うようにしたほうが良いでしょう。

 

デベロッパが付与するバッファオーバラン検知機能

今回のサンプルコードを書く際に使用した VisualStudio 2017 では、ウィザードで作成したプロジェクトのコンパイルオプションに /GS がデフォルトで付いています。

これは、スタックのバッファオーバランを止めるまではできないものの、「検知」して例外を発生させます。

これにより、深刻な被害になる前に、プログラムを停止させることができます。

仕組みとしては、関数の最初と最後に __security_check_cookie関数 を入れます。

これは、スタックにトークンを入れておき、バッファオーバーランが発生した場合にその値が変化したことで検知し、例外を発生させるというものです。

すっごい雑な絵で説明すると、以下のとおりになります。

 

 

一応欠点としては、関数の最初にトークンの設置、最後に検証処理が入るため、多少のオーバーヘッドにはなるという点です。

ただ、よほどでなければ気になるほどのオーバーヘッドでもないので、バッファオーバランによる問題発生に比べれば有効にしておいたほうがいいのでは、と思います。

 

Address Space Layout Randomization (ASLR)

これは直接的なコーディングではなく、バッファオーバーランの抑止でもありませんが。

プロセスのアドレス空間をランダム化する技術です。

これの利点は、実行される度に配置されるアドレス空間が変わるため、コードやスタックのアドレスを推測しづらくします

今回のような場合、事前にアドレスが分からないと、攻撃者もオーバーフローしたリターンアドレスを細工する際に、不正コードのあるアドレスに飛ばしづらい、ということになります。

あくまで緩和措置であり、完全ではないことや、相対ジャンプ等には効果が無いことは念頭に置く必要があります。

また、実行ファイルの特定のフラグをオフにすることで、ASLR を無効にしてプログラムを起動できるようになります。

 

ファジングテスト

これは、作成したプログラムでちゃんと試験をすることで、バッファオーバランなどの脆弱性をリリースする前に見つけるという技術です。

テストでは、正しい値をいれて想定通りに動くかや、ループ試験、境界値試験を重点的に行うケースが多いと思いますが、ファジングテストではあえて異常データを入れて誤動作しないかを確認します。

バッファオーバランも、このテストでみつかることがあると思います。

記事の元ネタになったSMBv3の脆弱性も、元はと言えば通信データに設計上では想定外の値が入っていたことが原因でした(以前の記事参照)。

(さてわ Micr〇soft 、ファジング試験やってなかったな!?とか言ってイジメない。)

 

IPAがファジングについてHPに掲載しているので、興味がある方は見てみるといいでしょう。

 

ファジング:FAQ

https://www.ipa.go.jp/security/vuln/fuzz_faq.html

 

 

さいごに

バッファオーバーランが問題だ、リモートコードが実行され得る、というのはよくコメントで見かけますが。

実際にプログラムの内部でどういうことが起こっているのか、というのは、なんとなくイメージはしつつも、実際の動きは見たことがなかった人もいたのではないでしょうか。

あるいは、イマイチピンときていなかった人もいるのではないでしょうか。

今回は、あえて脆弱なプログラムを用意して、実際にバッファオーバランが発生している状況を、パラメータを見ながら観察することで、発生の原理が分かったのではないかと思います。

発生の原理が分かれば、対策の重要性の理解も進み、対策の発案も可能になります。

 

単に「作成者が気を付ける」以外の方法も出てきているので、それらも活用してセキュアなプログラミングを実現しましょう。

 

だって、折角作ったプログラムを、本来の機能以外のところでケチがついたり悪用されたら、腹立つだろう?

 

記事書いた理由はそんだけだったりします。

セキュリティを勉強している人やプログラム開発者の何らかの参考になれば幸いです。

(博麗神〇例大祭が延期になって、3連休暇を持て余してたんだろう?とか身も蓋もないことイワナイ。艦〇れミニイベントはラスダンで道中すら抜けれず諦めの境地・・・)