4 章ではシンプルなアプリケーションのクラッシュダンプを解析しました。
続く 5 章では、シンプルなシステムクラッシュ発生時に取得したフルメモリダンプの解析を行います。
もくじ
- システムクラッシュを発生させてフルメモリダンプを作成する
- フルメモリダンプを WinDbg でロードする
- !analyze 拡張機能でクラッシュダンプを分析する
- クラッシュを発生させたプロセスを特定する
- クラッシュが発生した前後の命令を読む
- WinDbg で IAT(Import Address Table) を解析する
- 5 章のまとめ
- 各章へのリンク
システムクラッシュを発生させてフルメモリダンプを作成する
4 章で確認した通り、ユーザーモードで稼働するアプリケーションの処理内で例外が発生した際には、アプリケーションのクラッシュが発生し、アプリケーションクラッシュダンプが生成されました。
上記と同じように、カーネルモードで動作するシステムプロセス内で例外が発生した場合は、BSOD(Blue screen of death) と呼ばれるシステムクラッシュが発生し、システムクラッシュダンプが生成されます。
生成できるシステムクラッシュダンプにはいくつかの種類がありますが、今回取得するフルメモリダンプには、その端末で稼働する Windows システムがアクセス可能なすべての物理メモリ内のページ情報が全て含まれます。
フルメモリダンプは Windows の既定の設定では生成されませんが、本書の 1 章の手順でダンプファイルの取得設定を行っている環境の場合には、すでにフルメモリダンプの取得設定が完了しています。
そのため、D4C.exe を使用して解析用のフルメモリダンプの取得を行います。
まずは、1 章でダウンロードした D4C.exe を実行し、プロンプトに表示されるメニューで 3 を入力して Enter キーで実行します。
D4C.exe の 3 番の操作を実行すると、システムクラッシュが発生して自動的に端末が再起動します。
システムの再起動後、C:\Windows
フォルダの直下に FULL_MEMORY.DMP というファイルが生成されていれば、フルメモリダンプの取得は完了です。
フルメモリダンプを WinDbg でロードする
フルメモリダンプの解析を行う場合も、アプリケーションクラッシュダンプの際と同じく、管理者権限で起動した 64 bit 用の WinDbg でショートカットキー [Ctrl + D] を使用して、このフォルダの中から取得したダンプファイルをロードします。
ロードが完了し、For analysis of this file, run !analyze -v
というメッセージが表示されたらロードは完了です。(フルメモリダンプのファイルサイズによってはロードに少々時間がかかる場合があります。)
!analyze 拡張機能でクラッシュダンプを分析する
システムクラッシュに関する調査を行う場合も、アプリケーションクラッシュダンプの調査の際と同じく !analyze -v
コマンドが非常に役に立ちます。
そのため、Command ウインドウで !analyze -v
コマンドを実行した際の出力を順番に確認していきます。
最初のセクションには、以下のように .bugcheck
コマンドで取得できるバグチェックデータ1の解析結果が表示されます。
CRITICAL_PROCESS_DIED (ef)
A critical system process died
Arguments:
Arg1: ffffc18f36303080, Process object or thread object
Arg2: 0000000000000000, If this is 0, a process died. If this is 1, a thread died.
Arg3: 0000000000000000, The process object that initiated the termination.
Arg4: 0000000000000000
上記の出力結果を参照すると、システムクラッシュの発生原因が CRITICAL_PROCESS_DIED
であることがわかります。
実際に .bugcheck
コマンドを WinDbg で実行してみると、以下の出力を得ることができます。
6: kd> .bugcheck
Bugcheck code 000000EF
Arguments ffffc18f`36303080 00000000`00000000 00000000`00000000 00000000`00000000
Windows のバグチェックコードは以下の公式ドキュメントで公開されています。
このリファレンス内で 000000EF
の値を探してみると、!analyze -v
コマンドで表示された CRITICAL_PROCESS_DIED
のバグチェックコードと一致していることがわかります。
また、!analyze -v
コマンドの出力結果は、!analyze -show <バグチェックコード> <Arg1>
コマンドでバグチェックの解析を行った場合と同等です。
バグチェックコード:
これらのコマンドの出力結果から、システムクラッシュの原因が Windows の重要なシステムプロセスが終了したことによる CRITICAL_PROCESS_DIED(0xef)
であることがわかります。
では、Windows の重要なシステムプロセスとは何を指すのでしょうか。
Windows の重要なシステムプロセスとは、csrss.exe、wininit.exe、logonui.exe、smss.exe、services.exe、conhost.exe、winlogon.exe などのシステム組込みのプロセスが該当します。
バグチェック 0xEF: CRITICAL_PROCESS_DIED
さらに、Arg2 の値が 0 であることから、例外の発生原因はスレッドではなくプロセスの終了であることと、停止したプロセスオブジェクトの実態が 0xffffc18f36303080 に存在していることがわかります。
実際にこのアドレスに存在する EPROCESS 構造体が管理する情報を参照してみると、停止したシステムプロセスは PID が 0x3bc の svchost.exe であることを特定できます。
6: kd> !process ffffc18f`36303080 0
PROCESS ffffc18f36303080
SessionId: 0 Cid: 02dc Peb: 27dedd6000 ParentCid: 03bc
DirBase: 7fbd5002 ObjectTable: ffffe50cc9052800 HandleCount: 1465.
Image: svchost.exe
6: kd> dt nt!_EPROCESS 0xffffc18f`36303080
{{ 省略 }}
+0x440 UniqueProcessId : 0x00000000`000002dc Void
+0x5a8 ImageFileName : [15] "svchost.exe"
{{ 省略 }}
以上のように、!analyze -v
コマンドで参照できる最初の出力からバグチェックの解析を行うだけで、システムクラッシュの原因に大きく迫ることができました。
このまま次のセクションを見ていきます。
バグチェック解析の次のセクションにも非常に興味深い情報が出力されており、 CriticalProcessDied.Process
の情報からも、クラッシュ対象のプロセスが svchost.exe であることがわかります。
Debugging Details:
------------------
KEY_VALUES_STRING: 1
Key : Analysis.CPU.mSec
Value: 3765
Key : Analysis.DebugAnalysisManager
Value: Create
Key : Analysis.Elapsed.mSec
Value: 4092
Key : Analysis.Init.CPU.mSec
Value: 78343
Key : Analysis.Init.Elapsed.mSec
Value: 78474127
Key : Analysis.Memory.CommitPeak.Mb
Value: 128
Key : CriticalProcessDied.ExceptionCode
Value: 42bd7080
Key : CriticalProcessDied.Process
Value: svchost.exe
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: FULL_MEMORY.DMP
BUGCHECK_CODE: ef
BUGCHECK_P1: ffffc18f36303080
BUGCHECK_P2: 0
BUGCHECK_P3: 0
BUGCHECK_P4: 0
PROCESS_NAME: svchost.exe
CRITICAL_PROCESS: svchost.exe
ERROR_CODE: (NTSTATUS) 0x42bd7080 - <Unable to get error code text>
BLACKBOXBSD: 1 (!blackboxbsd)
BLACKBOXNTFS: 1 (!blackboxntfs)
BLACKBOXPNP: 1 (!blackboxpnp)
BLACKBOXWINLOGON: 1
この BUGCHECK_CODE
や BUGCHECK_P1
から BUGCHECK_P4
までの情報は、最初のセクションで確認したバグチェックの情報と一致します。
また、CRITICAL_PROCESS
からシステムクラッシュの原因となった停止プロセスが svchost.exe であることもわかります。
そして、次のセクションはスタックバックトレースです。
ここでは .cxr; .ecxr ; kb
コマンドを実行した場合と同等の出力が行われています。
STACK_TEXT:
ffffa209`4982f838 fffff801`2d70e592 : {{ 省略 }} : nt!KeBugCheckEx
ffffa209`4982f840 fffff801`2d616045 : {{ 省略 }} : nt!PspCatchCriticalBreak+0x10e
ffffa209`4982f8e0 fffff801`2d4819b0 : {{ 省略 }} : nt!PspTerminateAllThreads+0x15e655
ffffa209`4982f950 fffff801`2d4817ac : {{ 省略 }} : nt!PspTerminateProcess+0xe0
ffffa209`4982f990 fffff801`2d2105f5 : {{ 省略 }} : nt!NtTerminateProcess+0x9c
ffffa209`4982fa00 00007ff8`7accd3d4 : {{ 省略 }} : nt!KiSystemServiceCopyEnd+0x25
00000087`d59ef568 00000000`00000000 : {{ 省略 }} : ntdll!NtTerminateProcess+0x14
スタックバックトレースの一番上にある KeBugCheckEx 関数は、直接的にシステムクラッシュを発生させる関数です。2
KeBugCheckEx 関数は、ストップコードとストップコードごとに解釈される 4 つのパラメータを受け取ります。
これが、先ほど確認した .bugcheck
コマンドなどで参照できるバグチェックの情報にあたります。
逆に、スタックバックトレースの一番下の NtTerminateProcess 関数は、以下にてドキュメント化されている通り、通常はユーザーモードアプリケーションからプロセスを停止する API を呼び出す際に使用する関数です。
ZwTerminateProcess 関数 (ntddk.h):
https://learn.microsoft.com/ja-jp/windows-hardware/drivers/ddi/ntddk/nf-ntddk-zwterminateprocess
上記のスタックバックトレースの出力をバグチェックの情報と合わせて考えると、何らかのユーザーモードアプリケーションから重要なシステムプロセスである svchost.exe が停止されたために、システムクラッシュが発生した可能性が高いと判断できます。
また、!analyze -v
コマンドの最後のセクションである以下の出力においても、FAILURE_BUCKET_ID
に 0xEF_svchost.exe_BUGCHECK_CRITICAL_PROCESS_42bd7080_ntdll!NtTerminateProcess
と表示されており、今回のシステムクラッシュは NtTerminateProcess 関数によって svchost.exe が停止させられたことによって発生したものであると判断できます。
SYMBOL_NAME: ntdll!NtTerminateProcess+14
MODULE_NAME: ntdll
IMAGE_NAME: ntdll.dll
STACK_COMMAND: .cxr; .ecxr ; kb
BUCKET_ID_FUNC_OFFSET: 14
FAILURE_BUCKET_ID: 0xEF_svchost.exe_BUGCHECK_CRITICAL_PROCESS_42bd7080_ntdll!NtTerminateProcess
OS_VERSION: 10.0.19041.1
BUILDLAB_STR: vb_release
OSPLATFORM_TYPE: x64
OSNAME: Windows 10
FAILURE_ID_HASH: {f6ece2b4-3d35-e4e4-9739-fdbc46a086b0}
Followup: MachineOwner
クラッシュを発生させたプロセスを特定する
!analyze -v
コマンドの出力結果からクラッシュの直接的な原因になった例外を特定することはできました。
しかし、!analyze -v
コマンドの出力結果で表示されたスタックバックトレースには ntdll!NtTerminateProcess+0x14
より以前の情報が含まれていなかったため、具体的に何が例外の原因となったのかについては特定ができませんでした。
これは、今回の例外と紐づくコンテキストが「停止されられた側」の svchost.exe のプロセスコンテキストであるためです。
現在の例外と紐づくプロセスコンテキストは .ecxr; !peb
で確認できます。(!peb
拡張機能3は、現在のプロセスコンテキストに対応する PEB(プロセス環境ブロック) を表示できます。)
.ecxr
で指定可能な例外と紐づくコンテキストからは詳細な調査が困難でしたので、より詳細な調査を進めるためには、クラッシュを引き起こした可能性のあるプロセスにコンテキストを変更する必要があります。
クラッシュを引き起こしたプロセスとは、今回で言えば D4C.exe です。
.ecxr; !thread
コマンドで例外を引き起こしたコンテキストでスレッド情報を表示すると、Owning Process
が D4C.exe であり、プロセスオブジェクトのアドレスが 0xffffc18f45284080 と表示されます。
そこで、.process /r /P 0xffffc18f45284080
コマンドを使用してデバッガのプロセスコンテキストを D4C.exe プロセスに変更します。
プロセスのコンテキストを変更した状態で再度 !peb
コマンドを実行すると、表示される情報が svchost.exe の PEB ではなく D4C.exe の PEB に変化することを確認できます。
この、.process /r /P <プロセスオブジェクト(EPROCESS)のアドレス>
によるプロセスコンテキストの変更は、フルメモリダンプを解析する上で頻出のコマンドなので覚えておくと良いでしょう。
最後に、プロセスコンテキストを D4C.exe に変更した状態で再度 k
コマンドによりスタックバックトレースを出力してみると、ntdll!NtTerminateProcess+0x14
が呼び出される以前のスタックバックトレースも参照することができるようになりました。
6: kd> k
# Child-SP RetAddr Call Site
00 ffffa209`4982f838 fffff801`2d70e592 nt!KeBugCheckEx
01 ffffa209`4982f840 fffff801`2d616045 nt!PspCatchCriticalBreak+0x10e
02 ffffa209`4982f8e0 fffff801`2d4819b0 nt!PspTerminateAllThreads+0x15e655
03 ffffa209`4982f950 fffff801`2d4817ac nt!PspTerminateProcess+0xe0
04 ffffa209`4982f990 fffff801`2d2105f5 nt!NtTerminateProcess+0x9c
05 ffffa209`4982fa00 00007ff8`7accd3d4 nt!KiSystemServiceCopyEnd+0x25
06 00000087`d59ef568 00007ff8`789643d0 ntdll!NtTerminateProcess+0x14
07 00000087`d59ef570 00007ff6`e99012d3 KERNELBASE!TerminateProcess+0x30
08 00000087`d59ef5a0 00007ff6`e9901a40 D4C+0x12d3
09 00000087`d59ef860 00007ff8`7a497344 D4C+0x1a40
0a 00000087`d59ef8a0 00007ff8`7ac826b1 KERNEL32!BaseThreadInitThunk+0x14
0b 00000087`d59ef8d0 00000000`00000000 ntdll!RtlUserThreadStart+0x21
この結果から、D4C.exe のオフセット 0x12d3 の 1 つ前の命令が TerminateProcess API を呼び出し、svchost.exe の停止が行われた可能性が高いと推察できます。
クラッシュが発生した前後の命令を読む
ここまでの解析で、D4C.exe のオフセット 0x12d3 の 1 つ前の命令が TerminateProcess API を呼び指してシステムクラッシュを引き起こした可能性が高いことを確認しました。
次は、オフセット 0x12d3 の前後の命令がどのようなものであったかを調査していきます。
4 章と同じく u
コマンドや Disassembly ウインドウを使用してオフセット 0x12d3 の前後の命令を取得しました。
00007ff6`e99012b0 448b442448 mov r8d,dword ptr [rsp+48h]
00007ff6`e99012b5 8d48f5 lea ecx,[rax-0Bh]
00007ff6`e99012b8 33d2 xor edx,edx
00007ff6`e99012ba ff15781d0000 call qword ptr [D4C+0x3038 (00007ff6`e9903038)]
00007ff6`e99012c0 488bd8 mov rbx,rax
00007ff6`e99012c3 4885c0 test rax,rax
00007ff6`e99012c6 7416 je D4C+0x12de (00007ff6`e99012de)
00007ff6`e99012c8 33d2 xor edx,edx
00007ff6`e99012ca 488bc8 mov rcx,rax
00007ff6`e99012cd ff155d1d0000 call qword ptr [D4C+0x3030 (00007ff6`e9903030)]
00007ff6`e99012d3 488bcb mov rcx,rbx
00007ff6`e99012d6 ff158c1d0000 call qword ptr [D4C+0x3068 (00007ff6`e9903068)]
00007ff6`e99012dc eb11 jmp D4C+0x12ef (00007ff6`e99012ef)
00007ff6`e99012de 488d54246c lea rdx,[rsp+6Ch]
00007ff6`e99012e3 488d0d26410000 lea rcx,[D4C+0x5410 (00007ff6`e9905410)]
00007ff6`e99012ea e821fdffff call D4C+0x1010 (00007ff6`e9901010)
00007ff6`e99012ef 488d542440 lea rdx,[rsp+40h]
00007ff6`e99012f4 488bcf mov rcx,rdi
00007ff6`e99012f7 ff15431d0000 call qword ptr [D4C+0x3040 (00007ff6`e9903040)]
00007ff6`e99012fd 85c0 test eax,eax
私が取得したダンプファイルにおいて、D4C.exe のイメージベースアドレスは 0x00007ff6e9900000 ですので、オフセット(RVA) 0x12d3 の仮想アドレスは 0x00007ff6e99012d3
となります。
余談ですが、アドレスの計算などは ?
コマンドを使用した式の評価でも以下の通り行うことが可能です。
# D4C.exe のイメージベースアドレスを特定する
6: kd> ? !D4C
Evaluate expression: 140698457210880 = 00007ff6`e9900000
# D4C.exe のオフセット 0x12d3 のアドレスを計算する
6: kd> ? !D4C+0x12d3
Evaluate expression: 140698457215699 = 00007ff6`e99012d3
上記のディスアセンブル結果を参照すると、スタックバックトレースに追加されているオフセット 0x12d3 の直前の命令は call qword ptr [D4C+0x3030 (00007ff6e9903030)]
であることがわかります。
また、以降のスタックバックトレースの情報から、恐らくこの命令で TerminateProcess API を呼び出しているであろうことが推察できます。
では、実際にこの関数が TerminateProcess 関数を呼び出しているのかどうかをダンプファイルから確認してみます。
WinDbg で IAT(Import Address Table) を解析する
本書の 3 章で簡単に紹介しましたが、Windows システムにおいて実行されるプログラム(EXE ファイル)は、一般に PE ファイルフォーマットで作成されています。
Windows システムでプログラムを実行する場合、システムは PE ファイルのヘッダ内の情報からいくつかの情報をロードしてプロセスに割り当てたメモリ空間に展開します。
この内の一つが IAT(Import Address Table) と呼ばれる情報です。
D4C+0x3030
で呼び出している関数が TerminateProcess であるかを特定するため、この IAT の情報を参照してみましょう。
まずは、WinDbg を使用してこのダンプファイルからメモリに展開された D4C.exe のヘッダ情報を収集します。
WinDbg では !dh
拡張機能4を使用することで、特定のイメージのヘッダ情報を参照できるようになります。
ただし、!dh
拡張機能を使用してフルメモリダンプから特定の PE ファイルのヘッダ情報を参照する場合には、事前に .process /r /P <プロセスオブジェクト(EPROCESS)のアドレス>
コマンドでその PE ファイルの実行プロセスにコンテキストを変更しておく必要があります。
プロセスのコンテキストを D4C.exe に変更したら、!dh -f !D4C
コマンドを実行して情報を取得します。
6: kd> !dh -f !D4C
{{ 省略 }}
0 [ 0] address [size] of Export Directory
5B40 [ C8] address [size] of Import Directory
9000 [ 1E8] address [size] of Resource Directory
8000 [ 1D4] address [size] of Exception Directory
0 [ 0] address [size] of Security Directory
A000 [ 3C] address [size] of Base Relocation Directory
5610 [ 70] address [size] of Debug Directory
0 [ 0] address [size] of Description Directory
0 [ 0] address [size] of Special Directory
0 [ 0] address [size] of Thread Storage Directory
54D0 [ 140] address [size] of Load Configuration Directory
0 [ 0] address [size] of Bound Import Directory
3000 [ 248] 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
今回参照したいのは IAT ですので、上記の出力結果の Import Address Table Directory
の行に着目します。
この行の情報は、IAT のオフセットが 0x3000 であり、サイズが 0x248 であることを示しています。
次に、指定の範囲内のメモリの内容をシンボルテーブル内の一連のアドレスとして解決させる dps
コマンド5を使用して、IAT のシンボルを解決します。
このコマンドによってプロセスメモリ内に展開された IAT のシンボルが解決され、仮想アドレス 0x00007ff6e9903030、つまり D4C+0x3030
の関数が KERNEL32!TerminateProcessStub
であることを確認できます。
# dps !<module_name>+<IAT のアドレス> !<module_name>+<IAT のアドレス>+<IAT のサイズ>
6: kd> dps !D4C+0x3000 !D4C+0x3000+0x248
00007ff6`e9903000 00007ff8`7ab57880 ADVAPI32!AdjustTokenPrivilegesStub
00007ff6`e9903008 00007ff8`7ab56920 ADVAPI32!OpenProcessTokenStub
00007ff6`e9903010 00007ff8`7ab4f970 ADVAPI32!LookupPrivilegeValueW
00007ff6`e9903018 00000000`00000000
00007ff6`e9903020 00007ff8`7a495f00 KERNEL32!GetLastErrorStub
00007ff6`e9903028 00007ff8`7a4a4ba0 KERNEL32!GetCurrentProcessId
00007ff6`e9903030 00007ff8`7a4a0a70 KERNEL32!TerminateProcessStub
00007ff6`e9903038 00007ff8`7a49b0f0 KERNEL32!OpenProcessStub
00007ff6`e9903040 00007ff8`7a4a2740 KERNEL32!Process32NextW
00007ff6`e9903048 00007ff8`7a4a29a0 KERNEL32!Process32FirstW
{{ 省略 }}
なお、上記のようにわざわざ WinDbg で IAT の解析を実施せずとも、Ghidra などのデコンパイラの自動解析機能を利用することで、簡単に D4C+0x3030
の関数を特定することが可能です。
オフセット 0x3030 のディスアセンブル結果も自動的に解析され、ここで呼び出される関数が TerminateProcess 関数であることを簡単に調査することが可能です。
このように、WinDbg のみで解析を行うよりも、デコンパイラなどの複数の強力なツールを利用し、多角的に解析することでより効率的に解析を進められる場合があります。
これで、ダンプファイルを解析した結果、システムクラッシュの発生原因が「svchost.exe が D4C.exe が呼び出した TerminateProcess API によって停止されたこと」であることを特定することができました。
5 章のまとめ
以上で、シンプルなシステムクラッシュダンプの解析は終了です。
4 章で解析したアプリケーションクラッシュダンプと同じく、BSOD の発生した直接的な原因(例外)自体はダンプファイルから容易に特定できます。
もちろん、その例外を発生させた原因となるプログラムの挙動について調査を行う場合には、本章のように簡単に特定できることは少ないでしょう。
そのような場合には、WinDbg によるダンプファイルの解析のみではなく、プロセスモニターやパケットキャプチャなどを使用した問題再現時の環境のトレース、そしてソースコードやデコンパイル結果を使用した机上でのデバッグ、さらにはライブデバッグなど、様々なアプローチでの調査をお試しいただくと、より効率的な原因調査ができるのではないかと思います。
各章へのリンク
- まえがき
- 1 章 環境構築
- 2 章 WinDbg の基本操作
- 3 章 解析に必要な前提知識
- 4 章 アプリケーションのクラッシュダンプを解析する
- 5 章 システムクラッシュ時のフルメモリダンプを解析する
- 6 章 プロセスダンプからユーザモードアプリケーションのメモリリーク事象を調査する
- 7 章 フルメモリダンプからユーザモードメモリリーク事象を調査する
- 付録 A WinDbg の Tips
- 付録 B Volatility 3 でクラッシュダンプを解析する
-
ブルースクリーンデータ https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/blue-screen-data
↩ -
インサイド Windows 第 6 版 下 P.606 (Mark E. Russinovich・David A. Solomon・Alex Ionescu 著 / 株式会社クイープ 訳 / 日系 BP 社 / 2013 年)
↩ -
!peb 拡張機能 https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-peb
↩ -
!dh https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/-dh
↩ - ↩