リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
自作デバッガへの道 - その6 「ハードウェアブレークポイント」
前回は、ソフトウェアブレークポイントを学びました。
今回は、ハードウェアブレークポイントを実装してみます。
ハードウェアブレークポイント
ハードウェアブレークポイントは、CPUに組み込まれたデバッグ専用のレジスタ(以降、デバッグレジスタ)を使用して実現します。
教科書の例では、マルウェアなどのデバッグでソフトウェアブレークポイントを使用した場合、既存の命令からINT3命令に書き換えます。このとき、既存のCRCチェックサムと変更後のCRCチェックサムが異なれば自分自身をkillしてデバッグさせないようにしてしまうらしいです。
そこで、コードを書き換えないハードウェアブレークポイントが役に立つということですね。
デバッグレジスタは合計8つ(DR0-DR7)用意されています。
DR0-DR3は、ブレークしたいアドレスを格納します。
DR4-DR5は、予約済み領域として使用しません。
DR6は、デバッグステータスが記録されています。
DR7は、デバッグ時にさまざまな条件を指定するために使用します。
DR7
DR7は、デバッグ時にさまざまな条件を指定するために使用します。
DR7には、L0-3, G0-3, LE, GE, RTM, GD, RW0-3, LEN0-3というセクションがあります。
- L0-3は、現在のタスクでブレークポイント条件を有効にするときにセットします。このフラグは、タスクが切り替わった時に自動的にクリアされるようです。
- G0-3は、全てのタスクでブレークポイント条件を有効にするときにセットします。このフラグは、タスクが切り替わってもクリアされません。
- LEとGEは、このフラグをセットすると、ブレークポイント条件を満たした命令を正確に検出するようです。前後互換性を保つためにフラグをセットすることをおすすめしているようです。LとGは現在のタスクか全てのタスクかの違いでしょう。
- RTMは、RTM領域の高度なデバッグをセットしたい時に有効にします。RTM領域とはなんでしょうか。ちょっと資料を見た限りだと、XBEGINから始まりXENDで終わる領域だそうです。XENDに到達した時点でそのメモリ領域内の命令が瞬時に見られるそうです。どういうときに使うのでしょうか。
- GDは、このフラグをセットするとデバッグレジスタを保護します。MOV命令などで、デバッグレジスタにアクセスしようとする前に例外を発生させるようです。このとき、DR6のBDフラグがセットされます。デバッグハンドラへの入力時には、ハンドラがデバッグフラグにアクセスできるように、このフラグは自動的にクリアされます。
- RW0-3は、条件をフラグでセットします。制御レジスタのCR4のDEフラグがセットされている場合とされていない場合で条件が少し変わります。
[DEフラグがセットされている場合]
00 - 実行された時にのみブレーク
01 - データ書き込み時にのみブレーク
10 - I/Oの読み込みまたは書き込み時にブレーク
11 - データ書き込みまたは読み込み時にブレーク(ただし実行ではブレークしない)
[DEフラグがセットされていない場合]
00 - 実行された時にのみブレーク
01 - データ書き込み時にのみブレーク
10 - 未定義
11 - データ書き込みまたは読み込み時にブレーク(ただし実行ではブレークしない) - LEN0-3は、DRnで指定したアドレスのメモリ位置のサイズをフラグで指定します。このフラグについては、指定したアドレス範囲にブレークする条件があれば例外を発生させるということでしょうか。
00 - 1 byte
01 - 2 byte
10 - 未定義(プロセッサの種類によっては8byte)
11 - 4 byte
DR6
DR6は、最後に発生した例外のデバッグ状態が記録されています。
DR6には、B0-3,BD,BS,BT,RTMというセクションがあります。
- B0-3は、DR7のLENnとRWnのフラグがセットされている場合に、条件が満たされたかどうかがセットされるようです。
- BDは、DR7のGDフラグがセットされている場合に、次の命令がDR0-DR7のどれか一つにアクセスするときにセットされるようです。
- BSは、EFLAGSレジスタのTFがセットされている場合に、シングルステップ例外モードの時にこのフラグがセットされます。
- BTは、TSSのTフラグがセットされている場合に、タスクスイッチからの例外が発生した時にセットされるようです。タスクを切り替えた最初の命令の前に例外が発生するようです。
- RTMは、RTM領域の高度なフラグがセットされている場合に、デバッグ例外やブレークポイント例外が発生した時このフラグがセットされるようです。
詳しくはインテルデベロッパーズマニュアルを参照してみてください。(私が参照したのはVolume3-Chapter17です)
ソフトウェアブレークポイントとは違い、さまざまな条件を指定できるのも特徴です。
ここまで、ハードウェアブレークポイントについて学びました。
ここからは、ハードウェアブレークポイントを実際に実装してみます。
ハードウェアブレークポイント実装
...
CONDITION_EXECUTE = 0x0
CONDITION_WRITE_ONLY = 0x1
CONDITION_IO_READ_WRITE = 0x2
CONDITION_READ_WRITE = 0x3
LENGTH_BYTE = 0x0
LENGTH_WORD = 0x1
LENGTH_QWORD = 0x2
LENGTH_DWORD = 0x3
...
from ctypes import *
from ctypes import wintypes
from defines import *
from privilege import set_debug_privilege
from thread_context import get_thread_context, set_thread_context
kernel32 = windll.kernel32
set_debug_privilege()
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
def debug_event_detector(pid):
if kernel32.DebugActiveProcess(int(pid)):
print("attached :", pid)
debug_event = DEBUG_EVENT()
print("pid:", debug_event.dwProcessId)
while True:
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))
print(" ExceptionAddress: 0x{:016X}".format(exception_record.ExceptionAddress))
if exception_name(exception_record.ExceptionCode)=="EXCEPTION_SINGLE_STEP":
h_thread = kernel32.OpenThread(THREAD_ALL_ACCESS, None, debug_event.dwThreadId)
context = get_thread_context(h_thread)
print(" [EFlags]{:016b}".format(context.EFlags))
print(" [Dr0]{:016X}".format(context.Dr0))
print(" [Dr1]{:016X}".format(context.Dr1))
print(" [Dr2]{:016X}".format(context.Dr2))
print(" [Dr3]{:016X}".format(context.Dr3))
print(" [Dr6]{:016b}".format(context.Dr6))
print(" [Dr7]{:016b}".format(context.Dr7))
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()))
if __name__ == "__main__":
pid = input("pid: ")
detector(pid)
from ctypes import *
from ctypes import wintypes
from defines import *
from privilege import set_debug_privilege, show_privilege_information
from func_address import get_func_address
from event_detector import debug_event_detector
from thread_context import get_thread_context, set_thread_context
from thread_enumerate import get_thread_ids
kernel32 = windll.kernel32
set_debug_privilege()
DR_N = 0 # DR number (0-3)
def main():
address = get_func_address("msvcrt.dll", b"wprintf")
if not address:
print(WinError(GetLastError()))
exit()
pid = int(input("pid: "))
for tid in get_thread_ids(pid):
h_thread = kernel32.OpenThread(THREAD_ALL_ACCESS, None, tid)
if h_thread:
# print(" h_thread: ", h_thread)
context = get_thread_context(h_thread)
if DR_N==0:
context.Dr0 = address
elif DR_N==1:
context.Dr1 = address
elif DR_N==2:
context.Dr1 = address
elif DR_N==3:
context.Dr1 = address
else:
print("invalid DR_N")
context.Dr7 = 3<<(DR_N*2)
context.Dr7 |= CONDITION_EXECUTE<<(DR_N*4+16)
context.Dr7 |= LENGTH_BYTE<<(DR_N*4+18)
if set_thread_context(h_thread, context):
print("[Dr0]0x{:016X}".format(context.Dr0))
print("[Dr7]{:016b}".format(context.Dr7))
else:
print(WinError(GetLastError()))
kernel32.ResumeThread(h_thread)
else:
print(WinError(GetLastError()))
debug_event_detector(pid)
if __name__ == "__main__":
main()
前回作成したmy_printf_loop.pyに対してハードウェアブレークポイントを試してみました。
とりあえず、GもLもフラグをセットしています。
今までに作成した処理は関数にまとめていたりします。
Len0-3はやはりよく分かりませんでした。
あと、教科書にあるとおりシングルステップ例外が発生しても、BSフラグはセットされていませんでした。試しにTFをセットすると、正しくBSフラグはセットされました。
では、ハードウェアブレークポイントも自作デバッガに組み込みます。
自作デバッガに組み込み
...
def __init__(self):
...
self.first_breakpoint = True
self.hardware_breakpoints = {}
...
def get_debug_event(self):
...
elif exception==EXCEPTION_SINGLE_STEP:
continue_status = self.exception_handler_single_step()
...
def exception_handler_single_step(self):
print("[*] Hardware breakpoint handler.")
if self.context.Dr6&0x1 and 0 in self.hardware_breakpoints:
slot = 0
elif self.context.Dr6&0x2 and 1 in self.hardware_breakpoints:
slot = 1
elif self.context.Dr6&0x4 and 2 in self.hardware_breakpoints:
slot = 2
elif self.context.Dr6&0x8 and 3 in self.hardware_breakpoints:
slot = 3
else:
continue_status = DBG_EXCEPTION_NOT_HANDLED
if self.bp_del_hw(slot):
continue_status = DBG_CONTINUE
print("[*] Hardware breakpoint removed.")
return continue_status
def bp_del_hw(self, slot):
for thread_id in self.enumerate_threads():
context = self.get_thread_context(thread_id=thread_id)
context.Dr7 &= ~(3<<slot*2)
if slot==0:
context.Dr0 = 0x0
elif slot==1:
context.Dr1 = 0x0
elif slot==2:
context.Dr2 = 0x0
elif slot==3:
context.Dr3 = 0x0
context.Dr7 &= ~(3<<slot*4+16)
context.Dr7 &= ~(3<<slot*4+18)
h_thread = self.open_thread(thread_id)
kernel32.SetThreadContext(h_thread, byref(context))
del self.hardware_breakpoints[slot]
return True
...
def bp_set_hw(self, address, length, condition):
if length not in (LENGTH_BYTE, LENGTH_WORD, LENGTH_QWORD, LENGTH_DWORD):
return False
if condition not in (CONDITION_EXECUTE, CONDITION_WRITE_ONLY, CONDITION_IO_READ_WRITE, CONDITION_READ_WRITE):
return False
if 0 not in self.hardware_breakpoints:
available = 0
elif 1 not in self.hardware_breakpoints:
available = 1
elif 2 not in self.hardware_breakpoints:
available = 2
elif 3 not in self.hardware_breakpoints:
available = 3
else:
return False
for thread_id in self.enumerate_threads():
context = self.get_thread_context(thread_id=thread_id)
context.Dr7 |= 3<<(available*2)
if available==0:
context.Dr0 = address
elif available==1:
context.Dr1 = address
elif available==2:
context.Dr2 = address
elif available==3:
context.Dr3 = address
context.Dr7 |= condition<<(available*4+16)
context.Dr7 |= length<<(available*4+18)
h_thread = self.open_thread(thread_id)
kernel32.SetThreadContext(h_thread, byref(context))
self.hardware_breakpoints[available] = (address, length, condition)
return True
from ctypes import *
from ctypes import wintypes
import my_debugger
from defines import *
from privilege import set_debug_privilege
set_debug_privilege()
debugger = my_debugger.Debugger()
# debugger.load("C:\\Windows\\System32\\calc.exe")
pid = int(input("PID: "))
# print("0x{:016X}".format(debugger.h_process))
debugger.attach(pid)
# thread_list = debugger.enumerate_threads()
# for thread in thread_list:
# thread_context = debugger.get_thread_context(thread)
# print("[*] Dumping registers for thread ID: 0x{:016X}".format(thread))
# print("[**] RIP: 0x{:08X}".format(thread_context.Rip))
# print("[**] RSP: 0x{:08X}".format(thread_context.Rsp))
# print("[**] RBP: 0x{:08X}".format(thread_context.Rbp))
# print("[**] RAX: 0x{:08X}".format(thread_context.Rax))
# print("[**] RBX: 0x{:08X}".format(thread_context.Rbx))
# print("[**] RCX: 0x{:08X}".format(thread_context.Rcx))
# print("[**] RDX: 0x{:08X}".format(thread_context.Rdx))
# print("[*] END DUMP")
printf_address = debugger.func_resolve("msvcrt.dll", b"wprintf")
# printf_address = cast(printf_address, wintypes.LPCVOID)
print("[*] Address of printf: 0x{:016X}".format(printf_address))
# if not debugger.bp_set_sw(printf_address):
# print(WinError(GetLastError()))
# debugger.detach()
# exit()
debugger.bp_set_hw(printf_address, LENGTH_BYTE, CONDITION_EXECUTE)
debugger.run()
debugger.detach()
ここまでハードウェアブレークポイントを実装しました。
ハードウェアブレークポイントは4つまでしか設定できません。
そのかわり、実行時や読み書き時などの細やかな条件を設定できます。
今後、リバースエンジニアリングをしていくと使わざるおえないときが来るのでしょうか。
楽しみですね。
まとめ
- ハードウェアブレークポイントは、CPUに組み込まれたデバッグ専用のレジスタで実現します。
- コードを書き換えた時に不具合があるときに、ハードウェアブレークポイントを使う。
- デバッグレジスタは合計8つ(DR0-DR7)用意されている。
- ハードウェアブレークポイントは、4つまで設定可能。
- 細やかな条件を設定できる。