"Debugging Tools for Windows"の提供する拡張機能をPythonから呼び出せるPyDbgEngを使って、Pythonで簡単なデバッガを作ってみました。
PythonでWindowsネイティブアプリケーションのデバッガを作成するための基本方針は、 ctypes モジュールを使ってWin32APIの提供するデバッガ用APIやメモリ操作APIを活用します。それら基本的な使用方法をまとめたライブラリとして pydbg などが公開されており、 Python/Gray Hat Python 読書メモ で紹介した "Gray Hat Python" でも解説されています。
ただしこれらのライブラリは x86/32bit 用となっている場合が多く、64bitに対応している状況ではありません。
一方でMicrosoftから無償で提供されているネイティブデバッガ "Debugging Tools for Windows" は32bit/64bit両対応で、リモートデバッグやカーネルデバッグにも対応しています。デバッガとしてもCUIベースのCDBとNTSD, GUIベースのWinDBGを備えています。またコア部分が拡張機能として公開されているため、独自のデバッグコマンドをプラグインとして開発したり、デバッガプログラムそれ自体さえも独自に開発することが可能です。
以降、"Debugging Tools for Windows"を単にWinDBGと略称します。
拡張機能はDLLとして提供されています。PythonのctypesモジュールはDLLで公開されている関数を呼び出すことが出来ますので、ctypes経由で拡張機能を呼び出すことでPythonでWinDBGと同等、つまり64bitやカーネルデバッグに対応したデバッガを作れそうです。
しかし拡張機能、特にデバッガそれ自体を開発するためのコア機能はCOMコンポーネントとして提供されているため、ctypes経由で呼び出すことが煩雑で困難です。
この問題を解決するのが今回紹介するオープンソースのライブラリ、 PyDbgEng です。
PyDbgEngはPythonのみで作られたライブラリで、comtypesという同じくPythonのみで作られたCOMラッパライブラリを経由する事でWinDBGのコア拡張機能であるCOMコンポーネントを活用しています。
comtypesは内部的にはctypesを使っており、COMを利用するための煩雑な手順を隠蔽してくれます。
本記事では comtypes , PyDbgEng のインストールの解説と、PyDbgEngを使った簡単なデバッガの作り方を紹介します。
comtypes, PyDbgEngの順にインストール手順を紹介します。
その他に必要なツールとして以下を事前にインストールしておいてください。
本記事で検証した環境:
Windows XP/SP3(32bit), Windows 7/SP1(32bit) Visual C++ 2010 Express Edition Microsoft Windows SDK v 7.0 Windows Driver Kit 7600.16385.1 Debugging Tools for Windows v 6.12 : SDKオプション付きでインストールします。 WDKと同時にインストールした場合はSDKも入ります。 Python 2.7 comtypes-0.6.2 PyDbgEng-0.14 ActivePerl 5.12.x
Perl, Python には予めPATHを通しておきます。
setup.pyでインストール出来ます。
setup.py build setup.py install
Python 2.7 でのインストール時に "ImportError: cannot import name DistutilsOptionError" 発生する場合は次の記事を参照してください:
setup.pyを修正します:
from distutils.core import setup, Command, DistutilsOptionError
→
from distutils.core import setup, Command from distutils.errors import DistutilsOptionError
まずWinDBG拡張機能のCOMを使うためのIDLとTLBファイルを、自分の環境にあわせて作り直します。
必要なツール:
MIDL.exe, NMAKE.exe : Windows DDK や Windows SDK で提供されている。 Debugging Tools for Windows : SDK付きでインストール。 Perl : IDLを生成するためにPerlスクリプトを使っているため。
例:
> setup.py build > setup.py install --record PyDbgEng-0.14.install-files
ここからPyDbgEngの機能を確認しつつ、簡単なデバッガを作っていきます。
なお本記事ではデバッガの内部構造であるとか実装パターン、WinDBGの拡張機能の概要や構成については触れません。予め"Advanced Windows Debugging"やWinDBGのヘルプドキュメントを熟読しておいて下さい。またPyDbgEngそれ自体の構成については適宜ソースコードを参照しながら掴んでいってください。それほど巨大なものでは無いので、Python提供の "Module Docs" などを活用して追ってみてください。
最初にPyDbgEng関連モジュールを正常にimportでき、最低限度の動作確認として「何もしないデバッガ」を作ってみます。デバッガイベントにも反応せず、単にデバッグ対象プロセスを起動し、終わるまで待つだけです。
01_nothing.py :
import sys import PyDbgEng # dbgeng.dll, dbghelp.dllの場所を指定 DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) # 何もしないイベントハンドラ class EmptyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): def GetInterestMask(self): return 0 event_handler = EmptyDbgEventHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ event_callbacks_sink = event_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) dbg.wait_for_event(PyDbgEng.Defines.INFINITE)
メモ帳を起動してみます:
> 01_nothing.py notepad.exe (メモ帳を終了) >
この段階で動作しない場合、PyDbgEngの初期化で何か問題が発生している可能性があります。
PyDbgEng.py の PyDbgEng クラスの __init__ メソッド内で適当に dbg_eng_log を設定し、メッセージを確認してみてください。
続いて DEBUG_EVENT_{CREATE|EXIT}_{PROCESS|THREAD}, DEBUG_EVENT_{LOAD|UNLOAD}_MODULE イベントをモニタしてみます。
02_mon_events.py :
import sys import PyDbgEng DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) class MyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): # イベントハンドラでモニタするイベントを指定 def GetInterestMask(self): return \ PyDbgEng.DbgEng.DEBUG_EVENT_CREATE_PROCESS | \ PyDbgEng.DbgEng.DEBUG_EVENT_CREATE_THREAD | \ PyDbgEng.DbgEng.DEBUG_EVENT_LOAD_MODULE | \ PyDbgEng.DbgEng.DEBUG_EVENT_UNLOAD_MODULE | \ PyDbgEng.DbgEng.DEBUG_EVENT_EXIT_THREAD | \ PyDbgEng.DbgEng.DEBUG_EVENT_EXIT_PROCESS | \ 0 # 以下、各イベントハンドラの実装 def CreateProcess(self, dbg, \ ImageFileHandle, Handle, BaseOffset, ModuleSize, ModuleName, \ ImageName, CheckSum, TimeDateStamp, InitialThreadHandle, \ ThreadDataOffset, StartOffset \ ): print "<<CreateProcess>>" print "\tImageFileHandle : ", hex(ImageFileHandle) print "\tHandle : ", hex(Handle) print "\tBaseOffset : ", hex(BaseOffset) print "\tModuleSize : ", hex(ModuleSize) print "\tModuleName : ", ModuleName print "\tImageName : ", ImageName print "\tCheckSum : ", hex(CheckSum) print "\tTimeDateStamp : ", hex(TimeDateStamp) print "\tInitialThreadHandle : ", hex(InitialThreadHandle) print "\tThreadDataOffset : ", hex(ThreadDataOffset) print "\tStartDataOffset : ", hex(StartOffset) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE def CreateThread(self, dbg, Handle, DataOffset, StartOffset): print "<<CreateThread>>" print "\tHandle: ", if Handle: print hex(Handle) else: print "NULL" print "\tDataOffset: ", hex(DataOffset) print "\tStartOffset: ", hex(StartOffset) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE def LoadModule(self, dbg, \ ImageFileHandle, BaseOffset, ModuleSize, \ ModuleName, ImageName, CheckSum, TimeDateStamp \ ): print "<<LoadModule>>" print "\tImageFileHandle : ", hex(ImageFileHandle) print "\tBaseOffset : ", hex(BaseOffset) print "\tModuleSize : ", hex(ModuleSize) print "\tModuleName : ", ModuleName print "\tImageName : ", ImageName print "\tCheckSum : ", hex(CheckSum) print "\tTimeDateStamp : ", hex(TimeDateStamp) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE def UnloadModule(self, dbg, ImageBaseName, BaseOffset): print "<<UnloadModule>>" print "\tImageBaseName: ", ImageBaseName print "\tBaseOffset : ", hex(BaseOffset) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE def ExitThread(self, dbg, ExitCode): print "<<ExitThread>>" print "\tExitCode: ", hex(ExitCode) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE def ExitProcess(self, dbg, ExitCode): print "<<ExitProcess>>" print "\tExitCode: ", hex(ExitCode) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE event_handler = MyDbgEventHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ event_callbacks_sink = event_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) dbg.wait_for_event(PyDbgEng.Defines.INFINITE)
実行例:
> 02_mon_events.py notepad.exe <<CreateProcess>> ImageFileHandle : 0xe8L Handle : 0xd0L BaseOffset : 0x1000000L ModuleSize : 0x14000L ModuleName : notepad ImageName : notepad.exe CheckSum : 0x1315bL TimeDateStamp : 0xd4L InitialThreadHandle : 0xd4L ThreadDataOffset : 0x7ffde000L StartDataOffset : 0x100739dL <<LoadModule>> ImageFileHandle : 0xe0L BaseOffset : 0x7c940000L ModuleSize : 0x9f000L ModuleName : ntdll ImageName : ntdll.dll CheckSum : 0xa1033L TimeDateStamp : 0x4d00f27bL <<LoadModule>> ImageFileHandle : 0xe4L BaseOffset : 0x7c800000L ModuleSize : 0x133000L ModuleName : kernel32 ImageName : C:\WINDOWS\system32\kernel32.dll CheckSum : 0x13a635L TimeDateStamp : 0x49c4f49cL (...) <<CreateThread>> Handle: NULL DataOffset: 0x7ffdc000L StartOffset: 0x7c8106f9L (...) <<UnloadModule>> ImageBaseName: C:\WINDOWS\system32\RichEd20.dll BaseOffset : 0x74d70000L (...) <<ExitThread>> ExitCode: 0x0L <<ExitProcess>> ExitCode: 0x0L
イベントハンドラの引数については、WinDBG側のヘルプとPyDbgEngのソースコードを突き合わせて調べればなんとかなると思います。
デバッガで起動したときにシステムDLL内で発生する最初のイベントを拾うことが出来ます。拡張機能のコア、エンジン部分のオプションで DEBUG_ENGOPT_INITIAL_BREAK を指定します。
例外イベントとして送られますので、対応するイベントハンドラを用意します。
今回の例ではイベントハンドラに渡された主なパラメータを表示するにとどめ、特に何もせずそのまま処理を続行させます。
03_initial_break.py :
import sys import threading import PyDbgEng DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) class MyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): def GetInterestMask(self): return \ PyDbgEng.DbgEng.DEBUG_EVENT_EXCEPTION | \ 0 def Exception(self, dbg, \ ExceptionCode, ExceptionFlags, ExceptionRecord, \ ExceptionAddress, NumberParameters, \ ExceptionInformation0, ExceptionInformation1, ExceptionInformation2, \ ExceptionInformation3, ExceptionInformation4, ExceptionInformation5, \ ExceptionInformation6, ExceptionInformation7, ExceptionInformation8, \ ExceptionInformation9, ExceptionInformation10, ExceptionInformation11, \ ExceptionInformation12, ExceptionInformation13, ExceptionInformation14, \ FirstChance): print "<<Exception>>" print "\tCode : ", hex(ExceptionCode) print "\tFlags : ", hex(ExceptionFlags) print "\tAddress : ", hex(ExceptionAddress) print "\tNumberParameters : ", hex(NumberParameters) print "\tFirstChance : ", hex(FirstChance) return PyDbgEng.DbgEng.DEBUG_STATUS_NOT_CHANGED event_handler = MyDbgEventHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ event_callbacks_sink = event_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) dbg.idebug_control.AddEngineOptions(PyDbgEng.DbgEng.DEBUG_ENGOPT_INITIAL_BREAK) quit_event = threading.Event() dbg.event_loop_with_quit_event(quit_event)
実行例:
> 03_initial_break.py notepad.exe <<Exception>> Code : 0x80000003L Flags : 0x0L Address : 0x7c94120eL NumberParameters : 0x3L FirstChance : 0x1L (メモ帳を終了) >
簡単なコンソールアプリを作成し、main()関数でブレークしてみます。
helloworld.c:
#include <stdio.h> int main(int argc, char *argv[]) { printf("Hello, World!\n"); getchar(); }
Makefile.mk:
CFLAGS=/nologo /Zi /Wall /Od helloworld.exe: helloworld.c cl $(CFLAGS) $? /link /DEBUG /PDB:$*.pdb clean: del *.exe *.obj *.pdb *.ilk
コンパイル+実行:
> nmake /f Makefile.mk Microsoft(R) Program Maintenance Utility Version 10.00.30319.01 Copyright (C) Microsoft Corporation. All rights reserved. cl /nologo /Zi /Wall /Od helloworld.c /link /DEBUG /PDB:helloworld.pdb helloworld.c helloworld.c(3) : warning C4100: 'argv' : 引数は関数の本体部で 1 度も参照されません。 helloworld.c(3) : warning C4100: 'argc' : 引数は関数の本体部で 1 度も参照されません。 > helloworld.exe Hello, World! (RETURNキー押下) >
デバッグ対象が準備できたので、デバッガの方を作っていきます。
まずはブレークポイントイベントを拾ってみます。
04_break_at_main1.py:
import sys import threading import os import PyDbgEng DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) modname_exe = os.path.basename(sys.argv[1]).replace(".exe", "") # デバッガエンジンの出力を表示するための出力ハンドラを用意する class MyDbgOutputHandler(PyDbgEng.IDebugOutputCallbacksSink): def Output(self, this, Mask, Text): sys.stdout.write(Text) class MyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): def GetInterestMask(self): return \ PyDbgEng.DbgEng.DEBUG_EVENT_BREAKPOINT | \ PyDbgEng.DbgEng.DEBUG_EVENT_CREATE_PROCESS | \ 0 # ブレークポイント用のイベントハンドラ def Breakpoint(self, dbg, \ Offset, Id, BreakType, ProcType, Flags, \ DataSize, DataAccessType, PassCount, CurrentPassCount, \ MatchThread, CommandSize, OffsetExpressionSize): print "<<Breakpoint>>" print "\tOffset : ", hex(Offset) print "\tId : ", hex(Id) print "\tBreakType : ", if BreakType == PyDbgEng.DbgEng.DEBUG_BREAKPOINT_CODE: print "Software" else: print "Hardware" print "\tProcType : ", hex(ProcType) print "\tFlags : ", if Flags & PyDbgEng.DbgEng.DEBUG_BREAKPOINT_ENABLED: print "ENABLED " if Flags & PyDbgEng.DbgEng.DEBUG_BREAKPOINT_ADDER_ONLY: print "ADDER_ONLY " if Flags & PyDbgEng.DbgEng.DEBUG_BREAKPOINT_GO_ONLY: print "GO_ONLY " if Flags & PyDbgEng.DbgEng.DEBUG_BREAKPOINT_ONE_SHOT: print "ONE_SHOT " if Flags & PyDbgEng.DbgEng.DEBUG_BREAKPOINT_DEFERRED: print "DEFERRED " if BreakType == PyDbgEng.DbgEng.DEBUG_BREAKPOINT_DATA: print "\t(HWBRK)DataSize : ", hex(DataSize) print "\t(HWBRK)DataAccessType: ", if DataAccessType == PyDbgEng.DbgEng.DEBUG_BREAK_READ: print "READ " if DataAccessType == PyDbgEng.DbgEng.DEBUG_BREAK_WRITE: print "WRITE " if DataAccessType == PyDbgEng.DbgEng.DEBUG_BREAK_EXECUTE: print "EXECUTE " print "\tPassCount : ", hex(PassCount) print "\tCurrentPassCount : ", hex(CurrentPassCount) if MatchThread: print "\tMatchThreadId : ", hex(MatchThread) print "\tCommandSize : ", hex(CommandSize) print "\tOffsetExpressionSize : ", hex(OffsetExpressionSize) return PyDbgEng.DbgEng.DEBUG_STATUS_GO def CreateProcess(self, dbg, \ ImageFileHandle, Handle, BaseOffset, ModuleSize, ModuleName, \ ImageName, CheckSum, TimeDateStamp, InitialThreadHandle, \ ThreadDataOffset, StartOffset \ ): # "main()"シンボルのアドレスを取得し、"bp_set()"でブレークポイントを設定 main_symstr = modname_exe + "!main" main_addr = dbg.resolve_symbol(main_symstr) print main_symstr + " at " + hex(main_addr), dbg.bp_set(dbg.resolve_symbol(modname_exe + "!main")) print " : break point set" return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE event_handler = MyDbgEventHandler() output_handler = MyDbgOutputHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ output_callbacks_sink = output_handler, \ event_callbacks_sink = event_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) dbg.idebug_symbols.AddSymbolOptions(0x80000000) # "!sym noisy"に相当 quit_event = threading.Event() dbg.event_loop_with_quit_event(quit_event)
実行例:
> 04_break_at_main1.py helloworld.exe Microsoft (R) Windows Debugger Version 6.12.0002.633 X86 Copyright (c) Microsoft Corporation. All rights reserved. CommandLine: helloworld.exe DBGHELP: _NT_SYMBOL_PATH: srv*...;http://msdl.microsoft.com/download/symbols DBGHELP: Symbol Search Path: ... (...) *** WARNING: Unable to verify checksum for helloworld.exe DBGHELP: helloworld - private symbols & lines C:\(...)\helloworld.pdb helloworld!main at 0x401010L : break point set ModLoad: 7c940000 7c9df000 ntdll.dll ModLoad: 7c800000 7c933000 C:\WINDOWS\system32\kernel32.dll Breakpoint 0 hit <<Breakpoint>> Offset : 0x401010L Id : 0x0L BreakType : Software ProcType : 0x14cL Flags : ENABLED PassCount : 0x1L CurrentPassCount : 0x1L MatchThreadId : 0xffffffffL CommandSize : 0x0L OffsetExpressionSize : 0x0L Hello, World! (RETURNキー押下) >
main()でブレークさせることに成功したので、続いてブレークしたときのスタックトレースとレジスタを表示してみます。
04_break_at_main2.py :
import sys import threading import os import PyDbgEng DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) modname_exe = os.path.basename(sys.argv[1]).replace(".exe", "") class MyDbgOutputHandler(PyDbgEng.IDebugOutputCallbacksSink): def Output(self, this, Mask, Text): sys.stdout.write(Text) class MyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): def GetInterestMask(self): return \ PyDbgEng.DbgEng.DEBUG_EVENT_BREAKPOINT | \ PyDbgEng.DbgEng.DEBUG_EVENT_CREATE_PROCESS | \ 0 def Breakpoint(self, dbg, \ Offset, Id, BreakType, ProcType, Flags, \ DataSize, DataAccessType, PassCount, CurrentPassCount, \ MatchThread, CommandSize, OffsetExpressionSize): print "<<<StackFrame(TOP5)>>>" print "Offset : ", hex(Offset) print "#,INSTR,RTN,P0,P1,P2,P3" # スタックフレームを最大5フレーム取得・表示 frame_list = dbg.get_stack_trace(5) for f in frame_list: print "%d, 0x%08X, 0x%08X," % \ (f.FrameNumber, f.InstructionOffset, f.ReturnOffset), print "0x%08X, 0x%08X, 0x%08X, 0x%08X" % \ (f.Params[0], f.Params[1], f.Params[2], f.Params[3]) # dump_context_list()でレジスタを取得・表示 print "Registers:" reg = dbg.dump_context_list() for n, v in reg.items(): print n, ":", hex(v) return PyDbgEng.DbgEng.DEBUG_STATUS_GO def CreateProcess(self, dbg, \ ImageFileHandle, Handle, BaseOffset, ModuleSize, ModuleName, \ ImageName, CheckSum, TimeDateStamp, InitialThreadHandle, \ ThreadDataOffset, StartOffset \ ): main_symstr = modname_exe + "!main" main_addr = dbg.resolve_symbol(main_symstr) print main_symstr + " at " + hex(main_addr), dbg.bp_set(dbg.resolve_symbol(modname_exe + "!main")) print " : break point set" return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE event_handler = MyDbgEventHandler() output_handler = MyDbgOutputHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ output_callbacks_sink = output_handler, \ event_callbacks_sink = event_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) dbg.idebug_symbols.AddSymbolOptions(0x80000000) quit_event = threading.Event() dbg.event_loop_with_quit_event(quit_event)
実行例:(シンボル関連の出力は省略しています)
> 04_break_at_main2.py helloworld.exe (...) helloworld!main at 0x401010L : break point set ModLoad: 7c940000 7c9df000 ntdll.dll ModLoad: 7c800000 7c933000 C:\WINDOWS\system32\kernel32.dll Breakpoint 0 hit <<<StackFrame(TOP5)>>> Offset : 0x401010L #,INSTR,RTN,P0,P1,P2,P3 0, 0x00401010, 0x0040131E, 0x00000001, 0x00383860, 0x00383898, 0x348A6A2B 1, 0x0040131E, 0x7C817077, 0x00000000, 0x00000000, 0x7FFDC000, 0xFFFFFFFF80546CFD 2, 0x7C817077, 0x00000000, 0x00401374, 0x00000000, 0x78746341, 0x00000020 3, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000 0, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000 Registers: Callback failed with 80004005 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ !! Hello, World! (RETURNキー押下) >
スタックフレームの取得は成功しているが、レジスタの取得に失敗しています。
これは dump_context_list() -> get_register_value() -> IDebugRegisters.GetValue() の順で呼び、その戻り値を
return reg_value.u.I32
として返しているが、このメンバアクセスがPyDbgEngインストール時にPerlで自動変換したTLBファイルと異なっているためです。。TLBファイルの当該定義を手動で修正すれば直りますが、残念ながら、Perlによる自動変換で他にも何箇所か実際の定義とずれが生じているところもあるようです。
例えば今回の例なら、スタックフレームの表示で戻り先のアドレスのシンボル名を取得しようとget_symbol()を使おうとしましたが、こちらも正常に動作しませんでした。
こうした修正箇所をすべて手作業で修正するのは非常に手間です。
そこで、次の例ではWinDBGエンジンの提供するデバッガコマンドを使ってスタックフレームやレジスタを表示させてみます。スクリプトで値を取得して処理させるには向いていませんが、とりあえず値を表示するだけであればこのアプローチでも大丈夫でしょう。
execute()に文字列でデバッガコマンドを渡せば実行できます。
04_break_at_main3.py:
import sys import threading import os import PyDbgEng DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) modname_exe = os.path.basename(sys.argv[1]).replace(".exe", "") class MyDbgOutputHandler(PyDbgEng.IDebugOutputCallbacksSink): def Output(self, this, Mask, Text): sys.stdout.write(Text) class MyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): def GetInterestMask(self): return \ PyDbgEng.DbgEng.DEBUG_EVENT_BREAKPOINT | \ PyDbgEng.DbgEng.DEBUG_EVENT_CREATE_PROCESS | \ 0 def Breakpoint(self, dbg, \ Offset, Id, BreakType, ProcType, Flags, \ DataSize, DataAccessType, PassCount, CurrentPassCount, \ MatchThread, CommandSize, OffsetExpressionSize): print "breakin!" dbg.execute("r") dbg.execute("kb") print "" return PyDbgEng.DbgEng.DEBUG_STATUS_GO def CreateProcess(self, dbg, \ ImageFileHandle, Handle, BaseOffset, ModuleSize, ModuleName, \ ImageName, CheckSum, TimeDateStamp, InitialThreadHandle, \ ThreadDataOffset, StartOffset \ ): dbg.execute("!sym noisy") main_symstr = modname_exe + "!main" dbg.execute("bp " + main_symstr) return PyDbgEng.DbgEng.DEBUG_STATUS_NO_CHANGE event_handler = MyDbgEventHandler() output_handler = MyDbgOutputHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ output_callbacks_sink = output_handler, \ event_callbacks_sink = event_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) quit_event = threading.Event() dbg.event_loop_with_quit_event(quit_event)
実行例:(シンボル関連の出力は省略しています)
> 04_break_at_main3.py helloworld.exe (...) ModLoad: 00400000 00423000 helloworld.exe !sym noisy noisy mode - symbol prompts on bp helloworld!main ModLoad: 7c940000 7c9df000 ntdll.dll ModLoad: 7c800000 7c933000 C:\WINDOWS\system32\kernel32.dll Breakpoint 0 hit breakin! r eax=00383898 ebx=7ffde000 ecx=00000001 edx=0041eb10 esi=00000000 edi=00000000 eip=00401010 esp=0012ff7c ebp=0012ffc0 iopl=0 nv up ei pl zr na pe nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000246 helloworld!main: 00401010 55 push ebp kb ChildEBP RetAddr Args to Child 0012ff78 0040131e 00000001 00383860 00383898 helloworld!main 0012ffc0 7c817077 00000000 00000000 7ffde000 helloworld!__tmainCRTStartup+0x10b 0012fff0 00000000 00401374 00000000 78746341 kernel32!BaseProcessStart+0x23 Hello, World! (RETURNキー押下) >
WinDBGなどで "bp", "r", "kb" コマンドを実行したときと同様の表示を確認できました。
スクリプトによる自動化とは趣旨が異なってしまいますが、execute()を使うことで対話形式のデバッガを作成することも可能です。
対話形式を実装するポイントは次の3点です。
05_interact1.py:
import sys import threading import PyDbgEng import ctypes DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) class MyDbgOutputHandler(PyDbgEng.IDebugOutputCallbacksSink): def Output(self, dbg, Mask, Text): sys.stdout.write(Text) # イベントハンドラは空にしておく。 class MyDbgEventHandler(PyDbgEng.IDebugEventCallbacksSink): def GetInterestMask(self): return 0 event_handler = MyDbgEventHandler() output_handler = MyDbgOutputHandler() dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ event_callbacks_sink = event_handler, \ output_callbacks_sink = output_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) # 初期イベントで一旦ブレークする dbg.idebug_control.AddEngineOptions(PyDbgEng.DbgEng.DEBUG_ENGOPT_INITIAL_BREAK) while True: if dbg.wait_for_event(PyDbgEng.Defines.INFINITE): # デバッガにブレーク print "debugger break" while True: c = raw_input("DBG>") dbg.execute(c) status = dbg.idebug_control.GetExecutionStatus() # execute()の結果がデバッガブレークでなければ wait_for_event() に戻る if status != PyDbgEng.DbgEng.DEBUG_STATUS_BREAK: break else: break
実行例:
> 05_interact1.py helloworld.exe (...) ModLoad: 00400000 00423000 helloworld.exe ModLoad: 7c940000 7c9df000 ntdll.dll ModLoad: 7c800000 7c933000 C:\WINDOWS\system32\kernel32.dll (b30.99c): Break instruction exception - code 80000003 (first chance) debugger break DBG>kb kb ChildEBP RetAddr Args to Child 0012fb1c 7c9802ed 7ffdf000 7ffda000 00000000 ntdll!DbgBreakPoint 0012fc94 7c95fad7 0012fd30 7c940000 0012fce0 ntdll!LdrpInitializeProcess+0x1014 0012fd1c 7c94e457 0012fd30 7c940000 00000000 ntdll!_LdrpInitialize+0x183 00000000 00000000 00000000 00000000 00000000 ntdll!KiUserApcDispatcher+0x7 DBG>r r eax=00241eb4 ebx=7ffda000 ecx=00000001 edx=00000002 esi=00241f48 edi=00241eb4 eip=7c94120e esp=0012fb20 ebp=0012fc94 iopl=0 nv up ei pl nz na po nc cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00000202 ntdll!DbgBreakPoint: 7c94120e cc int 3 DBG>g g Hello, World! (RETURNキー押下) >
WinDBGやCDB, NTSDなどは Ctrl-C, Ctrl-BREAK で任意のタイミングでデバッガにブレークさせることが出来ます。
WinDBG拡張でこれを実現するには、ブレークの引き金を検出するスレッドを走らせておき、検出されたらIDebugControl.SetInterrupt()を呼びます。
このような動作をPythonで実装するための雛形を次の記事で作っています。
この雛形を流用して組み立ててみたのが次のコードです。
05_interact2.py:
import sys import threading import PyDbgEng DBGENG_DLL_PATH = "C:\\WinDDK\\7600.16385.1\\Debuggers" if 2 > len(sys.argv): print "usage: %s *.exe" % (sys.argv[0]) sys.exit(1) class MyDbgOutputHandler(PyDbgEng.IDebugOutputCallbacksSink): def Output(self, dbg, Mask, Text): sys.stdout.write(Text) output_handler = MyDbgOutputHandler() # イベントハンドラを省略し、デフォルトのもので済ませてしまう。 dbg = PyDbgEng.ProcessCreator( \ command_line = sys.argv[1], \ output_callbacks_sink = output_handler, \ dbg_eng_dll_path = DBGENG_DLL_PATH \ ) # デバッグループ def debugger_thread(dbg): while True: if dbg.wait_for_event(PyDbgEng.Defines.INFINITE): while True: c = "" try: c = raw_input("DBG>") except EOFError: # プロンプト表示中のCtrl-Cは無視する pass dbg.execute(c) status = dbg.idebug_control.GetExecutionStatus() if status != PyDbgEng.DbgEng.DEBUG_STATUS_BREAK: break else: break t = threading.Thread(target=debugger_thread, args=(dbg,)) t.start() # Ctrl-Cによるデバッガブレークの検出ループ while True: try: # 1秒間ずつスレッドの終了チェック t.join(1) if not t.isAlive(): break except KeyboardInterrupt: # デバッガブレーク発生 dbg.idebug_control.SetInterrupt(PyDbgEng.DbgEng.DEBUG_INTERRUPT_ACTIVE)
実行例は省略します。notepad.exeなどを動かして遊んでみてください。helloworld.exeなどコンソールアプリで標準入力を読む場合などは、タイミングによってはデバッガ側ではなくアプリ側でCtrl-Cを受信し、思うような動きにならない場合がありますので注意してください。
PyDbgEngライブラリを使い、WinDBGの拡張機能を使ったデバッガを作ってみました。簡単な例ではありましたが、これらをベースにして独自のデバッガやフォレンジックツールを組み上げてみてください。
ただし、TLBファイルの自動生成の限界など使っていく上でトラブルに遭遇しやすいことも確かです。大抵はCOM, TLB絡みの問題だと思いますので、手に負えない様であれば後半紹介したように直接デバッガコマンドを叩いてしまうというアプローチも試してみてください。
それでは楽しい楽しいグレーゾーンPythonをエンジョイしてください。
コメント