VOOZH about

URL: https://qiita.com/deta-mamoru/items/66c813aa9a77cd662500

⇱ リバースエンジニアリングへの道 - その6 #Security - Qiita


👁 Image
8

Go to list of users who liked

2

Share on X(Twitter)

Share on Facebook

Add to Hatena Bookmark

More than 5 years have passed since last update.

@deta-mamoru(出田 守)

リバースエンジニアリングへの道 - その6

8
Posted at

リバースエンジニアリングへの道

出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-Pythonによるバイナリ解析技法』という本(以降、「教科書」と呼びます)を読みました。
「こんな世界があるのか!かっこいい!」と感動し、私も触れてみたいということでド素人からリバースエンジニアリングができるまでを書いていきたいと思います。
ちなみに、教科書ではPython言語が使用されているので私もPython言語を使用しています。
ここを見ていただいた諸先輩方からの意見をお待ちしております。

軌跡

リバースエンジニアリングへの道 - その5

環境

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です)

ソフトウェアブレークポイントとは違い、さまざまな条件を指定できるのも特徴です。

ここまで、ハードウェアブレークポイントについて学びました。
ここからは、ハードウェアブレークポイントを実際に実装してみます。

ハードウェアブレークポイント実装

defines.py
...
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
...
event_detector.py
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)
test_set_hw_break.py
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フラグはセットされました。

では、ハードウェアブレークポイントも自作デバッガに組み込みます。

自作デバッガに組み込み

my_debugger.py
...
 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
my_test.py
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つまで設定可能。
  • 細やかな条件を設定できる。
8

Go to list of users who liked

2
0

Go to list of comments

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8

Go to list of users who liked

2