ページヒープ (PageHeap) によるヒープオーバーランの検出
検出されにくいヒープオーバーラン
はじめに、この資料でどのような問題を解決できるのか簡単に説明します。
次のコードを見てください。
#include <windows.h> #include <assert.h> #include <stdio.h> void foo() { printf ( "Entering foo()\n" ); char* p = NULL; p = (char*) HeapAlloc ( GetProcessHeap(), 0, 8 ); assert( p ); ZeroMemory ( p, 16 ); // BUGBUG HeapFree ( GetProcessHeap(), 0, p ); printf ( "Exit foo()\n" ); } int main(int argc, char* argv[]) { foo(); return 0; }
このコードでは、プロセスヒープから 8 バイトを割り当て、そのバッファから 16 バイトの領域を ZeroMemory を使ってゼロで埋めています。
これは明らかなバッファオーバーラン (特にヒープに対するものなので、ヒープオーバーラン) です。なるべく開発中に検出し、不具合を修正したいところです。
試しに、テストとしてこれを走らせてみましょう。
上記コードを pageheap1.cpp として保存して pageheap1.exe を作成して実行します。
> pageheap1 Entering foo() Exit foo() >
特に問題なくプログラムが終了しました。foo 関数の入り口と終わりでプリントしているメッセージも正しく出力されています。
つまり、このような問題はテスト中にクラッシュしてくれれば、検出してデバッグすることは可能ですが、このように何の問題もなくテストを素通りしてしまう場合もあるのです。
しかし、ヒープの仕組みを考えてみれば想像がつくように、オーバーランしていればヒープそのものは破損しているはずです。 (この点については本資料では確認しません。興味のある方は当サイト内のヒープの仕組みの記事をご覧ください。HEAP_ENTRY が上書きされてしまっていることが予想されます。) 破損したヒープから、メモリブロックの割り当て、解放などを行っていればいずれはどこかのタイミングで、プログラムがクラッシュすることになります。
こうした問題をなるべく速やかに検出できるようにするために、Windows では PageHeap (ページヒープ) という実行オプションが用意されています。
PageHeap の利用
それでは、PageHeap を pageheap1.exe に対して有効にして、プログラムを実行してみましょう。
PageHeap の有効化
gflags を使うと簡単にページヒープを有効化できます。
> gflags -p /full /enable pageheap1.exe path: SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options pageheap1.exe: page heap enabled >
gflags.exe は Debuggging Tools for Windows に含まれていますので、インストールしていない人はダウンロードしてインストールしてください。
上記のコマンドによって、Windows のプログラム実行オプションが次のように設定されます。
PageHeap を有効化してプログラムを実行 ~ オーバーランの検出
PageHeap を有効にして、サイドプログラムを実行してみましょう。
すると、次のようにプログラムがクラッシュして、何か問題があったことが分かります。
これだけでは、どこをどうデバッグすれば良いか分かりませんが、問題点を素通りして、予想できないタイミングでクラッシュするよりはよっぽどマシといえます。
PageHeap の無効化
デバッグが完了したら、ページヒープオプションは無効にしましょう。
> gflags -p /disable pageheap1.exe path: SOFTWARE\Microsoft\Windows NT\CurrentVersion\Image File Execution Options pageheap1.exe: page heap disabled >
デバッガによる問題点の追跡
それでは、どこで問題が発生したか突き止めデバッグできるように、デバッガ上で先ほどのプログラムを走らせます。
動画のデモはこちらです。
C:\src\test\pageheap\pageheap1>c:\debuggers\cdb .\chk\pageheap1.exe
Microsoft (R) Windows Debugger Version 6.11.0001.404 X86
Copyright (c) Microsoft Corporation. All rights reserved.
CommandLine: .\chk\pageheap1.exe
Symbol search path is: srv*C:\websymbols*http://msdl.microsoft.com/download/symbols
Executable search path is:
ModLoad: 00400000 00422000 pageheap1.exe
ModLoad: 776a0000 777c7000 ntdll.dll
ModLoad: 67000000 67031000 C:\Windows\system32\verifier.dll
Page heap: pid 0x1088: page heap enabled with flags 0x3.
ModLoad: 773f0000 774cc000 C:\Windows\system32\kernel32.dll
(1088.170c): Break instruction exception - code 80000003 (first chance)
eax=00000000 ebx=00000000 ecx=0012faf8 edx=77705e74 esi=fffffffe edi=776ec19e
eip=776e8b2e esp=0012fb10 ebp=0012fb40 iopl=0 nv up ei pl zr na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246
ntdll!DbgBreakPoint:
776e8b2e cc int 3
0:000> g
Entering foo()
(1088.170c): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=00000000 ebx=7ffda000 ecx=00000002 edx=00000000 esi=00000000 edi=0018d000
eip=004010ef esp=0012ff20 ebp=0012ff38 iopl=0 nv up ei pl nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00010202
pageheap1!memset+0x5f:
004010ef f3ab rep stos dword ptr es:[edi]
確かにアクセス違反が発生しました。
スタックバックトレースを確認します。
0:000> kbn # ChildEBP RetAddr Args to Child 00 0012ff20 00401053 0018cff8 00000000 00000010 pageheap1!memset+0x5f 01 0012ff38 00401088 0012ff88 00401b73 00000001 pageheap1!foo+0x53 02 0012ff40 00401b73 00000001 0143efe0 01440f30 pageheap1!main+0x8 03 0012ff88 7743d0e9 7ffda000 0012ffd4 776e19bb pageheap1!__tmainCRTStartup+0xfb 04 0012ff94 776e19bb 7ffda000 72037ece 00000000 kernel32!BaseThreadInitThunk+0xe 05 0012ffd4 776e198e 00401bca 7ffda000 00000000 ntdll!__RtlUserThreadStart+0x23 06 0012ffec 00000000 00401bca 7ffda000 00000000 ntdll!_RtlUserThreadStart+0x1b 0:000> ?10 Evaluate expression: 16 = 00000010
memset へ渡された引数を見れば、ここでは 0018cff8 から 16 バイトの領域を 0 で埋めようとしていることが分かります。
ZeroMemory は関数ではなくマクロで、WinNT.h で次のように定義されているのでしたね。
#define RtlZeroMemory(Destination,Length) memset((Destination),0,(Length))
アドレス 0018cff8 を確認します。
0:000> dc 0018cff8 0018cff8 00000000 00000000 ???????? ???????? ........???????? 0018d008 ???????? ???????? ???????? ???????? ???????????????? 0018d018 ???????? ???????? ???????? ???????? ???????????????? 0018d028 ???????? ???????? ???????? ???????? ???????????????? 0018d038 ???????? ???????? ???????? ???????? ???????????????? 0018d048 ???????? ???????? ???????? ???????? ???????????????? 0018d058 ???????? ???????? ???????? ???????? ???????????????? 0018d068 ???????? ???????? ???????? ???????? ????????????????
0018cff8 は次のように書き込み可能領域です。
0:000> !vprot 0018cff8 BaseAddress: 0018c000 AllocationBase: 00140000 AllocationProtect: 00000001 PAGE_NOACCESS RegionSize: 00001000 State: 00001000 MEM_COMMIT Protect: 00000004 PAGE_READWRITE Type: 00020000 MEM_PRIVATE
しかし、0018cff8 から 8 バイト先以降はデバッガで ??? と表示されているように、 アクセス不可のページが割り当てられています。
0:000> !vprot 0018cff8+0x8
BaseAddress: 0018d000
AllocationBase: 00140000
AllocationProtect: 00000001 PAGE_NOACCESS
RegionSize: 000b3000
State: 00002000 MEM_RESERVE
Type: 00020000 MEM_PRIVATE
0:000>
このように、ページヒープオプションを有効化すると、割り当てられたメモリブロックのオーバーランが発生したときに、 直ちにそれがアクセス違反として検出できるように、アクセス不可のページをメモリブロックの直後に配置します。 (これについてはアラインメントの問題などもありますが、それはまた別の機会に) これによって、オーバーランの検出が簡単になります。
以上、この資料では、フルページヒープオプションによる、バッファオーバーランの検出について簡単に説明しました。
しかし、ページヒープにはさらにオプションがありますし、オーバーランだけではなく、他の種類のヒープの問題についても高い確率で、不具合を検出できます。 ページヒープの詳細や他の状況については、また別の資料で解説したいと思います。