リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
自作デバッガへの道 - その2 「プロセスをアタッチ」
前回は、デバッガからプロセスを生成しました。
今回は、すでに生成されているプロセスをデバッガの制御下に置きます。このことをデバッガにプロセスをアタッチするというみたいです。
その前にプロセスのハンドルを取得しておきたいと思います。
ハンドルはWindows API関数を使っていろいろな操作をプロセスに対して行うのに必要になってきます。
ハンドルを取得するために使用するWindows API関数は「OpenProcess」です。
ハンドルを取得し、閉じる「OpenProcess」・「CloseProcess」
OpenProcessの仕様は以下です。
HANDLE OpenProcess(
DWORD dwDesiredAccess, // アクセスフラグ
BOOL bInheritHandle, // ハンドルの継承オプション
DWORD dwProcessId // プロセス識別子
);
引数のdwDesiredAccessは、そのプロセスに対するアクセス権を指定します、bInheritHandleはFalseに設定し、dwProcessIdはアタッチするプロセスのID(PID)です。
かえってきたハンドルを閉じるためのAPI関数は「CloseHandle」です。
CloseHandleの仕様は以下です。
BOOL CloseHandle(
HANDLE hObject // オブジェクトのハンドル
);
実際にOpenProcessを使用してみたいと思います。
from ctypes import *
PROCESS_ALL_ACCESS = 0x001F0FFF
from ctypes import *
from openprocess_define import *
kernel32 = windll.kernel32
pid = int(input("pid: "))
handle = kernel32.OpenProcess(PROCESS_ALL_ACCESS,
False,
pid)
print("0x{:08X}".format(handle))
if kernel32.CloseHandle(handle):
print("handle closed")
else:
print(WinError(GetLastError()))
OpenProcessに成功するとプロセスのハンドルがかえってき、失敗すると0がかえってくるようです。
ちなみに、PROCESS_ALL_ACCESSの値を調べるのにとても苦労しました。
以下に、載っているのかと思ったのですが、PROCESS_ALL_ACCESSの値だけ記載されておらず。
MSDN-Process Security and Access Rights
他のアクセス権の合計値(0x00101FFB)なのかなと予想していますが、とりあえず教科書どおりの値にしておきました。どこで調べられるかわかる方いたら教えてください!
GetLastErrorは最新のエラーコードがかえってきます。
このエラーコードをWinErrorに渡してあげることでエラーメッセージを表示させることが出来ました。WinErrorはctypesライブラリの関数のようです。
MSDN-GetLastError
PythonDocument-ctypes
ハンドルを取得したあとはいよいよプロセスをアタッチします。
逆にプロセスをデバッガから切り離す行為は、デタッチといいます。
アタッチするためのWindows API関数は「DebugActiveProcess」です。
デタッチするためのWindows API関数は「DebugActiveProcessStop」です。
プロセスのアタッチとデタッチ「DebugActiveProcess」・「DebugActiveProcessStop」
DebugActiveProcessの仕様は以下です。
BOOL DebugActiveProcess(
DWORD dwProcessId // デバッグ対象のプロセス
);
引数はdwProcessIdで先程も説明したとおり、アタッチしたいプロセスID(PID)です。
DebugActiveProcessStopもPIDだけを引数にとります。
では、実際にプロセスをアタッチしてデタッチしてみます。
from ctypes import *
kernel32 = windll.kernel32
pid = input("pid: ")
if kernel32.DebugActiveProcess(int(pid)):
print("attached :", pid)
kernel32.DebugActiveProcessStop(int(pid))
print("detached :", pid)
else:
print("Error pid:", pid)
print(WinError(GetLastError()))
テストでは電卓を立ち上げて、そのPIDをテストプログラムに入力しました。
実はここでかなり苦戦しました。
Windows 10 Proでこのテストプログラムを実行すると、エラーコード50(The request is not supported.)やエラーコード87(The parameter is incorrect.)と出ました。調べてみると、電卓が32bitで実行されていて、OSかctypesが64bitを想定しているため、エラーが発生するようです。
同じようなエラーになっている人もいるようで以下にリンクを掲載しておきます。
https://teratail.com/questions/104853
ただし、上記リンク先の解決策を試してもこちらの環境では解決されませんでした。
そこで、試しにWindows 10 Homeで環境を作り直し、先ほどのテストプログラムを実行したところ、うまくいきました。なので、以降はWindows 10 Homeで行います。
他に原因や解決策などわかる方いらっしゃいましたらコメント欄に記載いただけると助かります。
プロセスのアタッチとデタッチが出来たので、次はデバッガがイベントを捕捉できるようにします。
イベントを捕捉するためのWindows API関数は「WaitForDebugEvent」です。
イベントの捕捉「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を指定します。それぞれ、スレッドの実行を続けるか、例外の処理を続けるかという意味です。
では、実際にイベントを捕捉してみます。
構造体・共用体の定義
前回ctypesでの構造体の定義はすでに説明しました。
共用体の定義は構造体の定義とほぼ同じです。
from ctypes import *
class U(Union):
_fields_ = [
("メンバ名", 型),
...
]
では、必要な構造体・共用体の定義をします。
from ctypes import *
# types
DWORD = c_ulong
PVOID = c_void_p
ULONG_PTR = c_ulong
# constant
INFINITE = 0xFFFFFFFF
DBG_CONTINUE = 0x00010002
DBG_EXCEPTION_NOT_HANDLED = 0x80010001
class EXCEPTION_RECORD(Structure):
pass
EXCEPTION_RECORD._fields_ = [
("ExceptionCode", DWORD),
("ExceptionFlags", DWORD),
("ExceptionRecord", POINTER(EXCEPTION_RECORD)),
("ExceptionAddress", PVOID),
("NumberParameters", DWORD),
("ExceptionInformation", ULONG_PTR*15),
]
class EXCEPTION_RECORD(Structure):
_fields_ = [
("ExceptionCode", DWORD),
("ExceptionFlags", DWORD),
("ExceptionRecord", POINTER(EXCEPTION_RECORD)),
("ExceptionAddress", PVOID),
("NumberParameters", DWORD),
("ExceptionInformation", ULONG_PTR*15),
]
class EXCEPTION_DEBUG_INFO(Structure):
_fields_ = [
("ExceptionRecord", EXCEPTION_RECORD),
("dwFirstCance", DWORD),
]
class U(Union):
_fields_ = [
("Exception", EXCEPTION_DEBUG_INFO),
]
class DEBUG_EVENT(Structure):
_fields_ = [
("dwDebugEventCode", DWORD),
("dwProcessId", DWORD),
("dwThreadId", DWORD),
("u", U),
]
次にイベントを捕捉し、実行を再開するテストプログラムです。
from ctypes import *
from defines import *
kernel32 = windll.kernel32
pid = input("pid: ")
if kernel32.DebugActiveProcess(int(pid)):
print("attached :", pid)
debug_event = DEBUG_EVENT()
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
print("DebugEventCode:", debug_event.dwDebugEventCode)
print("pid:", debug_event.dwProcessId)
print("tid:", debug_event.dwThreadId)
input("Press Enter to continue...")
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()))
例によって電卓を立ち上げ、PIDを入力すると即座にイベントを捕捉しイベントコードを出力しました。私の環境ではイベントコードは3と出力されました。これはCREATE_PROCESS_DEBUG_EVENTで、おそらくプロセスをattachしたら最初に呼ばれるのかもしれません。
このテストプログラムの場合、ContinueDebugEventにはDBG_EXCEPTION_NOT_HANDLEDでもDBG_CONTINUEでも良さそうそうです。
私だけかもしれませんが、注意していただきたいのがWaitForDebugEventにDEBUG_EVENT構造体を渡すとき、byref(参照渡し)をつけて渡してあげてください。他のWindows API関数でも同じです。これを忘れていてイベントコードが0しか出力されず苦悶していました。
ここまでで、イベントを捕捉することができました。
では、これまでのことを踏まえて前回のデバッガに機能を追加していきます。
...
if kernel32.CreateProcessW(path_to_exe,
...)
self.h_process = self.open_process(process_information.dwProcessId)
...
def open_process(self, pid):
handle = kernel32.OpenProcess(PROCESS_ALL_ACCESS,
False,
pid)
if not handle:
print(WinError(GetLastError()))
return handle
def attach(self, pid):
self.handle = self.open_process(pid)
if kernel32.DebugActiveProcess(pid):
self.debugger_active = True
self.pid = int(pid)
return True
else:
print(WinError(GetLastError()))
return False
def run(self):
while self.debugger_active:
self.get_debug_event()
def get_debug_event(self):
debug_event = DEBUG_EVENT()
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
input("Press a key to continue...")
self.debugger_active = False
kernel32.ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
DBG_CONTINUE)
def detach(self):
if kernel32.DebugActiveProcessStop(self.pid):
print("[*] Finished debugging. Exiting...")
return True
else:
print(WinError(GetLastError()))
return False
次にテストプログラムとして、
import my_debugger
debugger = my_debugger.Debugger()
# debugger.load("C:\\Windows\\System32\\calc.exe")
pid = int(input("PID: "))
debugger.attach(pid)
debugger.run()
debugger.detach()
あとは、テストプログラムにPIDを入力して確かめるだけです。
以上で、デバッガがイベントを捕捉するところまで理解することが出来ました。
Windows 10 Proでの問題などありましたが、ひとまずOKとします。
まとめ
- アタッチするプロセスのハンドルを取得しておくと後々便利。
- ハンドルを取得・閉じるには「OpenProcess」・「CloseHandle」を使う。
- プロセスをアタッチ・デタッチするには「DebugActiveProcess」・「DebugActiveProcessStop」を使う。
- Windows 10 Proだと電卓をアタッチ出来なかった。Windows 10 Homeだと出来た。
- イベントの捕捉には「WaitForDebugEvent」を使う。
- プロセスの再開には「ContinueDebugEvent」を使う。