## 3.4 全能的斷點
現在我們已經有了一個能夠正常運行的調試器核心,是時候加入斷點功能了。用我們在第二章學到的,實現設置軟件,硬件,內存三種斷點的功能。接著實現與之對應的斷點處理 函數,最后在斷點被擊中之后干凈的恢復進程。
### 3.4.1 軟件斷點
為了設置軟件斷點,我們必須能夠將數據寫入目標進程的內存。這需要通過 ReadProcessMemory() 和 WriteProcessMemory()實現。它們非常相似:
```
BOOL WINAPI ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesRead
);
BOOL WINAPI WriteProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPCVOID lpBuffer,
SIZE_T nSize,
SIZE_T* lpNumberOfBytesWritten
);
```
這兩個函數都允許調試器觀察和更新被調試的進程的內存。參數也都很簡單。 lpBaseAddress 是要開始讀或者些的目標地址, lpBuffer 指向一塊緩沖區,用來接收lpBaseAddress讀出的數據或者寫入 lpBaseAddress 。 nSize 是 想 要 讀 寫 的 數 據 大 小 , lpNumberOfBytesWritten 由函數填寫,通過它我們就能夠知道一次操作過后實際讀寫了的數 據。
現在讓我們的調試器實現軟件斷點就相當容易了。修改調試器的核心類,以支持設置和 處理軟件斷點。
```
#my_debugger.py
...
class debugger():
def init (self):
self.h_process = None
self.pid = None
self.debugger_active = False
...
self.h_thread = None
self.context = None
self.breakpoints = {}
def read_process_memory(self,address,length): data = ""
read_buf = create_string_buffer(length)
count = c_ulong(0)
if not kernel32.ReadProcessMemory(self.h_process,
address,
read_buf,
length,
byref(count)):
return False
else:
data += read_buf.raw return data
def write_process_memory(self,address,data): count = c_ulong(0)
length = len(data)
c_data = c_char_p(data[count.value:])
if not kernel32.WriteProcessMemory(self.h_process,
address,
c_data,
length,
byref(count)):
return False
else:
return True
def bp_set(self,address):
if not self.breakpoints.has_key(address):
try:
# store the original byte
original_byte = self.read_process_memory(address, 1)
# write the INT3 opcode
self.write_process_memory(address, "\xCC")
# register the breakpoint in our internal list self.breakpoints[address] = (address, original_byte)
except:
return False
return True
```
現在調試器已經支持軟件斷點了,我們需要找個地址設置一個試試看。一般斷點設置在 函數調用的地方,為了這次實驗,我們就用老朋友 printf()作為將要捕獲的目標函數。WIndows 調試 API 提供了簡潔的 方法以確定一個函數的虛擬地址, GetProcAddress(),同樣也是從 kernel32.dll 導出的。這個 函數需要的主要參數就是一個模塊(一個 dll 或者一個.exe 文件)的句柄。模塊中一般都包含 了我們感興趣的函數; 可以通過 GetModuleHandle()獲得模塊的句柄。原型如下:
```
FARPROC WINAPI GetProcAddress(
HMODULE hModule,
LPCSTR lpProcName
);
HMODULE WINAPI GetModuleHandle(
LPCSTR lpModuleName
);
```
這是一個很清晰的事件鏈:獲得一個模塊的句柄,然后查找從中導出感興趣的函數的地 址。讓我們增加一個調試函數,完成剛才做的。回到 my_debugger.py.。
```
my_debugger.py
...
class debugger():
...
def func_resolve(self,dll,function):
handle = kernel32.GetModuleHandleA(dll)
address = kernel32.GetProcAddress(handle, function)
kernel32.CloseHandle(handle)
return address
```
現在創建第二個測試套件,循環的調用 printf()。我們將解析出函數的地址, 然后在這個地址上設置一個斷點。之后斷點被觸發,就能看見輸出結果,最后被測試的進程 繼續執行循環。創建一個新的 Python 腳本 printf_loop.py,輸入下面代碼。
```
#printf_loop.py from ctypes
import * import time
msvcrt = cdll.msvcrt
counter = 0
while 1:
msvcrt.printf("Loop iteration %d!\n" % counter)
time.sleep(2)
counter += 1
```
現在更新測試套件,附加到進程,在 printf()上設置斷點。
```
#my_test.py
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ")
debugger.attach(int(pid))
printf_address = debugger.func_resolve("msvcrt.dll","printf")
print "[*] Address of printf: 0x%08x" % printf_address
debugger.bp_set(printf_address)
debugger.run()
```
現在開始測試,在命令行里運行 printf_loop.py。從 Windows 任務管理器里獲得 python.exe 的 PID。然后運行 my_test.py ,鍵入 PID。你將看到如下的輸出:
```
Enter the PID of the process to attach to: 4048
[*] Address of printf: 0x77c4186a
[*] Setting breakpoint at: 0x77c4186a
Event Code: 3 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 6 Thread ID: 3148
Event Code: 2 Thread ID: 3620
Event Code: 1 Thread ID: 3620
[*] Exception address: 0x7c901230
[*] Hit the first breakpoint.
Event Code: 4 Thread ID: 3620
Event Code: 1 Thread ID: 3148
[*] Exception address: 0x77c4186a
[*] Hit user defined breakpoint.
```
Listing 3-3: 處理軟件斷點事件的事件順序
我們首先看到 printf()的函數地址在 0x77c4186a,然后在這里設置斷點。第一個捕捉到 的異常是由 Windows 設置的斷點觸發的。第二個異常發生的地址在 0x77c4186a,也就是 printf() 函數的地址。斷點處理之后,進程將恢復循環。現在我們的調試器已經支持軟件斷點,接下 來輪到硬件斷點了。
### 3.4.2 硬件斷點
第二種類型的斷點是硬件斷點,通過設置相對應的 CPU 調試寄存器來實現。我們在之 前的章節已經詳細的講解了過程,現在來具體的實現它們。有一件很重要的事情要記住,當 我們使用硬件斷點的時候要跟蹤四個可用的調試寄存器哪個是可用的哪個已經被使用了。必 須確保我們使用的那個寄存器是空的,否則硬件斷點就不能在我們希望的地方觸發。
讓我們開始枚舉進程里的所有線程,然后獲取它們的 CPU 內容拷貝。通過得到內容拷 貝,我們能夠定義 DR0 到 DR3 寄存器的其中一個,讓它包含目標斷點地址。之后我們在 DR7 寄存器的相應的位上設置斷 點的屬性和長度。
設置斷點的代碼之前我們已經完成了,剩下的就是修改處理調試事件的主函數,讓它能 夠處理由硬件斷點引發的異常。我們知道硬件斷點由 INT1 (或者說是步進事件),所以我們就 只要就當的添加另一個異常處理函數到調試循環里。讓我們設置斷點。
```
#my_debugger.py
...
class debugger():
def init (self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
self.first_breakpoint= True
self.hardware_breakpoints = {}
...
def bp_set_hw(self, address, length, condition):
# Check for a valid length value
if length not in (1, 2, 4):
return False
else:
length -= 1
# Check for a valid condition
if condition not in (HW_ACCESS, HW_EXECUTE, HW_WRITE): return False
# Check for available slots
if not self.hardware_breakpoints.has_key(0):
available = 0
elif not self.hardware_breakpoints.has_key(1):
available = 1
elif not self.hardware_breakpoints.has_key(2):
available = 2
elif not self.hardware_breakpoints.has_key(3):
available = 3
else:
return False
# We want to set the debug register in every thread
for thread_id in self.enumerate_threads():
context = self.get_thread_context(thread_id=thread_id)
# Enable the appropriate flag in the DR7
# register to set the breakpoint
context.Dr7 |= 1 << (available * 2)
# Save the address of the breakpoint in the
# free register that we found
if available == 0:
context.Dr0 = address
elif available == 1:
context.Dr1 = address
elif available == 2:
context.Dr2 = address
elif available == 3:
context.Dr3 = address
# Set the breakpoint condition
context.Dr7 |= condition << ((available * 4) + 16)
# Set the length
context.Dr7 |= length << ((available * 4) + 18)
# Set thread context with the break set
h_thread = self.open_thread(thread_id)
kernel32.SetThreadContext(h_thread,byref(context))
# update the internal hardware breakpoint array at the used
# slot index.
self.hardware_breakpoints[available] = (address,length,condition)
return True
```
通過確認全局的硬件斷點字典,我們選擇了一個空的調試寄存器存儲硬件斷點。一 旦我們得到空位,接下來做的就是將硬件斷點的地址填入調試寄存器,然后對 DR7 的標志 位進行更新適當的更新,啟動斷點。現在我們已經能夠處理硬件斷點了,讓我們更新事件處 理函數添加一個 INT1 中斷的異常處理。
```
#my_debugger.py
...
class debugger():
...
...
def get_debug_event(self):
if self.exception == EXCEPTION_ACCESS_VIOLATION:
print "Access Violation Detected."
elif self.exception == EXCEPTION_BREAKPOINT:
continue_status = self.exception_handler_breakpoint()
elif self.exception == EXCEPTION_GUARD_PAGE:
print "Guard Page Access Detected."
elif self.exception == EXCEPTION_SINGLE_STEP:
self.exception_handler_single_step()
def exception_handler_single_step(self):
# Comment from PyDbg:
# determine if this single step event occurred in reaction to a
# hardware breakpoint and grab the hit breakpoint.
# according to the Intel docs, we should be able to check for
# the BS flag in Dr6\. but it appears that Windows
# isn't properly propagating that flag down to us.
if self.context.Dr6 & 0x1 and self.hardware_breakpoints.has_key(0):
slot = 0
elif self.context.Dr6 & 0x2 and self.hardware_breakpoints.has_key(1):
slot = 1
elif self.context.Dr6 & 0x4 and self.hardware_breakpoints.has_key(2):
slot = 2
elif self.context.Dr6 & 0x8 and self.hardware_breakpoints.has_key(3):
slot = 3
else:
# This wasn't an INT1 generated by a hw breakpoint
continue_status = DBG_EXCEPTION_NOT_HANDLED
# Now let's remove the breakpoint from the list
if self.bp_del_hw(slot):
continue_status = DBG_CONTINUE
print "[*] Hardware breakpoint removed."
return continue_status
def bp_del_hw(self,slot):
# Disable the breakpoint for all active threads
for thread_id in self.enumerate_threads():
context = self.get_thread_context(thread_id=thread_id)
# Reset the flags to remove the breakpoint
context.Dr7 &= ~(1 << (slot * 2))
# Zero out the address
if slot == 0:
context.Dr0 = 0x00000000
elif slot == 1:
context.Dr1 = 0x00000000
elif slot == 2:
context.Dr2 = 0x00000000
elif slot == 3:
context.Dr3 = 0x00000000
# Remove the condition flag
context.Dr7 &= ~(3 << ((slot * 4) + 16))
# Remove the length flag
context.Dr7 &= ~(3 << ((slot * 4) + 18))
# Reset the thread's context with the breakpoint removed
h_thread = self.open_thread(thread_id)
kernel32.SetThreadContext(h_thread,byref(context))
# remove the breakpoint from the internal list.
del self.hardware_breakpoints[slot]
return True
```
代碼很容易理解;當 INT1 被擊中(觸發)的時候,查看是否有調試寄存器能夠設置硬 件斷點(通過檢測 DR6)。如果有能夠使用的就繼續。接著如果在發生異常的地址發現一個 硬件斷點,就將 DR7 的標志位置零,在其中的一個寄存器中填入斷點的地址。讓我們修改 my_test.py 并在 printf()上設置硬件斷點看看。
```
#my_test.py
import my_debugger
from my_debugger_defines import *
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ") debugger.attach(int(pid))
printf = debugger.func_resolve("msvcrt.dll","printf")
print "[*] Address of printf: 0x%08x" % printf
debugger.bp_set_hw(printf,1,HW_EXECUTE) debugger.run()
```
這個測試模塊在 printf()上設置了一個斷點,只要調用函數,就會觸發調試事件。斷點 的長度是一個字節。你應該注意到在這個模塊中我們導入了 my_debugger_defines.py 文件; 為的是訪問 HW_EXECUTE 變量,這樣書寫能使代碼更清晰。
運行后輸出結果如下:
```
Enter the PID of the process to attach to: 2504
[*] Address of printf: 0x77c4186a
Event Code: 3 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 6 Thread ID: 3704
Event Code: 2 Thread ID: 2228
Event Code: 1 Thread ID: 2228
[*] Exception address: 0x7c901230
[*] Hit the first breakpoint.
Event Code: 4 Thread ID: 2228
Event Code: 1 Thread ID: 3704
[*] Hardware breakpoint removed.
```
Listing 3-4: 處理一個硬件斷點事件的順序
一切都在預料中,程序拋出異常,處理程序移除斷點。事件處理完之后,程序繼續 循環執行代碼。現在我們的輕量級調試器已經支持硬件和軟件斷點了,最后來實現內存斷點 吧。
### 3.4.3 內存斷點
最后一個要實現的功能是內存斷點。大概流程如下;首先查詢一個內存塊以并找到基地 址(頁面在虛擬內存中的起始地址)。一旦確定了頁面大小,接著就設置頁面權限,使其成 為保護(guard)頁。當 CPU 嘗試訪問這塊內存時,就會拋出一個 GUARD_PAGE_EXCEPTION 異常。我們用對應的異常處理函數,將頁面權限恢復到以前,最后讓程序繼續執行。
為了能準確的計算出頁面的大小,就要向系統查詢信息獲得一個內存頁的默認大小。這 由 GetSystemInfo()函數完成,函數會裝填一個 SYSTEM_INFO 結構,這個結構包含 wPageSize 成員,這就是操作系統內存頁默認大小。
```
#my_debugger.py
...
class debugger():
def init (self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_thread = None
self.context = None
self.breakpoints = {}
self.first_breakpoint= True self.hardware_breakpoints = {}
# Here let's determine and store
# the default page size for the system
system_info = SYSTEM_INFO()
kernel32.GetSystemInfo(byref(system_info))
self.page_size = system_info.dwPageSize
...
```
已經獲得默認頁大小,那剩下的就是查詢和控制頁面的權限。第一步讓我們查詢出內存斷點存在于內存里的哪一個頁面。調用 VirtualQueryEx() 函數,將會填充一個 MEMORY_BASIC_INFORMATION 結構,這個結構中包含了頁的信息。函數和結構定義如 下:
```
SIZE_T WINAPI VirtualQuery(
HANDLE hProcess,
LPCVOID lpAddress,
PMEMORY_BASIC_INFORMATION lpBuffer,
SIZE_T dwLength
);
typedef struct MEMORY_BASIC_INFORMATION{
PVOID BaseAddress;
PVOID AllocationBase;
DWORD AllocationProtect;
SIZE_T RegionSize;
DWORD State;
DWORD Protect;
DWORD Type;
}
```
上面的結構中 BaseAddress 的值就是我們要設置權限的頁面的開始地址。接下來用 VirtualProtectEx()設置權限,函數原型如下:
```
BOOL WINAPI VirtualProtectEx(
HANDLE hProcess,
LPVOID lpAddress,
SIZE_T dwSize,
DWORD flNewProtect,
PDWORD lpflOldProtect
);
```
讓我們著手寫代碼。我們將創建 2 個全局列表,其中一個包含所有已經設置了好了 的保護頁,另一個包含了所有的內存斷點,在處理 GUARD_PAGE_EXCEPTION 異常的時 候將用得著。之后我們將在斷點地址上,以及周圍的區域設置權限。(因為斷點地址有可能 橫跨 2 個頁面)。
```
#my_debugger.py
...
class debugger():
def init (self):
...
self.guarded_pages = []
self.memory_breakpoints = {}
...
def bp_set_mem (self, address, size):
mbi = MEMORY_BASIC_INFORMATION()
# If our VirtualQueryEx() call doesn’t return
# a full-sized MEMORY_BASIC_INFORMATION
# then return False
if kernel32.VirtualQueryEx(self.h_process,
address,
byref(mbi),
sizeof(mbi)) < sizeof(mbi):
return False
current_page = mbi.BaseAddress
# We will set the permissions on all pages that are
# affected by our memory breakpoint.
while current_page <= address + size:
# Add the page to the list; this will
# differentiate our guarded pages from those
# that were set by the OS or the debuggee process self.guarded_pages.append(current_page)
old_protection = c_ulong(0)
if not kernel32.VirtualProtectEx(self.h_process,
current_page,
size,
mbi.Protect | PAGE_GUARD,
byref(old_protection)):
return False
# Increase our range by the size of the
# default system memory page size
current_page += self.page_size
# Add the memory breakpoint to our global list self.memory_breakpoints[address] = (address, size, mbi)
return True
```
現在我們已經能夠設置內存斷點了。如果用以前的 printf() 循環作為測試對象,你將看 到測試模塊只是簡單的輸出 Guard Page Access Detected。不過有一件好事,就是系統替我們 完成了掃尾工作,一旦保護頁被訪問,就會拋出一個異常,這時候系統會移除頁面的保護屬 性,然后允許程序繼續執行。不過你能做些別的,在調試的循環代碼里,加入特定的處理過 程,在斷點觸發的時候,重設斷點,讀取斷點處的內存,喝瓶‘蟻力神’(這個不強求,哈), 或者干點別的。
總結
目前為止我們已經開發了一個基于 Windows 的輕量級調試器。不僅對創建調試器有了 深刻的領會,也學會了很多重要的技術,無論將來做不做調試都非常有用。至少在用別的調 試器的時候你能夠明白底層做了些什么,也能夠修改調試器,讓它更好用。這些能讓你更強! 更強!
下一步是展示下調試器的高級用法,分別是 PyDbg 和 Immunity Debugger,它們成熟穩 定而且都有基于 Windows 的版本。揭開 PyDbg 工作的方式,你將得到更多的有用的東西, 也將更容易的深入了解它。Immunity 調試器結構有輕微的不同,卻提供了非常多不同的優 點。明白這它們實現特定調試任務的方法對于我們實現自動化調試非常重要。接下來輪到 PyDbg 上產。好戲開場。我先睡覺 ing。
- 序
- 1 搭建開發環境
- 1.1 操作系統準備
- 1.2 獲取和安裝 Python2.5
- 1.3 配置 Eclipse 和 PyDev
- 2 調試器設計
- 2.1 通用 CPU 寄存器
- 2.2 棧
- 2.3 調試事件
- 2.4 斷點
- 3 自己動手寫一個 windows 調試器
- 3.2 獲得 CPU 寄存器狀態
- 3.3 實現調試事件處理
- 3.4 全能的斷點
- 4 PyDBG---純 PYTHON 調試器
- 4.1 擴展斷點處理
- 4.2 處理訪問違例
- 4.3 進程快照
- 5 IMMUNITY----最好的調試器
- 5.1 安裝 Immunity 調試器
- 5.2 Immunity Debugger 101
- 5.3 Exploit 開發
- 5.4 搞定反調試機制
- 6 HOOKING
- 6.1 用 PyDbg 實現 Soft Hooking
- 6.2 Hard Hooking
- 7 Dll 和代碼注入
- 7.1 創建遠線程
- 7.2 邪惡的代碼
- 8 FUZZING
- 8.1 Bug 的分類
- 8.2 File Fuzzer
- 8.3 改進你的 Fuzzer
- 9 SULLEY
- 9.1 安裝 Sulley
- 9.2 Sulley primitives
- 9.3 獵殺 WarFTPD
- 10 Fuzzing Windows 驅動
- 10.1 驅動通信
- 10.2 用 Immunity fuzzing 驅動
- 10.4 構建 Driver Fuzzer
- 11 IDAPYTHON --- IDA 腳本
- 11.1 安裝 IDAPython
- 11.2 IDAPython 函數
- 11.3 腳本例子
- 12 PyEmu
- 12.1 安裝 PyEmu
- 12.2 PyEmu 一覽
- 12.3 IDAPyEmu