[x86 Assembler] [Windows] __security_check_cookie とは
最近のコンパイラは、かなり頭の使う処理を行ってくれています。VIsual Studio でも、普通に書いたプログラムなのに、いろいろなセキュリティー関連のチェック機構を埋め込んでいくれています。動作させる環境を守ってくれるのでとても有難いのですが、何も知らずにプログラムを使っていると、思わぬところで悩まされることがあります。
Visual Studio で /GS オプションを付けて C/C++ プログラムをコンパイルすると、関数呼び出しの際にバッファー オーバー ラン (BOR) を自動的に検出してくれるコードが埋め込まれます。異なるバージョンのコンパイラで作ったライブラリをリンクさせようとすると、たまに 「__security_check_cookie なんちゃらのシンボルがない」 というリンク エラーが出ることがあります。自分で書いたプログラムを見てもそんな関数は使ってないわけですが、コンパイラが勝手に埋め込んでるわけです。
このセキュリティ チェック機構に関しては、次のMSDN の ページが詳しいです。
http://msdn.microsoft.com/ja-jp/library/aa290051(v=vs.71).aspx
IT Pro でも解説がなされていました。次のページの第 4~6 回ですが、なかなかひどい文章です。作者は身元を明かせよ、と。
http://itpro.nikkeibp.co.jp/article/COLUMN/20060119/227538/?ST=develop
アセンブラ関連の投稿一発目ということで、自分でこの辺を見てみることにした。アセンブラに関してはまだまだ勉強中なので、変なことを言っているかもしれません。
環境は以下。
コンパイラ: Visual Studio 2010
コンパイル環境: Windows 7 x64
実行環境: WIndows 7 x86
とりあえずプログラムを書く。なんでもいい。
//
// main.cpp
//
void bor() {
char buf[64];
buf[0]= 0;
}
int wmain(int argc, wchar_t *argv[]) {
bor();
return 0;
}
これを /GS オプションをつけて (デフォルトでオンになっているけど) コンパイル。bor 関数のアセンブラは以下のようになっている。適当にコメント入れました。
ちなみに、表記フォーマットはこんな風になっています。
<ソースコードの行番号> <プログラムのメモリ上の位 置> <機械語> <アセンブラ>
BOR!bor [e:\visual studio 2010\projects\bor\main.cpp @ 6]:
; プロローグ(関数が呼ばれた時のおまじない)
6 00181380 55 push ebp
6 00181381 8bec mov ebp,esp
6 00181383 81ec0c010000 sub esp,10Ch
6 00181389 53 push ebx
6 0018138a 56 push esi
6 0018138b 57 push edi
; ローカル変数用のスタックを CC で埋める
6 0018138c 8dbdf4feffff lea edi,[ebp-10Ch]
6 00181392 b943000000 mov ecx,43h
6 00181397 b8cccccccc mov eax,0CCCCCCCCh
6 0018139c f3ab rep stos dword ptr es:[edi]
; BOR 検出用のクッキーを保存
6 0018139e a100701800 mov eax,dword ptr [BOR!__security_cookie (00187000)]
6 001813a3 33c5 xor eax,ebp
6 001813a5 8945fc mov dword ptr [ebp-4],eax
; buf[0]= 0; の処理
8 001813a8 c645b800 mov byte ptr [ebp-48h],0
; なんだこいつは…
9 001813ac 52 push edx
9 001813ad 8bcd mov ecx,ebp
9 001813af 50 push eax
9 001813b0 8d15d0131800 lea edx,[BOR!bor+0x50 (001813d0)]
9 001813b6 e8c7fcffff call BOR!ILT+125(_RTC_CheckStackVars (00181082)
9 001813bb 58 pop eax
9 001813bc 5a pop edx
; エピローグ1(関数が終わる時のおまじない)
9 001813bd 5f pop edi
9 001813be 5e pop esi
9 001813bf 5b pop ebx
; ここで BOR を検出させる
9 001813c0 8b4dfc mov ecx,dword ptr [ebp-4]
9 001813c3 33cd xor ecx,ebp
9 001813c5 e84afcffff call BOR!ILT+15(__security_check_cookie (00181014)
; エピローグ2(関数が終わる時のおまじない)
9 001813ca 8be5 mov esp,ebp
9 001813cc 5d pop ebp
9 001813cd c3 ret
C++ で二行書いただけなのに、これだけの長さになってしまうところがアセンブラの面白いところ。VCでデバッグしているときに、変数が CC で埋められているのを疑問に思っていたけど、関数が呼ばれるたびにこんな風に明確にリセットされているとは思わなかった。あと、ローカル変数は 64 バイトなのに、268 (=0x10C) バイトもリセットしている。そんなに使うのだろうか。
肝心の BOR 検出処理は 2ヶ所に分かれています。まず、関数が呼ばれた段階(プロローグと、ローカル変数の初期化処理の後)で、BOR!__security_cookiee という DWORD 値(クッキー)とレジスタ esp を XOR 演算した値を ebp-4、すなわち、この関数スコープにおけるスタック領域の一番下に代入します。先の MSDN のページによると、クッキーの値は単なる乱数値で、__security_init_cookie の中で起動時に生成されるようです。
関数の最初にクッキーを保存しておいて、次に、関数の処理が終わったあとに __security_check_cookie 関数を呼び出して照合します。関数をコールする前に、ebp-4 に保存しておいたクッキーに再度 esp レジスタの値を XOR し、元々のクッキーの値を ecx レジスタに入れておきます。
__security_check_cookie のアセンブラは以下のようになっています。
BOR!__security_check_cookie [f:\dd\vctools\crt_bld\self_x86\crt\src\intel\secchk.c @ 52]:
52 00181460 3b0d00701800 cmp ecx,dword ptr [BOR!__security_cookie (00187000)]
56 00181466 7502 jne BOR!__security_check_cookie+0xa (0018146a)
BOR!__security_check_cookie+0x8 [f:\dd\vctools\crt_bld\self_x86\crt\src\intel\secchk.c @ 57]:
57 00181468 f3c3 rep ret
BOR!__security_check_cookie+0xa [f:\dd\vctools\crt_bld\self_x86\crt\src\intel\secchk.c @ 59]:
59 0018146a e918fcffff jmp BOR!ILT+130(___report_gsfailure) (00181087)
処理はかなり単純で、赤字で示した処理で、ecx レジスタの値とクッキーの値を比較するだけです。値が異なっていれば、関数の処理中に ebp-4 に保存されていたクッキーが変更された、つまり、ローカル変数用に確保されていたバッファー領域を超過して BOR が発生したか、途中で esp レジスタが変更されてしまったということで、___report_gsfailure という関数を呼びに行きます。
値が一致すれば BOR チェックとしては成功で、関数は終わります。が、単純に ret で終わるのではなく、rep ret で終わるのです。これについて調べたところ、はっきりとした理由は不明ですが、パフォーマンス向上のために rep ret を使う慣習があるらしい。
この人のブログに言及があります。
http://mikedimmick.blogspot.com/2008/03/what-heck-does-ret-mean.html
以下の怪しいページに、AMD が推奨している、と書いてある。怪しいけど、とりあえず深追いは避ける。
http://sourceware.org/ml/libc-alpha/2004-12/msg00022.html
関数 bor に戻って、BOR!ILT+125(_RTC_CheckStackVars を呼び出す処理。これはなんだろう。名前からして、マシンスタックの正常性を確認してくれる処理だろうか。
BOR!ILT+125 では、jmp 命令で BOR!_RTC_CheckStackVars に飛ばされます。で、その関数がこれ。
BOR!_RTC_CheckStackVars:
00fc14b0 8bff mov edi,edi
00fc14b2 55 push ebp
00fc14b3 8bec mov ebp,esp
00fc14b5 51 push ecx
00fc14b6 53 push ebx
00fc14b7 56 push esi
00fc14b8 57 push edi
00fc14b9 33ff xor edi,edi
00fc14bb 8bf2 mov esi,edx
00fc14bd 8bd9 mov ebx,ecx
00fc14bf 897dfc mov dword ptr [ebp-4],edi
00fc14c2 393e cmp dword ptr [esi],edi
00fc14c4 7e48 jle BOR!_RTC_CheckStackVars+0x5e (00fc150e)
BOR!_RTC_CheckStackVars+0x16:
00fc14c6 eb08 jmp BOR!_RTC_CheckStackVars+0x20 (00fc14d0)
BOR!_RTC_CheckStackVars+0x20:
00fc14d0 8b4604 mov eax,dword ptr [esi+4]
00fc14d3 8b0c38 mov ecx,dword ptr [eax+edi]
00fc14d6 817c19fccccccccc cmp dword ptr [ecx+ebx-4],0CCCCCCCCh
00fc14de 750f jne BOR!_RTC_CheckStackVars+0x3f (00fc14ef)
BOR!_RTC_CheckStackVars+0x30:
00fc14e0 8b543804 mov edx,dword ptr [eax+edi+4]
00fc14e4 03d1 add edx,ecx
00fc14e6 813c1acccccccc cmp dword ptr [edx+ebx],0CCCCCCCCh
00fc14ed 7411 je BOR!_RTC_CheckStackVars+0x50 (00fc1500)
BOR!_RTC_CheckStackVars+0x3f:
00fc14ef 8b4c3808 mov ecx,dword ptr [eax+edi+8]
00fc14f3 8b5504 mov edx,dword ptr [ebp+4]
00fc14f6 51 push ecx
00fc14f7 52 push edx
00fc14f8 e8cbfbffff call BOR!ILT+195(?_RTC_StackFailureYAXPAXPBDZ) (00fc10c8)
00fc14fd 83c408 add esp,8
BOR!_RTC_CheckStackVars+0x50:
00fc1500 8b45fc mov eax,dword ptr [ebp-4]
00fc1503 40 inc eax
00fc1504 83c70c add edi,0Ch
00fc1507 8945fc mov dword ptr [ebp-4],eax
00fc150a 3b06 cmp eax,dword ptr [esi]
00fc150c 7cc2 jl BOR!_RTC_CheckStackVars+0x20 (00fc14d0)
BOR!_RTC_CheckStackVars+0x5e:
00fc150e 5f pop edi
00fc150f 5e pop esi
00fc1510 5b pop ebx
00fc1511 8be5 mov esp,ebp
00fc1513 5d pop ebp
00fc1514 c3 ret
クッキーのチェックより長い。初心者にはソースコードがないとキツい・・・、というわけでこれはまた今度。