前回は,プログラミング言語Cで記述された単純なサンプル・プログラムを実行させ,バッファ・オーバーラン(BOR)が発生する過程をアセンブラ・レベルで学習しました。今回は,BORの発生を防止する対策コードを紹介するとともに,その動作背景をアセンブラ・レベルで検討してみます。本記事を最後まで読むと分かりますが,プログラミング作業を行う上でたいへん興味深い問題が浮かび上がってきます。それでは,早々本論に入りましょう。

BOR発生の防止策

 BORの発生を防止するには,そもそもBORとは何か,を定義しておく必要があります。この定義は前回紹介したように,きわめて簡単です。BORとは次のようなものです。

BORとは,ある大きさを持つメモリー・ブロックに,その大きさを超えるデータを転送した結果発生する不具合である

 ご覧のようにきわめて簡単です。このため,BOR発生の防止策も次のように簡単に定義できます。

BOR発生の防止策とは,転送先メモリー・ブロックの大きさを超えたデータ量を転送しないことである

 この防止策定義もきわめてありふれたものです。BOR発生を防止するには,一般的には,次のような対策が取られるといわれています。

  • プログラム・コードを人手により厳しくレビューする
  • プログラミング言語をCから抽象化の進んだC++などの言語に変更する(クラスベース・プログラミング)
  • コンパイラなどのBOR対策オプションを活用する
  • Visual Studio .NETなどの開発環境のデバッグ機能を活用する
  • Numegaなどの専用ツール・メーカーの各種ツールを導入する
  • ネイティブ環境を隠ぺいする仮想マシン環境を導入する

 対策はこれ以外にもあるでしょうが,単純に言ってしまえば,プログラム製品コード品質の向上プロセスを自動化するかしないか,といってよいと思います。今回は,コード品質向上プロセスを自動化せず,プログラム・コードを人手で厳しくレビューする方針で臨みます。

 すでに触れたように,BORは,ある大きさを持つメモリー・ブロックに,その大きさを超えるデータを転送した結果発生する問題です。このため,基本的には,転送先メモリー・ブロックと転送元メモリー・ブロックのサイズを比較し,小さい方のメモリー・ブロック・サイズを転送量管理引数として採用すれば,必要な対策は完了してしまいます。それではここで,防止策を講じたサンプル・プログラムを紹介いたします。

#include 
#include 
#define buffer_size 10
void Server(char* pbData,SIZE_T cbData)
{
  char buff [buffer_size];
  CopyMemory(buff,pbData,min(cbData,
    (unsigned int)buffer_size));
  printf(buff);
}
int main()
{
  char* des = "Takashi Toyota\n";
  SIZE_T cb = 0;
  cb = strlen(des);
  Server(des,cb+1);
  return (0);
}

 前回のサンプル・プログラムにちょっとしたした変更を加えていますが,特にこれといった大きな変更はありません。BOR防止策コードは,次のソースコード行内に組み入れられています。

CopyMemory(buff,pbData,min(cbData,(unsigned int)buffer_size));

 このソースコードは,Microsoftの公開SDK(Software Development Kit)情報と,同社セキュリティ・チームの一員であるMichael Howard氏が公開するサンプルコードを忠実に検討し,そのエッセンスを再利用しています。プログラミング経験のある方は,Server関数を呼び出す周辺のコードに何らかの不可解さや違和感を感じるかもしれません。今回はサンプル・プログラムのプロジェクト・ファイルをダウンロードできるようにしてありますから,後日自分の目で確認してください。必要な参考コメントは挿入しておきました。

 今回は,ソースコード内のCopyMemory関数が受け取っている第3引数に注目します。ソースコードを見ると分かるように,CopyMemory関数はmin(cbData,(unsigned int)buffer_size)という引数を受け取っています。このminは関数ではなく,マクロと呼ばれているものです。使用上の注意点などは,後ほどアセンブラ・レベルで詳しく説明します。

 このサンプル・ソースコードは,あるメモリー・ブロックから別のメモリー・ブロックにデータを転送する際,転送する実際のデータ量を厳しくチェックすることを「まことしやかに」要求しています。

 ところで皆さん,私たちの常識に沿って考えてみると,データ転送量のチェックはごく当たり前のことだとは思いませんか? ネットワークからネットワークにルーター経由でデータが転送されるインターネット全盛の現在,データ転送量の管理はごく当たり前のことだと思います。また,このサンプルコードが示しているように,データ転送量のチェック管理は決して技術的に難しいものではありません。極論すれば,転送データ量のチェック方法(アルゴリズム)は開発者の数だけ存在するといってもよいでしょう。

 しかし,問題は,当たり前のことが起こると,その影響は大きく,しかも,収拾作業は難航するということです。そして,当たり前の問題は,いったん収拾できたとしても,将来(ほぼ間違いなく)再発するという性質を帯びています。

 問題が頻繁に発生することで評判を落としているMicrosoftのInternet Explorer(IE)は,当たり前の問題が繰り返し顕在化する典型的なアプリケーション例ではないかと思います。これは長期的に見ると,ソフトウエア開発会社としての存続にかかわる問題ですから,Microsoftは,統合開発環境のビルド・オプションとして,BOR防止機能を組み入れています(BOR防止の自動化)。IE問題とビルド・オプションについては,後日詳しく紹介する予定です。

 それでは次に,minマクロの動作背景をアセンブラ・レベルで追いかけてみましょう。一般的には,あるソースコードの背景をアセンブラ・レベルで分析する作業は専門的すぎ,かつ,経費も時間もかかります。しかし,以降の文章を読むと分かりますが,コンパイラが生成するアセンブラ・コードを分析していると,「プログラムを作る喜び」を存分に味わうことができます。

minマクロの動作背景

 minマクロは,今回のサンプル・プログラム内では次のように使われています。

min(cbData,(unsigned int)buffer_size)

 ご覧のように,minマクロは2つの引数を受け取っています。このマクロは,minという名称から想像できるように,受け取った2つの引数を比較し,その小さいほうの値を返す機能を持っています。調べてみると,このマクロは次のように定義されています。

#define min(a,b)      (((a) < (b)) ? (a) : (b))

 この定義コードは,minマクロは次のような機能を実装していることを示しています。

引数aが引数bより小さい場合,aを返し,それ以外の場合,bを返す

 この解釈はどこにも問題を抱えていないように見えます。しかし,人によっては,次のように解釈すると思います。

引数bが引数aより大きい場合,aを返し,それ以外の場合,bを返す

 いったいどちらが正しいのでしょう。あるいはまた,複数存在しえる私たちの解釈内容はコンピュータ(厳密には,コンパイラ)に正確に伝わるものなのでしょうか?それでは,私たちの解釈内容が忠実にアセンブラ・コードに反映されているかどうか調べてみましょう。次のコードをご覧ください。

CopyMemory(buff,pbData,
    min(cbData,(unsigned int)buffer_size));

00401026  cmp     dword ptr [cbData],0Ah 
0040102A  jae     Server+14h (401034h) ←注目!
0040102C  mov     eax,dword ptr [cbData] 
0040102F  mov     dword ptr [ebp-10h],eax 
00401032  jmp     Server+1Bh (40103Bh) 
00401034  mov     dword ptr [ebp-10h],0Ah 
0040103B  mov     ecx,dword ptr [ebp-10h] 
0040103E  push    ecx

 「注目!」と記されたアセンブラ・コード行を見てください。jaeというアセンブラ・コードが生成されています。このコードは,次のような意味を持っています。

引数a(cbData)が引数b((unsigned int)buffer_size))より大きいか等しい場合,bの値をデータ転送量管理に使用する

 この意味と先ほどの第1minマクロ定義文("引数aが引数bより小さい場合,aを返し,それ以外の場合,bを返す")をゆっくり比較して読んでください。意味がほぼ逆になっていませんか? 結論を先に述べると,コンパイラは「自分で勝手に意味を解釈している」,のです。このような現象を専門的には「コンパイラに依存する」と表現します。

 minマクロは,2つの引数を受け取り,小さいほうの値を返す機能を持っていますから,ここで,minマクロに渡す2つの引数の位置を次のように入れ替えてみましょう。

min((unsigned int)buffer_size,cbData)

 ご覧のように,cbDataと(unsigned int)buffer_sizeの位置を入れ替ています。それでは生成されるアセンブラ・コードを見てみましょう。

CopyMemory(buff,pbData,
    min((unsigned int)buffer_size,cbData));

00401026  cmp     dword ptr [cbData],0Ah 
0040102A  jbe     Server+15h (401035h) // ←注目!
0040102C  mov     dword ptr [ebp-10h],0Ah 
00401033  jmp     Server+1Bh (40103Bh) 
00401035  mov     eax,dword ptr [cbData] 
00401038  mov     dword ptr [ebp-10h],eax 
0040103B  mov     ecx,dword ptr [ebp-10h] 
0040103E  push    ecx 

 ここでも「注目!」と記されたアセンブラ・コード行を見てください。jbeというアセンブラ・コードが生成されています。このコードは,次のような意味を持っています。

引数b(cbData)が引数a((unsigned int)buffer_size))より小さいか等しい場合,bの値をデータ転送量管理に使用する

 引数入れ替え前と入れ替え後の2つのアセンブラ・コードの意味をじっくり比較してください。意味がまたほぼ逆になっていますね。つまり,コンパイラは自分で勝手に私たちの意図を参酌しています。

 ところで皆さん,これまでに紹介した2種類のアセンブラ・コードですが,いずれが理想的だと思いますか? 採用できる選択肢が複数ある,というのは日常的にありえる話です。このような場合,プログラム開発の力点を実行速度か,それとも,分かりやすさか,のいずれに置くかを考えるとよいと思います。2種類のアセンブラ・コードを比較すると,次のような2つの共通点がはっきりしてきます。

  • 実行バイト数と実行速度はほぼ同じ
  • 引数bがデータ転送量管理に使われている

 私はこの種の判断に迷った場合,「自分は今何をしようとしているのか?」を再確認することにしています。さて,私たちは今何をしているところですか? 私たちは今,「BOR発生を防止するために,データ転送量を管理している」,ところでしたね。それでは,データ転送量を管理するためにふさわしいのは,buffer_size(転送先メモリー・サイズ)でしょうか,それとも,cbData(転送元メモリー・サイズ)でしょうか?

 データ転送量を管理するのは,転送先メモリー・ブロックを守るためですから,buffer_size(転送先メモリー・サイズ)をb引数にすることがベターな選択といってよいでしょう。これは私たちの常識的な判断に沿っています。さて皆さんはどのように判断しますか?

 MicrosoftのセキュリティチームのMichael Howard氏は,次のようなコードを公開してくれています。

CopyMemory(buff,pbData,min(cbData,buffer_size));

 このコードは,転送先メモリー・ブロックの大きさを示すbuffer_sizeを引数bとして採用しています。さすが!,ですね。

今回のまとめ

  • BORの防止策では,データ転送量を管理することが大切である
  • コンパイラは私たちの意図を勝手に解釈することがある

 今回は以上で終了です。次回またお会いいたしましょう。ごきげんよう!