これまでの連載では,私たちWindowsユーザーが置かれている現状を確認しました。今回からは具体的な技術項目を取り上げていきます。その1回目の今回は,バッファ・オーバーランの発生メカニズムを具体的に学習します。
ご承知のように,バッファ・オーバーランは,別名,バッファ・オーバー・フローとも呼ばれています(この連載では,バッファ・オーバーランを採用し,BORと略記することにします)。これら2つの用語は,私たちのIT業界ではすでに市民権を得ていますが,その意味を実際に理解している人はそれほどいないのが実状かと思います。今回は,アセンブラ・レベルでBOR発生メカニズムの動作を追い,その意味を,具体的かつ明解に理解することにします。今回の学習手順は,次のようになります。
手順1:Cコードのサンプル・プログラムの紹介
手順2:BOR発生過程のアセンブラ・レベルでの検討
それでは,早々学習作業に取り掛かりましょう。
手順1:Cコードのサンプル・プログラムの紹介
今回のサンプル・プログラムは,次のようなソースコードで構成されています。
#include <windows.h> #include <stdio.h> #define buffer_size 10 void test(char*,unsigned int); int main(void) { char* des = "Takashi Toyota\n"; size_t cb = 0; cb = strlen(des); test(des,cb); } void test(char* pbData,size_t cbData) { char buff [buffer_size]; CopyMemory(buff,pbData,cbData); printf(buff); }
このサンプル・プログラムは,Cというプログラミング言語で記述されています。プログラミング経験のある方は,void test(char*,unsigned int);とvoid test(char* pbData,size_t cbData)が一致しないのでは,と疑問を持つと思いますが,今回は気にしないでください。実は,それ以外にもいくつかのミスを組み入れています。まずは,プログラムの機能概要を把握してください。
このプログラムは,最終的に「Takashi Toyota\n」という文字列を表示しようとしていますが,その過程で,今回のテーマであるバッファ・オーバラン(BOR)を内部で発生させています。
ところで皆さん,このプログラムが実際にCプログラムであることをどのように判断したらよいと思いますか?プログラミング時には必ずライブラリというものを使用します。このプログラムでは,<windows.h>や<stdio.h>などがインクルードされています。このようなファイルをヘッダー・ファイルといいます。
ヘッダー・ファイルはあくまでもインクルードする外部コードのインタフェースを記述しているだけですから,外部コードがその内部でC++コードを呼び出している可能性もあります。このあたりは私たちには見えません。それではここで次のような情報をご覧ください。
インデックス 言語 関数 ----------------------------------------------------- *1 C ITProNo4.exe!main() 2 C ITProNo4.exe!mainCRTStartup() 3 kernel32.dll!_BaseProcessStart@4()
ご覧のように,このサンプル・プログラムはC言語コードで記述されている,と認識されています。私は今回のサンプル・プログラムを作成する際,Visual Studio .NET環境のコンパイラとリンカオプションをかなり厳しく設定しています。
図1 |
図2 |
ご覧のように,このCopyMemory関数は,戻り値を返しません。つまり,この関数は,セキュリティ問題を発生させる以前に,常識に欠けるところがあるため,使い方が結構面倒であることが(なんとなく)分かります。本論に入る前にこの点を頭の隅に入れて置いてください。
このサンプル・プログラムをVisual Studio .NETのデバッグ環境で実行させると,BORが発生します。その理由はきわめて単純です。それは,10バイトのメモリー・ブロックに,10バイトを超える「Takashi Toyota\n」という文字をコピーしようとしているからです。サンプル・プログラムをデバッグ環境で動作させると,図2[拡大表示]のようなエラー・メッセージが表示されてきます。
この画面には,0x00401057,0xC0000005,0x000a6174という3つの数値を含むエラー・メッセージが表示されています。これら3つの数値の意味を簡単に説明すると,次のようになります。
0x00401057:エラーを発生させたプログラム・コードの格納位置
0xC0000005:エラー・メッセージ識別番号
0x000a6174:不正にアクセスされたメモリー・アドレス
結論としては,本来アクセスされてはならないアドレス0x000a6174が,プログラム内の0x00401057からアクセスされた,ということになります。問題は,アドレス0x000a6174にありますが,実は,このアドレスはバッファ・オーバラン(BOR)により「作り出された」アドレスなのです。このようなエラーが発生すると,(最悪の場合には)プログラムの動作が停止しますから,インターネット環境では,DoS(Denial of Service)という現象を発生させることになります。それでは,問題アドレス0x000a6174が生成される過程をじっくり見てみましょう。
手順2:BOR発生過程のアセンブラレベルでの検討
いきなりですが,次のような情報を見ていただきましょう。
void test(char* pbData,size_t cbData) { 00401060 push ebp 00401061 mov ebp,esp 00401063 sub esp,0Ch char buff [buffer_size]; CopyMemory(buff,pbData,cbData); 00401066 mov eax,dword ptr [cbData] 00401069 push eax 0040106A mov ecx,dword ptr [pbData] 0040106D push ecx 0040106E lea edx,[buff] ESPレジスタ周辺 0x0012FEC8 00401050 00423b40 0000000f 0000000f P.@.@;B......... 0x0012FED8 00423b40 0012ffc0 004016d3 00000001 @;B.Ay..O.@..... 0x0012FEE8 003735e0 00373660 00000094 00000005 a57.`67.?....... 0x0012FEF8 00000002 00000ece 00000002 00000000 ....I........... EBPレジスタ周辺 0x0012FEDC 0012ffc0 004016d3 00000001 003735e0 Ay..O.@.....a57. 0x0012FEEC 00373660 00000094 00000005 00000002 `67.?........... 0x0012FEFC 00000ece 00000002 00000000 7ffde014 I............ay 0x0012FF0C 00000000 00401550 00000000 00000000 ....P.@.........
このような情報をここで初めて目にした人は,驚きを禁じえないでしょう。今回は触れませんが,C言語習得上の最大の難点といわれているポインタ(char* pbData)の姿もあります。ここで注目していただきたいのは,次の2点だけです。
ESPレジスタ周辺
0x0012FEC8 00401050
EBPレジスタ周辺
0x0012FEDC
結論だけを述べておきますが,00401050は,関数testが処理完了後に戻る実行アドレスです。一方,0x0012FEDCは現在のスタック・フレームベース・ポインタです。スタック・フレームベース・ポインタというのはかなり難しい表現なのですが,ここでは,スタックと呼ばれるメモリー・ブロックの使い方を定める基本データである,と考えておいてください。私たちの日常生活でも基本データは大変重要なものです。これはコンピュータの世界でも同じです。今回のサンプル・プログラムは,この基本データを破壊してしまいます。
このサンプル・プログラムの中心は,CopyMemory(buff,pbData,cbData);APIです。このAPIは,次のような機能を持っています。
「CopyMemoryは,pbDataからcbData分の文字をbuffにコピーする」
図3● |
図4● |
図3は,CopyMemory関数の実体であるmemcpy関数実行直前の内部状態を示しています。つまり,CopyMemory関数の動作がすでに開始されてはいますが,実際のコピー処理は行われていません。赤で囲んだ数値を見ると分かるように,戻りアドレス00401050とスタックフレーム・ベースポインタ0x0012FEDCは以前のままの値となっています。つまり,CopyMemory関数は処理完了後,自分を呼び出した関数に正常に復帰できることを示しています。それではここで,memcpy関数実行直後の状態を見てみましょう(図4[拡大表示])。
図4はmemcpy関数直後に取得しています。つまり,10バイトを超える「Takashi Toyota\n」という文字列がコピーされ,BORがすでに発生しているのです。紫の四角で囲まれた数値に注目してください。000a6174という数値が見えませんか?この数値は以前は,0x0012FEDCとなっていました。これはいったいどういうことかといえば,基本データであるスタックフレーム・ベースポインタがBORにより破壊された,ということなのです。
ところで皆さん,冒頭で紹介したエラー・メッセージを覚えていますか? そのメッセージには,000a6174という数値が含まれていましたね。その通りなのです。この000a6174は,バッファ・オーバーラン(BOR)により「作り出された」アドレスだったのです。バッファ・オーバーラン(BOR)は,最悪の場合,プログラムの動作を停止させますから,Webサーバー内部などで発生すると深刻な問題になります。
ところで皆さん,memcpy関数実行直後の画面をもう一度眺めてください。000a6174と00401050という2つの数値が連続して格納されていますね。ということは,アドレス00401050,つまり,戻りアドレスもBORにより簡単に書き換えられる可能性があることになります。戻りアドレスが書き換えられた場合,CopyMemory関数実行後は,その書き換えられた戻りアドレスが黙って実行されてしまいます。このような現象は,「BORにより不正なコードが実行される」と表現されます。
以上でBORの意味と危険性を具体的に理解できたと思います。本来なら今回のプロジェクト・ファイルをダウンロードできるようにし,ご自分の目で確認していただきたいのですが,危険度が高いため,Web経由での配布は控えさせていただきます。
今回のまとめ
- バッファ・オーバーラン(BOR)はスタックフレーム・ベースポインタ(EBP)を書き換える
- バッファ・オーバーラン(BOR)はスタックポインタ(ESP)を書き換える
今回は以上で終了です。次回またお会いいたしましょう。ごきげんよう!