home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

Python/WinDBG拡張機能でデバッガを作ってみる (v1)

Python/WinDBG拡張機能でデバッガを作ってみる (v1)

Python / WinDBG拡張機能でデバッガを作ってみる (v1)
id: 935 所有者: msakamoto-sf    作成日: 2011-03-23 18:31:58
カテゴリ: Python WinDBG Windows 

"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 のインストール

comtypes, PyDbgEngの順にインストール手順を紹介します。
その他に必要なツールとして以下を事前にインストールしておいてください。

  • Python : multiprocessingモジュールを使用しているため、Python 2.6 以降が必要です。
  • Debugging Tools for Windows
    • MSDNのサイトから入手できます。
  • MIDL.exe, NMAKE.exe
    • Visual C++, Windows SDK, Windows Driver Kit のいずれかに入っていますのでインストールしておいてください。WinSDK, WDKには"Debugging Tools for Windows"も同梱されていますので、いずれか片方を入れておくと便利です。
  • Perl
    • Windows用のActivePerl ( http://www.activestate.com/activeperl ) を使いました。PyDbgEngでWinDBGの拡張機能用ヘッダファイルからCOMが使うIDLを生成するスクリプトで必要です。

本記事で検証した環境:

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を通しておきます。

comtypes のインストール

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

PyDbgEng のインストール

まずWinDBG拡張機能のCOMを使うためのIDLとTLBファイルを、自分の環境にあわせて作り直します。
必要なツール:

MIDL.exe, NMAKE.exe : Windows DDK や Windows SDK で提供されている。
Debugging Tools for Windows : SDK付きでインストール。
Perl : IDLを生成するためにPerlスクリプトを使っているため。
  1. WinSDKなりWDKなりのコマンドプロンプトを開き、"PyDbgEng-0.14\DbgEngTlb"ディレクトリに移動します。
  2. "out"ディレクトリが既にあれば、念のためリネームするなどしてバックアップしておきます。
  3. Makefileを修正します。 "INPUT_HEADER = sdk\dbgeng.h" を自分の環境のdbgeng.hを指すように変更します。dbgeng.hはWinDBGインストールディレクトリの "sdk\inc" 内にあるはずです。
  4. nmakeでmakeします。
  5. "out"ディレクトリに自分の環境でのIDLとTLBファイルが生成されます。
  6. "out"中の body.idl, DbgEng.idl, DbgEng.tlb を "PyDbgEng-0.14\PyDbgEng\data" ディレクトリにコピーします。既に同じファイル名があれば、バックアップした後上書きコピーします。
  7. "PyDbgEng-0.14"ディレクトリに移動し、"setup.py"でインストールする。

例:

> setup.py build
> setup.py install --record PyDbgEng-0.14.install-files

デバッガを作ってみる

ここからPyDbgEngの機能を確認しつつ、簡単なデバッガを作っていきます。

なお本記事ではデバッガの内部構造であるとか実装パターン、WinDBGの拡張機能の概要や構成については触れません。予め"Advanced Windows Debugging"やWinDBGのヘルプドキュメントを熟読しておいて下さい。またPyDbgEngそれ自体の構成については適宜ソースコードを参照しながら掴んでいってください。それほど巨大なものでは無いので、Python提供の "Module Docs" などを活用して追ってみてください。

01_nothing.py : 何もしないデバッガ

最初に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 を設定し、メッセージを確認してみてください。

02_mon_events.py : {CREATE|EXIT}_{PROCESS|THREAD},{LOAD|UNLOAD}_MODULEイベント

続いて 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のソースコードを突き合わせて調べればなんとかなると思います。

03_initial_break.py : デバッガの初期イベントと例外イベントハンドラ

デバッガで起動したときにシステム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()関数でブレークしてみる

簡単なコンソールアプリを作成し、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 : ブレークポイントイベントを拾う

まずはブレークポイントイベントを拾ってみます。

  1. "!sym noisy"に相当する設定を行い、シンボルがロードされたことを確認できるようにしておく。
  2. "main()"にブレークポイントを設定する。
  3. ブレークポイント用のイベントハンドラを用意し、渡された情報を表示してみる。

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キー押下)
>

04_break_at_main2.py : ブレークしたときのスタックトレースとレジスタを表示してみる

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エンジンの提供するデバッガコマンドを使ってスタックフレームやレジスタを表示させてみます。スクリプトで値を取得して処理させるには向いていませんが、とりあえず値を表示するだけであればこのアプローチでも大丈夫でしょう。

04_break_at_main3.py : デバッガコマンドを実行してみる

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()を使うことで対話形式のデバッガを作成することも可能です。

05_interact1.py : 簡単な対話形式

対話形式を実装するポイントは次の3点です。

  1. イベントハンドラは空にしておく。
  2. wait_for_event()が真を返したら対話形式に移行する。
  3. execute()の実行後、ステータスを確認しデバッガにブレークした状態でなければ、またwait_for_event()に戻る。

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キー押下)

>

05_interact2.py : 任意のタイミングでデバッガにブレークさせる

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をエンジョイしてください。



プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2011-04-02 16:20:06
md5:f537214224e03f758aba42236bb438c4
sha1:45d06e8ab7b00a25fce2c444c2a7ee1ba1dc6ca2
コメント
コメントを投稿するにはログインして下さい。