アーカイブ

第7章 6.メモリクリア失敗対策

公開日:2007年9月26日

独立行政法人情報処理推進機構
セキュリティセンター

本ページの情報は2007年9月時点のものです。
記載の資料は資料公開当時のもので、現在は公開されていないものも含みます。

メモリ上の秘密データを消去する目的で memset() 等のメモリクリア用ライブラリ関数を使用する場合、使用するコンパイラによっては最適化によってメモリ消去処理が削除されてしまうことがある。その代表的な例として、Microsoft の Visual C++ が挙げられる。

Visual C++ の最適化オプション

最適化オプションの内容によっては、Visual C++ はコード生成において、参照されない領域に対する memset() 呼び出しを削除する。

Visual Studio 2005 の最適化は、主に下記のような指定が可能である。

  • 適化しない (/Od)
  • プログラムサイズの最適化 (/O1)
  • 実行速度の最適化 (/O2)
  • 最大限の最適化 (/Ox)

このうち、実行速度(/O2)あるいは最大限の最適化(/Ox)を指定すると、参照されない領域に対する memset() 呼び出しのコードが除去される。

最適化により memset() 呼び出しが除去されてしまう問題

Windows Visual Studio 2005 の環境にて、コンパイラの最適化により処理が除去される例を次に示す。

main () {
 char secret[256];

 if (get_secret (secret, sizeof (secret))) {
  // secretに関する何からの処理...
 }

 // 作業領域として使用したメモリのクリア
 memset(secret, 0, sizeof (secret));
}

get_secret (char *buff, int size)
{
 strncpy (buff, "SECRET_DATA", size);
}

このソースコードは、最適化コンパイルを行うと次のようなアセンブラコードとなり、実行される。(行頭が 004010xx となっている行が、アセンブラコードである。)

 main () {
00401000 sub esp,104h
00401006 mov eax,dword ptr [___security_cookie (403000h)]
0040100B xor eax,esp
0040100D mov dword ptr [esp+100h],eax
  char secret[256];

  if (get_secret (secret, sizeof (secret))) {
00401014 push 100h
00401019 lea eax,[esp+4]
0040101D push offset string "SECRET_DATA" (4020E4h)
00401022 push eax
00401023 call strncpy (401052h)  ← get_secret() のインライン処理が展開されている
   // secretに関する何からの処理...
  }

  // 作業領域として使用したメモリのクリア
  memset(secret, 0, sizeof (secret));
   ← 注1) memset に相当する処理が生成されない
 }
00401028 mov ecx,dword ptr [esp+10Ch]
0040102F add esp,0Ch
00401032 xor ecx,esp
00401034 xor eax,eax
00401036 call __security_check_cookie (401042h)
0040103B add esp,104h
00401041 ret

上記は、ソースコードとアセンブラコードを対応させたリストである。この中の注1)の部分にはソースコードに対応するアセンブラコードが無いことがわかる。

本来ここでメモリクリア処理を行う予定なのだが、この部分が削除されてしまっている。そのため、最後の ret 命令の時点でも秘密のデータ secret は、クリアされずに残ってしまっている。

このコンパイルしてできたアセンブラコードで、注1)にあるはずの処理が削除されてしまったのは、関数内の変数 secret を memset() 呼び出しで 0 クリアしようとしているが、この変数がその後使われないためである。このためコンパイラは、この 0 クリア処理は省略できるものとして除去してしまうのである。

  • 図7-6: メモリクリア失敗

最適化によってメモリクリア処理が除去されないための対策

(1) 最適化の抑止

メモリクリアのためのmemset()等が削除されてしまうのは、コンパイル時に最適化が行われてしまうためである。よって、対策としては、この最適化自体を行わない、あるいは抑止することが考えられる。この最適化の抑止をするには、コンパイルオプションを指定することで可能である。

しかし、コンパイルオプションに指定してしまうとソースファイル全体に対する最適化がすべて抑止されてしまう。よって、最適化で除去されてしまう部分に対して最適化を抑止できることが望ましい。このように一部に対して最適化を抑止するには、次のような方法がある。

  • SecureZeroMemory を使用する
  • volatile 変数を利用する
  • "#pragma optimize" を使用する

1) SecureZeroMemory を使用する

Visula C++ に含まれる SecureZeroMemory() は、WinBase.h に定義されており、最適化しても除去されないように、volatile を使用して実現されている。

PVOID SecureZeroMemory(
 PVOID ptr, // 0クリアするメモリの先頭アドレス
 SIZE_T cnt // 0クリアするメモリのサイズ
);

次の例のようにすると、最適化のオプションが働いてもメモリ secret はクリアされる。

[例]

#include <windows.h>
#include <winbase.h>

void test2 () {
 char secret[256];

 if (get_secret (secret, sizeof (secret))) {
   // secretに関する何からの処理...
 }

 // 作業領域として使用したメモリのクリア
 SecureZeroMemory (secret, sizeof (secret));
}

2) volatile を使用する

volatile 宣言されたポインタは、ポインタのアドレスに対する書き込みは、最適化されずに、実際のメモリに対して書き込むようにするという指定である。
SecureZeroMemoryでもvolatile を使用しており、自前でvolatileを使用した処理を記述してもSecureZeroMemoryと同じことができる。

[例 volatile を使用して最適化を抑止する]

test3 () {
 char secret[256];
 volatile char *p; ←volatileの宣言する
 int i;

 p = (volatile char *)secret;
 if (get_secret(p, sizeof (secret))) {
   // secretに関する何からの処理...
 }

 // 作業領域として使用したメモリのクリア
 for (i = 0; i < sizeof (secret); i++) {
  *p++ = 0; ←最適化しても除去されない
 }
}

3) "#pragma optimize" による最適化抑止

C のソースコード中の "#pragma" の記述は、コンパイラへの指示となり、パラメータ optimize で最適化の on, off を指定することができる。

つまり、SecureZeroMemoryやvolatileによるメモリクリア処理を行っていない場合でも、optimizeのon, offで指定された範囲を最適化対象から外すことがきて、メモリクリア処理が削除されずに済む。

次の例では、 #pragma optimize により、コンパイラに対して関数 test1() は、最適化をしないように指示している。

もし最適化されると、リターンの直前の memset() が除去され、secret の内容がメモリ(スタック上)に残ってしまう。

[例 #pragma optimize により最適化を抑止する]

#pragma optimize("", off)  // ここから最適化をすべて抑止する
void test1 () {
 char secret[256];

 if (get_secret (secret, sizeof (secret))) {
   // secretに関する何からの処理...
 }

 // 作業領域として使用したメモリのクリア
 memset(secret, 0, sizeof (secret)); ←《最適化すると除去されるコード》
}
#pragma optimize("", on)  // ここまで(最適化をデフォルトに戻す)

(2) 後続に変数参照があれば memset() は除去されない

もし、最適化の抑止の対策が行われていなくても、memset() 呼び出しの後、クリア対象となったポインタ変数が別の関数呼び出しに使用されているような場合、memset() 呼び出しは除去されない。

例えば、malloc() と free() の呼び出しに挟まれている memset() 呼び出しは除去されない。

[例 malloc() により確保したエリアを、開放前にクリアする]

#define MALLOC_SIZE 16

void func1 () {
 char *p;

 p = (void *)malloc(MALLOC_SIZE);
 if (p == (char *)NULL) {
  // エラー処理(省略)
  return;
 }
 if (get_secret(p, MALLOC_SIZE)) {
   // pに関する何からの処理...
 }

 // 作業領域として使用したメモリのクリア
 memset(p, 0, MALLOC_SIZE); // 最適化されずにクリア処理される (*1)
 free (p);
}

(注1) Windowsの場合、malloc したサイズを _msize() により取得することが可能。下記のようにすることもできる。
 memset(p, 0, _msize(p));

参考:gcc での最適化

C/C++コンパイラとして Microsoft の Visual C++ のほかに Unix系およびGNU/Linux系で広く利用されているひとつに gcc がある。
この gcc を利用する場合は、コンパイル時に最適化を行っても memset() は除去されずに処理される。

まとめ

Microsoft の Visual C++ においてmemset() 等を用いてメモリクリアを行う際には、最適化によってメモリクリア処理が削除されないために、次の対策を施す。

  • memset()を行った後でfree()等の関数で変数参照が行われるようにする。
  • ソースコードの一部を最適化しないように抑止処理を記述する。
    • SecureZeroMemory を使用する
    • volatile 変数を利用する
    • "#pragma optimize" を使用する

その他の処理系においても同様に、最適化によってメモリクリア処理が削除される懸念がある。