リバースエンジニアリングへの道
出田 守です。
最近、情報セキュリティに興味を持ち、『リバースエンジニアリング-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で紹介されています。
自作デバッガへの道 - その5 「ソフトウェアブレークポイント」
前回は、デバッグイベントハンドラを学びました。
今回は、ソフトウェアブレークポイントを実装します。
ブレークポイント
ブレークポイントには3種類あるようです。
1つ目はお馴染みのソフトウェアブレークポイント、2つ目はハードウェアブレークポイント、3つ目はメモリブレークポイントです。
ソフトウェアブレークポイントは、ブレークしたいメモリにCPUにあらかじめ仕込まれたINT3命令(0xCC)と書き換えることで実現します。
ハードウェアブレークポイントは、ハードウェアにあらかじめ内蔵されているブレークポイント用のレジスタがあり、それを使用して実現します。
メモリブレークポイントは、ブレークしたいメモリをアクセスできないような権限に変更することで実現します。
では実際にソフトウェアブレークポイントをアタッチしたプロセスに設定していきます。
まず、メモリのアクセス権限を操作します。
書き込みたいメモリ範囲が読み込み専用だとエラーを吐きます。そこで、読み込み専用から読み書き両用に変更してみたいと思います。
メモリページのアクセス権限の確認はWindows API関数「VirtualQueryEx」を使用します。
メモリページのアクセス権限の変更はWindows API関数「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には変更前のアクセス権限が格納されます。
では、メモリページのアクセス権限を操作してみます。
...
class MEMORY_BASIC_INFORMATION64(Structure):
_pack_ = 16
_fields_ = [
("BaseAddress", DWORD64),
("AllocationBase", DWORD64),
("AllocationProtect", DWORD),
("__alignment1", DWORD),
("RegionSize", DWORD64),
("State", DWORD),
("Protect", DWORD),
("Type", DWORD),
("__alignment2", DWORD),
]
from ctypes import *
from ctypes import wintypes
from defines import *
from privilege import set_debug_privilege
kernel32 = windll.kernel32
set_debug_privilege()
def open_process(pid):
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if not h_process:
print(WinError(GetLastError()))
return False
return h_process
def get_page_info(h_process, address):
mem_basic_info64 = MEMORY_BASIC_INFORMATION64()
if not kernel32.VirtualQueryEx(h_process,
address,
byref(mem_basic_info64),
sizeof(mem_basic_info64)):
return False
# print("Protect=0x{:016X}".format(mem_basic_info64.AllocationProtect))
# print("RegionSize=", mem_basic_info64.RegionSize)
# print("Protect=0x{:016X}".format(mem_basic_info64.Protect))
return mem_basic_info64
def set_page_protection(h_process, address, size, new_protect):
old_protect = wintypes.DWORD()
if not kernel32.VirtualProtectEx(h_process,
address,
size,
new_protect,
byref(old_protect)):
return False
print("old_protect=0x{:016X}".format(old_protect.value))
return old_protect
def show_protection(protect):
import defines
vdir = vars(defines)
res = [k for k, v in vdir.items() if v==protect and "PAGE" in k]
if res:
print("page_protect=", res[0])
else:
print("page_protect=UNKNOWN_PROTECT")
def main():
pid = input("pid: ")
snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, int(pid))
lpme = MODULEENTRY32()
lpme.dwSize = sizeof(lpme)
res = kernel32.Module32First(snapshot, byref(lpme))
address = None
while res:
if lpme.th32ProcessID==int(pid):
if lpme.szModule==b"msctf.dll":
print("PID: ", lpme.th32ProcessID)
print("MID: ", lpme.th32ModuleID)
# print("MODULE_ADDRESS 0x{:016X}".format(lpme.modBaseAddr))
print("MODULE_SIZE: ", lpme.modBaseSize)
print("MODULE_NAME: ", lpme.szModule)
print("MODULE_PATH: ", lpme.szExePath)
address = lpme.modBaseAddr
res = kernel32.Module32Next(snapshot, byref(lpme))
h_process = open_process(int(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()
show_protection(page_info.Protect)
old_protect = set_page_protection(h_process, address, page_info.RegionSize, PAGE_EXECUTE_READWRITE)
if not old_protect:
print(WinError(GetLastError()))
exit()
page_info = get_page_info(h_process, address)
if not page_info:
print(WinError(GetLastError()))
exit()
show_protection(page_info.Protect)
if __name__ == "__main__":
main()
次にプロセスのメモリに対して読み書きを行います。
プロセスのメモリに対して読み込みを行うには、Windows API関数「ReadProcessMemory」を使用します。
プロセスのメモリに対して書き込みを行うには、Windows API関数「WriteProcessMemory」を使用します。
プロセスのメモリの読み書き「ReadProcessMemory」・「WriteProcessMemory」
ReadProcessMemory関数の仕様は以下です。
BOOL ReadProcessMemory(
HANDLE hProcess, // プロセスのハンドル
LPCVOID lpBaseAddress, // 読み取り開始アドレス
LPVOID lpBuffer, // データを格納するバッファ
DWORD nSize, // 読み取りたいバイト数
LPDWORD lpNumberOfBytesRead // 読み取ったバイト数
);
WriteProcessMemory関数の仕様は以下です。
BOOL WriteProcessMemory(
HANDLE hProcess, // プロセスのハンドル
LPVOID lpBaseAddress, // 書き込み開始アドレス
LPVOID lpBuffer, // データバッファ
DWORD nSize, // 書き込みたいバイト数
LPDWORD lpNumberOfBytesWritten // 実際に書き込まれたバイト数
);
テストプログラムでプロセスのメモリの読み書きを試してみます。
from ctypes import *
from defines import *
from privilege import set_debug_privilege
from page_protection import get_page_info, set_page_protection, show_protection
kernel32 = windll.kernel32
set_debug_privilege()
def open_process(pid):
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
if not h_process:
print(WinError(GetLastError()))
return False
return h_process
def read_process_memory(h_process, address, length):
data = ""
read_buf = create_string_buffer(length)
count = c_ulonglong(0)
if not kernel32.ReadProcessMemory(h_process, address, read_buf, length, byref(count)):
print(WinError(GetLastError()))
return False
else:
data += str(read_buf.raw)
return data
def write_process_memory(h_process, address, data, length):
if not kernel32.WriteProcessMemory(h_process, address, data, length, 0):
print(WinError(GetLastError()))
return False
return True
def main():
pid = input("pid: ")
snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, int(pid))
lpme = MODULEENTRY32()
lpme.dwSize = sizeof(lpme)
res = kernel32.Module32First(snapshot, byref(lpme))
address = None
while res:
if lpme.th32ProcessID==int(pid):
if lpme.szModule==b"msctf.dll":
print("PID: ", lpme.th32ProcessID)
print("MID: ", lpme.th32ModuleID)
# print("MODULE_ADDRESS 0x{:016X}".format(lpme.modBaseAddr))
print("MODULE_SIZE: ", lpme.modBaseSize)
print("MODULE_NAME: ", lpme.szModule)
print("MODULE_PATH: ", lpme.szExePath)
address = lpme.modBaseAddr
res = kernel32.Module32Next(snapshot, byref(lpme))
h_process = open_process(int(pid))
page_info = get_page_info(h_process, address)
if not page_info:
print(WinError(GetLastError()))
exit()
# show_protection(page_info.Protect)
old_protect = set_page_protection(h_process, address, page_info.RegionSize, PAGE_EXECUTE_READWRITE)
if not old_protect:
print(WinError(GetLastError()))
exit()
page_info = get_page_info(h_process, address)
if not page_info:
print(WinError(GetLastError()))
exit()
show_protection(page_info.Protect)
data = read_process_memory(h_process, address, 1)
print("before: ", data)
data = "\xCC"
write_process_memory(h_process, address, data, 1)
data = read_process_memory(h_process, address, 1)
print("after: ", data)
if __name__ == "__main__":
main()
ここでは以前、番外編で紹介したモジュールリストの列挙で取得できるアドレスでメモリの読み書きをしてみました。
次に、メモリにマッピングされているモジュールのアドレスを取得します。
モジュールのアドレスを取得するにはまず、モジュールのハンドルを取得する必要があります。
モジュールのハンドルを取得するにはWindows API「GetModuleHandle」関数を使用します。
モジュールのアドレスを取得するにはWindows API「GetProcAddress」関数を使用します。
モジュールハンドルの取得・モジュールアドレスの取得「GetModuleHandle」・「GetProcAddress」
GetModuleHandle関数の仕様は以下です。
HMODULE GetModuleHandle(
LPCTSTR lpModuleName // モジュール名
);
GetProcAddress関数の仕様は以下です。
FARPROC GetProcAddress(
HMODULE hModule, // DLL モジュールのハンドル
LPCSTR lpProcName // 関数名
);
では、モジュールのアドレスを取得してみます。
from ctypes import *
from ctypes import wintypes
from defines import *
from privilege import set_debug_privilege
kernel32 = windll.kernel32
set_debug_privilege()
kernel32.GetModuleHandleW.argtypes = [
wintypes.LPCWSTR,
]
kernel32.GetModuleHandleW.restype = wintypes.HMODULE
kernel32.GetProcAddress.argtypes = [
wintypes.HMODULE,
wintypes.LPCVOID,
]
kernel32.GetProcAddress.restype = DWORD64
def main():
h_module = kernel32.GetModuleHandleW("msvcrt.dll")
# h_module = kernel32.LoadLibraryW("kernel32.dll")
if not h_module:
print(WinError(GetLastError()))
exit()
print("h_module=0x{:016X}".format(h_module))
address = kernel32.GetProcAddress(h_module, b"wprintf")
if address:
print("address=0x{:016X}".format(address))
else:
print(WinError(GetLastError()))
if __name__ == "__main__":
main()
いろいろ調べていたら、Kernel32.dllなどの主要なライブラリは全てのプロセスの共有のメモリにマッピングされるみたいです。
ここまで、メモリページのアクセス権限操作とメモリの読み書き、モジュールアドレスの取得を学びました。
最後に、自作デバッガにこれらを組み込みます。
自作デバッガへ組み込み
まず教科書ではprintf関数を使った簡単なプログラムに対してメモリ操作を試しているのでそれに習います。
import os
import time
from ctypes import *
msvcrt = cdll.msvcrt
counter = 0
pid = os.getpid()
while True:
msvcrt.wprintf("[%d]Loop iteration %d!\n" % (pid, counter))
time.sleep(2)
counter += 1
次に自作デバッガの拡張です。
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.value))
if not debugger.bp_set_sw(printf_address):
print(WinError(GetLastError()))
debugger.detach()
exit()
debugger.run()
debugger.detach()
kernel32.GetModuleHandleW.argtypes = [
wintypes.LPCWSTR,
]
kernel32.GetModuleHandleW.restype = wintypes.HMODULE
kernel32.GetProcAddress.argtypes = [
wintypes.HMODULE,
wintypes.LPCVOID,
]
kernel32.GetProcAddress.restype = DWORD64
kernel32.CloseHandle.argtypes = [
wintypes.HMODULE,
]
kernel32.CloseHandle.restype = wintypes.BOOL
...
def __init__(self):
...
self.software_breakpoints = {}
...
def read_process_memory(self, address, length):
page_info = get_page_info(self.h_process, address)
if not page_info:
print(WinError(GetLastError()))
return False
old_protect = set_page_protection(self.h_process, address, page_info.RegionSize, PAGE_EXECUTE_READWRITE)
data = ""
read_buf = create_string_buffer(length)
count = c_ulonglong(0)
if not kernel32.ReadProcessMemory(self.h_process, address, read_buf, length, byref(count)):
print(WinError(GetLastError()))
return False
else:
data = str(read_buf.raw)
return data
def write_process_memory(self, address, data):
page_info = get_page_info(self.h_process, address)
if not page_info:
print(WinError(GetLastError()))
return False
old_protect = set_page_protection(self.h_process, address, page_info.RegionSize, PAGE_EXECUTE_READWRITE)
if not kernel32.WriteProcessMemory(self.h_process, address, data, len(data), 0):
print(WinError(GetLastError()))
return False
return True
def bp_set_sw(self, address):
# print("[*] Setting breakpoint at: 0x{:016X}".format(address))
if address.value not in self.software_breakpoints:
original_byte = self.read_process_memory(address, 1)
if not original_byte:
return False
self.write_process_memory(address, "\xCC")
self.software_breakpoints[address.value] = (original_byte)
return True
def func_resolve(self, dll, function):
handle = kernel32.GetModuleHandleW(dll)
if not handle:
print(WinError(GetLastError()))
return False
address = kernel32.GetProcAddress(handle, function)
if not address:
print(WinError(GetLastError()))
kernel32.CloseHandle(handle)
return False
kernel32.CloseHandle(handle)
return address
まず、my_printf_loop.pyを実行してから、my_test.pyを実行します。
すると、wprintf関数にソフトウェアブレークポイントが仕込まれるので、そこで例外が補足されます。
今回は、ソフトウェアブレークポイントの実装をしてみました。
アクセス権限操作やメモリの読み書きなど重要そうな回だったと思います。
まとめ
- ブレークポイントには3種類ある。
- メモリページのアクセス権限の確認と変更はそれぞれ「VirtualQueryEx」・「VirtualProtectEx」関数を用いる。
- プロセスのメモリの読み書きはそれぞれ「ReadProcessMemory」・「WriteProcessMemory」関数を用いる
- モジュールハンドルの取得・モジュールアドレスの取得はそれぞれ「GetModuleHandle」・「GetProcAddress」関数を用いる。