Windowsには様々なセキュリティ機能が備わっている、しかしそれらすべてを使いこなしている人は想像以上に少ない。
自分もまた使いこなしているとはとてもじゃないが口にできない。
それでも、理解を深めようと努力することはできる。
というわけで、今回はRun-Time Error ChecksとBuffer Security Checkというセキュリティ機能について解説したいと思います。
一応続き物なので、初めての人は前回の記事からどうぞ。
今回も本記事を読む前にサンプルコード集をDLしてください。
rao_exploit.zip
ちなみにraoはreturn address overwriteの頭文字です。
この記事の目次
exploit1:return address overwrite攻撃
前回スレッドスタック(以後スタック)上には様々な情報が配置されると書きましたが、その中にreturnアドレスがあったのを覚えているでしょうか?
returnアドレスとは、関数呼び出し時にスタック上に積まれる呼び出し元のアドレスのことで、関数の実行が終わった後に元の処理を再開するために使われます。
ですが、スタック上にあるということは当然ローカル変数のバッファオーバーフローによる影響を受けます。
そう、スタック上のreturnアドレスを利用した攻撃が存在するのです。
return address overwrite攻撃とは
return address overwrite攻撃とは、SEH overwriteと同様スタック上に配置されたreturnアドレスを任意のアドレスで上書きし、そこを実行させることにより任意のコード実行を行う攻撃を指します。
SEH overwriteとの主な違いは
- returnするまでクラッシュさせずに処理を継続させる必要がある
- 例外ハンドラとして実行されるわけではないのでSafeSEHの影響を受けない
- 同様にSoftwareDEPの影響も受けない
となります。
おそらく説明だけではピンと来ないかと思うので、さっそくためしてみましょう。
実際に試してみよう
今回のソースコードはこちら。
#include <cstdio> #include <Windows.h> void call() { ::MessageBoxA(NULL, "message", "title", MB_OK); ::exit(0); } int main(const unsigned int argc, const char * const * argv) { FILE * const fp = ::fopen("file", "rb"); unsigned int size; char s[256]; ::fread(&size, 4, 1, fp); ::fread(s, 1, size, fp); for (unsigned int i = 0; i < size; i++) { ::printf("%02x ", s[i]); } ::fclose(fp); return 0; }
前回同様、ファイルの先頭4byteを長さとして本体を読み込むもののバッファは256byteしかとっていないのでバッファオーバーフローが発生しうるコードです。
callはどこからも実行されていないのも以前と同じ。
今回はさらにSafeSEHのオプション付きコンパイルされており、可能ならDEPを有効にしてもかまいません。
では実行してみましょう。
exploit1のtest.exeを実行してみれば、callはどこからも呼ばれていないにもかかわらずmessageとメッセージボックスが上がったかと思います。
これが今回問題となるreturn address overwrite攻撃になります。
ちなみにfileの中身はreturnアドレスまでバッファオーバーフローさせてcallのアドレスで上書きしているだけ。
例外を吐かせるわけにはいかないので、fpやsizeを上書きしてしまうなら調整が必要でしたが上書き範囲外だったので無視しています。
※説明はしていませんが以降のexploitではfpやsizeでクラッシュしないよう調整を行っています。
exploit1まとめ
return address overwrite攻撃とはスタック上のreturnアドレスを上書きして任意コードを実行させる攻撃。
SafeSEHやDEPでは防げない。
次項では一部の開発者間ではバッファオーバーフローに有効とされているRun-Time Error Checksを使用して防いでみましょう。
exploit1_RunTimeErrorChecks:バッファオーバーフローの検出
バッファオーバーフローには実は弱点があります。
始点から目標とするメモリーまでの間全てを上書きしなければならないという点です。
それを利用したバッファオーバーフロー検出手段として、Run-Time Error Checksというものがあるのです。
Run-Time Error Checksとは
Run-Time Error Checksとは大きく分けて三つの機能があります。
- 切り捨てが発生する整数キャストを検出
- 未初期化の変数の使用を検出
- スタックオーバーフローを検出
今回は必要ないのでスタックオーバーフローの検出以外は説明しませんが、興味のある方は「/RTC msdn」などでググるといいでしょう。
さてどのようにスタックオーバーフローを検出するのかというと、スタックオーバーフローが発生しそうな変数、今回の場合はsのすぐ後ろに未使用の領域をとり、そこを0xCCCCCCCCで埋めておき、関数を抜ける直前にチェック、0xCCCCCCCCから変化していたらバッファオーバーフローが発生したとしてアプリを強制終了させるというものです。
では今回も試してみましょう。
実際に試してみよう
今回はバッファオーバーフローの検出を行う/RTCsオプションをつけただけでソースコードに変更はありません。
ではさっそくexploit1_RunTimeErrorChecksのtest.exeを実行してみましょう。
どうでしょう、ちゃんと強制終了したでしょうか。
デバッグ情報付きでコンパイルしたうえで逆アセンブルしてみるとmain関数を抜ける直前で_RTC_CheckStackVarsという関数を呼び出しています。
その中では変数sの次のメモリーが0xCCCCCCCCであることをチェックしようとして失敗し、そこでエラーを吐いて強制終了する、という流れです。
exploit1_RunTimeErrorChecksまとめ
Run-Time Error Checksを使えば単純なバッファオーバーフローは検出できる。
ですが、実はRun-Time Error Checksは処理速度を大幅に低下させるなどの副作用があり、デバッグ専用のオプションとされリリースするアプリで使用することは非推奨となっています。
なぜ処理速度が低下するのかというと、変数の数に応じて仕込むデータの量が多くなるため、関数によっては仕込むのと検証に必要な時間が無視できないレベルまで伸びるためです。
また、「単純な」とついていることからわかるようにRun-Time Error Checksは簡単に回避が可能です。
次項ではそのあたりについて触れることにしましょう。
exploit2:Run-Time Error Checksは騙せる
実はRun-Time Error Checksはセキュリティー上の防御機能として設計されたわけではなく、あくまでコーディングミスによるバッファオーバーフローを検出しようとして作られたものです。
そのためセキュリティー機能としては致命的な弱点を持っているのです。
Run-Time Error Checksの弱点とは
Run-Time Error Checksの説明で「上書きチェック用に埋められるデータは0xCCCCCCCCである」と書きました。
これはRun-Time Error Checksを誰がどう使おうと変動することはありません、常に0xCCCCCCCCです。
そしてデータが配置される位置も当然変動することはありません。
つまり、Run-Time Error Checksが動作していると分かっていれば、そこを0xCCCCCCCCで埋めてしまえば容易にチェックを潜り抜けることが可能になるのです。
実際に試してみよう
今回もソースコードに変更はありません、当然/RTCsのオプションもついたままで、読み込むfileの中身を一部0xCCCCCCCCに書き換えただけになります。
ではexploit2のtest.exeを実行してみましょう。
どうでしたか?
おそらくmessageとメッセージボックスが表示されたかと思います。
exploit2まとめ
Run-Time Error Checksはセキュリティ機能ではなくデバッグ支援用のバッファオーバーフロー検出に過ぎない。
適切な個所を0xCCCCCCCCで埋めてしまえばRun-Time Error Checksによる検出回避は容易。
どうでしたでしょうか?
一部ではRun-Time Error Checksは処理速度低下のデメリットが大きいだけでセキュリティ向上するのだから有効にするべきだとする人もいるそうですが
このようにいとも簡単に突破できてしまう以上、処理速度を犠牲にしてまで有効にするべき機能ではないと私は考えます。
もちろんそれはRun-Time Error Checksを上回るセキュリティ機能が存在するからという話でもありますが。
次項では、そのRun-Time Error Checksを上回るセキュリティ機能、Buffer Security Checkについてです。
exploit2_BufferSecurityCheck:return address overwriteにとって最大の天敵
Run-Time Error Checksの弱点について説明した時、0xCCCCCCCC固定ではなく乱数にすればよいのではないか、と思った方もいるのではないでしょうか?
そう、それを実現した機能がBuffer Security Checkになります。
Buffer Security Checkとは
Run-Time Error Checksでは0xCCCCCCCC固定なのと処理速度が低下するのが問題だと書きました。
その両方に対応したのがBuffer Security Checkであり、これ単独で実質上return address overwriteを完璧に防ぐことのできるセキュリティ機能となります。
原理は簡単で、起動時に乱数を生成してグローバル領域に保持、バッファオーバーフローしうる関数の開始時点でreturnアドレスの直後にその乱数列とスタックの先頭アドレスをxorしたものを入力しておく、その後returnする直前でその値が変わっていないことを検証し、検証に失敗すればそこで強制終了するというものです。
Run-Time Error Checksに比べて優れている点は二点。
第一に、乱数計算は事前に済ませておく、データ挿入も検証も関数呼び出し一回につき一回だけ、と出来るだけ軽量な処理で実行できるということ。
第二に、乱数を利用しているため起動するごとに別の値となり、32bitアプリケーションであれば1/4,294,967,296の確率でしか突破できない堅牢さ。
この二点です。
実際に試してみよう
今回もソースコードに変更なし、コンパイルオプションとして新たにBuffer Security Checkを有効にする/GSを追加しています。
ではexploit2_BufferSecurityCheckのtest.exeを実行してみましょう。
どうでしたか?
うまくいっていれば攻撃は見事阻止されアプリは強制終了したかと思います。
デバッグ情報付きでコンパイルしたうえで逆アセンブルしてみると、まず最初に__security_init_cookieで__security_cookieに乱数値を設定、main関数最初で__security_cookieとスタックの先頭アドレスをxorしたものをセット、return直前に__security_check_cookie関数を呼び出して検証しているのが分かります。
総まとめ
バッファオーバーフローでreturnアドレスを上書きすれば任意コード実行が可能。
Run-Time Error Checksを有効にすれば単純なバッファオーバーフローなら検出して防御できる。
だが上書きチェックに埋めておくデータは0xCCCCCCCC固定なので、そうと分かれば検出回避は容易。
Buffer Security Checkなら乱数でチェックされるのでほぼ完全に阻止できる。
というわけで、みなさんBuffer Security Check(/GSオプション)は有効にしましょう。
Run-Time Error Checksはデバッグには便利ですが、セキュリティー的には大した効果がないので、リリースバイナリではかける必要はありません。
これでreturn address overwrite攻撃対策は完璧ですね。
終わりに
前回と今回でバッファオーバーフローを用いた二大攻撃手段であるSEH overwriteとreturn address overwriteの詳細と対策を述べました。
まだまだ攻撃方法はいろいろありますが、この二つが防げるだけでもだいぶ安全性が違うので、みなさんもぜひ導入しましょう。
とはいえ、前回の/SAFESEHと違って/GSはデフォルトでONなので知らず知らずに使っている開発者さんも多いかと思います。
そういう場合はラッキーと思いつつ、このオプション一個で左右されるセキュリティの大きさに思いをはせるなどしてみるのもいいかもしれません。
さて、次回は攻撃方法を特に限定することなく全般的に攻撃を困難にするASLR(アドレス空間レイアウトランダマイズ)の解説をしようかと思います。
ではまた!