重箱の隅のデバッグ(1) - インポートセクションで設定するブレークポイント
皆さんこんにちは。
Platform SDK チームの Chiharun です。
「重箱の隅のデバッグ」シリーズ第 1 回としてデバッグ関連のトピックをご紹介します。ここで利用する WinDbg デバッガはマイクロソフトが公開している高機能なデバッグツールです。入手方法については、以下のURLをご覧下さい。
デバッグ ツールとシンボル: はじめに
https://msdn.microsoft.com/ja-jp/windows/hardware/gg462988
インポートセクションについて
日々の仕事で提供元が不明だったり、デバッグシンボルの存在しないプログラムをデバッグする状況は多々あります。そのような場合にデバッガコマンドでインポートセクションを確認することによってプログラムが呼び出す API の概要を把握し、インポートセクションをライブデバッグで利用する方法について説明したいと思います。
一般的にプログラムが動作する時にメインの EXE プログラム単体ですべての処理を実行することはありません。 必要に応じてプログラムが利用する API の関数本体が実装された外部の DLL に存在する関数を呼び出します。 メインの EXE プログラムは関数へのポインタが設定されたインポートセクションから関数のアドレスを取得して他の DLL に存在する関数を呼び出します。
Windows OS がプログラムをロードする時に、プログラムが参照する外部の DLL の API のアドレスをプログラムが参照できるようにするため、プログラムのインポートアドレステーブルを外部の DLLに存在する API の関数アドレスで初期化します。関連する情報についてはMSDNマガジンの2002年2月号で解説された記事がありますのでリンクをご紹介します。
MSDN Magazine 2002 February - An In-Depth Look into the Win32 Portable Executable File Format
https://msdn.microsoft.com/en-us/magazine/cc301805.aspx
では本題に戻ります。インポートセクションの情報をを確認する方法はいくつかあります。
以下の例では少し大きめの緑色の太文字で入力コマンド部分を示します。
方法 1 dumpbin
Visual Studio 付属のツール dumpbin を用いてインポートセクションを確認することができます。
以下の例では notepad.exe に対して dumpbin /imports コマンドを発行し、notepad.exe が kernel32.dll の提供している CreateFileW を参照することを確認した例です。
dumpbin /imports notepad.exe
Microsoft (R) COFF/PE Dumper Version 10.00.40219.01
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file notepad.exe
File Type: EXECUTABLE IMAGE
Section contains the following imports:
<途中略>
KERNEL32.dll
10000C058 Import Address Table
10000D240 Import Name Table
FFFFFFFF time date stamp
FFFFFFFF Index of first forwarder reference
78D2CF20 209 GetLocalTime
<途中略>
78D32A30 8F CreateFileW
78D362A0 466 SetErrorMode
78D32E60 558 lstrcmpiW
<後略>
補足: Visual Studio でセットアップされた各種ツールをコマンドラインから利用するためには、環境変数 PATH を設定する必要があります。私の環境では以下のコマンドを実行するバッチファイルを PATH の通った場所に VC.CMD、VC64.CMD として作成し、コマンドプロンプトからバッチファイルを起動することによって Visual Studio のツールを利用するために必要な環境設定をおこなっています。
VC.CMD の内容
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\vcvars32.bat"
VC64.CMD の内容
"C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\amd64\vcvars64.bat"
方法 2 デバッグシンボルを用いる
デバッグシンボルが手元にあればデバッグシンボル内の _imp_ を含むシンボル情報を WinDbg デバッガの x コマンドを用いてインポートセクションの情報を確認することができます。以下の例では _imp_ で始まる CreateFile の名前を含むシンボルを検索した例です。アスタリスク(*)はワイルドカードして評価されます。
0:000> x notepad!_imp_*CreateFile*
00000000`ffffc240 notepad!_imp_CreateFileW = <no type information>
00000000`ffffc118 notepad!_imp_CreateFileMappingW = <no type information>
方法 3 デバッガ !dh -f と dps コマンド
同様の情報は WinDbg デバッガの !dh -f コマンドと dps コマンドの組み合わせで確認することができます。
!dh -f コマンドで notepad.exe のヘッダ情報を表示し、Import Address Table Directory の情報を確認します。
0:000> !dh -f notepad
File Type: EXECUTABLE IMAGE
FILE HEADER VALUES
8664 machine (X64)
6 number of sections
4A5BC9B3 time date stamp Tue Jul 14 08:56:35 2009
0 file pointer to symbol table
0 number of symbols
F0 size of optional header
22 characteristics
Executable
App can handle >2gb addresses
<途中略>
0 [ 0] address [size] of Special Directory
0 [ 0] address [size] of Thread Storage Directory
0 [ 0] address [size] of Load Configuration Directory
2E0 [ 138] address [size] of Bound Import Directory
C000 [ 7F0] address [size] of Import Address Table Directory
0 [ 0] address [size] of Delay Import Directory
0 [ 0] address [size] of COR20 Header Directory
0 [ 0] address [size] of Reserved Directory
Import Address Table Directory には notepad.exe のインポートセクション自体の相対的開始オフセットとサイズの情報が含まれます。 インポートセクションには関数へのポインタの情報が設定されています。 dps コマンドでインポートセクションのデータを表示すると、関数へのポインタをデバッグシンボルと照合し、該当する DLL 名および関数名を表示してくれます。
0:000> dps notepad+c000 notepad+c000+7f0
00000000`ffffc000 000007fe`fde11ed0 ADVAPI32!RegSetValueExWStub
00000000`ffffc008 000007fe`fde1c2d0 ADVAPI32!RegQueryValueExWStub
00000000`ffffc010 000007fe`fde11f00 ADVAPI32!RegCreateKeyW
<途中略>
00000000`ffffc240 00000000`77431870 kernel32!CreateFileWImplementation
<後略>
!dh のメリットとライブデバッグでの活用
方法 3 のメリットは、デバッグシンボルが手元に無い場合でも、WinDbgデバッガ上でプログラムがどのような関数を呼び出すのか確認できる点です。
上記の例ではメインの EXE プログラムとして notepad.exe のインポートセクションを確認しておりますが、DLL のインポートセクションに対しても同じ方法で確認することができます。したがいまして、開発元が不明な DLL が呼び出す可能性のある関数の一覧をインポートセクションで確認し DLL がどのような動作をおこなうか調べる時のヒントとなる情報を得ることもできます。
さらに、ライブデバッグにおいてインポートセクション自体に ba コマンドでブレークポイントを設定することによって、特定の EXE または特定の DLL から API が呼び出された瞬間にブレークさせることも可能となります。 外部の関数を呼び出すために、インポートセクションに保持されている関数ポインタを取得した場合に即座にブレークするように設定することができます。
これにより、kernel32!CreateFileWに BP を設定する場合よりも、特定の DLL ファイルまたは EXE ファイルが呼び出す特定の関数でのみブレークするように的を絞ったブレークポイントを設定することができます。
以下の例では notepad.exe の kernel32!CreateFileW 関数へのポインタを保持するインポートセクションに ba コマンドでブレークポイントを設定してブレークさせた時の例です。
CreateFileWImplementation 関数へのポインタを保持するインポートセクションにポインタとして 8 バイトの Read アクセスでブレークするように設定します。デバッグ対象のプログラムが 32 ビットプログラムの場合は ba r4 コマンドで 4 バイトの Read アクセスでブレークするように設定します。
0:000> ba r8 00000000`ffffc240
設定したブレークポイントを確認します。
0:000> bl
0 e 00000000`ffffc240 r 8 0001 (0001) 0:****実行開始します。
0:000> g
設定したブレークポイントにヒットしました。
Breakpoint 0 hit
kernel32!CreateFileWImplementation:
00000000`77431870 48895c2408 mov qword ptr [rsp+8],rbx ss:00000000`001bf770=0000000000000000コールスタックを確認します。
0:000> k
Child-SP RetAddr Call Site
00000000`001bf768 00000000`ffff8b85 kernel32!CreateFileWImplementation
00000000`001bf770 00000000`ffff4ce6 notepad!SaveFile+0x55
00000000`001bf7f0 00000000`ffff14eb notepad!NPCommand+0x2d4
00000000`001bf920 00000000`77089bd1 notepad!NPWndProc+0x540
00000000`001bf960 00000000`770898da USER32!UserCallWinProcCheckWow+0x1ad
00000000`001bfa20 00000000`ffff10bc USER32!DispatchMessageWorker+0x3b5
00000000`001bfaa0 00000000`ffff133c notepad!WinMain+0x16f
00000000`001bfb20 00000000`7743652d notepad!DisplayNonGenuineDlgWorker+0x2da
00000000`001bfbe0 00000000`7756c521 kernel32!BaseThreadInitThunk+0xd
00000000`001bfc10 00000000`00000000 ntdll!RtlUserThreadStart+0x1dリターンアドレスの前方を逆アセンブルします。設定したブレークポイントにヒットしたのはインポートセクションを参照した call 命令です。
0:000> ub ffff8b85
notepad!SaveFile+0x34:
00000000`ffff8b64 3bde cmp ebx,esi
00000000`ffff8b66 450f45c4 cmovne r8d,r12d
00000000`ffff8b6a ba000000c0 mov edx,0C0000000h
00000000`ffff8b6f 488bcd mov rcx,rbp
00000000`ffff8b72 4533c9 xor r9d,r9d
00000000`ffff8b75 897020 mov dword ptr [rax+20h],esi
00000000`ffff8b78 c740a804000000 mov dword ptr [rax-58h],4
00000000`ffff8b7f ff15bb360000 call qword ptr [notepad!_imp_CreateFileW (00000000`ffffc240)] <<< ブレークCreateFileW の第 1 引数を確認します。CreateFileW API の最初の引数はファイル名へのポインタです。x64 では最初の引数は RCX レジスタに設定されます。 RCX レジスタのポイントするメモリを2バイト文字列として確認しました。 ファイル名が確認できます。
0:000> du @rcx
00000000`0020a510 "C:\Debug\test\test.txt"
まとめ
この方法はメインの EXE プログラムだけではなく、特定の DLL が呼び出す特定の API でブレークポイントを設定したいときにも役立ちます。 !dh コマンドを用いて、インポートセクションを確認し、インポートセクションに ba コマンドでブレークポイントを設定する方法はカーネルモードで動作するドライバをライブデバッグするときにも有効な方法です。デバッグが必要になる状況は千差万別ですが、覚えておくと役に立つこともあります。
あとがき
この記事のタイトルを「重箱の隅のデバッグ(1)」 としてナンバーを振りました。デバッグの汎用的な情報については MSDN の公開情報に任せることにして、「重箱の隅のデバッグ」シリーズでは特定のシナリオで役に立つ小技的なツールの利用方法を説明していきたいと考えています。
今後ともよろしくお願いします。
参考技術情報
32 ビット版 Debugging Tools for Windows
https://msdn.microsoft.com/ja-jp/windows/hardware/gg463016
64 ビット版 Debugging Tools for Windows
https://msdn.microsoft.com/ja-jp/windows/hardware/gg463012
DUMPBIN /imports
https://msdn.microsoft.com/ja-jp/library/d7k09ee7(v=vs.80).aspx
デバッガリファレンス General Extension Commands !dh
https://msdn.microsoft.com/en-us/library/windows/hardware/ff562360(v=vs.85).aspx
x64 呼び出し規約 Parameter Passing
https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx
MSDN リファレンス CreateFile
https://msdn.microsoft.com/ja-jp/library/cc429198.aspx
--------------------------------------
「重箱の隅のデバッグ」シリーズ
重箱の隅のデバッグ(2) - エラーの意味を探る
https://blogs.msdn.com/b/japan_platform_sdkwindows_sdk_support_team_blog/archive/2012/05/17/10303696.aspx