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

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

## 軌跡
[リバースエンジニアリングへの道 - その5](https://qiita.com/deta-mamoru/items/31fd5b5ddef2a74376d4)

## 環境
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 「ハードウェアブレークポイント」
[前回](https://qiita.com/deta-mamoru/items/31fd5b5ddef2a74376d4)は、ソフトウェアブレークポイントを学びました。
今回は、ハードウェアブレークポイントを実装してみます。

### ハードウェアブレークポイント
ハードウェアブレークポイントは、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領域の高度なフラグがセットされている場合に、デバッグ例外やブレークポイント例外が発生した時このフラグがセットされるようです。

詳しくは[インテルデベロッパーズマニュアル](https://software.intel.com/en-us/articles/intel-sdm)を参照してみてください。(私が参照したのはVolume3-Chapter17です)

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

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

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

```py: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
...
```

```py: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)
```

```py: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フラグはセットされました。

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

```py: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
```

```py: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つまで設定可能。
- 細やかな条件を設定できる。