本章では、シンプルなアプリケーションクラッシュダンプを WinDbg で解析していきます。
もくじ
- 解析用のアプリケーションクラッシュダンプを作成する
- アプリケーションクラッシュダンプを WinDbg でロードする
- !analyze 拡張機能でクラッシュダンプを分析する
- クラッシュが発生した前後の命令を読む
- Appendix:Ghidra でバイナリ解析を行う
- 4 章のまとめ
- 各章へのリンク
解析用のアプリケーションクラッシュダンプを作成する
まずは、解析用のアプリケーションクラッシュダンプを作成します。
1 章でダウンロードした D4C.exe を起動して、1 番のメニューを指定 Enter キーを押下すると、D4C.exe がクラッシュしてユーザモードクラッシュダンプが生成されます。
Windows 10 などの最新の OS では、ユーザーモードアプリケーションがクラッシュすると、WER サービスが起動されてクラッシュレポートの作成が行われますが、この過程でアプリケーションのユーザモードクラッシュダンプが生成されます。1
生成されたユーザモードクラッシュダンプは、既定では C:\Users\<ユーザ名>\AppData\Local\CrashDumps
に保存されます。
また、既定値ではアプリケーションのクラッシュ時に生成されるダンプファイルの種類はミニダンプです。
ミニダンプとは、アプリケーションのクラッシュが発生した時点のスレッドのレジスタとスタックの情報を、レジスタが参照しているメモリのページとともにキャプチャしたものです。2
クラッシュ事象の調査に必要な最小限の情報はミニダンプからも確認が可能ですが、より詳細なトラブルシューティングを行う場合には、ターゲットプロセスのユーザーモードのメモリ領域をすべてキャプチャしたフルダンプを取得することが望ましいです。 (ただし、1 章に記載した通り、指定するオプションによってはプロセスのミニダンプにはフルダンプよりも多くの情報が含まれる場合があります)
アプリケーションクラッシュが発生した場合に生成されるダンプの種類を変更する場合、 レジストリキー HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps
内の DWORD 値 DumpType を 2 に変更します。
なお、D4C.exe によるフルメモリダンプの取得設定を実施している環境の場合、生成されるアプリケーションのクラッシュダンプの種類はすでにフルダンプに変更されているため、D4C.exe のクラッシュ時にもフルダンプが生成されます。
ユーザーモードダンプの収集:
https://learn.microsoft.com/ja-jp/windows/win32/wer/collecting-user-mode-dumps
アプリケーションクラッシュダンプを WinDbg でロードする
D4C.exe のクラッシュ時に生成されたアプリケーションクラッシュダンプは既定では C:\Users\<ユーザ名>\AppData\Local\CrashDumps
に保存されます。
解析を行うために、管理者権限で起動した 64 bit 用の WinDbg でショートカットキー [Ctrl + D] を使用して、このフォルダの中から取得したダンプファイルをロードします。
WinDbg の起動直後の Command ウインドウには、Path validation summary
の情報が表示されています。
Path validation summary
には解析に有用な情報が記載されているので、いくつか内容を確認してみましょう。
まずは以下の前半部分ですが、こちらには現在設定している .sympath の情報や OS バージョンなどが記載されています。
Response Time (ms) Location
Deferred srv*https://msdl.microsoft.com/download/symbols
Symbol search path is: srv*https://msdl.microsoft.com/download/symbols
Executable search path is:
Windows 10 Version 19045 MP (8 procs) Free x64
Product: WinNt, suite: SingleUserTS
Edition build lab: 19041.1.amd64fre.vb_release.191206-1406
続いて以下の行ですが、Debug session time
にはプロセスクラッシュが発生した時刻が、また、System Uptime
と Process Uptime
には、アプリケーションがクラッシュするまでのシステムの稼働時間とプロセスの稼働時間がそれぞれ記載されています。
Debug session time: Wed Sep 13 20:36:00.000 2023 (UTC + 9:00)
System Uptime: 1 days 20:35:58.000
Process Uptime: 0 days 0:00:56.000
また、以下の行には、アプリケーションのクラッシュを直接的に引き起こした例外コードが表示されています。
This dump file has an exception of interest stored in it.
The stored exception information can be accessed via .ecxr.
(2f10.598): Access violation - code c0000005 (first/second chance not available)
For analysis of this file, run !analyze -v
ntdll!NtWaitForMultipleObjects+0x14:
00007fff`35fcd9a4 c3 ret
ここに記載されている Access violation - code c0000005 (first/second chance not available)
という情報から、アプリケーションクラッシュの直接の原因がメモリアクセス違反 (Access violation) であるということを容易に特定できました。
アクセス違反 C0000005:
https://learn.microsoft.com/ja-jp/shows/inside/c0000005
ここから、さらに具体的に問題が発生した処理を特定していきます。
!analyze 拡張機能でクラッシュダンプを分析する
WinDbg でクラッシュダンプを解析する上で最もよく使うコマンドの一つは !analyze -v
かと思います。
このコマンドでは、!analyze
拡張機能を使用してクラッシュダンプで収集した例外に関する詳細情報を表示できます。
!analyze (WinDbg):
https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-analyze
なお、エクスクラメーション(!
) から始まるコマンドは通常、デバッガの拡張機能を呼び出すコマンドを意味します。
デバッガの拡張機能は、デバッガとは別のモジュールとして用意した DLL を WinDbg にロードすることで使用できます。
WinDbg には、既定でロードされている拡張機能がいくつか存在しますが、ユーザが独自に拡張機能を作成してデバッガにロードすることもできます。
WinDbg に現在ロードされている拡張機能の一覧は、.chain
コマンドで取得できます。
私の手元の環境で実際に.chain
コマンドを実行すると、以下の結果を取得することができました。
Extension DLL chain:
ext: image 10.0.22621.1778, API 1.0.0,
[path: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext\ext.dll]
ELFBinComposition: image 10.0.22621.1778, API 0.0.0,
[path: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext\ELFBinComposition.dll]
dbghelp: image 10.0.22621.1778, API 10.0.6,
[path: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\dbghelp.dll]
exts: image 10.0.22621.1778, API 1.0.0,
[path: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\WINXP\exts.dll]
uext: image 10.0.22621.1778, API 1.0.0,
[path: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\winext\uext.dll]
ntsdexts: image 10.0.22621.1778, API 1.0.0,
[path: C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\WINXP\ntsdexts.dll]
しかし、この一覧を見ると、analyze という拡張機能は一覧に存在していないことがわかります。
これは、!analyze
拡張機能は、厳密には WinDbg に既定でロードされている ext 拡張機能に含まれる機能の一つであるからです。
WinDbg では、拡張機能のモジュールについては !<拡張モジュールのエイリアス名>
または !<拡張機能名>.<拡張モジュールのモジュール名>
コマンドのどちらかを実行することで呼び出すことが可能です。
つまり、!ext.analyze
コマンドを実行することでも、!analyze
コマンドを実行した場合と同等の出力を得ることができます。
なお、余談ですが、ext 拡張機能に含まれるモジュールの一覧は !help
もしくは !ext.help
コマンドで一覧参照できます。
以下のように、analyze 以外にも使用頻度の多い address などが含まれていることを確認できます。
0:000> !ext.help
analyze [-v][level] - Analyzes current exception or bugcheck (levels are 0..9)
owner [symbol!module] - Displays the Owner for current exception or bugcheck
comment - Displays the Dump's Comment(s)
error [errorcode] - Displays Win32 or NTSTATUS error string
gle [-all] - Displays the Last Error & Last Status of the current thread
address [address] - Displays the address space layout
[-UsageType] - Displays the address space regions of the given type
cpuid [processor] - Displays the CPU information for a specific or all CPUs
exchain - Displays exception chain for the current thread
for_each_process <cmd> - Executes command for each process
for_each_thread <cmd> - Executes command for each thread
for_each_frame <cmd> - Executes command for each frame in the current thread
for_each_local <cmd> $$<n> - Executes command for each local variable in the current frame,
substituting the fixed-name alias $u<n> for each occurrence of $$<n>
imggp <imagebase> - Displays GP directory entry for 64-bit image
imgreloc <imagebase> - Relocates modules for an image
str <address> - Displays ANSI_STRING or OEM_STRING
ustr <address> - Displays UNICODE_STRING
list [-? | parameters] - Displays lists
cppexr <exraddress> - Displays a C++ EXCEPTION_RECORD
obja <address> - Displays OBJECT_ATTRIBUTES[32|64]
rtlavl <address> - Displays RTL_AVL_TABLE
std_map <address> - Displays a std::map<>
拡張機能について確認できたところで、実際に !analyze -v
コマンドを実行してダンプファイルの解析を行ってみましょう。
!analyze -v
コマンドの出力は少々多いので、分割して確認していきます。
最初のセクションは、環境情報やダンプファイルに関する情報が表示されています。
KEY_VALUES_STRING: 1
Key : AV.Dereference
Value: NullPtr
Key : AV.Fault
Value: Write
Key : Analysis.CPU.mSec
Value: 561
Key : Analysis.DebugAnalysisManager
Value: Create
Key : Analysis.Elapsed.mSec
Value: 577
Key : Analysis.Init.CPU.mSec
Value: 12827
Key : Analysis.Init.Elapsed.mSec
Value: 85744066
Key : Analysis.Memory.CommitPeak.Mb
Value: 84
Key : Timeline.OS.Boot.DeltaSec
Value: 44
Key : Timeline.Process.Start.DeltaSec
Value: 7
Key : WER.OS.Branch
Value: vb_release
Key : WER.OS.Timestamp
Value: 2019-12-06T14:06:00Z
Key : WER.OS.Version
Value: 10.0.19041.1
FILE_IN_CAB: D4C.exe.9608.dmp
NTGLOBALFLAG: 0
PROCESS_BAM_CURRENT_THROTTLED: 0
PROCESS_BAM_PREVIOUS_THROTTLED: 0
APPLICATION_VERIFIER_FLAGS: 0
次のセクションでは、発生した例外と紐づく Register Context3 を表示する .ecxr
コマンドの出力結果が表示されます。
この情報は、メモリリーク事象などの調査のために手動で取得したダンプファイルの場合は参照する必要はありませんが、今回のようなクラッシュダンプの調査を行う場合には非常に重要になります。
CONTEXT: (.ecxr)
rax=0000000000000017 rbx=0000021b6cad2460 rcx=00007ff6abb95300
rdx=000000bfc50ff948 rsi=00007ff6abb953f8 rdi=0000021b6cad7490
rip=00007ff6abb91412 rsp=000000bfc50ff910 rbp=0000000000000000
r8=000000bfc50fdd28 r9=0000021b6cad9477 r10=0000000000000000
r11=000000bfc50ff810 r12=0000000000000000 r13=0000000000000000
r14=0000000000000000 r15=0000000000000000
iopl=0 nv up ei pl nz na po nc
cs=0033 ss=002b ds=002b es=002b fs=0053 gs=002b efl=00010206
D4C+0x1412:
00007ff6`abb91412 41c706e8030000 mov dword ptr [r14],3E8h ds:00000000`00000000=????????
Resetting default scope
実際に上記の .ecxr
コマンドの出力結果を見てみると、アプリケーションのクラッシュがオフセット 0x1412 の命令で発生していることがわかります。
また、クラッシュを引き起こした命令は mov dword ptr [r14],3E8h
であることがわかります。
D4C+0x1412:
00007ff6`abb91412 41c706e8030000 mov dword ptr [r14],3E8h ds:00000000`00000000=????????
この命令は、r14 レジスタが保持するポインタアドレスが指す DWORD(32bit) の領域に 0x3E8(1000) という値を保存する命令です。
しかし、r14=0000000000000000
と表示されている通り、r14 レジスタには有効なポインタアドレスが格納されていません。
つまり、存在しないメモリアドレスに対して値を格納しようとした結果、メモリアクセス違反 (Access violation) によりアプリケーションがクラッシュしたと判断できます。
ここまでの情報ですでにアプリケーションがクラッシュした詳細な原因は特定できましたが、今回はこのままダンプファイルの解析を継続します。
次のセクションには .exr -1
コマンドの実行結果と同等の出力が表示されています。
このコマンドは、システムで発生した例外に関連する情報を出力します。
引数に -1
を指定した場合には最新の例外の情報が表示されます。4
EXCEPTION_RECORD: (.exr -1)
ExceptionAddress: 00007ff7f7481412 (D4C+0x0000000000001412)
ExceptionCode: c0000005 (Access violation)
ExceptionFlags: 00000000
NumberParameters: 2
Parameter[0]: 0000000000000001
Parameter[1]: 0000000000000000
Attempt to write to address 0000000000000000
上記の結果の ExceptionAddress から、発生した例外が c0000005 (Access violation)
であり、例外の発生箇所が D4C.exe のオフセット 0x1412 であることがわかります。
また、例外が c0000005 (Access violation)
の場合、.exr -1
コマンドで表示される 2 つのパラメータは、それぞれ以下の意味を持ちます。5
Parameter[0]
: メモリアクセスの種類(読み取り:0 / 書き込み:1 / 実行: 8)Parameter[1]
: 対象のメモリアドレス
つまり、上記の結果から、発生したメモリアクセス違反は、アドレス 0x0 に対する書き込みアクセスに起因していることがわかります。
さらに次のセクションを見ていきます。
PROCESS_NAME: D4C.exe
WRITE_ADDRESS: 0000000000000000
ERROR_CODE: (NTSTATUS) 0xc0000005 - 0x%p ???? 0x%p ???????????????? %s ???????????????
EXCEPTION_CODE_STR: c0000005
EXCEPTION_PARAMETER1: 0000000000000001
EXCEPTION_PARAMETER2: 0000000000000000
STACK_TEXT:
00000065`1e9cf580 00007ff7`f7481a40 : {省略} : D4C+0x1412
00000065`1e9cf840 00007fff`35e17344 : {省略} : D4C+0x1a40
00000065`1e9cf880 00007fff`35f826b1 : {省略} : kernel32!BaseThreadInitThunk+0x14
00000065`1e9cf8b0 00000000`00000000 : {省略} : ntdll!RtlUserThreadStart+0x21
STACK_COMMAND: ~0s; .ecxr ; kb
前半の情報は、.exr -1
の出力結果と同じく、直前の例外の種類と例外発生時のパラメータを示しています。
また、後半の STACK_TEXT には、例外が発生するまでのスタックバックトレースが表示されています。
このスタックバックトレースの情報は、~0s; .ecxr ; kb
コマンドによって取得できる情報と同等です。
このコマンドを分解すると、~0s
と .ecxr
コマンドによって 1 番目のスレッドの Register Context を取得した後に kb
コマンドで引数付きのスタックバックトレースの情報を出力しています。
つまり、今回のクラッシュダンプの場合、この出力は kb
コマンドによって出力される引数付きのスタックバックトレースの情報から、例外ディスパッチャ(KiUserExceptionDispatch
)6が呼び出される前までの情報を抜き出したものと同等であると言えます。
実際に WinDbg で kb
コマンドを実行すると以下の情報を得ることができます。
0:000> kb
# RetAddr : Args to Child : Call Site
00 00007fff`33701be0 : {省略} : ntdll!NtWaitForMultipleObjects+0x14
01 00007fff`33701ade : {省略} : KERNELBASE!WaitForMultipleObjectsEx+0xf0
02 00007fff`35e6f93a : {省略} : KERNELBASE!WaitForMultipleObjects+0xe
03 00007fff`35e6f376 : {省略} : kernel32!WerpReportFaultInternal+0x58a
04 00007fff`337de099 : {省略} : kernel32!WerpReportFault+0xbe
05 00007fff`35fd5330 : {省略} : KERNELBASE!UnhandledExceptionFilter+0x3d9
06 00007fff`35fbc876 : {省略} : ntdll!RtlUserThreadStart$filt$0+0xa2
07 00007fff`35fd221f : {省略} : ntdll!_C_specific_handler+0x96
08 00007fff`35f814b4 : {省略} : ntdll!RtlpExecuteHandlerForException+0xf
09 00007fff`35fd0d2e : {省略} : ntdll!RtlDispatchException+0x244
0a 00007ff7`f7481412 : {省略} : ntdll!KiUserExceptionDispatcher+0x2e
0b 00007ff7`f7481a40 : {省略} : D4C+0x1412
0c 00007fff`35e17344 : {省略} : D4C+0x1a40
0d 00007fff`35f826b1 : {省略} : kernel32!BaseThreadInitThunk+0x14
0e 00000000`00000000 : {省略} : ntdll!RtlUserThreadStart+0x21
上記のスタックバックトレースを順を追って読んでいくと、Windows においてユーザモードスレッドを開始する RtlUserThreadStart 関数の実行から始まり、オフセット 0x1412 をスタックにプッシュした後、例外ディスパッチャと WER サービスに接続する WerpReportFault 関数の呼び出しという、クラッシュダンプを作成するまでの一連の流れを追うことができます。7
続いて、!analyze -v
の最後のセクションを確認します。
SYMBOL_NAME: D4C+1412
MODULE_NAME: D4C
IMAGE_NAME: D4C.exe
FAILURE_BUCKET_ID: NULL_POINTER_WRITE_c0000005_D4C.exe!Unknown
OS_VERSION: 10.0.19041.1
BUILDLAB_STR: vb_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
FAILURE_ID_HASH: {3a8ea6b5-fbef-8da5-2baa-142fae4fc055}
Followup: MachineOwner
FAILURE_BUCKET_ID
は、ダンプファイルの解析において非常に重要な情報の 1 つです。
クラッシュダンプファイルをデバッガにロードすると、BUCKET ID というクラッシュの種類を識別するシグネチャが生成されます。8 9
今回のダンプファイルでは、FAILURE_BUCKET_ID
に NULL_POINTER_WRITE_c0000005_D4C.exe!Unknown
と表示されています。
この点からも、アプリケーションのクラッシュが発生した原因が NULL ポインタに対する書き込み時のメモリアクセス違反であると判断できます。
以上で、!analyze -v
コマンドのすべての出力結果を確認できました。
今回のようにシンプルなクラッシュダンプであれば、!analyze -v
コマンドの出力結果を参照するだけで、アプリケーションクラッシュが発生した原因を容易に特定できます。
クラッシュが発生した前後の命令を読む
すでに !analyze -v
コマンドの出力結果からアプリケーションクラッシュの原因は特定できましたが、より詳細な情報を得るために、さらにダンプファイルの解析を進めていきます。
!analyze -v
コマンドの出力結果から、アプリケーションのクラッシュが発生した命令はオフセット 0x1412 の mov dword ptr [r14],3E8h
であることを確認しました。
このセクションでは、クラッシュが発生する直前の処理を追跡します。
まずは、ext 拡張機能に含まれる threads を呼び出してアプリケーションのスレッドを一覧参照しましょう。
以下の出力結果から、このアプリケーションではメインスレッド 1 つのみが実行されていることがわかります。
0:000> !threads
Index TID TEB StackBase StackLimit DeAlloc StackSize ThreadProc
0 0000000000000000 0x000000651eac0000 0x000000651e9d0000 0x000000651e9cc000 0x000000651e8d0000 0x0000000000004000 0x0
Total VM consumed by thread stacks 0x00004000
上記の結果から解析対象はメインスレッドで問題ないことがわかるので、~0s; .ecxr ; k
コマンドで例外が発生した Register Context を指定した状態でスタックバックトレースを表示します。
なお、kb
コマンドで取得できる Args to Child の情報は、x64 バイナリの場合は呼び出し規約の関係でほとんど参考にならないので、以下の例では k
コマンドだけで情報を表示しています。
0:000> ~0s; .ecxr ; k
{省略}
D4C+0x1412:
00007ff7`f7481412 41c706e8030000 mov dword ptr [r14],3E8h ds:00000000`00000000=????????
*** Stack trace for last set context - .thread/.cxr resets it
# Child-SP RetAddr Call Site
00 00000065`1e9cf580 00007ff7`f7481a40 D4C+0x1412
01 00000065`1e9cf840 00007fff`35e17344 D4C+0x1a40
02 00000065`1e9cf880 00007fff`35f826b1 kernel32!BaseThreadInitThunk+0x14
03 00000065`1e9cf8b0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
今回のようなアプリケーションクラッシュダンプの場合、最上位の Call Site は通常、例外を発生させた命令のオフセットになります。
今回のダンプファイルの出力結果においては、例外ディスパッチャが呼び出される直前の呼び出しが D4C+0x1412
ですので、クラッシュが発生した命令は D4C.exe のオフセット 0x1412 であることがわかります。
すでに .ecxr
コマンドの出力結果にてこのアプリケーションクラッシュはオフセット 0x1412 の命令でアクセス違反が発生したことを原因として発生したことは確認済みですが、このオフセットの前後の命令も念のため参照してみましょう。
[Alt + 7] キーで起動した Disassembly ウインドウを使用することでも特定のオフセットの前後の命令を参照することが可能です。
Disassembly ウインドウを使用する場合、ウインドウ上部の Offset 欄に !<モジュール名>+オフセット
を入力します。
つまり、D4C.exe のオフセット 0x1412 に存在する命令を参照したい場合は、ウインドウ上部の Offset 欄に !D4C+0x1412
と入力してみてください。
また、WinDbg で特定のオフセットの命令を参照するために、u / ub / uu
コマンド10を使用することも可能です。
コマンドを使用する場合も、Disassembly ウインドウと同じフォーマットでオフセットを指定できます。
オフセット 0x1412 の命令を取得する場合は、u !D4C+0x1412
コマンドを実行します。
u
コマンドのオプションを指定しない場合は、指定したオフセット以降の 8 つの命令が表示されます。(指定したオフセットの命令を含みます)
一方で、指定オフセットの直前の命令を参照したい場合には ub
コマンドを利用できます。
ub !D4C+0x1412
コマンドを実行すると、指定のオフセットより前の 8 つもしくは 9 つの命令が表示されます。(指定したオフセットの命令は含まれません)
実際に各コマンドの出力結果を確認してみると、以下の通り指定のオフセットの前後の命令を正しく取得できていることがわかります。
# u コマンドで指定のオフセット以降の 8 つの命令を取得(一部省略)
0:000> u !D4C+0x1412
D4C+0x1412:
mov dword ptr [r14],3E8h
call D4C+0x14e0 (00007ff7`f74814e0)
jmp D4C+0x1431 (00007ff7`f7481431)
lea rcx,[D4C+0x3538 (00007ff7`f7483538)]
call D4C+0x1010 (00007ff7`f7481010)
call D4C+0x1740 (00007ff7`f7481740)
lea rcx,[D4C+0x3310 (00007ff7`f7483310)]
call D4C+0x1010 (00007ff7`f7481010)
# ub コマンドで指定のオフセットより前の 8 つ(または 9 つ)の命令を取得(一部省略)
0:000> ub !D4C+0x1412
D4C+0x13e7:
jmp D4C+0x1431 (00007ff7`f7481431)
lea rcx,[D4C+0x3560 (00007ff7`f7483560)]
call D4C+0x1010 (00007ff7`f7481010)
lea rcx,[D4C+0x52e8 (00007ff7`f74852e8)]
call D4C+0x14e0 (00007ff7`f74814e0)
mov qword ptr [rsp+38h],r14
lea rdx,[rsp+38h]
lea rcx,[D4C+0x5300 (00007ff7`f7485300)]
# ub コマンドで特定したオフセットから 16 (0x10) 個の命令を取得(一部省略)
0:000> u !D4C+0x13e7 L10
D4C+0x13e7:
jmp D4C+0x1431 (00007ff7`f7481431)
lea rcx,[D4C+0x3560 (00007ff7`f7483560)]
call D4C+0x1010 (00007ff7`f7481010)
lea rcx,[D4C+0x52e8 (00007ff7`f74852e8)]
call D4C+0x14e0 (00007ff7`f74814e0)
mov qword ptr [rsp+38h],r14
lea rdx,[rsp+38h]
lea rcx,[D4C+0x5300 (00007ff7`f7485300)]
mov dword ptr [r14],3E8h
call D4C+0x14e0 (00007ff7`f74814e0)
jmp D4C+0x1431 (00007ff7`f7481431)
lea rcx,[D4C+0x3538 (00007ff7`f7483538)]
call D4C+0x1010 (00007ff7`f7481010)
call D4C+0x1740 (00007ff7`f7481740)
lea rcx,[D4C+0x3310 (00007ff7`f7483310)]
call D4C+0x1010 (00007ff7`f7481010)
これで、アプリケーションクラッシュが発生した前後の命令を WinDbg で参照することができました。
Appendix:Ghidra でバイナリ解析を行う
一般的に、クラッシュダンプの解析はトラブルシューティングのために行う一連の活動の中の一部です。
今回のように、ダンプファイルの解析結果からアプリケーションクラッシュの具体的な原因とエラーが発生したコードのオフセットが特定できている場合、より詳細な原因や回避方法を特定するためには、ソースコードの確認やライブデバッグ、問題の再現環境での設定変更などによる切り分けなどのアプローチの方がダンプファイルの解析より効果的な可能性があります。
しかし、ダンプファイルの解析者が解析対象のプログラムのソースコードやシンボルを入手できず、上記のアプローチによるトラブルシューティングが困難である場面はしばしば発生します。
そのような場合の選択肢の一つとして、デコンパイラと呼ばれるツールを用いて、アプリケーションをアセンブリや疑似コードに復元するテクニックを覚えておくとよいと思います。
本書では、オープンソースの高機能なデコンパイラである Ghidra のバージョン 2.3 を使用してアプリケーションのデコンパイルを行います。
Ghidra はアメリカ国家安全保障局 (NSA) によって開発されたツールで、アプリケーションの実行ファイルのディスアセンブルやデコンパイルを手軽に行うことができます。
ただし、解析対象のプログラムが .Net や Java などのプログラム言語で作成されている場合には、ILSpy11 や jadx12 などの専用のデコンパイラを使用する方が効率的です。
解析を行う場合には、対象のプログラムの種類や開発に使用しているプログラム言語、フレームワークなどに合わせて最適なツールや複数のツールを組み合わせて使用するとよいと思います。
なお、D4C.exe のような C 言語で作成されたコマンドラインアプリケーションを Ghidra でデコンパイルして解析を行うことは非常に簡単です。
まずは、1 章でセットアップした Ghidra を起動して、プロジェクト画面に実行ファイル D4C.exe をドラッグ & ドロップします。
この時、ファイルの種類が Ghidra によって自動的に分析されて x64 プラットフォーム用の PE バイナリ(Windows 用の実行プログラム)であることが表示されたら、[OK] をクリックしてファイルをロードします。
次に、ロードしたファイルをダブルクリックして解析ウインドウを起動します。
以下のような確認プロンプトがいくつか表示されたら [Yes] を選択します。
続けて表示される解析オプションの選択はデフォルト設定のままで問題ないので、[Analyze] をクリックしてプログラムの解析を開始します。
自動解析を開始して数十秒から数分程度経過すると、Ghidra を使用してアプリケーションのデコンパイル結果を参照できるようになります。
Ghidra の解析ウインドウは、既定では以下のような配置になっています。
まず、右側の Decompiler ウインドウには、アプリケーションの関数を C 言語に近い疑似コードでデコンパイルした結果が表示されます。
中央の Listing ウインドウにはアプリケーションのディスアセンブル結果が表示されます。
Listing ウインドウに表示されるコードは、WinDbg の Disassembly ウインドウで参照できるディスアセンブル結果と基本的には同じですが、Ghidra は IAT などの情報を自動的に解析した結果を表示してくれるので、WinDbg で参照するディスアセンブル結果よりも比較的読みやすいコードが表示されることが多いです。
ウインドウの左側には、プログラムのセクション、シンボルツリー、データタイプを参照できるウインドウが並べられています。
プログラムの解析を行うために、まずはプログラムで最初に実行される main 関数を特定しましょう。
Windows が PE ファイル(exe)13を実行する場合、PE ファイルのファイルヘッダに埋め込まれた AddressOfEntryPoint の値が実行の開始位置になります。
Windows は AddressOfEntryPoint で指定されたアドレスからプログラムの実行を開始し、いくつかの初期化処理を経た後で main 関数が実行されます。
この AddressOfEntryPoint で指定されたエントリポイントは、Ghidra の解析ウインドウ左側の Symbol Tree ウインドウから [Functions] のツリーを展開して entry 関数を見つけることで特定できます。
続けて、Listing ウインドウや Decompiler ウインドウに表示されている FUN_140001934()
という関数をクリックして、その関数のオフセットにジャンプします。(関数オフセットはバイナリによって変わります)
この関数 FUN_140001934()
のデコンパイル結果は以下のようになっています。(一部省略)
uint FUN_140001934(void)
{
{{ 省略 }}
uVar4 = __scrt_initialize_crt(1);
if ((char)uVar4 == '\0') {
FUN_140001fa8(7);
}
else {
{{ 省略 }}
puVar7 = (undefined *)_get_initial_narrow_environment();
puVar8 = (undefined8 *)__p___argv();
uVar1 = *puVar8;
puVar9 = (uint *)__p___argc();
uVar10 = (ulonglong)*puVar9;
unaff_EBX = FUN_140001070(uVar10,uVar1,puVar7,in_R9);
{{ 省略 }}
}
}
FUN_140001fa8(7);
LAB_140001aa0:
exit(unaff_EBX);
}
コードの詳細については本書の範囲外となるので詳しく解説はしませんが、関数 FUN_140001934()
はアプリケーションを実行する際の初期化処理にあたります。14
特に、Ghidra 2.3 のデフォルト設定で特定されているシンボル名である _get_initial_narrow_environment
、__p___argv
、__p___argc
を引数としている unaff_EBX = FUN_140001070(uVar10,uVar1,puVar7,in_R9);
の行に着目します。
公式のドキュメント上では情報が見つかりませんでしたが、Stack Overflow などのインターネット上の Web サイトでこれらの 3 つの値を引数について検索を行うと、Windows で CRT が main 関数を実行するための以下の処理に該当する可能性が高いことを特定できます。
static int __cdecl invoke_main()
{
return main(__argc, __argv, _get_initial_narrow_environment());
}
つまり、関数 FUN_140001934()
内の unaff_EBX = FUN_140001070(uVar10,uVar1,puVar7,in_R9);
行で呼び出される 関数 FUN_140001070()
が、このプログラムに置ける main 関数に該当すると判断できます。
実際に Ghidra で関数 FUN_140001070()
にジャンプして Decompiler ウインドウを確認すると、プログラムの起動時にコンソールに表示される Welcome.
などの文字列を含むコードが呼び出されており、この関数 FUN_140001070()
が D4C.exe の main 関数であることを確認できます。
Ghidra では、関数名を右クリックして [Rename Function] を選択することで関数名を任意に変更できます。
解析をスムーズに進めるためにも、FUN_140001070()
を main にリネームしておきます。
また、FUN_1400014e0("Welcome.\n",param_2,param_3,param_4);
と FUN_140001010(L"\n0. Setting: Full memory dump(You need reboot OS.)\n",param_2,param_3,param_4);
についても、それぞれ ASCII 文字列とワイド文字列をコンソールに出力する関数であると推測できます。
そのため、FUN_1400014e0()
を printf に、FUN_140001010()
を wprintf にそれぞれリネームします。
これでかなりデコンパイル結果を読みやすくなりました。
最後に、アプリケーションクラッシュが発生したより詳細な原因を Ghidra を使用して特定します。
すでに確認している通り、このアプリケーションクラッシュは最初のメニューで 1 を選択した後に、不正なアドレスに対して書き込みアクセスが行われようとしたことで発生しました。
そこで、main 関数のデコンパイル結果からアプリケーションクラッシュが発生した箇所のコードを調査してみます。
Decompiler ウインドウの情報を参照すると、アプリケーションクラッシュが発生したのは以下の箇所であることがわかります。
else if (local_38 == 1) {
wprintf(L"User Mode Trouble: Simple process crash.\n",pwVar9,NewState,param_4);
printf("OK, Start application.\n",pwVar9,NewState,param_4);
local_280 = 0;
pwVar9 = (wchar_t *)&local_280;
uRam0000000000000000 = 1000;
printf(&DAT_140005300,pwVar9,NewState,param_4);
}
実際に書き込みアクセスを行っている uRam0000000000000000 = 1000;
を選択してみると、Listing ウインドウの表示から対応するコードのオフセットが 0x1412 であることを確認できます。
このオフセット 0x1412 は、WinDbg でダンプファイルの解析を行った際に特定したオフセットと一致しています。
さらに、Ghidra のデコンパイル結果を参照してみると、書き込み先のアドレスはハードコードされた 0 という値を参照していることがわかりました。
これで、今回調査したアプリケーションクラッシュの発生原因はメモリアクセス違反であり、開発者が 1000 という値を書き込む先を 0 に固定してしまったことで発生したということを特定できました。
4 章のまとめ
以上で、シンプルなアプリケーションクラッシュダンプの解析は終了です。
現実のトラブルシューティングでここまで単純なクラッシュ事象の調査を行うことはまずありませんが、基本的な解析のステップは大きくは変わりません。
アプリケーションクラッシュダンプが正常に出力されている状況であれば、根本的な原因が何であれ、クラッシュを直接的に引き起こした例外の種類と、クラッシュが発生した命令コードのオフセットについてはダンプファイルから調査することが可能です。
本章を通して、一見複雑そうに見えるダンプファイルの解析も、最低限の確認ポイントを押さえるだけで実は簡単に原因特定ができる場合があるということを感じてもらえたら良いと思っています。
各章へのリンク
- まえがき
- 1 章 環境構築
- 2 章 WinDbg の基本操作
- 3 章 解析に必要な前提知識
- 4 章 アプリケーションのクラッシュダンプを解析する
- 5 章 システムクラッシュ時のフルメモリダンプを解析する
- 6 章 プロセスダンプからユーザモードアプリケーションのメモリリーク事象を調査する
- 7 章 フルメモリダンプからユーザモードメモリリーク事象を調査する
- 付録 A WinDbg の Tips
- 付録 B Volatility 3 でクラッシュダンプを解析する
-
インサイド Windows 第 7 版 下 P.564 (Andrea Allievi, Mark E.Russinovich, Alex Ionescu, David A.Solomon 著 / 山内和朗 訳 / 日系 BP 社 / 2022 年)
↩ -
インサイド Windows 第 7 版 上 P.534 (Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich, David A. Solomon 著 / 山内 和朗 訳 / 日系 BP 社 / 2018 年)
↩ -
Register Context https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/changing-contexts#register-context
↩ -
.exr 例外レコードの表示 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-exr—display-exception-record-
↩ -
アクセス違反 C0000005 https://learn.microsoft.com/ja-jp/shows/inside/c0000005
↩ -
インサイド Windows 第 7 版 下 P.91 (Andrea Allievi, Mark E.Russinovich, Alex Ionescu, David A.Solomon 著 / 山内和朗 訳 / 日系 BP 社 / 2022 年)
↩ -
インサイド Windows 第 7 版 下 P.565 (Andrea Allievi, Mark E.Russinovich, Alex Ionescu, David A.Solomon 著 / 山内和朗 訳 / 日系 BP 社 / 2022 年)
↩ -
インサイド Windows 第 6 版 下 P.624 (Mark E. Russinovich・David A. Solomon・Alex Ionescu 著 / 株式会社クイープ 訳 / 日系 BP 社 / 2013 年)
↩ -
!analyze 拡張機能の使用 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/using-the—analyze-extension
↩ -
u、ub、uu Unassemble https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/u—unassemble-
↩ -
ILSpy https://github.com/icsharpcode/ILSpy
↩ -
jadx https://github.com/skylot/jadx
↩ -
PE format https://learn.microsoft.com/ja-jp/windows/win32/debug/pe-format
↩ -
CRT の初期化 https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/crt-initialization?view=msvc-170
↩