ページヒープ (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>

このように、ページヒープオプションを有効化すると、割り当てられたメモリブロックのオーバーランが発生したときに、 直ちにそれがアクセス違反として検出できるように、アクセス不可のページをメモリブロックの直後に配置します。 (これについてはアラインメントの問題などもありますが、それはまた別の機会に) これによって、オーバーランの検出が簡単になります。

以上、この資料では、フルページヒープオプションによる、バッファオーバーランの検出について簡単に説明しました。

しかし、ページヒープにはさらにオプションがありますし、オーバーランだけではなく、他の種類のヒープの問題についても高い確率で、不具合を検出できます。 ページヒープの詳細や他の状況については、また別の資料で解説したいと思います。

ここまでお読みいただき、誠にありがとうございます。SNS 等でこの記事をシェアしていただけますと、大変励みになります。どうぞよろしくお願いします。

© 2024 Web/DB プログラミング徹底解説