All Articles

Magical WinDbg VOL.1【5 章 システムクラッシュ時のフルメモリダンプを解析する】

4 章ではシンプルなアプリケーションのクラッシュダンプを解析しました。

続く 5 章では、シンプルなシステムクラッシュ発生時に取得したフルメモリダンプの解析を行います。

もくじ

システムクラッシュを発生させてフルメモリダンプを作成する

4 章で確認した通り、ユーザーモードで稼働するアプリケーションの処理内で例外が発生した際には、アプリケーションのクラッシュが発生し、アプリケーションクラッシュダンプが生成されました。

上記と同じように、カーネルモードで動作するシステムプロセス内で例外が発生した場合は、BSOD(Blue screen of death) と呼ばれるシステムクラッシュが発生し、システムクラッシュダンプが生成されます。

一般的な BSOD 画面

生成できるシステムクラッシュダンプにはいくつかの種類がありますが、今回取得するフルメモリダンプには、その端末で稼働する Windows システムがアクセス可能なすべての物理メモリ内のページ情報が全て含まれます。

フルメモリダンプは Windows の既定の設定では生成されませんが、本書の 1 章の手順でダンプファイルの取得設定を行っている環境の場合には、すでにフルメモリダンプの取得設定が完了しています。

そのため、D4C.exe を使用して解析用のフルメモリダンプの取得を行います。

まずは、1 章でダウンロードした D4C.exe を実行し、プロンプトに表示されるメニューで 3 を入力して Enter キーで実行します。

D4C.exe でシステムクラッシュを発生させる

D4C.exe の 3 番の操作を実行すると、システムクラッシュが発生して自動的に端末が再起動します。

システムの再起動後、C:\Windows フォルダの直下に FULL_MEMORY.DMP というファイルが生成されていれば、フルメモリダンプの取得は完了です。

フルメモリダンプを WinDbg でロードする

フルメモリダンプの解析を行う場合も、アプリケーションクラッシュダンプの際と同じく、管理者権限で起動した 64 bit 用の WinDbg でショートカットキー [Ctrl + D] を使用して、このフォルダの中から取得したダンプファイルをロードします。

ロードが完了し、For analysis of this file, run !analyze -v というメッセージが表示されたらロードは完了です。(フルメモリダンプのファイルサイズによってはロードに少々時間がかかる場合があります。)

フルメモリダンプを WinDbg にロードする

!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> コマンドでバグチェックの解析を行った場合と同等です。


バグチェックコード:

https://learn.microsoft.com/ja-jp/windows-hardware/drivers/debugger/bug-check-code-reference2#bug-check-codes


これらのコマンドの出力結果から、システムクラッシュの原因が 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

https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/bug-check-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_CODEBUGCHECK_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_ID0xEF_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 の関数を特定することが可能です。

Ghidra のディスアセンブル結果

オフセット 0x3030 のディスアセンブル結果も自動的に解析され、ここで呼び出される関数が TerminateProcess 関数であることを簡単に調査することが可能です。

Ghidra のディスアセンブル結果

このように、WinDbg のみで解析を行うよりも、デコンパイラなどの複数の強力なツールを利用し、多角的に解析することでより効率的に解析を進められる場合があります。

これで、ダンプファイルを解析した結果、システムクラッシュの発生原因が「svchost.exe が D4C.exe が呼び出した TerminateProcess API によって停止されたこと」であることを特定することができました。

5 章のまとめ

以上で、シンプルなシステムクラッシュダンプの解析は終了です。

4 章で解析したアプリケーションクラッシュダンプと同じく、BSOD の発生した直接的な原因(例外)自体はダンプファイルから容易に特定できます。

もちろん、その例外を発生させた原因となるプログラムの挙動について調査を行う場合には、本章のように簡単に特定できることは少ないでしょう。

そのような場合には、WinDbg によるダンプファイルの解析のみではなく、プロセスモニターやパケットキャプチャなどを使用した問題再現時の環境のトレース、そしてソースコードやデコンパイル結果を使用した机上でのデバッグ、さらにはライブデバッグなど、様々なアプローチでの調査をお試しいただくと、より効率的な原因調査ができるのではないかと思います。

各章へのリンク