home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

C言語系/memos/VC++/09, スタック破壊の検出・防止("/GS", "/RTC", "/GZ")

C言語系/memos/VC++/09, スタック破壊の検出・防止("/GS", "/RTC", "/GZ")

C言語系 / memos / VC++ / 09, スタック破壊の検出・防止(" / GS", " / RTC", " / GZ")
id: 693 所有者: msakamoto-sf    作成日: 2010-07-04 14:29:05
カテゴリ: C言語 Windows セキュリティ 

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.

"/GZ" -> "/RTC" コンパイラオプションによるランタイムエラーチェック

  • VC++2005までは "/GZ" オプションとして提供されていた機能。
  • VC++2005以降は "/RTC" オプションに統合され、"/GZ"オプションは非推奨となった。
  • "/RTC"はランタイムエラーチェックを有効にするコンパイラオプションで、何種類かバリエーションがある。
    • "/GZ"と同様にスタックの破壊を検出するには "/RTCs" を指定する。
  • 最適化オプションと組み合わせることは出来ず、開発時(=デバッグ)ビルドでのみ使える。

効果確認用サンプルコード test1.c

"/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);
}

"/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ループに入った直後のスタックを確認してみる:

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" や "/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"オプションで生成されている:

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などで関連情報を確認する:

> 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" オプションについて見ていきたい。

"/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"無しでオーバーランさせた時と同じデータを使える。

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"無しの時と同様、オーバーランによる戻り先アドレスの書き換えに成功した。

"/GS" 有り(デフォルト)の効果

一旦オーバーランしない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"(コンパイラ セキュリティの徹底調査) を参照のこと。

参考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


プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2010-07-04 14:35:00
md5:744b53aa91ae793e888ea3bf3d896a64
sha1:60d158cf3d70426552b91ba14d335de6119a0594
コメント
コメントを投稿するにはログインして下さい。