メモリ内にパスワード残ってしまう話(続き)
前回、password バッファを SecureZeroMemory() でクリアしているにもかかわらず、ダンプファイル内にパスワードが残ってしまうという話をしました。なぜなのか、追っていきます。
動作確認環境
- Windows 10 Home 21H2, 64bit
- Visual Studio Community 2019
- WinDbg
プログラム再掲
前回使ったプログラムを再掲します。
password バッファを SecureZeroMemory() でクリアしていますが、メモリ内にパスワードが残存し、その結果ダンプファイル内にもパスワードが残存してしまいます。
// EnterPassword.c
#include <stdio.h>
#include <conio.h>
#include <windows.h>
int main()
{
char password[32];
// バッファのアドレスを表示(調査用)
printf("password バッファのアドレス : %p\n", password);
// ユーザーが入力したパスワードをバッファに格納
printf("パスワードを入力してください : ");
scanf("%32s", password);
// バッファの内容を表示
printf("パスワードは %s です。\n", password);
// パスワード漏洩防止のためバッファの内容をクリア
SecureZeroMemory(password, sizeof(password));
// 例外を発生させる。
// 設定にもよるが、ダンプファイルが生成される。
*((char *)NULL) = 0;
return 0;
}
調査モードでプログラム実行
調査のため「/Zi」を付加して再コンパイルします。「/Zi」は「デバッグ情報を有効にする」の意味です。
>cl /O2 /Zi EnterPassword.c
また、ヒープの確保元を追うため、マイクロソフトの「gflags.exe」ツールを使って、本プログラムに対する[Create user mode stack trace database]を ON にします。
この状態でプログラムを実行し、パスワードとして「LeakedPassword」と入力します。
>EnterPassword.exe
C:\tp\EnterPassword>EnterPassword
password バッファのアドレス : 0043F980
パスワードを入力してください : LeakedPassword
パスワードは LeakedPassword です。
★ ここで NULL ポインタに書き込みをして例外発生。ダンプファイルが生成される。
ダンプファイルが生成されたので、WinDbg の [File]-[Open Crash Dump] で開き、「LeadedPassword」 を検索します。
0:000> s -a 0 L?-1 "LeakedPassword"
0043df79 4c 65 61 6b 65 64 50 61-73 73 77 6f 72 64 20 82 LeakedPassword .
030717f5 4c 65 61 6b 65 64 50 61-73 73 77 6f 72 64 20 82 LeakedPassword .
03072800 4c 65 61 6b 65 64 50 61-73 73 77 6f 72 64 0a 0a LeakedPassword..
3 箇所にパスワードが残ってしまっています。
メモリの内容を調査
どこにパスワードが残るのでしょうか。
最初のアドレスの用途を調べます。
0:000> !address 0043df79
Usage: Stack
スタックです。プログラムの実行時にスタックに一時的にパスワードが積まれたと思われます。
2 番目のアドレスの用途を調べます。
0:000> !address 030717f5
Usage: Heap
ヒープです。
だれが確保したのか調べます。
0:000> !heap -p -a 30717f5
address 030717f5 found in
_HEAP @ 3060000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
030717d0 0203 0000 [00] 030717e8 01000 - (busy)
77ca7934 ntdll!RtlpCallInterceptRoutine+0x00000026
77c16e7c ntdll!RtlpAllocateHeapInternal+0x0000108c
77c15dde ntdll!RtlAllocateHeap+0x0000003e
4b52ce EnterPassword!_malloc_base+0x00000038
4b8ef5 EnterPassword!__acrt_stdio_begin_temporary_buffering_nolock+0x0000006b
48c499 EnterPassword!<lambda_0be4ab1c2a6918fda4e39227d83ea893>::operator()+0x00000023
486311 EnterPassword!__crt_seh_guarded_call<int>::operator()<<lambda_c29ee0499b841886b80d843682cc403a>,<lambda_0be4ab1c2a6918fda4e39227d83ea893> &,<lambda_5a3ed3da325c8ea037a470278c0f2d16> >+0x00000027
49b420 EnterPassword!__stdio_common_vfprintf+0x00000081
477ed7 EnterPassword!printf+0x00000027
7771fa29 kernel32!BaseThreadInitThunk+0x00000019
77c37a9e ntdll!__RtlUserThreadStart+0x0000002f
77c37a6e ntdll!_RtlUserThreadStart+0x0000001b
printf 関数が内部で確保したヒープにパスワードが格納されたことが分かります。
3 番目のアドレスの用途を調べます。
0:000> !address 03072800
Usage: Heap
こちらもヒープです。
だれが確保したのか調べます。
0:000> !heap -p -a 3072800
address 03072800 found in
_HEAP @ 3060000
HEAP_ENTRY Size Prev Flags UserPtr UserSize - state
030727e8 0203 0000 [00] 03072800 01000 - (busy)
unknown!printable
77ca7934 ntdll!RtlpCallInterceptRoutine+0x00000026
77c16e7c ntdll!RtlpAllocateHeapInternal+0x0000108c
77c15dde ntdll!RtlAllocateHeap+0x0000003e
4b31b6 EnterPassword!_calloc_base+0x00000047
4cb17c EnterPassword!__acrt_stdio_allocate_buffer_nolock+0x0000001d
4cacc2 EnterPassword!common_refill_and_read_nolock<char>+0x00000084
4bb153 EnterPassword!_fgetc_nolock+0x0000002d
4a27c5 EnterPassword!__crt_stdio_input::skip_whitespace<__crt_stdio_input::stream_input_adapter,char>+0x00000011
4a9785 EnterPassword!__crt_stdio_input::input_processor<char,__crt_stdio_input::stream_input_adapter<char> >::process_whitespace+0x0000000f
4a8ddf EnterPassword!__crt_stdio_input::input_processor<char,__crt_stdio_input::stream_input_adapter<char> >::process_conversion_specifier+0x00000016
49b97b EnterPassword!__crt_seh_guarded_call<int>::operator()<<lambda_274ecf0a8038e561263518ab346655e8>,<lambda_21448eb78dd3c4a522ed7c65a98d88e6> &,<lambda_0ca1de2171e49cefb1e8dc85c06db622> >+0x00000027
4ab8f1 EnterPassword!__stdio_common_vfscanf+0x00000081
477f17 EnterPassword!scanf+0x00000027
7771fa29 kernel32!BaseThreadInitThunk+0x00000019
77c37a9e ntdll!__RtlUserThreadStart+0x0000002f
77c37a6e ntdll!_RtlUserThreadStart+0x0000001b
scanf 関数が内部で確保したヒープにパスワードが格納されたことが分かります。
メモリからパスワードを消す
メモリに残存しているパスワードを消す方法を考えます。
最初のスタックについては、大きなローカル変数をもつダミーの関数を呼び出し、当該ローカル変数の内容をゼロクリアすることで消せそうです。プログラムとしては、次のような感じです。
void ClearStack()
{
char pcBuf[30000];
SecureZeroMemory(pcBuf, sizeof(pcBuf));
}
2 番目の、printf 関数が内部で作業用に確保したヒープについては、ダミーの「malloc() → ゼロクリア → free()」を繰り返しても消せそうにありません。というのも、当該ヒープの state が busy となっており、使用中だからです。
少し調べたところ、printf 関数を最初に呼び出したタイミングでヒープが確保され、後続の printf 関数ではそのヒープが使い回されるようでした。そこで、確実性には劣りますが、パスワードを printf() した直後にダミーの文字列を printf() し、ヒープの内容が上書きされるようにします。
printf("パスワードは %s です。\n", password);
printf("----------------------------------------------------\n");
3 番目の、scanf 関数が内部で作業用に確保したヒープについても、printf 関数と同様です。
scanf 関数を最初に呼び出したタイミングでヒープが確保され、後続の scanf 関数ではそのヒープが使い回されるようでしたので、ユーザーに不便をしいることになってしまいますが、パスワードの scanf() 直後にダミーの scanf() を依頼し、ヒープの内容が上書きされるようにします。
printf("パスワードを入力してください : ");
scanf("%32s", password);
printf("同じ長さの * を入力してください : ");
scanf("%32s", dummy);
以上をまとめると、プログラムは次のようになります。
// EnterPassword.c
#include <stdio.h>
#include <conio.h>
#include <windows.h>
void ClearStack()
{
char pcBuf[30000];
SecureZeroMemory(pcBuf, sizeof(pcBuf));
}
int main()
{
char password[32];
char dummy[32];
// バッファのアドレスを表示(調査用)
printf("password バッファのアドレス : %p\n", password);
// ユーザーが入力したパスワードをバッファに格納
printf("パスワードを入力してください : ");
scanf("%32s", password);
printf("同じ長さの * を入力してください : ");
scanf("%32s", dummy);
// バッファの内容を表示
printf("パスワードは %s です。\n", password);
printf("----------------------------------------------------\n");
// パスワード漏洩防止のためバッファの内容をクリア
SecureZeroMemory(password, sizeof(password));
ClearStack(); // スタックもクリア
// 例外を発生させる。
// 設定にもよるが、ダンプファイルが生成される。
*((char *)NULL) = 0;
return 0;
}
パスワードが消えたことを確認
それでは修正したプログラムを再コンパイルして実行します。
今回はパスワードとして「HelloWorldJapanTokyo」と入力します。
>cl /O2 EnterPassword.c
>EnterPassword.exe
password バッファのアドレス : 00EFFDC8
パスワードを入力してください : HelloWorldJapanTokyo
同じ長さの * を入力してください : ********************
パスワードは HelloWorldJapanTokyo です。
----------------------------------------------------
生成されたダンプファイルを WinDbg で開き、「HelloWorldJapanTokyo」を探します。
0:000> s -a 0 L?-1 "HelloWorldJapanTokyo"
0:000>
見つかりません。
「Hello」のみを探したり、「Tokyo」のみを探したり、Unicode で探したりしても見つかりません。
0:000> s -a 0 L?-1 "Hello"
0:000>
0:000> s -u 0 L?-1 "Hello"
0:000>
0:000> s -a 0 L?-1 "Tokyo"
0:000>
0:000> s -u 0 L?-1 "Tokyo"
0:000>
メモリからパスワードの痕跡が消えました。
ライブラリの内部構造に依存するため絶対にパスワードが残らないと断言はできませんが、当初のプログラムよりずいぶん安全性が高まったと思います。