#navi_header|C言語系| VC++では、コンパイラレベルでスタックの破壊を「検出」する為の補助機能を提供している。 スタックの破壊は、プログラムのバグや安全でない関数の使用などが原因となり発生する。 これら補助機能でスタックの破壊を「防ぐ」ことは出来ないが、破壊された事を「検出」するチャンスは得られる。 状況によっては補助機能による検出をすり抜けてしまう場合もあるが、最低限度の安全弁として活用していきたい。 なおバッファ本来の領域を越えてデータを書き込んでしまう現象には「オーバーラン」と「アンダーラン」の二種類がある。 x86ではアドレスの小さい方向にスタックが伸びるため、「オーバーラン」の方がスタックの破壊とそれに伴うセキュリティ問題の原因と成りやすい。 バッファ本来の領域を越えて、 - アドレスの大きい方向へデータを書き込んでしまうのが「オーバーラン」 - アドレスの小さい方向へデータを書き込んでしまうのが「アンダーラン」 #more|| 対象:Visual C++ 2008 Express Edition, Windows XP SP3 (Japanese) > cl Microsoft(R) 32-bit C/C++ Optimizing Compiler Version 15.00.30729.01 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. > link Microsoft (R) Incremental Linker Version 9.00.30729.01 Copyright (C) Microsoft Corporation. All rights reserved. #outline|| ---- * "/GZ" -> "/RTC" コンパイラオプションによるランタイムエラーチェック - VC++2005までは "/GZ" オプションとして提供されていた機能。 - VC++2005以降は "/RTC" オプションに統合され、"/GZ"オプションは非推奨となった。 - "/RTC"はランタイムエラーチェックを有効にするコンパイラオプションで、何種類かバリエーションがある。 -- "/GZ"と同様にスタックの破壊を検出するには "/RTCs" を指定する。 - 最適化オプションと組み合わせることは出来ず、開発時(=デバッグ)ビルドでのみ使える。 ** 効果確認用サンプルコード test1.c "/RTC"オプションの効果を確認する為、わざとバッファオーバーラン可能なソースコードを用意してみた。 test1.c: #code|c|> #include #include char msg[] = {0x30, 0x31, 0x32, 0x33, 0x0}; // コピー先はスタック上だが、コピー先のバッファサイズ // チェックをしていないため、容易にオーバーランが発生する。 // len > 5 にするだけで簡単に発生する。 int mycpy(const unsigned char *src, int len) { unsigned char dest[5]; int i; for (i = 0; i < len; i++, src++) { printf("copying src[%d] -> dest[%d]\n", i, i); dest[i] = *src; } return i; } int main(int argc, char *argv[]) { mycpy(msg, 5); return 0; } // オーバーランを使って実行させたい関数 void hiho(void) { printf("hi-ho! :P\n"); exit(0); } ||< ** "/RTC" 無しでコンパイルし、オーバーランを使って"hiho()"を実行してみる コンパイル: > cl /Fetest1_nogz.exe test1.c /FAcs /GS- /Od /nologo この時点では、mycpy()に渡すlenが5、つまりコピー先バッファサイズと同じなのでオーバーランは発生しない。 各関数のobj内でのアドレスや、ImageBaseを確認しておく。 > dumpbin /symbols test1.obj ... 00D 00000000 SECT4 notype () External | _mycpy 00E 00000000 UNDEF notype () External | _printf 00F 00000060 SECT4 notype () External | _main 010 00000080 SECT4 notype () External | _hiho ... > dumpbin /headers test1_nogz.exe ... 400000 image base (00400000 to 0040EFFF) オーバーラン用のデータを作る為に、mycpy()が呼ばれてforループに入った直後のスタックを確認してみる: #pre||> ESP : 0012FF5Ch EBP : 0012FF68h --------------------- 0012FF5C : 004027B0 # -> この8バイトがdest[5]領域。メモリバスに合わせる為、 0012FF60 : D3651E6D # 4の倍数に丸められたものと思われる。 0012FF64 : 00000000 # -> int iの領域で0クリア後の状態 0012FF68 : 0012FF78 # -> 以前のスタックフレーム(EBP) 0012FF6C : 0040106F # -> return先で、main()の中 0012FF70 : 0040C000 # -> "src"のアドレス 0012FF74 : 00000005 # -> "len" ||< ここから、return先を書き換えるには "dest" から16バイト先の4バイトを書き換えることになる。 しかしループカウンタの"int i"やEBPなども途中に存在する為、それらも考慮してデータを作る必要がある。 return先のアドレスは"hiho()"関数にする。まずImageBaseは400000h、".text"セクションはRVAで1000hから始まり、さらに"hiho()"はobjファイルの段階で先頭から80hにある。よってこれらを加算した 401080h が"hiho()"の実行時のアドレスになる。 これらをもとに、test1.cのmsgを20バイトに修正し、mycpy()に渡すlenも20に修正する。 #pre||> char msg[] = {0x30, 0x31, 0x32, 0x33, 0x1, 0x0, 0x0, 0x0, 0x08, 0x00, 0x00, 0x00, // "int i" の値を壊さないようにする。 0x78, 0xFF, 0x12, 0x00, // スタックフレームのEBPを壊さないようにする。 0x80, 0x10, 0x40, 0x00, // "hiho()"のアドレス }; ||< 実行: #pre||> > test1_nogz.exe copying src[0] -> dest[0] copying src[1] -> dest[1] copying src[2] -> dest[2] ... copying src[18] -> dest[18] copying src[19] -> dest[19] hi-ho! :P ||< オーバーランによる戻り先アドレスの書き換えに成功した。 ** "/GZ" や "/RTC" を使ってコンパイルしてみる まず "/GZ" を使ってコンパイルしてみる: > cl /Fetest1_gz.exe test1.c /FAcs /GS- /GZ /Zi /nologo /link /debug cl : コマンド ライン warning D9035 : オプション 'GZ' の使用は現在推奨されていません。 今後のバージョンからは削除されます。 cl : コマンド ライン warning D9036 : 'RTC1' を使用してください ('GZ' は使用不可) このように警告が表示された。実行ファイル自体は生成されている。 アセンブラを確認してみると、自動的に"/RTCs"オプションで生成されている: #pre||> PUBLIC _mycpy EXTRN _printf:PROC EXTRN __RTC_CheckEsp:PROC EXTRN @_RTC_CheckStackVars@8:PROC EXTRN __RTC_Shutdown:PROC EXTRN __RTC_InitBase:PROC ... ; Function compile flags: /Odtp /RTCs ||< 他にもスタック上のバッファが0以外の値で初期化されたり、returnする前に"_RTC_CheckStackVars"や"_RTC_CheckEsp"をcallするようなコードが自動的に追加されている。(実際のアセンブラコードは省略) コンパイラの警告に従い、"/RTC1"を使用してみる。"/RTC1"には"/RTCs"も含まれている。 > cl /Fetest1_rtc.exe test1.c /FAcs /GS- /RTC1 /Zi /nologo /link /debug 先と同様のアセンブラが生成された。 デバッグを無効にし、最適化オプションを組み合わせてみるとコンパイルエラーが発生することを確認出来る: > cl /Fetest1_rtc.exe test1.c /FAcs /GS- /RTC1 /O1 /nologo cl : コマンド ライン error D8016 : コマンド ライン オプション '/RTC1' と '/O1' は同時に指定できません ** "/RTC"有効状態でオーバーランが検出されることを確認する dumpbinなどで関連情報を確認する: #pre||> > dumpbin /symbols test1.obj ... 00D 00000000 SECT4 notype () External | _mycpy ... 01C 000000A0 SECT4 notype () External | _main 01D 000000C0 SECT4 notype () External | _hiho ... > dumpbin /headers test1_rtc.exe ... 400000 image base (00400000 to 00428FFF) ... SECTION HEADER #1 .text name 1D0BC virtual size 1000 virtual address (00401000 to 0041E0BB) ... ||< 以上より "hiho()" の実行時アドレスは 4010C0h と推測されるが、実際に実行してみるとRTCの影響か"mycpy()"自体が30h後ろにずれ、"hiho()"も同様にずれて 4010F0h となった。 mycpy()のforループに入り、ループカウンタに0がセットされた直後のスタックを確認する: #pre||> ESP : 0012FF54h EBP : 0012FF68h ---------------- 0012FF54 : 00000000 # "int i" 0012FF58 : CCCCCCCC # 0012FF5C : CCCCCCCC # "dest[5]" 0012FF60 : CCCCCCCC # 0012FF64 : CCCCCCCC # 0012FF68 : 0012FF78 # EBP 0012FF6C : 004010DF # 戻り先 0012FF70 : 00424000 # "src"引数 0012FF74 : 00000005 # "len"引数 ||< 12FF58h - 12FF64hのどこが"dest[5]"かは、実際に実行して特定した。 以上の情報を基に、戻り先アドレスを"hiho()"にするデータを用意してみる。 #pre||> char msg[] = { 0x30, 0x31, 0x32, 0x33, 0x01, 0x00, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x78, 0xFF, 0x12, 0x00, 0xF0, 0x10, 0x40, 0x00, }; ||< 実行してみると、メモリ値自体は狙った通りに書き換えられたが、returnする時の"_RTC_CheckStackVars"で例外が発生してしまった。 例外発生による実行中断を「検出」と呼ぶか「防止」と呼ぶのかは微妙なところだが、少なくとも"hiho()"の実行は防止出来た。 このように "/RTC" オプションがデバッグビルドにおけるバッファオーバーランの検出の一助となることを確認出来た。 引き続き、リリースビルドでも利用可能な "/GS" オプションについて見ていきたい。 * "/GS" コンパイラオプションによるスタック破壊チェック - "/GS" はデフォルトで有効となっている。無効にするには "/GS-" コンパイラオプションを指定する。 - 基本的な動作原理: -- 1. スタック上に"Cookie値"を積んでおく。"Cookie値"というのはCRT初期化時に一度だけ計算される値(__security_init_cookie関数)。 -- 2. 関数の処理が終わり、スタックをアンワインドする時にスタック上の"Cookie値"をチェックする(__security_check_cookie関数)。 -- 3. もしもCookie値が変更されていたら、スタックを破壊された可能性があるので処理終了。 - 制限: -- CRT提供のエントリポイントを使うか、__security_init_cookie()を手動で呼び出して"Cookie値"を計算させる必要がある。 -- 文字列バッファを使わない関数や、"naked"関数は保護対象外。 -- (その他の制限についてはMSDN参照) ** "/GS" 無しでコンパイルし、オーバーランを使って"hiho()"を実行してみる コンパイル: > cl /Fetest1_nogs.exe test1.c /FAcs /GS- /nologo オーバーラン用のデータは、ソースが同じ事もあり"/RTC"無しでオーバーランさせた時と同じデータを使える。 #pre||> char msg[] = {0x30, 0x31, 0x32, 0x33, 0x1, 0x0, 0x0, 0x0, 0x08, 0x00, 0x00, 0x00, // "int i" の値を壊さないようにする。 0x78, 0xFF, 0x12, 0x00, // スタックフレームのEBPを壊さないようにする。 0x80, 0x10, 0x40, 0x00, // "hiho()"のアドレス }; ||< 実行: #pre||> > test1_nogz.exe copying src[0] -> dest[0] copying src[1] -> dest[1] copying src[2] -> dest[2] ... copying src[18] -> dest[18] copying src[19] -> dest[19] hi-ho! :P ||< "/RTC"無しの時と同様、オーバーランによる戻り先アドレスの書き換えに成功した。 ** "/GS" 有り(デフォルト)の効果 一旦オーバーランしないtest1.cで"/GS"有りでコンパイルし、スタックがどうなるか確認する。 コンパイル: > cl /Fetest1_gs.exe test1.c /FAcs /nologo forループのループカウンタ初期化直後のスタック: #pre||> ESP : 0012FF58h EBP : 0012FF68h ----------------- 0012FF58 : 0012FFB0 # "dest[5]" 0012FF5C : 004027D0 # 0012FF60 : 4F9D1C53 # Cookie値 0012FF64 : 00000000 # "int i" 0012FF68 : 0012FF78 # スタックフレームのEBP 0012FF6C : 0040107F # return先 0012FF70 : 0040C000 # "src"引数 0012FF74 : 00000005 # "len"引数 ||< Cookie値は実行するたびに変わる為、データに予め埋め込んでおくことは難しい。 試しに(失敗することが分かっている上で)オーバーランしてreturn先を書き換えるデータを作ってみる。 char msg[] = { 0x30, 0x31, 0x32, 0x33, // "dest[5]" 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Cookie値 0x0C, 0x00, 0x00, 0x00, // "int i"カウンタ値を壊さない値 0x78, 0xFF, 0x12, 0x00, // スタックフレームを壊さない値 0x90, 0x10, 0x40, 0x00, // "hiho()"のアドレス }; 実行すると、アンワインド時に呼ばれる"__security_check_cookie"内で実行が中断され、例外が発生した。 このように "/GS" オプションを使ってデバッグ/リリースビルドにかかわらずスタックの破壊を検出できることを確認した。 しかし "/GS" オプションでもスタックの破壊を検出出来ないケースも存在する。アンワインド時にチェックされるということは、アンワインドされる前のスタック破壊はチェック出来ないことを意味する。スタックデータに関数アドレスが存在するケースでは、"__security_check_cookie"が呼ばれる前にスタックを破壊することで任意のアドレスにジャンプできる可能性がある。それら、"/GS" オプションでもスタック破壊を検出出来ないケースについては下記参考URLの "Compiler Security Checks In Depth"(コンパイラ セキュリティの徹底調査) を参照のこと。 * 参考URL "/GS"オプションの解説記事:日本語訳も提供されている。 - Compiler Security Checks In Depth -- http://msdn.microsoft.com/en-us/library/Aa290051 --- 2010/7/3時点でのMSDN階層 : "MSDN Library" > "Development Tools and Languages" > "Visual Studio .NET" > "Articles and Columns" > "Security Articles" > "Compiler Security Checks In Depth" - コンパイラ セキュリティの徹底調査 -- http://msdn.microsoft.com/ja-jp/library/cc404950 --- 2010/7/3時点でのMSDN階層 : "MSDN ライブラリ" > "開発ツールと言語ドキュメント" > "Visual Studio .NET" > "技術文書" > "技術解説記事およびコラム" > "セキュリティに関する技術解説記事" 少し昔の話になるが、Microsoft Platform SDK January 2000版やServer2003 SP1とServer2003用のDDKを組み合わせて使っていると、"/GS"オプション付きと無しでコンパイルされたCRTが混在し、"/GS"付でコンパイルされたオブジェクトを"/GS"無しでコンパイルされたCRTとリンク→当然"__security_cookie"シンボルが見つからずリンカエラーになった事があったようだ。 - You may receive the "Linker tools error LNK2001" error messages when you build source code by using the Win32 Software Development Kit (SDK) or the Windows Server 2003 Driver Development Kit (DDK) for Windows Server 2003 Service Pack 1 -- http://support.microsoft.com/kb/894573 #navi_footer|C言語系|