VC++では、コンパイラレベルでスタックの破壊を「検出」する為の補助機能を提供している。
スタックの破壊は、プログラムのバグや安全でない関数の使用などが原因となり発生する。
これら補助機能でスタックの破壊を「防ぐ」ことは出来ないが、破壊された事を「検出」するチャンスは得られる。
状況によっては補助機能による検出をすり抜けてしまう場合もあるが、最低限度の安全弁として活用していきたい。
なおバッファ本来の領域を越えてデータを書き込んでしまう現象には「オーバーラン」と「アンダーラン」の二種類がある。
x86ではアドレスの小さい方向にスタックが伸びるため、「オーバーラン」の方がスタックの破壊とそれに伴うセキュリティ問題の原因と成りやすい。
バッファ本来の領域を越えて、
対象: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.
"/RTC"オプションの効果を確認する為、わざとバッファオーバーラン可能なソースコードを用意してみた。
test1.c:
#include <stdio.h> #include <stdlib.h> 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); }
コンパイル:
> 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ループに入った直後のスタックを確認してみる:
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に修正する。
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()"のアドレス };
実行:
> 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" を使ってコンパイルしてみる:
> cl /Fetest1_gz.exe test1.c /FAcs /GS- /GZ /Zi /nologo /link /debug cl : コマンド ライン warning D9035 : オプション 'GZ' の使用は現在推奨されていません。 今後のバージョンからは削除されます。 cl : コマンド ライン warning D9036 : 'RTC1' を使用してください ('GZ' は使用不可)
このように警告が表示された。実行ファイル自体は生成されている。
アセンブラを確認してみると、自動的に"/RTCs"オプションで生成されている:
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' は同時に指定できません
dumpbinなどで関連情報を確認する:
> 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がセットされた直後のスタックを確認する:
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()"にするデータを用意してみる。
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" オプションについて見ていきたい。
コンパイル:
> cl /Fetest1_nogs.exe test1.c /FAcs /GS- /nologo
オーバーラン用のデータは、ソースが同じ事もあり"/RTC"無しでオーバーランさせた時と同じデータを使える。
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()"のアドレス };
実行:
> 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"無しの時と同様、オーバーランによる戻り先アドレスの書き換えに成功した。
一旦オーバーランしないtest1.cで"/GS"有りでコンパイルし、スタックがどうなるか確認する。
コンパイル:
> cl /Fetest1_gs.exe test1.c /FAcs /nologo
forループのループカウンタ初期化直後のスタック:
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"(コンパイラ セキュリティの徹底調査) を参照のこと。
"/GS"オプションの解説記事:日本語訳も提供されている。
少し昔の話になるが、Microsoft Platform SDK January 2000版やServer2003 SP1とServer2003用のDDKを組み合わせて使っていると、"/GS"オプション付きと無しでコンパイルされたCRTが混在し、"/GS"付でコンパイルされたオブジェクトを"/GS"無しでコンパイルされたCRTとリンク→当然"__security_cookie"シンボルが見つからずリンカエラーになった事があったようだ。