リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
自作デバッガへの道 - その7 「メモリブレークポイント」
前回は、ハードウェアブレークポイントを学びました。
今回は、メモリブレークポイントを実装してみます。
メモリブレークポイントは、保護された目的のメモリページにCPUがアクセスした時点で例外を発生させることで実現します。
以前に、メモリページのアクセス権限を操作する方法を学びました。
これを利用して、目的のメモリページを保護する権限を付加します。
Windows API関数「VirtualQueryEx」「VirtualProtectEx」を使用します。
再掲しておきます。
[再掲]メモリページのアクセス権限の確認と変更「VirtualQueryEx」・「VirtualProtectEx」
VirtualQueryEx関数の仕様は以下です。
DWORD VirtualQueryEx(
HANDLE hProcess, // プロセスのハンドル
LPCVOID lpAddress, // 領域のアドレス
PMEMORY_BASIC_INFORMATION lpBuffer, // 情報バッファのアドレス
DWORD dwLength // バッファのサイズ
);
lpBufferはMEMORY_BASIC_INFORMATION構造体のポインタです。
MEMORY_BASIC_INFORMATION構造体の仕様は以下です。
typedef struct _MEMORY_BASIC_INFORMATION {
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
} MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;
BaseAddressは指定したページのアドレスです。
AllocationBaseはVirtualAlloc関数を使用した場合のページのアドレスみたいです。
AllcoationProtectはAllocationBaseのページのアクセス権限です。
RegionSizeはベースアドレスからのサイズです。
Stateはページ領域の状態です。
Protectはページ領域のアクセス権限です。
Typeはページ領域の種類です。
VirtualProtectEx関数の仕様は以下です。
BOOL VirtualProtectEx(
HANDLE hProcess, // プロセスのハンドル
LPVOID lpAddress, // コミット済みページ領域のアドレス
DWORD dwSize, // 領域のサイズ
DWORD flNewProtect, // 希望のアクセス保護
PDWORD lpflOldProtect // 従来のアクセス保護を取得する変数のアドレス
);
lpflOldProtectには変更前のアクセス権限が格納されます。
次に、教科書では正確なページサイズを取得するために、Windows API関数「GetSystemInfo」を使用しています。
システム情報取得「GetSystemInfo」
仕様は以下です。
VOID GetSystemInfo(
LPSYSTEM_INFO lpSystemInfo // システム情報
);
lpSystemInfoは、SYSTEM_INFO構造体のポインタです。
typedef struct _SYSTEM_INFO {
union {
DWORD dwOemId;
struct {
WORD wProcessorArchitecture;
WORD wReserved;
};
};
DWORD dwPageSize;
LPVOID lpMinimumApplicationAddress;
LPVOID lpMaximumApplicationAddress;
DWORD_PTR dwActiveProcessorMask;
DWORD dwNumberOfProcessors;
DWORD dwProcessorType;
DWORD dwAllocationGranularity;
WORD wProcessorLevel;
WORD wProcessorRevision;
} SYSTEM_INFO;
dwOemIdは、古いバージョンの互換性を保つためのメンバです。
wProcessorArchitectureは、CPUの種類を表します。
wReservedは、予約メンバです。
dwPageSizeは、ページサイズです。
lpMinimumAppricationAddressは、アプリケーションとDLLへアクセス可能な最下位アドレスのポインタです。
lpMaximumAppricationAddressは、アプリケーションとDLLへアクセス可能な最上位アドレスのポインタです。
dwActiveProcessorMaskは、システムで有効なプロセッサをマスクで表しているということでしょうか。
dwNumberOfProcessorsは、現在のグループの論理プロセッサ数を表すみたいです。ただし、取得にはGetLogicalProcessorInformation関数を使用します。
dwProcessorTypeは、古いバージョンの互換性を保つためのメンバです。
dwAllocationGranularityは、仮想メモリを割り当てることが出来る開始アドレスの細かさみたいです。
dwProcessorLevelは、アーキテクチャ依存のプロセッサレベルだそうです。これは何でしょうか。
dwProcessorRevisionは、アーキテクチャ依存のプロセッサリビジョンだそうです。これもよく分からんですね。
では、システム情報を取得してみます。
...
class PROCESSOR_ARCHITECTURE(Structure):
_fields_ = [
("wProcessorArchitecture", WORD),
("wReserved", WORD),
]
class U_PROCESSOR_ARCHITECTURE(Union):
_fields_ = [
("dwOemId", DWORD),
("proc_arch", PROCESSOR_ARCHITECTURE),
]
class SYSTEM_INFO(Structure):
_fields_ = [
("uproc_arch", U_PROCESSOR_ARCHITECTURE),
("dwPageSize", DWORD),
("lpMinimumApplivationAddress", LPVOID),
("lpMaximumApplivationAddress", LPVOID),
("dwActiveProcessorMask", POINTER(DWORD)),
("dwNumberOfProcessors", DWORD),
("dwProcessorType", DWORD),
("wProcessorLevel", WORD),
("wProcessorRevision", WORD),
]
from ctypes import *
from ctypes import wintypes
from defines import *
from privilege import set_debug_privilege
kernel32 = windll.kernel32
set_debug_privilege()
def main():
sys_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(sys_info))
print("wProcessorArchitecture:0x{:04X}".format(sys_info.uproc_arch.proc_arch.wProcessorArchitecture))
print("dwPageSize:0x{:08X}:{}".format(sys_info.dwPageSize, sys_info.dwPageSize))
print("lpMinimumApplivationAddress:0x{:08X}".format(sys_info.lpMinimumApplivationAddress))
print("lpMaximumApplivationAddress:0x{:08X}".format(sys_info.lpMaximumApplivationAddress))
# print("dwActiveProcessorMask:0x{:08X}".format(sys_info.dwActiveProcessorMask.contents.value))
print("dwNumberOfProcessors:0x{:08X}".format(sys_info.dwNumberOfProcessors))
print("dwProcessorType:0x{:08X}".format(sys_info.dwProcessorType))
print("wProcessorLevel:0x{:04X}".format(sys_info.wProcessorLevel))
print("wProcessorRevision:0x{:04X}".format(sys_info.wProcessorRevision))
if __name__ == "__main__":
main()
私の環境でのページサイズは4096バイトでした。
dwActiveProcessorMaskを表示しようとすると、何故かpythonが止まってしまうのでコメントアウトしています。
では次に、目的のメモリページに保護権限を付加してみます。
from ctypes import *
from ctypes import wintypes
from defines import *
from privilege import set_debug_privilege
from page_protection import get_page_info, set_page_protection, show_protection
from proc_address import get_func_address
kernel32 = windll.kernel32
set_debug_privilege()
def main():
sys_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(sys_info))
page_size = sys_info.dwPageSize
if page_size<=0:
print(WinError(GetLastError()))
exit()
address = cast(get_func_address("msvcrt.dll", b"wprintf"), POINTER(LPVOID))
if not address:
print(WinError(GetLastError()))
exit()
pid = int(input("pid: "))
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if not h_process:
print(WinError(GetLastError()))
exit()
page_info = get_page_info(h_process, address)
if not page_info:
print(WinError(GetLastError()))
exit()
print("BaseAddress:0x{:016X}".format(page_info.BaseAddress))
show_protection(page_info.Protect)
current_page = page_info.BaseAddress
size = 16
while current_page <= page_info.BaseAddress+size:
old_protect = set_page_protection(h_process, cast(current_page, POINTER(LPVOID)), size, page_info.Protect|PAGE_GUARD)
if not old_protect:
print(WinError(GetLastError()))
exit()
current_page += page_size
page_info = get_page_info(h_process, address)
show_protection(page_info.Protect)
if __name__ == "__main__":
main()
教科書ではページ間を超えた保護が必要な場合に備えて実装されていましたのでそれにならいます。
おなじみのmy_printf_loop.pyを起動し、以前作成したdetector.pyも起動し、最後にtest_page_guard.pyを起動すると、detector.pyにEXCEPTION_GUARD_PAGE_VIOLATIONが表示されることを確認しました。
ちなみに、例外が捕捉した時にプロセッサは自動的にそのページの保護を解除するようです。
最後に、自作デバッガにメモリブレークポイントを組み込みます。
自作デバッガへ組み込み
...
def __init__(self):
...
self.guarded_pages = []
self.memory_breakpoints = {}
sys_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(sys_info))
self.page_size = sys_info.dwPageSize
...
def bp_set_mem(self, address, size):
page_info = MEMORY_BASIC_INFORMATION64()
if kernel32.VirtualQueryEx(self.h_process, cast(address, POINTER(LPVOID)), byref(page_info), sizeof(page_info))<sizeof(page_info):
return False
current_page = page_info.BaseAddress
while current_page<=address+size:
self.guarded_pages.append(current_page)
old_protection = wintypes.DWORD()
if not kernel32.VirtualProtectEx(self.h_process, cast(current_page, POINTER(LPVOID)), size, page_info.Protect|PAGE_GUARD, byref(old_protection)):
return False
current_page += self.page_size
self.memory_breakpoints[address] = (size, page_info)
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.bp_set_mem(printf_address, 16)
debugger.run()
debugger.detach()
ちなみにサイズ16は適当です。
ここまで、メモリブレークポイントを学びました。
一応教科書では自作デバッガ作成はここまででした。
デバッガについて基本的な一部を学べました。
まとめ
- メモリブレークポイントは、保護された目的のメモリページにCPUがアクセスした時点で例外を発生させることで実現。
- システム情報を取得するには「GetSystemInfo」関数を用いる。
- メモリページのアクセス権限の確認と変更には「VirtualQueryEx」・「VirtualProtectEx」関数を用いる。