All Articles

Magical WinDbg VOL.1【4 章 アプリケーションのクラッシュダンプを解析する】

本章では、シンプルなアプリケーションクラッシュダンプを WinDbg で解析していきます。

もくじ

解析用のアプリケーションクラッシュダンプを作成する

まずは、解析用のアプリケーションクラッシュダンプを作成します。

1 章でダウンロードした D4C.exe を起動して、1 番のメニューを指定 Enter キーを押下すると、D4C.exe がクラッシュしてユーザモードクラッシュダンプが生成されます。

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 でユーザーモードフルダンプをロード

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 UptimeProcess 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_IDNULL_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 と入力してみてください。

Disassembly ウインドウで命令コードを参照する

また、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] を選択します。

Ghidra の確認プロンプト

続けて表示される解析オプションの選択はデフォルト設定のままで問題ないので、[Analyze] をクリックしてプログラムの解析を開始します。

Ghidra の解析オプションの選択

自動解析を開始して数十秒から数分程度経過すると、Ghidra を使用してアプリケーションのデコンパイル結果を参照できるようになります。

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 関数を見つけることで特定できます。

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 関数であることを確認できます。

FUN_140001070() が 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 のデコンパイル結果を確認

このオフセット 0x1412 は、WinDbg でダンプファイルの解析を行った際に特定したオフセットと一致しています。

さらに、Ghidra のデコンパイル結果を参照してみると、書き込み先のアドレスはハードコードされた 0 という値を参照していることがわかりました。

NULL ポインタ参照の発生箇所

これで、今回調査したアプリケーションクラッシュの発生原因はメモリアクセス違反であり、開発者が 1000 という値を書き込む先を 0 に固定してしまったことで発生したということを特定できました。

4 章のまとめ

以上で、シンプルなアプリケーションクラッシュダンプの解析は終了です。

現実のトラブルシューティングでここまで単純なクラッシュ事象の調査を行うことはまずありませんが、基本的な解析のステップは大きくは変わりません。

アプリケーションクラッシュダンプが正常に出力されている状況であれば、根本的な原因が何であれ、クラッシュを直接的に引き起こした例外の種類と、クラッシュが発生した命令コードのオフセットについてはダンプファイルから調査することが可能です。

本章を通して、一見複雑そうに見えるダンプファイルの解析も、最低限の確認ポイントを押さえるだけで実は簡単に原因特定ができる場合があるということを感じてもらえたら良いと思っています。

各章へのリンク


  1. インサイド Windows 第 7 版 下 P.564 (Andrea Allievi, Mark E.Russinovich, Alex Ionescu, David A.Solomon 著 / 山内和朗 訳 / 日系 BP 社 / 2022 年)

  2. インサイド Windows 第 7 版 上 P.534 (Pavel Yosifovich, Alex Ionescu, Mark E. Russinovich, David A. Solomon 著 / 山内 和朗 訳 / 日系 BP 社 / 2018 年)

  3. Register Context https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/changing-contexts#register-context

  4. .exr 例外レコードの表示 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-exr—display-exception-record-

  5. アクセス違反 C0000005 https://learn.microsoft.com/ja-jp/shows/inside/c0000005

  6. インサイド Windows 第 7 版 下 P.91 (Andrea Allievi, Mark E.Russinovich, Alex Ionescu, David A.Solomon 著 / 山内和朗 訳 / 日系 BP 社 / 2022 年)

  7. インサイド Windows 第 7 版 下 P.565 (Andrea Allievi, Mark E.Russinovich, Alex Ionescu, David A.Solomon 著 / 山内和朗 訳 / 日系 BP 社 / 2022 年)

  8. インサイド Windows 第 6 版 下 P.624 (Mark E. Russinovich・David A. Solomon・Alex Ionescu 著 / 株式会社クイープ 訳 / 日系 BP 社 / 2013 年)

  9. !analyze 拡張機能の使用 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/using-the—analyze-extension

  10. u、ub、uu Unassemble https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/u—unassemble-

  11. ILSpy https://github.com/icsharpcode/ILSpy

  12. jadx https://github.com/skylot/jadx

  13. PE format https://learn.microsoft.com/ja-jp/windows/win32/debug/pe-format

  14. CRT の初期化 https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/crt-initialization?view=msvc-170