リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。
軌跡
環境
OS: Windows10 64bit Home (日本語)
CPU: Intel® Core™ i3-6006U CPU @ 2.00GHz × 1
メモリ: 2048MB
Python: 3.6.5
私の環境は、普段Ubuntu16.04を使っていますが、ここではWindows10 64bitを仮想マシン上で立ち上げております。
ちなみに教科書では、Windowsの32bitで紹介されています。
自作デバッガへの道 - その4 「デバッグイベントハンドラ」
前回は、スレッドの取得とレジスタの捕捉を学びました。
今回は、デバッグイベントに対してデバッガが何らかの処理を行えるようにします。
前々回に、Windows API関数の「WaitForDebugEvent」を紹介しました。
これは、デバッグイベントが発生すると、DEBUG_EVENT構造体にその情報を格納してくれます。この情報を使ってデバッガは各デバッグイベントに対して処理を行えるようにします。
今回は前々回の続きみたいなものですね。
ハンドラ
ハンドラについて、少し調べてみました。
ハンドラは、何らかのイベントを捕捉すると呼び出される関数やサブルーチンのことのようです。
プログラムの流れには直接関与せず、何らかのイベントが発生するまで待機しています。
今回でいうと、イベント(ブレークポイントやメモリアクセス違反など)を捕捉すると各イベントに応じたデバッグイベントハンドラが実行されるといったことですね。
一応前々回の内容を載せておきます。
[再掲]イベントの捕捉「WaitForDebugEvent」
WaitForDebugEventの仕様は以下です。
BOOL WaitForDebugEvent(
LPDEBUG_EVENT lpDebugEvent, // デバッグイベントの情報が入った構造体へのポインタ
DWORD dwMilliseconds // イベント待機時間(ミリ秒)
);
引数のlpDebugEventは、捕捉したイベントの情報を格納するための構造体を指定し、dwMillisecondsには、デバッガがイベントを捕捉するまで戻らないように、INFINITEを指定します。
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
EXCEPTION_DEBUG_INFO Exception;
CREATE_THREAD_DEBUG_INFO CreateThread;
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
EXIT_THREAD_DEBUG_INFO ExitThread;
EXIT_PROCESS_DEBUG_INFO ExitProcess;
LOAD_DLL_DEBUG_INFO LoadDll;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
引数のdwDebugEventCodeは捕捉したイベントコードを記録します。各イベントコードはリンク先を参照してください。
dwProcessIdとThreadIdは捕捉したPIDとTIDが記録されます。
共用体の中の各フィールドには捕捉したイベントの情報が記録されます。
今回は、EXCEPTION_DEBUG_INFOのみ仕様を以下に記載します。
typedef struct _EXCEPTION_DEBUG_INFO {
EXCEPTION_RECORD ExceptionRecord;
DWORD dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
引数のExceptionRecordは、EXCEPTION_RECORDという構造体で例外コード、フラグ、アドレスなどが記録されるようです。
dwFirstChanceはデバッガが以前にExceptionRecordで指定された例外を検出したかどうかの値が記録されるようです。値が0以外の場合はこの例外を検出したのは初めてという意味です。多くの場合ブレークポイントやシングルステップで例外が捕捉された時に0以外の値が記録されます。一方で値が0の場合は、以前に同じ例外が検出されているという意味です。この場合は、ハンドラが見つからなかったか、例外処理を継続した場合に0が記録されるようです。
さらに、ExceptionRecord構造体の仕様を以下に記載します。
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode;
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress;
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;
引数の、ExceptionCodeは定数が記録されます。例えば、アクセス違反(EXCEPTION_ACCESS_VIOLATION)などがありました。
ExceptionFlagsは継続可能な例外かそうじゃないか(EXCEPTION_NONCONTINUABLE)を示します。
*ExceptionRecordはExceptionRecord自身のポインタです。他の例外が発生している場合は、このようにどんどんつなげていくようです。
ExceptionAddressはどこで例外が発生したかのアドレスです。
NumberParametersは例外のパラメータの数。ExceptionInformationは例外を記述する追加の要素の配列らしいです。この2つはよくわからなかったです。
イベントを捕捉し、何らかの処理が完了したあとは、プロセスの実行を再開させます。
プロセスの実行を再開させるためのWindows API関数は「ContinueDebugEvent」です。
[再掲]プロセスの再開「ContinueDebugEvent」
ContinueDebugEventの仕様は以下です。
BOOL ContinueDebugEvent(
DWORD dwProcessId, // 続行するプロセス
DWORD dwThreadId, // 続行するスレッド
DWORD dwContinueStatus // 続行状態
);
dwProcessIdとdwThreadIdは、捕捉したイベントのDEBUG_EVENT構造体に格納されている値を使用します。
dwContinueStatusはDBG_CONTINUEかDBG_EXCEPTION_NOT_HANDLEDを指定します。それぞれ、スレッドの実行を続けるか、例外の処理を続けるかという意味です。
では、デバッグイベントコードを出力するテストプログラムを作成します。
from ctypes import *
from defines import *
kernel32 = windll.kernel32
def eventname(event_code):
name = "UNKNOWN_EVENT"
if event_code==1:
name = "EXCEPTION_DEBUG_EVENT"
elif event_code==2:
name = "CREATE_THREAD_DEBUG_EVENT"
elif event_code==3:
name = "CREATE_PROCESS_DEBUG_EVENT"
elif event_code==4:
name = "EXIT_THREAD_DEBUG_EVENT"
elif event_code==5:
name = "EXIT_PROCESS_DEBUG_EVENT"
elif event_code==6:
name = "LOAD_DLL_DEBUG_EVENT"
elif event_code==7:
name = "UNLOAD_DLL_DEBUG_EVENT"
elif event_code==8:
name = "OUTPUT_DEBUG_STRING_EVENT"
elif event_code==9:
name = "RIP_EVENT"
return name
pid = input("pid: ")
if kernel32.DebugActiveProcess(int(pid)):
print("attached :", pid)
debug_event = DEBUG_EVENT()
for i in range(100):
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
print("DebugEventCode:", debug_event.dwDebugEventCode)
print("DebugEventName:", eventname(debug_event.dwDebugEventCode))
print("pid:", debug_event.dwProcessId)
print("tid:", debug_event.dwThreadId)
kernel32.ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
# DBG_EXCEPTION_NOT_HANDLED)
DBG_CONTINUE)
kernel32.DebugActiveProcessStop(int(pid))
print("detached :", pid)
else:
print("Error pid:", pid)
print(WinError(GetLastError()))
電卓のプロセスIDを指定すると、かなりの量のデバッグイベントが発生していることがわかりました。
疑問
ここで疑問に思いました。
なぜ、すでに電卓を立ち上げていた状態でアタッチしたのに、デバッグイベントでCREATE_PROCESS_DEBUG_EVENTやCREATE_THREAD_DEBUG_EVENTなどが最初のほうで発生しているのかと。
私の予想では、デバッガが電卓をアタッチした時点以降のデバッグイベントを捕捉するのかなと思っていました。
この結果から、デバッガはアタッチした時点のプロセスをデバッグ用プロセスとして複製しているんじゃないかと考えております。ただ、そうなるとPIDは元のものから変わっているはず・・・。
あるいは、プロセスは過去のイベントを保持しているのかもしれません。
実際はどうなのでしょう。
次は、EXCEPTION_DEBUG_EVENTだけに着目し、EXCEPTION名を出力してみます。
from ctypes import *
from defines import *
kernel32 = windll.kernel32
def eventname(event_code):
name = "UNKNOWN_EVENT"
if event_code==1:
name = "EXCEPTION_DEBUG_EVENT"
elif event_code==2:
name = "CREATE_THREAD_DEBUG_EVENT"
elif event_code==3:
name = "CREATE_PROCESS_DEBUG_EVENT"
elif event_code==4:
name = "EXIT_THREAD_DEBUG_EVENT"
elif event_code==5:
name = "EXIT_PROCESS_DEBUG_EVENT"
elif event_code==6:
name = "LOAD_DLL_DEBUG_EVENT"
elif event_code==7:
name = "UNLOAD_DLL_DEBUG_EVENT"
elif event_code==8:
name = "OUTPUT_DEBUG_STRING_EVENT"
elif event_code==9:
name = "RIP_EVENT"
return name
def exception_name(exception_code):
name = "UNKNOWN_EXCEPTION"
if exception_code==EXCEPTION_ACCESS_VIOLATION:
name = "EXCEPTION_ACCESS_VIOLATION"
elif exception_code==EXCEPTION_GUARD_PAGE_VIOLATION:
name = "EXCEPTION_GUARD_PAGE_VIOLATION"
elif exception_code==EXCEPTION_BREAKPOINT:
name = "EXCEPTION_BREAKPOINT"
elif exception_code==EXCEPTION_SINGLE_STEP:
name = "EXCEPTION_SINGLE_STEP"
elif exception_code==EXCEPTION_STACK_OVERFLOW:
name = "EXCEPTION_STACK_OVERFLOW"
return name
pid = input("pid: ")
if kernel32.DebugActiveProcess(int(pid)):
print("attached :", pid)
debug_event = DEBUG_EVENT()
print("pid:", debug_event.dwProcessId)
for i in range(100):
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
print(" tid:", debug_event.dwThreadId)
print(" DebugEventCode:", debug_event.dwDebugEventCode)
print(" DebugEventName:", eventname(debug_event.dwDebugEventCode))
if eventname(debug_event.dwDebugEventCode)=="EXCEPTION_DEBUG_EVENT":
exception_record = debug_event.u.Exception.ExceptionRecord
print(" ExceptionCode:", exception_record.ExceptionCode)
print(" ExceptionName:", exception_name(exception_record.ExceptionCode))
kernel32.ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
# DBG_EXCEPTION_NOT_HANDLED)
DBG_CONTINUE)
kernel32.DebugActiveProcessStop(int(pid))
print("detached :", pid)
else:
print("Error pid:", pid)
print(WinError(GetLastError()))
各Exception値の定義は私の環境では、
c:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/shared
・ ntstatus.h
に記述がありました。
今回は教科書通りに(+何となくスタックオーバーフローも)Exceptionは以下だけを定義しました。
- EXCEPTION_ACCESS_VIOLATION
- EXCEPTION_GUARD_PAGE_VIOLATION
- EXCEPTION_BREAKPOINT
- EXCEPTION_SINGLE_STEP
- EXCEPTION_STACK_OVERFLOW
デバッグイベントハンドラを定義する準備が出来たので、デバッガに組み込みます。
・・・
def get_debug_event(self):
debug_event = DEBUG_EVENT()
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
self.h_thread = self.open_thread(debug_event.dwThreadId)
self.context = self.get_thread_context(h_thread=self.h_thread)
print("Event Code: {} Thread ID: {}".format(debug_event.dwDebugEventCode, debug_event.dwThreadId))
if debug_event.dwDebugEventCode==EXCEPTION_DEBUG_EVENT:
exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode
self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress
if exception==EXCEPTION_ACCESS_VIOLATION:
print("Access Violation Detected.")
elif exception==EXCEPTION_BREAKPOINT:
continue_status = self.exception_handler_breakpoint()
elif exception==EXCEPTION_GUARD_PAGE_VIOLATION:
print("Guard Page Access Violation Detected.")
elif exception==EXCEPTION_SINGLE_STEP:
print("Single Stepping.")
elif exception==EXCEPTION_STACK_OVERFLOW:
print("Stack Overflow detected.")
# self.debugger_active = False
kernel32.ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
DBG_CONTINUE)
def exception_handler_breakpoint(self):
print("[*] Inside the breakpoint handler.")
print("Exception Address: 0x{:08X}".format(self.exception_address))
return DBG_CONTINUE
・・・
以上で、デバッグイベントハンドラが定義できました。
前々回のプログラムに少し書き足した程度でした。
まとめ
- デバッグイベントハンドラはプログラムが何らかのイベントを発生させた時に捕捉し、何らかの処理を実行するもの
- [疑問] すでに立ち上がっているプロセスをアタッチしてイベントを出力しても、CREATE_PROCESS_DEBUG_EVENTなどが発生するのはなぜか
- 私の環境ではc:/Program Files (x86)/Windows Kits/10/Include/10.0.17134.0/shared/ntstatus.hにExceptionの定義がある