All Articles

WinDbg でダンプ解析、ライブデバッグを行う時のチートシート

個人的によく使う WinDbg コマンドなどをまとめたチートシートです。

内容については今後追加していく予定です。

もくじ

まえがき

本記事の内容はすべて一般に公開されている情報や、出版された書籍、または個人の検証環境で動作確認を実施した結果のみを元に作成しています。

関連記事はこちら。

よく使うリンク集

参考:Debugging Resources - Windows drivers | Microsoft Learn

参考:Commands - Windows drivers | Microsoft Learn

参考:Docs 検索用リンク

参考:WinDBG

参考:WinDbg Release notes - Windows drivers | Microsoft Learn

参考:WinDbgAto_Z

参考:CrashMe Application / Mike Dos Zhang: Search results for WinDbg

参考:Inside Show | Microsoft Learn

モジュール一覧の操作、情報取得(lm, x, ln)

# モジュールの列挙
lm

# a から始まるモジュールの列挙
lm m a*

# モジュール情報の表示、タイムスタンプなども表示できる
# !lmi でも、モジュールの詳細情報を取得できる
lm Dvm <modulename>

# シンボルからモジュールの main 関数のアドレスを解決
x modulename!*main*

# a から始まる関数名の列挙
x /D /f modulename!a*

# アドレスからシンボル名を解決
ln address

参考:lm (List Loaded Modules) - Windows drivers | Microsoft Learn

参考:x (Examine Symbols) - Windows drivers | Microsoft Learn

参考:ln (List Nearest Symbols) - Windows drivers | Microsoft Learn

レジスタの参照と操作(r)

# レジスタの一覧表示
r

# 特定レジスタ、複数レジスタの表示
r eip
r zf
r eax,ebx,ecx,edx,ebp,eip

# レジスタの値の書き換え
r eax=00000001
r zf=0

# 2 番目のスレッドに割り当てられているレジスタの表示
# または、~1s などで現在のスレッドを切り替えてから出力する
~1 r

# すべてのスレッドに関連づけられているレジスタの表示
~* r eax

# xmm0 を符号なし 16 バイトとして表示
# xmm1 は倍精度浮動小数点形式で表示
r xmm0:16ub, xmm1:d

# 疑似レジスタの表示
r $peb,$teb

# eax に ebx の値をコピー
r eax = @ebx

参考:r (Registers) - Windows drivers | Microsoft Learn

メモリの参照と編集(dc,dps,etc..)

# メモリアドレスの範囲またはオフセットを指定してメモリ内の Hex と ASCII を出力
dc 0x1000 0x1200

# L はオブジェクトカウントを指す
# dc @addr L1 は DWORD 1 つ分、dq @addr L1 は QWORD 1 つ分
dc @eip L1
dc 0x1000 L20

# アドレス単体や、アドレス値を持つレジスタを参照することも可能
dc 0x1000
dc eax

# ポインタサイズの値を取得できる
dps 0x1000
dqs 0x1000

# 32bit(ddp)、64bit(dqp)でメモリを表示
ddp 0x1000
dqp 0x1000

# ASCII 文字列で表示
da 0x1000

# バイト値の書き換え
eb 0x1000 

# ASCII 文字列の書き換え
ea 0x1000 "change ascii"

参考:d, da, db, dc, dd, dD, df, dp, dq, du, dw (Display Memory) - Windows drivers | Microsoft Learn

参考:dda, ddp, ddu, dpa, dpp, dpu, dqa, dqp, dqu (Display Referenced Memory) - Windows drivers | Microsoft Learn

参考:dds, dps, dqs (Display Words and Symbols) - Windows drivers | Microsoft Learn

参考:e, ea, eb, ed, eD, ef, ep, eq, eu, ew, eza (Enter Values) - Windows drivers | Microsoft Learn

メモリ内の検索(s)

基本的なメモリ内の探索は以下のコマンドで実現できます。

# RSP のアドレスから 1000000 バイトの範囲で DWORD 値 H を検索する
s -d @rsp L1000000 'H'

# RSP のアドレスから 1000000 バイトの範囲で ASCII 文字列 Hello を検索する
s -a @rsp L10000000 "Hello"  

# 0 から 0x7fffffff までの広い範囲で ASCII 文字列 Hello を検索する
# 256 MB 以上の大きな範囲を検索する場合は、 L? の指定が必ず必要になる
s -a 0 L?7fffffff "Hello"

# ASCII と同様に、Unicord 文字列も検索することができる
s -u @rsp L1000000 'Hello'
s -u 0 L?7fffffff "Hello"

以下は、ASCII 文字列と Unicord 文字列をそれぞれ検索した場合の差分を確認した結果です。

image-20230401234723850

また、特定のバイトパターンを指定してメモリ内の情報を検索することもできます。

# RSP のアドレスから 400 バイトの範囲で「長さ 4 文字以上の ASCII 文字列」を検索
s -[l4]sa @rsp L400

# RSP のアドレスから 400 バイトの範囲内の「書き込み可能アドレス」から「長さ 4 文字以上の ASCII 文字列」を検索
s -[wl4]sa @rsp L400

参考:s (Search Memory) - Windows drivers | Microsoft Learn

アセンブリコードの出力(u,ub,etc…)

以下のコマンドで Disassembly ウインドウで参照できるものと同じアセンブリコードを出力できます。

# EIP から数行のアセンブリコードを出力(ub は逆方向に出力)
u
ub

# 指定のアドレスから 10 行分のアセンブリコードを出力
u 0x1000 L10

# 特定のレジスタ内のアドレスを使用することも可能
u eax L10
u eip L20

# 関数のアドレスまたはシンボル名から特定の関数全体のコードを出力
uf 0x1000
uf module!function

# 対象関するルーチン内の call 命令のみ抽出
uf /c 0x1000

参考:u, ub, uu (Unassemble) - Windows drivers | Microsoft Learn

参考:uf (Unassemble Function) - Windows drivers | Microsoft Learn

変数と構造体の出力(dv,dt)

変数や構造体情報を取得できるコマンドです。

特に、dt でカーネル構造体の情報を取得することは非常に多くあります。

# 現在のスコープ内のローカル変数を出力(/t で型名を表示)
dv
dv /t

# 変数の型名は dt コマンドからも解決することができる
dt local_val_name

# シンボル名またはアドレスから構造体を解決
dt MyStruct

# 構造体のメンバになっているサブ構造体の情報も再帰的に列挙するために -r もしくは -b を使用する
dt -r MyStruct

# -r1 -r2 などのようにして再帰的に表示する構造体の階層を指定できる
dt -r2 MyStruct

# カーネル構造体の型情報を列挙できる
dt nt!_*
dt nt!*PEB*

# 特定の構造体のアドレスを指定することで実際の値を取得できる
dt nt!_EPROCESS <EPROCESSのアドレス>

# サブタイプ名を指定することで、特定の値のみ抽出することもできる
dt nt!_EPROCESS ImageFileName <EPROCESSのアドレス>

# -l オプションでリンクされたリストをたどって構造体情報をダンプする
# -y オプションで「指定の文字列が名前の先頭部分に一致する」行を出力する
# -o オプションでオフセットを非表示にする
# -i オプションでサブタイプのインデントを行わない
dt nt!_EPROCESS -l ActiveProcessLinks.Flink  -y Ima -yoi Uni <EPROCESSのアドレス>

# dt ではサブ構造体の情報を取得できないので -r か -b スイッチを使う
dt ntdll!_EPROCESS -r <EPROCESSのアドレス>
dt ntdll!_EPROCESS -b <EPROCESSのアドレス>

参考:dv (Display Local Variables) - Windows drivers | Microsoft Learn

参考:dt (Display Type) - Windows drivers | Microsoft Learn

参考:インサイド Windows 第 7 版 上 P.44

解析対象の構造体に双方向リストが含まれる場合、dt コマンドで再帰的にリストで連結された情報を取得することができます。(カーネルデバッグ時の_EPROCESS構造体の列挙など)

また、dv コマンドは 64bit 呼び出し規約やFast calling convention(引数を可能な限りレジスタ経由で渡す呼び出し規約)では有効に動作しません。

例えば、x86 Windows で標準的に使用される呼び出し規約__cdeclは引数がすべてスタック経由で与えられるため、dv コマンドで安定してローカル引数を参照することが可能です。

参考:抄訳メモ/Calling Conventions Demystified - Glamenv-Septzen.net

PE バイナリの情報を参照する

# テーブル情報を参照する
!dh -f !<module_name>

# テーブル情報から特定したアドレスとサイズからインポートテーブルの情報を列挙する
dps !<module_name>+<IAT のアドレス> !<module_name>+<IAT のアドレス>+<IAT のサイズ>

# DOS ヘッダの列挙
dt /r _IMAGE_DOS_HEADER @@masm(!<module_name>)

# NT ヘッダ(FileHeader / OptionalHeader)の列挙
# e_lfanew は、DOS ヘッダの 0x3C にある PE ヘッダのポインタから取得
dt /r _IMAGE_NT_HEADERS64 @@masm(!<module_name>+<e_lfanew の値>)

# NT ヘッダ の FileHeader のサイズに SizeOfOptionalHeader の値を足したアドレスから最初の SectionHeader を取得する 
dt /r _IMAGE_SECTION_HEADER @@masm(!<module_name>+0xF8+0x018+<SizeOfOptionalHeader>)

# _IMAGE_SECTION_HEADER のサイズである 0x28 を足して行くと次のセクションの情報を参照できる(事前に構造体のシンボルをロードする必要あり)
dt /r _IMAGE_SECTION_HEADER @@masm(!<module_name>+0xF8+0x018+<<SizeOfOptionalHeader>+0x28)

ブレークポイントの設定(bp,bu,bm)

ブレークポイントは、bp、bu、bm の 3 つのコマンドを使用して設定することができます。

bp は通常のブレークポイント設定で、コマンドで指定したシンボルやアドレスにブレークポイントを設定します。

一方で、未解決のアドレスや bp で解決できなかったアドレスについては bu を使用してブレークポイントを設定します。

さらに、bm コマンドを使用することでパターンを使用してシンボルに対してブレークポイントを設定できます。

# オフセットを指定してブレークポイントを設定
bp module+0x123
bu dllmodule!DLLMain

# mem から始まる複数のモジュールにまとめてブレークポイントを設定
bm myprogram!mem*

また、ブレークポイントを設定する場合にはブレークポイントにマッチした場合の処理や、停止されるまでに該当のコードが呼び出される回数を指定することも可能です。

加えて、IF 文などの条件分岐を定義してブレーク時の動作を細かく条件付けできます。

条件式に使用できる演算子しては、MASM Numbers and Operators などが利用できます。

条件分岐は .if や j コマンドで定義できますが、現在はこの記法は推奨されておらず、Conditional breakpoints in WinDbg の通りbp /w "(Condition)" Addressの記法を使用することができます。

# ブレークポイントを設定した処理が 7 回目に呼び出された場合に実行を停止する
bp MyTest+0xb 7

# ブレークポイントにマッチした後、 EAX と変数 MyVar の出力を行い、自動的に実行を再開する
bp ntdll!RtlRaiseException "r eax; dt MyVar; gc"

# echo で任意の文字列を出力して処理を続行
bp module!myFunction ".echo myFunction executed; gc"

# IF 文条件式の基本的な構文
bp module!myFunction ".if () {} .else {}"

# var のアドレスを poi で逆解決し、10 進数の 10 と比較
# 一致すれば何もせずブレークし、一致しない場合は処理を継続する
bp module!myFunction ".if ( poi(var) == 0n10 ) {} .else { gc }"

# 以下も上記と同等の結果を得られる
bp module!myFunction "j ( poi(var) == 0n10 ) ''; 'gc'"

ちなみに、gc コマンドは条件付きブレークポイントからの移動を指示するコマンドで、これをブレーク時の条件に含める場合、このブレークポイントに到達した際の実行コマンド(g や F10 など)と同じ方法で処理を進めるように指定することができます。

参考:bp, bu, bm (Set Breakpoint) - Windows drivers | Microsoft Learn

参考:gc (Go from Conditional Breakpoint) - Windows drivers | Microsoft Learn

参考:j (Execute If - Else) - Windows drivers | Microsoft Learn

参考:Conditional breakpoints in WinDbg and other Windows debuggers - Windows drivers | Microsoft Learn

また、ブレークポイントの条件はスクリプトファイルを使用して制御することも可能です。

詳しくはスクリプトファイルの使用に記載します。

読み取り、書き込みアクセスを監視する(ba)

ba コマンドを使用することで、実行コードだけでなく、特定のメモリ領域にアクセスが発生した場合のブレークポイントも設定することができます。

この時指定可能な値は大きく「実行(e)」、「読み取り/書き込み(r)」、「書き込み(w)」の 3 つです。

また、カーネルデバッグ時限定で、「I/O(i)」オプションで特定の I/O ポートに対するアクセスが行われた場合のブレークポイントを構成することもできます。

# 変数名または変数のアドレスから「4 バイト分の領域に読み取りアクセスが発生した場合」にブレークする
ba r4 myVar
ba r4 0x1000

# 対象に書き込みアクセスが発生した場合にブレークする
ba w4 0x1000

# 3f8 から 3f8+4 の範囲のポートで I/O が発生した場合にブレークする(カーネルデバッグ時のみ)
ba i4 3f8

参考:ba (Break on Access) - Windows drivers | Microsoft Learn

ブレークポイント設定の管理(bl,bd,etc…)

現在のブレークポイント設定の確認や有効化/無効化は以下のコマンドで行うことができます。

# 現在設定されているブレークポイントの列挙
# ここで表示されるブレークポイントの ID は他の管理コマンドで対象を指定する際に使用できる
bl

# すべてのブレークポイントを無効化する
bd *

# ID 1 のブレークポイントのみを無効化する
bd 1

# すべてのブレークポイントを有効化する
be *

# ID 1 のブレークポイントのみを有効化する
be 1

# 現在のブレークポイント設定に使用したコマンドを上から順にリストする
.bpcmds

参考:bl (Breakpoint List) - Windows drivers | Microsoft Learn

参考:be (Breakpoint Enable) - Windows drivers | Microsoft Learn

参考:bd (Breakpoint Disable) - Windows drivers | Microsoft Learn

また、現在のブレークポイント設定や条件式を更新したい場合は以下のコマンドを使用します。

# ブレークポイントの ID を変更します
# 複数の ID 変更を一括で行うこともできます
br OldID NewID
br OldID NewID OldID2 NewID2 OldID3 NewID3 

# ブレークポイントのコマンド部分を修正できます
bs ID ["CommandString"] 

# ブレークポイントの条件文を変更できます
bsc ID Condition ["CommandString"] 

参考:br (Breakpoint Renumber) - Windows drivers | Microsoft Learn

参考:bs (Update Breakpoint Command) - Windows drivers | Microsoft Learn

参考:bsc (Update Conditional Breakpoint) - Windows drivers | Microsoft Learn

コード実行コマンド

WinDbg でコード実行を行う場合、大きく分けて以下の 3 つの操作を行うことができます。

  • Step:単一の命令またはコードを実行する(Step Over)
  • Trace:単一の命令またはソース行を実行する(Step Into)
  • Go:プロセスまたはスレッドの実行を再開する

Step Over(p)

p コマンドや F10 キーで単一の命令またはコードを実行します。

WinDbg では p コマンドや F10 キーで実行する操作は Step Over に該当し、 call 命令では呼び出し先の関数には移動せず、call 命令の次の行の処理に進みます。

ちなみに、デフォルトでは Step Over 時にレジスタとフラグが出力されますが、pr のように各コマンドに r を付与するか、.prompt_allow -regコマンドを実行することでレジスタとフラグの出力を無効化することができます。(元に戻す場合は.prompt_allow +regコマンドを実行します)

# ステップオーバーの処理を行う
p (または F10)

# 5 行分ステップオーバーを実行する
p 5

# ステップオーバーの実行後に WinDbg コマンドを実行する
p "k;r eax"

# 指定のアドレスに到達するまで Step Over を繰り返す
pa StopAddress

# 次の call 命令の呼び出し行まで Step Over する
pc

# pc の場合もカウント実行が可能
pc 3

# 次の ret 命令の呼び出し行まで Step Over する
pt

# 次の call 命令または ret 命令の呼び出し行まで Step Over する
pct

# 次の条件分岐の呼び出しまで Step Over する
ph

参考:p (Step) - Windows drivers | Microsoft Learn

参考:pa (Step to Address) - Windows drivers | Microsoft Learn

参考:pc (Step to Next Call) - Windows drivers | Microsoft Learn

参考:pt (Step to Next Return) - Windows drivers | Microsoft Learn

参考:pct (Step to Next Call or Return) - Windows drivers | Microsoft Learn

参考:ph (Step to Next Branching Instruction) - Windows drivers | Microsoft Learn

Step Into(t)

TODO:追記予定

参考:t (Trace) - Windows drivers | Microsoft Learn

go(g)

TODO:追記予定

参考:g (Go) - Windows drivers | Microsoft Learn

コマンド出力をファイルに保存する

.logopen コマンドを使用すると、WinDbg のコマンド実行とその出力結果を任意のファイルに保存することができます。

デフォルトでは出力されるテキストは ASCII 文字列ですが、 /u オプションを使用することで Unicord 文字列を指定することができます。

特に、process コマンドなどのような大量の情報を一気に出力する場合などに便利です。

# 任意のログファイルにコマンド出力を記録する(ASCII テキスト)
.logopen C:\Users\Public\windbg.log

# プロセス ID と現在の日時を付与(/t)して任意のログファイルにコマンド出力を記録する
.logopen /t C:\Users\Public\windbg.log

# プロセスやファイル情報などから自動的にファイル名を決定して保存
.logopen /d

# 既存のログファイルに追記する
.logappend C:\Users\Public\windbg.log

# ファイルを閉じて記録を終了する
.logclose

参考:Keeping a Log File in WinDbg - Windows drivers | Microsoft Learn

参考:.logopen (Open Log File) - Windows drivers | Microsoft Learn

参考:.logappend (Append Log File) - Windows drivers | Microsoft Learn

参考:.logclose (Close Log File) - Windows drivers | Microsoft Learn

コマンドラインオプションを使用して出力をファイルに保存する

-logo FilePathオプションを使用して WinDbg を起動することで、コマンドウインドウの出力をすべて指定のファイルに記録することができます。

これは、アプリケーションの Debugger フラグに WinDbg のパスを指定する場合にも使用できます。

また、ショートカットの編集から、WinDbg の起動時に-logoオプションを指定することもできます。

例えば、以下のコマンドラインをショートカットに設定すると、C:\Users\Public\debug.logに常に出力結果が保存されるようになります。

C:\Program Files (x86)\Windows Kits\10\Debuggers\x64\windbg.exe -logo  C:\Users\Public\debug.log

なお、上記の例で-logoオプションを使用する場合、ファイルは毎回 WinDbg を起動するたびに上書きされるので注意が必要です。

拡張機能

ロードされている拡張機能は.chainで確認できます。

デフォルトの拡張機能は以下の種類があり、それぞれ DLL として読み込まれます。

  • ext
  • wow64exts
  • dbghelp
  • exts
  • uext
  • ntsdexts

!help:拡張機能のヘルプ

!help拡張機能は既定では ext 拡張機能のコマンド一覧を出力します。

ただし、help 拡張機能は他にもいくつかの拡張機能に存在しており、!ext.help!wow64exts.helpなどの記法でそれぞれの拡張機能のコマンド一覧にアクセスできます。

# ext 拡張機能のヘルプを出力
!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:例外やバグチェックに関する情報の取得

!analyzeは ext 拡張に含まれるコマンドで、クラッシュダンプなどの解析で非常に有用です。

# 主要な解析の実行
!analyze -v

# グローバルフラグ
NTGLOBALFLAG

# プロセスやアプリケーション関連の情報が記録される
PROCESS_BAM_CURRENT_THROTTLED
PROCESS_BAM_PREVIOUS_THROTTLED
APPLICATION_VERIFIER_FLAGS

# EXCEPTION_RECORD には例外発生、クラッシュしたプロセスの情報が記録される
EXCEPTION_RECORD:  (.exr -1)
ExceptionAddress: 00007ffce5c60910 (ntdll!LdrpDoDebuggerBreak+0x0000000000000030)
	ExceptionCode: 80000003 (Break instruction exception)
	ExceptionFlags: 00000000

# 例外発生時のスタックコールバックがきろくされる
# ~0s ; .cxr ; kb のコマンドでも同等の内容を出力できる
STACK_TEXT

# 問題のカテゴリを表示する
FAILURE_BUCKET_ID

参考:WinDBG !analyze extension command

参考:analyze (WinDbg) - Windows drivers | Microsoft Learn

!address:メモリやプロセスに関する情報の参照

!addressも ext 拡張に含まれるコマンドで、指定したアドレスのメモリ領域やプロセスに関する情報を表示します。

例えば、malloc関数の戻り値のアドレスに対して!addressコマンドを発行するなどによって、対象のメモリ領域に関する情報を取得することができます。

# 各メモリブロックの情報を出力する 
!address

# 指定のアドレスに関する情報を出力する
!address <Address>

# ユーザモードデバッグでメモリに関する統計情報を出力する
!address -summary 

以下の Docs のパラメータ表の通り、対象のメモリ領域の用途やステータス、保護の状態など様々な情報を取得することができます。

参考:address (WinDbg) - Windows drivers | Microsoft Learn

!handle:システムまたはプロセスのハンドル情報の参照

!handleも ext 拡張に含まれるコマンドで、システムやプロセスのハンドルに関する情報を取得できます。

ユーザモードデバッグで使用した場合、そのプロセスのハンドル情報の一覧や統計情報を取得できる。

オプションなしの!handleコマンドで取得できる情報は、Process Explorer のハンドルビューと非常に近しい情報を取得することができます。

# ハンドル情報の取得
!handle

# すべてのハンドルの詳細な情報をダンプ
!handle 0 0xf

# ハンドル ID が 430 のハンドルの詳細な情報を取得
!handle 430 0xf

参考:handle (WinDbg) - Windows drivers | Microsoft Learn

スレッド環境ブロック(TEB)の情報取得

# TEB の情報をダンプ
!teb

参考:teb (WinDbg) - Windows drivers | Microsoft Learn

プロセス環境ブロック(PEB)の情報取得

# PEB の情報をダンプ
!peb

!dumpext 拡張機能による PE ファイルの抽出

オープンソースの !dumpext 拡張機能を使用することで、PE ヘッダの情報を簡単にダンプしたり、ダンプファイル内から PE バイナリを抽出することができます。

事前に、以下の Github リポジトリの手順で拡張機能をビルドしておきます。

参考:pstolarz/dumpext: WinDbg debugger extension library providing various tools to analyse, dump and fix (restore) Microsoft Portable Executable files for both 32 (PE) and 64-bit (PE+) platforms.

あとは以下でダンプファイルから PE バイナリをエクスポートできます。

# ビルド済みバイナリを https://kashiwaba-yuki.com/file/dumpext.dll からダウンロードしておく。
.load C:\Users\User\Downloads\dumpext.dll

# プロセスダンプから PE バイナリを "C:\Program Files (x86)\Windows Kits\10\Debuggers\x64" に dump.out としてエクスポートする
!dumpext.dump_pe !<module_name>

参考:peb (WinDbg) - Windows drivers | Microsoft Learn

Config コマンド

# シンボル設定の確認
.sympath

# シンボルキャッシュとソースの設定
.sympath cache*C:\symbols;srv*https://msdl.microsoft.com/download/symbols
.reload

# シンボル読み込み時の詳細を出力/抑制
!sym noisy
!sym quiet

# 不一致シンボルの強制読み込み(非推奨)
.reload /f /i <modulename>

# ロードされている拡張機能のリストを出力
.chain

# 追加の拡張機能のロード
.load <拡張機能 DLL のパス>

# 拡張機能の詳細を表示
.extmatch /D /e <モジュール> *

# ヘルプウィンドウの起動
.hh

# ダンプファイルとして保存
# https://learn.microsoft.com/en-us/windows-hardware/drivers/debugger/-dump--create-dump-file-
.dump [options] FileName

その他のコマンド

# ?<Hex> で 10 進数変換が可能
?ff
> Evaluate expression: 255 = 000000ff

# 以下のように演算結果を評価することも可能
?ffff-f
> Evaluate expression: 65520 = 0000fff0

# ユーザモードデバッグで現在のプロセスのステータスを表示する
|

# デバッグ中のシステムを表示する
||

参考:Commands - Windows drivers | Microsoft Learn

スクリプトファイルの使用

WinDgb コマンドを記載したテキストファイルを使用することで、WinDgb のコマンド実行をある程度自動化することができます。

# ファイルで定義したスクリプトを実行
$<C:\windbgcmd.txt

# 単一のコマンドブロックとして実行(ブレークポイントを使うならこちらの方がよい)
$><C:\windbgcmd.txt

# コマンド引数を設定(標準入力ではない)
# .echo argument is ${$arg1} などのコマンドを使う場合に使用可能
$$>a<C:\windbgcmd.txt test_text

# eax レジスタに 1234 が含まれるまで実行を続けるスクリプト eaxstep を再帰的に呼び出す
# .if (@eax == 1234) { .echo 1234 } .else { t "$<eaxstep" }
t "$<C:\\eaxstep"

image-20230407210644712

参考:,, , ,, , $$ a (Run Script File) - Windows drivers | Microsoft Learn

参考:Executing Until a Specified State is Reached - Windows drivers | Microsoft Learn

条件分岐

WinDbg コマンドもしくはスクリプトの内部では以下のような条件式を扱うことができます。

# IF トークンの使用
.if (Condition) { Commands } 
.if (Condition) { Commands } .else { Commands } 
.if (Condition) { Commands } .elsif (Condition) { Commands } 
.if (Condition) { Commands } .elsif (Condition) { Commands } .else { Commands }

参考:.if (WinDbg) - Windows drivers | Microsoft Learn

参考:Control Flow Tokens - Windows drivers | Microsoft Learn

この時、Condition には様々な上限を含めることができます。

WinDbg の場合、MASM 演算子と C++ の演算子がサポートされています。

特に MASM 演算子のpoiによるアドレス解決や Non-Numeric な演算子である$scmp("String1", "String2")による文字列比較などは頻繁に使用します。

# $scmp は C++ の strcmp と同等の評価を行うため、文字列が一致した場合は 0 を返す
.if ($scmp("${$ImageName}","notepad.exe")) {  } .else { .echo match }

# 疑似レジスタ $t1 のアドレスが指す値を $t1 に再代入する
r $t1 = poi(@$t1)

参考:MASM Numbers and Operators - Windows drivers | Microsoft Learn

参考:C++ numbers and operators - Windows drivers | Microsoft Learn

その他、条件式に使える演算子は以下から探すことができます。

参考:Numerical Expression Syntax - Windows drivers | Microsoft Learn

ループコマンド

WinDbg の.forコマンドは C 言語の for とほぼ同等の動作になります。

また、.foreachコマンドは WinDbg のコマンド出力やファイル内の情報を要素ごとに分割して 1 つずつ指定のコマンドに与えることができます。

.foreachコマンドで与えられる要素は、処理中のplaceを置き換えます。

また、.while.doもループコマンドとして使用できます。

# WinDbg の for 構文(カンマではなくセミコロンで分割)
.for (InitialCommand ; Condition ; IncrementCommands) { Commands } 

# foreach 構文
.foreach [Options] ( Variable  { InCommands } ) { OutCommands } 
.foreach [Options] /s ( Variable  "InString" ) { OutCommands } 
.foreach [Options] /f ( Variable  "InFile" ) { OutCommands } 

# 処理をループさせて実行させたい場合
.while (1) { Commands }

参考:.for (WinDbg) - Windows drivers | Microsoft Learn

参考:.foreach (WinDbg) - Windows drivers | Microsoft Learn

参考:.while (WinDbg) - Windows drivers | Microsoft Learn

参考:.do (WinDbg) - Windows drivers | Microsoft Learn

.forコマンドは以下のような構文で使用できます。

.continue.breakによるループ操作は、C 言語と同じように動作します。

# 疑似レジスタを使用して for ループを実行
.for (r $t1 = 0; $t1 < 7; r $t1 = $t1 + 1) { r $t1 }

# continue で次のループにジャンプする
.for (r $t1 = 0; $t1 < 7; r $t1 = $t1 + 1) { .if ( $t1 == 3 ) { .echo skip ; .continue } ; r $t1 }

# break でループを停止する
.for (r $t1 = 0; $t1 < 7; r $t1 = $t1 + 1) { .if ( $t1 == 3 ) { .echo stop ; .break } ; r $t1 }

以下は、関数内の call 時の引数をすべてファイルにダンプすることができるサンプルです。

.logopen /t C:\Users\Public\windbg.log
.while (1) { pc;.echo rcd;dc @rcx L10;.echo rdx; dc @rdx L10;.echo r8; dc @r8 L10;.echo r9; dc @r9 L10;.echo stack; dc @rsp L10;.echo rip; u rip L1 }

ブロックを作成する

.blockコマンドは、コード内に新しいブロックを追加します。

これは、後述する Alias を変数のように使用する場合などに頻繁に使用します。

Commands ; .block { Commands } ; Commands 

参考:.block (WinDbg) - Windows drivers | Microsoft Learn

エイリアスを作成、削除する

asaSコマンドは、Alias の定義を行います。

WinDbg では、疑似レジスタと同様に、Alias を変数のように使用できます。

ただし、定義した Alias は現在のブロックでは機能しないため、Alias を定義した後に.blockコマンドで新しいブロックを作成する必要があります。

# 文字列や数値を定義する
as Name 10
aS Name "Test Value" ; .block { .echo Name }

# Address からヌル終端までの ASCII 文字列を定義
as /ma Name Address 

# Address からヌル終端までの Unicode 文字列を定義
as /mu Name Address 

# Address から 64bit の値を定義
as /x Name Expression 

# ファイル の内容と同等の値を定義
aS /f Name File 

# コマンド出力の値を定義
as /c Name CommandString 

参考:as, aS (Set Alias) - Windows drivers | Microsoft Learn

疑似レジスタを使用する

WinDbg では値を保持するための疑似レジスタをサポートしており、既定の疑似レジスタを通して値を参照したり、任意の疑似レジスタを登録することができます。

疑似レジスタは、$マークを先頭に持ちます。MASM 構文を使用する場合は@を付与することができます。

# 疑似レジスタの値を出力
? $ip
? @$ip
r $ip
r @$ip

# 任意の疑似レジスタを定義する
r $t0 = 7

# 疑似レジスタを使用して、任意のアドレスのポインタを poi で参照する
r $t0 = nt!PsActiveProcessHead;r $t1 = poi(@$t0);r $t1

# 疑似レジスタで演算を行う
?? @$t0+10

# r コマンドで型を指定して疑似レジスタを定義する
r? $t15 = * (UNICODE_STRING*) 0x12ffbc

# 現在のスレッドが ntopenfile を呼び出した場合のみ処理を停止する
bp /t @$thread nt!ntopenfile

参考:Pseudo-Register Syntax - Windows drivers | Microsoft Learn

既定で設定されている疑似レジスタは 疑似レジスタ にまとめられていますが、いくつかの疑似レジスタを以下に記載します。

  • $ea:最後の命令の実効アドレス
  • $ip:命令ポインタレジスタ
  • $scopeip:現在のローカル コンテキストの命令ポインター
  • $exentry:現在のプロセスの実行可能ファイルのエントリ ポイントのアドレス
  • $thread:現在のスレッドのアドレス(カーネルデバッグの場合は ETHREAD ブロック、ユーザモードの場合は TEB のアドレス)
  • $peb:現在のプロセスの PEB のアドレス
  • $teb:現在のプロセスの TEB のアドレス
  • $tpid: 現在のプロセスの PID を取得する

スクリプトサンプル

  • カーネルデバッグ中にプロセスを列挙し、notepad.exeのアドレスを取得するスクリプト。
$$  Get process list LIST_ENTRY in $t0.
r $t0 = nt!PsActiveProcessHead

$$  Iterate over all processes in list.
.for (r $t1 = poi(@$t0);
      (@$t1 != 0) & (@$t1 != @$t0);
      r $t1 = poi(@$t1))
{
    r? $t2 = #CONTAINING_RECORD(@$t1, nt!_EPROCESS, ActiveProcessLinks);
    as /x Procc @$t2

    $$  Get image name into $ImageName.
    as /ma $ImageName @@c++(&@$t2->ImageFileName[0])

    .block
    {
	.if ($scmp("notepad.exe","${$ImageName}")) { } .else {.echo ${$ImageName} at ${Procc}}
    }

    ad $ImageName
    ad Procc
}

ユーザモードプロセスの解析

ユーザモードスレッド

# ユーザモードデバッグもしくはプロセスダンプの解析にて、すべてのスレッドのコールスタックをダンプ
~*k

# TEB の情報をダンプ
!teb

# !teb で特定した TEB のアドレスを使用して構造体を出力
dt ntdll!_TEB <TEB のアドレス>

ユーザモードプロセス

# PEB の情報をダンプ
!peb

# !peb で特定した PEB のアドレスを使用して構造体を出力
dt ntdll!_PEB <PEB のアドレス>

スタックバックトレースとフレームの表示

# スタックバックトレースの表示(先頭の数字はフレーム番号)
# フレーム番号(n)、フレームポインタ(v)、フレームが使用しているメモリサイズ(f)を含めて表示させる
kvnf

# すべてのスレッドのスタックバックトレースの表示
~*kvnf

# 特定のスレッドのスタックバックトレースのみ表示
~1kvnf

# 現在のプロセスのすべてのスレッドのコールスタックを表示
!uniqstack

参考:k, kb, kc, kd, kp, kP, kv (Display Stack Backtrace) - Windows drivers | Microsoft Learn

kvnコマンドなどを実行してスタックトレースのChildEBP(保存された EBP)やRetAddrArgs to Childを取得できますが、これは基本的にスタック内の情報を参照しています。

そのため、64bit バイナリの呼び出し規約では有効な値を含まないため注意が必要です。

参考:x64 calling convention | Microsoft Learn

# フレーム番号を指定してローカル変数を表示
# /t は変数の型、/V はローカル変数の相対アドレスを含めて表示するオプション
.frame 01;dv
.frame 01;dv /t /V

参考:dv (Display Local Variables) - Windows drivers | Microsoft Learn

参考:.frame (Set Local Context) - Windows drivers | Microsoft Learn

カーネルモードダンプの解析

フルメモリダンプの取得設定

以下の記事にまとめています。

参考:Windows環境でカーネルメモリダンプを手動で取得し、WinDbgで解析する方法

カーネル構造体の情報を列挙する

# NT カーネル構造体の情報を列挙する
dt nt!_*
dt nt!_*interrupt*

# カーネル構造体の情報をダンプする
dt nt!_KINTERRUPT

プロセス情報を列挙する

カーネルデバッグやフルダンプの解析を行う場合、以下のコマンドでプロセスの情報をダンプできます。

.processは、デバッガのコンテキストを変更できるコマンドで、非常に有用です。

カーネルデバッグは通常、ほとんどのユーザモードアドレス空間の情報は表示されませんが、.processで特定のユーザモードプロセスの紺的とに変更することで、カーネルデバッグ中に特定のユーザモードプロセスを解析することができるようになります。

※ コンテキストを変更した後、元のようにカーネル情報を表示する状態に戻す方法はわからないので、都度カーネルデバッガを再接続させています。

# システム内のすべてのプロセスを列挙
!process 0 0

# 特定のプロセスの完全な詳細情報を取得
!process 0 7 notepad.exe

# 指定のプロセスの EPROCESS 構造体の情報を取得する
dt nt_EPROCESS <!process で特定した EPROCESS ブロックのアドレス>

# デバッガのコンテキストを指定のプロセスに変更する(!peb などが使えるようになる)
.process /r /P <!process で特定した EPROCESS ブロックのアドレス>

# すべてのプロセスを探索する(すべてのプロセスに対してコマンドを発行することも可能)
!for_each_process

参考:!process (WinDbg) - Windows drivers | Microsoft Learn

参考:.process (Set Process Context) - Windows drivers | Microsoft Learn

参考:foreachprocess - Windows drivers | Microsoft Learn

参考:Changing Contexts - Windows drivers | Microsoft Learn

nt!_EPROCESSからプロセス情報を直接列挙することもできます。

# System プロセスの ActiveProcessLinks へのポインタアドレスを取得
r $t0 = nt!PsActiveProcessHead;r $t1 = poi(@$t0);r $t1

# ActiveProcessLinks.Flink のオフセットを参照
dt nt!_EPROCESS ActiveProcessLinks
> +0x448 ActiveProcessLinks 

# _EPROCESS をダンプ(.process 0 0 と同等の出力)
dt nt!_EPROCESS -l ActiveProcessLinks.Flink -y Ima -yoi Uni $t1-0x448

参考:デバッガー コマンド プログラムの例 - Windows drivers | Microsoft Learn

スレッド情報を列挙する

カーネルデバッグの場合でも、!teb コマンドによる TEB の参照はユーザモードと同様に使用できます。

その際には、以下のように対象の TEB のアドレスを指定します。

# TEB の情報を参照する
!teb <TEB のアドレス>

プロセスのハンドル情報を列挙する

!handleコマンドは、ユーザモードデバッグとカーネルデバッグで少々利用方法に差異が発生します。

!handle notepad.exe

参考:handle (WinDbg) - Windows drivers | Microsoft Learn

物理、仮想メモリの情報を出力する

!memusage コマンドで物理メモリ、!vm コマンドで仮想メモリの使用統計情報を取得できます。

特に !memusage の出力はかなり大きいので事前に出力ログを設定しておく方が良いと思います。

# 物理メモリの使用統計情報を出力
!memusage

# 仮想メモリの使用統計情報を出力
!vm

参考:memusage (WinDbg) - Windows drivers | Microsoft Learn

参考:vm (WinDbg) - Windows drivers | Microsoft Learn

また、!addressコマンドはカーネルデバッグでも使用できます。

ただし、カーネルデバッグ時は出力される情報が少し少なくなります。

# 特定のプロセスの使用するメモリ領域を特定する
!address <!process で特定した EPROCESS ブロックのアドレス>

参考:address (WinDbg) - Windows drivers | Microsoft Learn

セッション ID とプロセスの調査

セッション ID を調べることで、プロセスがどのセッションのメンバーなのか調べることができます。

また、!sprocess で取得した MMSESSION_SPACE のアドレスを使用して構造体情報をダンプすることもできます。

# アクティブなセッションの一覧を取得
!session

# ID 1 のセッションを現在のアクティブなセッションに指定
!session -s 1

# 現在のセッション内のプロセスを列挙
!sprocess

# セッションの詳細情報を参照する
dt nt!_MM_SESSION_SPACE ffffd080ba83d000

# セッション領域のメモリ使用状況をそれぞれ出力することもできる
!vm 4

参考:session (WinDbg) - Windows drivers | Microsoft Learn

参考:インサイド Windows 第 7 版 上 P.389

参考:struct MMSESSIONSPACE

KPCR と KPRCB を参照して CPU の統計情報を読む

カーネルプロセッサ制御領域(KPCR)とカーネルプロセッサ制御ブロック(KPRCB)には、プロセッサ固有のデータや状態、統計に関する情報が記録されています。

# 指定のプロセッサの KPRCB の情報を出力
!prcb <CPU 番号>

# KPCR の情報をダンプ
dt nt!_KPCR @$pcr

ライブデバッグ

カーネルデバッガの接続(Hyper-V、VirtualBox)

以下の記事にまとめています。

参考:WinDbgでWindows10環境のカーネルデバッグを行う最初の一歩

例外や特定のイベント発生時のデバッガの操作を設定する

TODO:追記予定

参考:sx, sxd, sxe, sxi, sxn, sxr, sx- (Set exceptions) - Windows drivers | Microsoft Learn

フィルタドライバの調査

fltkd 拡張機能のコマンド

# フィルタドライバの情報を列挙(fltmc コマンドでの列挙も有用)
!fltkd.filters 1

# 特定のフィルタの FLT_FILTER アドレスを指定することで、特定のフィルタの詳細情報を取得
!fltkd.filter ffffac8ce4df1010

# ここで特定した DeviceObject のアドレスを使用して詳細情報を表示
dt nt!_DRIVER_OBJECT ffffac8ce357c850
>
+0x000 Type             : 0n4
+0x030 DriverExtension  : 0xffffac8c`e357c9a0 _DRIVER_EXTENSION
+0x070 MajorFunction    : [28] 0xfffff800`65e72000     long  +fffff80065e72000

# 次に、DeviceObject 内の MajorFunction までのオフセットを特定して、MajorFunction の情報を表示
# MajorFunction のリンクをクリックしてもちかい情報が得られる
dps ffffac8ce357c850 + 0x70 L0n28

# DRIVER_EXTENSION のアドレスから構造体を調査することも可能
dt nt!_DRIVER_EXTENSION ffffac8ce357c850+0x30

参考:Tools for minifilter development and testing - Windows drivers | Microsoft Learn

参考:Of Filesystems And Other Demons: Debugging Minifilters: Finding the Callbacks

解析に必要な知識

32bit と 64bit の呼び出し規約を理解する

TODO:別記事で作成予定。

NTGLOBALFLAG

NTGLOBALFLAGは PEB に存在しており、デフォルト値は 0 ですがデバッグ時に値が設定されます。

32bit プロセスの場合は 0x68、64bit プロセスの場合は0xBCのオフセットに存在しています。

そのため、プログラムが PEB 構造体のアドレスに対して ZwQueryInformationProcess などでアクセスし、アンチデバッグの目的で使用する場合もあります。

参考:NtGlobalFlag - CTF Wiki EN

参考:Debugger Detection Using NtGlobalFlag | 🔐Blog of Osanda

EXCEPTION_RECORD

EXCEPTION_RECORDには例外やクラッシュに関する情報が含まれます。

参考:EXCEPTION_RECORD (winnt.h) - Win32 apps | Microsoft Learn

ExceptionCodeには例外の発生理由が含まれています。

一般的に記録される例外の種類については上記のページに記載があります。

EPROCESS、ETHREAD

EPROCESS構造体はドキュメンテーションされていない構造体ですが、システムプロセスを記述するカーネルメモリ構造体であり、nt!_EPROCESSとしてアクセスできることが知られています。

この構造体は、双方向連結リスト(LIST_ENTRY)ActiveProcessLinksによって連結されており、cmd /c tasklistコマンドなどが実行された場合には、このリストをたどってEPROCESS構造体が探索されることで、プロセスの一覧として出力されます。

そのため、WinDbg 上でもカーネルデバッガでこのリストをたどることで、アクティブなプロセスを探索することができます。

一方で、マルウェアなどがカーネルメモリにアクセスできる場合には、ActiveProcessLinksのポインタを付け替えることで、プロセス一覧に悪意のあるプロセスが表示されないように隠ぺいを行う場合があります。

参考:struct EPROCESS

参考:struct ETHREAD

参考:Windows kernel opaque structures - Windows drivers | Microsoft Learn

参考:Manipulating ActiveProcessLinks to Hide Processes in Userland - Red Team Notes

Guard page

プログラムが Guard Page として作成されたメモリ領域にアクセスした場合、STATUS_GUARD_PAGE_VIOLATION (0x80000001) 例外が発生します。

参考:Creating Guard Pages - Win32 apps | Microsoft Learn

sysenter 命令

TODO:追記

参考:SYSENTER - OSDev Wiki

よくある例外

Access Violation 例外

Access Violation 例外(エラーコード:C0000005)は、プログラムが保護されたメモリや破損したメモリ、存在しないポインタアドレスなどに対して読み取りや書き込みを試みた際の例外です。

例えば、Use after free のように解放済みのメモリにアクセスした場合などにもこの例外がトリガーされます。

また、0x0 から 0x10000(64K) までの領域のメモリにアクセスした場合は NULL ポインタ参照によって例外がトリガーされます。

参考:Access Violation C0000005 | Microsoft Learn

環境設定

Visual Studio のリンカ設定を変更する

最新の Visual Studio のデフォルト設定では、エクスポートテーブルがシンボルファイルに含まれません。

そのため、プロジェクトの設定から、以下のように/DEBUG:FULLの構成を設定します。

image-20230401113053380

Release ビルド時の最適化を無効化する

WinDbg の検証目的でプログラムをビルドする場合は、Release ビルド時の最適化設定を無効化しておくと良い場合があります。

image-20230401221935008

なお、Debug ビルドはデフォルトで最適化が無効化されています。

WinDbg のトラブルシューティング

「NT symbols are incorrect, please fix symbols」のエラーが出力される場合

!process 0 0 コマンド実行時などに 「NT symbols are incorrect, please fix symbols」 というエラーが出力される場合、シンボルの読み込みに何らかの問題が発生している可能性があります。

このような場合、まずは以下の例のようなコマンドでシンボルロード時にどのような問題が発生しているのかを特定します。

# nt シンボルロード時の詳細な情報を出力
!sym noisy; .reload /f nt

参考:windows 10 - !process 0 0 - NT symbols are incorrect, please fix symbols - Stack Overflow

僕の環境の場合、誤ったシンボルパス設定を解消することで、この問題も解消しました。

その他の Tips

WinDbg(X86) と WinDbg(X64) どちらを使用すべきですか?

基本的には、64bit アプリケーションのデバッグやダンプ解析を行う場合には WinDbg(X64) を使用し、32 bit アプリケーションの場合は WinDbg(X86) を使用します。

WinDbg(X64) は 64bit と 32bit どちらのアプリケーションも解析することができますが、WinDbg(X64) で 32bit アプリケーションを解析する場合には WOW64 レイヤーが追加される影響で、とくにダンプの解析を行う場合に解析を困難にする問題を引き起こす可能性があります。

なお、.effmach x86コマンドを使用することで問題を回避できる場合もありますが、WinDbg(X86) を使用する場合とイコールではないため、可能な限り WinDbg(X86) を使用する方が良いです。

参考:.effmach (Effective Machine) - Windows drivers | Microsoft Learn

WinDbg(Classic) でダークテーマを使用する

僕が普段使用しているテーマは以下から取得できます。

参考:kash1064/WinDbg-Classic-Dark: The Dark theme of WinDbg Classic

見た目としては以下のような感じで、できるだけ色の種類を減らしてシンプルにしつつ、使い慣れた VSCode のダークテーマに近い配色を目指してカスタマイズしています。

img