前回の記事で、__security_check_cookie 周りのアセンブラを調べていたら、_RTC_CheckStackVars という、コンパイラが差し込む他のセキュリティ コードを発見してしまいました。__security_check_cookie に比べて、_RTC_CheckStackVars については、日本語の解説が全然ない。コンパイル エラーで _RTC_CheckStackVars のシンボルが出てくることは多いらしいけど。

このセキュリティ コードは、コンパイル オプション /RTC1 や /RTCs をつけたときに差し込まれます。Visual Studio でいうところの [C/C++ > Code Generation > Basic Runtime Checks] です。ちなみに前回の /GS オプションは [C/C++ > Code Generation > Buffer Security Check ] でした。それぞれ独立した機構となっているようです。

前置きは以上で、アセンブラを読みましょう。たったこれだけのコードを解読するのに半日かかりました。「今、このレジスタには何が入っているか」 を覚えるのが大変。メモリ構造もまた然り。マシンスタックなどの絵を書きながらじゃないと頭に入らない。

まずはプログラムの準備。関数は 2 つにしました。中途半端な処理をさせているのは、処理が単純過ぎると _RTC_CheckStackVars が差し込まれないためです。

// 
// main.cpp 
//

void bor2() { 
    char buf[8]; 
    int *p= (int*)(buf+0); 
    int *q= (int*)(buf+4); 
    *p= 0x12345678; 
    *q= 0x87654321; 
}

void bor1() { 
    char buf[64]; 
    buf[0]= 0; 
}

int wmain(int argc, wchar_t *argv[]) { 
    bor1(); 
    bor2(); 
    return 0; 
}

環境は前と同じで。WOW64 や AMD64 の仕組みをちゃんと把握できていないので、念のため x86 で実行します。

コンパイラ: Visual Studio 2010
コンパイル環境: Windows 7 x64
実行環境: WIndows 7 x86

前回と同じ bor1 から見ていきます。

BOR!bor1: 
00071430 55              push    ebp 
00071431 8bec            mov     ebp,esp 
00071433 81ec0c010000    sub     esp,10Ch 
00071439 53              push    ebx 
0007143a 56              push    esi 
0007143b 57              push    edi 
0007143c 8dbdf4feffff    lea     edi,[ebp-10Ch] 
00071442 b943000000      mov     ecx,43h 
00071447 b8cccccccc      mov     eax,0CCCCCCCCh 
0007144c f3ab            rep stos dword ptr es:[edi] 
0007144e a100700700      mov     eax,dword ptr [BOR!__security_cookie (00077000)] 
00071453 33c5            xor     eax,ebp 
00071455 8945fc          mov     dword ptr [ebp-4],eax 
00071458 c645b800        mov     byte ptr [ebp-48h],0 
0007145c 52              push    edx 
0007145d 8bcd            mov     ecx,ebp 
0007145f 50              push    eax 
00071460 8d1580140700    lea     edx,[BOR!bor1+0x50 (00071480)] 
00071466 e817fcffff      call    BOR!ILT+125(_RTC_CheckStackVars (00071082) 
0007146b 58              pop     eax 
0007146c 5a              pop     edx 
0007146d 5f              pop     edi 
0007146e 5e              pop     esi 
0007146f 5b              pop     ebx 
00071470 8b4dfc          mov     ecx,dword ptr [ebp-4] 
00071473 33cd            xor     ecx,ebp 
00071475 e89afbffff      call    BOR!ILT+15(__security_check_cookie (00071014) 
0007147a 8be5            mov     esp,ebp 
0007147c 5d              pop     ebp 
0007147d c3              ret

青字が呼び出し部分です。ここで重要なのは 2 点です。「ecx レジスタには bor!bor1 の ebp が入っている」 ことと、「edx レジスタに bor!bor1+0x50 のアドレスが入っている」 ということです。bor!bor1+0x50 って何よ、って話ですよね。当然気になります。bor!bor1 関数のリターン コードはアドレス 0007147d にある ret 命令です。0007147d = bor!bor1 (00071430) + 0x4D なので、+0x50 は、bor!bor1 がロードされている直後の DWORD 境界の位置を示しているらしいことが分かります。

確認のため bor!bor2 についても調べてみると、やはり bor!bor2 関数の直後の DWORD 位置を edx にストアしています。bor2 の場合は偶然 DWORD 境界で関数が終わっていますが、bor!bor1 の場合は 0007147e と 0007147e の 2 バイトはのりしろになります。

BOR!bor2: 
00071390 55              push    ebp 
00071391 8bec            mov     ebp,esp

中略

000713d6 52              push    edx 
000713d7 8bcd            mov     ecx,ebp 
000713d9 50              push    eax 
000713da 8d15f8130700    lea     edx,[BOR!bor2+0x68 (000713f8)] 
000713e0 e89dfcffff      call    BOR!ILT+125(_RTC_CheckStackVars (00071082) 
000713e5 58              pop     eax 
000713e6 5a              pop     edx

中略

000713f4 8be5            mov     esp,ebp 
000713f6 5d              pop     ebp 
000713f7 c3              ret

で、ここには何が入っているのだろうか、ということで見てみます。bor!bor1 の直後の部分です。プロセスが起動してメモリ上にマップされるときに Windows 側が行う処理と思われるので、実際にどういう数値がセットされるのかは不明です。

; 00071460 8d1580140700    lea     edx,[BOR!bor1+0x50 (00071480)]

0:000> dd 00071470 
00071470  33fc4d8b fb9ae8cd e58bffff ff8bc35d 
00071480  00000001 00071488 ffffffb8 00000040 
00071490  00071494 00667562 cccccccc cccccccc 
000714a0  cccccccc cccccccc cccccccc cccccccc

赤字の C3 が bor!bor1 の ret 命令。00071498 から cccccccc が続いているところを見ると、意味を持つ値は 24 バイト分あるようです。便宜上、この 24 バイト分を 6 つの DWORD 値に分け、それぞれ A B C D E F という名前で呼ぶことにします。値はこんな感じ。

A= 00000001 B= 00071488 C= ffffffb8 D= 00000040 E= 00071494 F= 00667562

さて、ecx, と edx レジスタをセットしたところで、いよいよ _RTC_CheckStackVars の処理です。適宜注釈を加えたアセンブラがこれ。

BOR!ILT+125(_RTC_CheckStackVars: 
00071082 e9d9040000      jmp     BOR!_RTC_CheckStackVars (00071560)

BOR!_RTC_CheckStackVars: 
; プロローグ 
00071560 8bff            mov     edi,edi 
00071562 55              push    ebp 
00071563 8bec            mov     ebp,esp 
00071565 51              push    ecx 
00071566 53              push    ebx 
00071567 56              push    esi 
00071568 57              push    edi

; レジスタ初期化 
00071569 33ff            xor     edi,edi 
0007156b 8bf2            mov     esi,edx  --> esi stores BOR!bor+0x50 
0007156d 8bd9            mov     ebx,ecx  --> ebx stores parent's EBP 
0007156f 897dfc          mov     dword ptr [ebp-4],edi --> loop counter 
00071572 393e            cmp     dword ptr [esi],edi 
00071574 7e48            jle     BOR!_RTC_CheckStackVars+0x5e (000715be) 
                                           -> passed 
00071576 eb08            jmp     BOR!_RTC_CheckStackVars+0x20 (00071580)

; ループ開始 
BOR!_RTC_CheckStackVars+0x20: 
00071580 8b4604          mov     eax,dword ptr [esi+4] 
00071583 8b0c38          mov     ecx,dword ptr [eax+edi] 
00071586 817c19fccccccccc cmp     dword ptr [ecx+ebx-4],0CCCCCCCCh  <比較1> 
0007158e 750f            jne     BOR!_RTC_CheckStackVars+0x3f (0007159f) 
                                            -> error

BOR!_RTC_CheckStackVars+0x30: 
00071590 8b543804        mov     edx,dword ptr [eax+edi+4] 
00071594 03d1            add     edx,ecx 
00071596 813c1acccccccc  cmp     dword ptr [edx+ebx],0CCCCCCCCh  <比較2> 
0007159d 7411            je      BOR!_RTC_CheckStackVars+0x50 (000715b0)

; エラールーチン呼び出し 
BOR!_RTC_CheckStackVars+0x3f: 
0007159f 8b4c3808        mov     ecx,dword ptr [eax+edi+8] 
000715a3 8b5504          mov     edx,dword ptr [ebp+4] 
000715a6 51              push    ecx 
000715a7 52              push    edx 
000715a8 e81bfbffff      call    BOR!ILT+195(?_RTC_StackFailureYAXPAXPBDZ) (000710c8) 
000715ad 83c408          add     esp,8

BOR!_RTC_CheckStackVars+0x50: 
000715b0 8b45fc          mov     eax,dword ptr [ebp-4] 
000715b3 40              inc     eax 
000715b4 83c70c          add     edi,0Ch 
000715b7 8945fc          mov     dword ptr [ebp-4],eax 
000715ba 3b06            cmp     eax,dword ptr [esi] 
000715bc 7cc2            jl      BOR!_RTC_CheckStackVars+0x20 (00071580) 
                                           -> loop

; エピローグ 
BOR!_RTC_CheckStackVars+0x5e: 
000715be 5f              pop     edi 
000715bf 5e              pop     esi 
000715c0 5b              pop     ebx 
000715c1 8be5            mov     esp,ebp 
000715c3 5d              pop     ebp 
000715c4 c3              ret 

数分間見つめていると分かりますが、for 文です。ループ カウンタは ebp-4 の DWORD を使います。ループ回数は、例の bor!bor1+0x50 から始まるメモリ ブロックにある最初の DWORD 値、すなわち <A> です。これは 000715ba の cmp 命令を見ると分かります。esi レジスタには、終始 <A> の値が入っていて、その値とループカウンタを比較しています。<A> は何だったでしょうか。0x00000001 でした。ここがどういうときに 2 以上になるのかは不明です。bor!bor2 でも <A> の値は 1 でした。

この関数がスタックの異常を検出している部分は 2 ヶ所あり、それぞれ <比較1> <比較2> と呼ぶことにします。

さて、比較1 です。ここからレジスタ地獄・・・。

00071580 8b4604          mov     eax,dword ptr [esi+4] 
00071583 8b0c38          mov     ecx,dword ptr [eax+edi] 
00071586 817c19fccccccccc cmp     dword ptr [ecx+ebx-4],0CCCCCCCCh  <比較1> 

まずは ebx レジスタから見ます。これは元を辿ると、bor!bor1 で ecx に保存しておいた ebp レジスタの値です。_RTC_CheckStackVars の中では ebx レジスタは終始 bor!bor1 における ebp レジスタの値を保持しています。

ecx は、00071580 の mov 命令から順番に見ていったほうが分かりやすいです。まず [esi+4] です。esi はさっき出てきたように <A> です。ということは [esi+4] は <B> の値です。

ここでメモリの値を再掲します。

; 00071460 8d1580140700    lea     edx,[BOR!bor1+0x50 (00071480)]

0:000> dd 00071470 
00071470  33fc4d8b fb9ae8cd e58bffff ff8bc35d 
00071480  00000001 00071488 ffffffb8 00000040 
00071490  00071494 00667562 cccccccc cccccccc 
000714a0  cccccccc cccccccc cccccccc cccccccc

赤字で示した <B> の値は、自分の次の DWORD である <C> のアドレスを指しています。ついでに見てみると、青字で示した <E> は <F> のアドレスを指しています。どうしてこういう実装になっているかは不明です。<B> の値が B+4 のアドレス以外を指す場合があるのかどうかも不明です。いずれにしろ、00071580 の mov 命令の実行によって eax レジスタには <C> の「アドレス」が入ります。そして、00071583 にある mov 命令で、eax に入った <C> のアドレスに edi を加えたアドレスの値を ecx レジスタにストアします。edi は初登場です。上のアセンブラにあるように、00071569 で 0 が設定されます。後のほうを見ると、000715b4 で 12 が加算されます。ループカウンタに連動している値で、i*12 の値を示していることが分かります。初回なので edi は 0 です。というか先にも書きましたが、ループ回数は 1 なので、これが最後の実行です。何はともあれ、ecx には <C> の値がセットされました。まとめるとレジスタの値は次のようになります。

ebx = bor!bor のスタックベースポインタ
eax = <B> の値 = <C> のアドレス
ecx = <C> + i*12 の値 = i=0 なので <C> の値
ecx+ebx-4 = bor!bor のスタックベースポインタ + <C> の値 - 4

今回の場合、<C> の値は 0xffffffb8 = –72 なので bor!bor のスタックベースから 76 バイト上流にある値が 0xCCCCCCCC 以外の値だとエラーになります。これが <比較1> の処理です。

bor!bor のスタックベースから 76 バイト上流にある値とは何でしょうか。ここでヒントになるのが、bor1 関数の C++ における唯一の処理 buf[0]=0; です。これはアセンブラでも一行です。

00071458 c645b800        mov     byte ptr [ebp-48h],0

buf[0] は ebp-0x48 にあることが分かります。そして 0x48 = 72 です。つまり、<比較1> では ebp-72-4 の値をチェックすることで、マシンスタックの上流側が初期化された 0xCCCCCCCC のままであるかどうかを確認していることが分かります。

次に <比較2> についてもレジスタの内容を順番に調べてみます。

00071590 8b543804        mov     edx,dword ptr [eax+edi+4] 
00071594 03d1            add     edx,ecx 
00071596 813c1acccccccc  cmp     dword ptr [edx+ebx],0CCCCCCCCh  <比較2> 

ebx = bor!bor のスタックベースポインタ
edx = [eax+edi+4] + ecx
eax = <C> のアドレス
edi = i12
ecx = <C> + i
12 の値

edi は 0 なので、[eax+edi+4] は <D> の値を示していることになります。メモリを調べると、ここは 0x00000040 です。この <D> 値に <C> の値を加算します。すると、0xffffffb8 + 0x00000040 = –8 となります。ということは、<比較2> は、bor1 における ebp-8 の値を調べていることになります。今回の場合、ebp-4 にはクッキーが入っているので、ebp-8 は、クッキーのちょうど上にある DWORD のアドレスです。今回のプログラムで buf[0] は ebp-48h でした。つまり、buf[63] は ebp-9 に保存されることになります。ちょうどクッキーと buf[63] の間の ebp-8 にある DWORD 分が残されるわけです。ここが壊されないようにチェックしているのですね。

念のため bor!bor2 も見てみると、確かに <C> + <D> = 0xfffffff8 = –8 になります。

bor!bor2 
000713da 8d15f8130700    lea     edx,[BOR!bor2+0x68 (000713f8)]

0:000> dd 000713f0 
000713f0  fffffc20 c35de58b 00000001 00071400 
00071400  fffffff0 00000008 0007140c 00667562 
00071410  cccccccc cccccccc cccccccc cccccccc 

ようやく _RTC_CheckStackVars の全貌が見えてきました。ローカル変数用に確保されているマシンスタック内のメモリ領域のうち、上流部分を <比較1> 、下流部分を <比較2> でチェックし、初期化状態の 0xCCCCCCCC 以外であればエラーだと判断しているようです。こんなところに CC で埋める意味が隠されていたとは驚きです。

ここまで分かると、アセンブラを見るのも楽になってきます。今回、キーとなっていたのは、<C> と <D> にストアされている値でした。これらはともにスタックベースポインタからのオフセット値であり、ローカル変数領域のうち上流側のオフセットが <C> で、下流側のオフセットは <C>+<D> に保存されているわけです。つまり、<D> の値は、ローカル変数の合計サイズを表していることになります。確かに bor!bor1 では <D> = 64 で、bor!bor2 では <D> = 8 です。

今回はループが 1 回のみでしたが、仮に 2 回以上ループする場合は、マシンスタックのどの部分を確認することになるでしょうか。もはや簡単ですね。今回は、<C> と <D> を使ってスタックの両端を確認しましたが、ループが 2 順目に入った場合、これらに 12 を加算した値を使います。つまり、A B C D E F の後に G H I という 24 バイトが続くとして、<F> と <G> を使うことになります。複数のスタック領域を同時に調べられるようになっているようです。

もしかして、この 24 バイト領域って常識?