本文網頁排版有些差,已上傳了doc,可以下載閱讀。本文中的所有代碼已打包,下載地址在此。
--------------------------------------------------------------------------------------------------------------------------------------------------------------
手寫一個調試器有助於我們理解hook、進程注入等底層黑客技術具體實現,在編寫過程中需要涉及大量Windows內核編程知識,因此手寫調試器也可以作為啟發式學習內核編程的任務驅動。(本文中代碼大量參考《Gray hat python》(Python灰帽子),此書中詳細講解了調試器的基本原理,因此本文假設讀者已具備基本調試技能,能理解調試器大致原理以及斷點原理,讀者在不理解本文內容時可參考此書,不過《Gray hat python》中的代碼是32位的,並且有不少錯誤,筆者花了一周的時間調試修復了書中的bug,並且將其修改成了64位版本,本文旨在總結該書中第三章的內容並提供一份64位版本的代碼,該代碼在本人64位Win7,i3-2310M CPU主機上能成功運行)
首先來看看一個調試器需要實現哪些基本需求:
- 以調試級狀態啟動目標進程,或附加在目標進程上,監聽調試事件;
- 為程序打斷點,包括int3斷點(軟斷點),硬件斷點,內存斷點;
- 遇到斷點后掛起目標進程,並能做相應處理;
- 掛起目標進程后獲取上下文,並能修改上下文
完成這些需求都需要調用kernel32.dll里的函數,這些函數都是用C寫成的,在MSDN里可以查到這些函數的官方文檔, 因此用C語言調用它們會更方便,但是C語言的開發效率不高,我們選擇用Python來編寫調試器。
映射C語言數據類型
在調用kernel32.dll里的函數時需要傳入C語言中的數據類型聲明的變量和結構體,Python提供ctypes模塊來與C語言對接,C語言里的數據類型都可以映射到Python里,因此先讓我們從映射數據類型着手開始我們的調試器編寫。
上圖展示了C語言中各數據類型在ctypes中對應的類型,而調用kernel32.dll需要使用微軟自己宏定義的數據類型,這些數據類型實際上就是C語言中的基本數據類型。
建立Python工程my_debugger,再創建一個包main_package,在main_package下新建文件my_debugger_defines.py,該文件主要用來儲存數據類型和結構體的定義,輸入以下代碼:
1 from ctypes import * 2 3 BYTE = c_ubyte 4 WORD = c_ushort 5 DWORD = c_ulong 6 LPBYTE = POINTER(c_ubyte) 7 LPTSTR = POINTER(c_char) 8 HANDLE = c_void_p 9 PVOID = c_void_p 10 ULONG_PTR = POINTER(c_ulong) 11 LPVOID = c_void_p 12 UINT_PTR = c_ulong 13 SIZE_T = c_ulong 14 DWORD64 = c_uint64
初步構建調試器
新建文件my_debugger.py,我們用該文件實現調試器。輸入以下代碼:
1 from ctypes import * 2 from main_package.my_debugger_defines import * 3 4 kernel32 = windll.LoadLibrary("kernel32.dll")
我們調用LoadLibrary函數將kernel32.dll裝載進來,然后就可以用kernel32來引用它了。
接下來我們要開始構造調試器,我們將調試器的功能封裝進一個類,用h_process來保存調試器附加的進程的進程句柄,用pid來保存目標進程的pid,用debugger_active來作為調試器是否啟動的標志,我們在__init__里聲明這三個變量:
1 class debugger(): 2 3 def __init__(self): 4 self.h_process = None 5 self.pid = None 6 self.debugger_active = False
接下來考慮我們的第一個需求:以調試級狀態啟動目標進程,或附加在目標進程上,監聽調試事件。
以調試級狀態啟動目標進程
進程運行分調試級狀態和非調試級狀態,當進程為調試級狀態,觸發調試事件或是拋出異常時,操作系統會將該進程掛起,並通知附加它的調試進程。
我們編寫一個load函數來讓我們的調試器以調試級狀態啟動目標進程,這樣我們就能用debugger監聽目標進程的調試事件了。kernel32.dll提供CreateProcessA函數來創建進程,有關該函數的信息請自行查閱MSDN。調用CreateProcessA需要用到兩個關鍵參數:
- STARTUPINFO和PROCESS_INFORMATION兩個結構體,在MSDN里同樣可以找到它們的文檔,我們需要把它們映射到my_debugger_defines.py里面來;
- creation_flags,該參數我們設置成DEBUG_PROCESS,這樣就使得目標進程為調試狀態啟動
在my_debugger_defines.py里添加DEBUG_PROCESS的聲明:
1 DEBUG_PROCESS = 0X00000001
繼續映射兩個結構體:
1 class STARTUPINFO(Structure): 2 _fields_ = [ 3 ("cb", DWORD), 4 ("lpReserved", LPTSTR), 5 ("lpDesktop", LPTSTR), 6 ("lpTitle", LPTSTR), 7 ("dwX", DWORD), 8 ("dwY", DWORD), 9 ("dwXSize", DWORD), 10 ("dwYSize", DWORD), 11 ("dwXCountChars", DWORD), 12 ("dwYCountChars", DWORD), 13 ("dwFillAttribute", DWORD), 14 ("dwFlags", DWORD), 15 ("wShowWindow", WORD), 16 ("cbReserved2", WORD), 17 ("lpReserved2", LPBYTE), 18 ("hStdInput", HANDLE), 19 ("hStdOutput", HANDLE), 20 ("hStdError", HANDLE) 21 ] 22 23 class PROCESS_INFORMATION(Structure): 24 _fields_ = [ 25 ("hProcess", HANDLE), 26 ("hThread", HANDLE), 27 ("dwProcessId", DWORD), 28 ("dwThreadId", DWORD) 29 ]
kernel32提供OpenProcess來為指定的pid打開進程,返回句柄,我們寫一個函數來封裝它:
首先定義PROCESS_ALL_ACCESS:
1 PROCESS_ALL_ACCESS = 0X1F0FFF
接下來編寫open_process:
1 1 #get process handle 2 2 def open_process(self,pid): 3 3 h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS,False,pid) 4 4 return h_process
接下來在my_debugger.py中編寫load函數:
1 def load(self, path_to_exe): 2 3 creation_flags = DEBUG_PROCESS 4 5 startupinfo = STARTUPINFO() 6 process_information = PROCESS_INFORMATION() 7 8 startupinfo.dwFlags = 0X1 9 startupinfo.wShowWindow = 0X0 10 startupinfo.cb = sizeof(startupinfo) 11 12 if kernel32.CreateProcessA(path_to_exe, 13 None, 14 None, 15 None, 16 None, 17 creation_flags, 18 None, 19 None, 20 byref(startupinfo), 21 byref(process_information)): 22 print "[*] We have successfully launched the process!!" 23 print "[*] PID: %d" % process_information.dwProcessId 24 self.h_process = self.open_process(process_information.dwProcessId) #keep a process handle 25 self.debugger_active = True 26 else: 27 print "[*] Error: 0x%08x." % kernel32.GetLastError()
該代碼調用CreateProcessA創建一個進程,並且成功后打印進程的pid,調用open_process打開該進程的句柄並保存。至於下面三行代碼的意思請自行MSDN:
1 startupinfo.dwFlags = 0X1 2 startupinfo.wShowWindow = 0X0 3 startupinfo.cb = sizeof(startupinfo)
至此我們初步實現了需求1的第一個功能:以調試狀態啟動目標進程。你可以創建一個debugger,像這樣來測試它debugger.load(‘C:\Windows\System32\calc.exe’)。
有時我們需要將調試器附加在已啟動的進程上,因此我們還需要編寫一個attach(pid)。
kernel32提供DebugActiveProcess來將本進程附加在指定pid的進程上:
1 def attach(self,pid): 2 self.h_process = self.open_process(pid) 3 if kernel32.DebugActiveProcess(pid): 4 self.debugger_active = True 5 self.pid = int(pid) 6 else: 7 print "[*] Unable to attach the process."
值得注意的是,pid必須是整數,如果你用raw_input來輸入pid的話,記得把它轉換成整數。
我們現在只剩下最后一個功能(監聽調試事件)就能完成需求1了。
監聽調試事件
Kernel32.dll提供WaitForDebugEvent來監聽調試事件,當目標進程發生調試事件時會通知我們的調試器進行處理,我們用一個循環不斷調用此函數來在處理完一個調試事件后立即監聽下一個調試事件。
1 def run(self): 2 while self.debugger_active == True: 3 self.get_debug_event()
只要調試器是啟動的(self.debugger_active == True),則不斷獲取調試事件。
編寫get_debug_event(),調用WaitForDebugEvent需要用DEBUG_EVENT結構體來保存調試事件信息,我們把它映射進來:
1 class DEBUG_EVENT(Structure): 2 _fields_ = [ 3 ("dwDebugEventCode", DWORD), 4 ("dwProcessId", DWORD), 5 ("dwThreadId", DWORD), 6 ("u", _DEBUG_EVENT_UNION) 7 ]
該結構體需要用到聯合體_DEBUG_EVENT_UNION,我們也把它映射進來:
1 class _DEBUG_EVENT_UNION(Union): 2 _fields_ = [ 3 ("Exception", EXCEPTION_DEBUG_INFO), 4 ]
該聯合體實際上包含許多成員,但我們的調試器只需要用到EXCEPTION_DEBUG_INFO一個就足夠了。
1 class EXCEPTION_DEBUG_INFO(Structure): 2 _fields_ = [ 3 ("ExceptionRecord", EXCEPTION_RECORD), 4 ("dwFirstChance", DWORD) 5 ] 6 class EXCEPTION_RECORD(Structure): 7 Pass 8 EXCEPTION_RECORD._fields_ = [ 9 ("ExceptionCode", DWORD), 10 ("ExceptionFlags", DWORD), 11 ("ExceptionRecord", POINTER(EXCEPTION_RECORD)), 12 ("ExceptionAddress", PVOID), 13 ("NumberParameters", DWORD), 14 ("ExceptionInformation", UINT_PTR * 15), 15 ]
接下來就可以開始編寫get_debug_event了:
1 def get_debug_event(self): 2 3 debug_event = DEBUG_EVENT() 4 continue_status = DBG_CONTINUE 5 if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE): 6 kernel32.ContinueDebugEvent(debug_event.dwProcessId,debug_event.dwThreadId, continue_status )
其中:
1 INFINITE = 0xFFFFFFFF 2 DBG_CONTINUE = 0X00010002
我們在獲取到調試事件后直接調用ContinueDebugEvent來使掛起的目標繼續執行。
筆者在WaitForDebugEvent監聽到一個調試事件后曾將debug_event保存下來,結果導致程序出現了一個非常詭異的bug,調試了很久才發現WaitForDebugEvent並不會等debug_event的所有成員變量釋放鎖后才通知調試進程,因此千萬不要對debug_event進行操作,否則會導致死鎖發生。
現在我們的調試器已完成了需求1,你可以在監測到調試事件后添加輸出一些信息的代碼,attach到一個計算器上測試一下。
設置斷點
作為一個調試器,最重要的功能就是打斷點。首先來實現最簡單的軟斷點。
軟斷點
軟斷點就是int3斷點,當程序執行到int3指令時會觸發一個異常中斷下來,並查看是否有調試器附加在該程序上,如果有,則交給調試進程處理。因此打軟斷點就是將我們要中斷的地址的字節修改為’\xCC’(int3指令),在斷下來后將該字節改回去,讓程序正常執行。
首先要實現的是讀取內存和修改內存:
1 def read_process_memory(self,address,length): 2 data = "" 3 read_buf = create_string_buffer(length) 4 count = c_ulong(0) 5 if not kernel32.ReadProcessMemory(self.h_process, 6 address, 7 read_buf, 8 length, 9 byref(count)): 10 return False 11 else: 12 data += read_buf.raw 13 return data 14 15 def write_process_memory(self,address,data): 16 count = c_ulong(0) 17 length = len(data) 18 c_data = c_char_p(data[count.value:]) 19 if not kernel32.WriteProcessMemory(self.h_process, 20 address, 21 c_data, 22 length, 23 byref(count)): 24 return False 25 else: 26 return True
這樣我們就能將要打斷點的地址的內容修改成’\xCC’了。
我們用self.breakpoints來保存軟斷點,在__init__中添加:
1 self.breakpoints = {}
然后編寫bp_set來打軟斷點:
1 def bp_set(self,address): 2 print "[*] Setting breakpoint at: 0x%08x" % address 3 if not self.breakpoints.has_key(address): 4 try: 5 original_byte = self.read_process_memory(address, 1) 6 self.write_process_memory(address, '\xCC') 7 self.breakpoints[address] = original_byte 8 except: 9 return False 10 return True
該函數先檢查斷點字典中是否有該地址,如果沒有,則記錄該地址首字節,並修改成’\xCC’,將其添加進self.breakpoints字典。
硬件斷點
軟斷點最大的缺點是需要修改進程內存,這會破壞CRC,因此軟斷點是極其容易被反調試技術Anti的。硬件斷點由於只修改寄存器,因此不容易被目標進程察覺。
硬件斷點是用8個調試寄存器DR0-DR7實現的。其中DR0-DR3用來儲存斷點地址,DR4-DR5保留,DR6是調試狀態寄存器,在進程觸發硬件斷點時返回觸發的是哪一個斷點給調試器,DR7是調試控制寄存器。
打硬件斷點首先需要在DR0-DR3中找一個空閑的寄存器,將斷點地址寫進去,然后修改DR7相應標志位來設置斷點長度和斷點條件。
斷點條件有三個:讀、寫、執行。分別表示在讀該地址、寫該地址、執行該地址的時候中斷:
1 HW_ACCESS = 0x00000003 2 HW_EXECUTE = 0x00000000 3 HW_WRITE = 0x00000001
我們還需要知道斷點的長度才能判斷是否應該中斷,斷點長度也有三個選擇:1字節、2字節、4字節。
如何修改寄存器呢?操作系統為每一個線程維護了一個結構體來保存上下文,當線程中斷時,操作系統會將所有寄存器放進該結構體里保存起來,當線程恢復執行時將該結構體取出,恢復寄存器的值。因此我們可以通過修改這個結構體來實現修改寄存器的目的。
線程上下文結構體如下:
class WOW64_CONTEXT(Structure): _pack_ = 16 _fields_ = [ ("P1Home", DWORD64), ("P2Home", DWORD64), ("P3Home", DWORD64), ("P4Home", DWORD64), ("P5Home", DWORD64), ("P6Home", DWORD64), ("ContextFlags", DWORD), ("MxCsr", DWORD), ("SegCs", WORD), ("SegDs", WORD), ("SegEs", WORD), ("SegFs", WORD), ("SegGs", WORD), ("SegSs", WORD), ("EFlags", DWORD), ("Dr0", DWORD64), ("Dr1", DWORD64), ("Dr2", DWORD64), ("Dr3", DWORD64), ("Dr6", DWORD64), ("Dr7", DWORD64), ("Rax", DWORD64), ("Rcx", DWORD64), ("Rdx", DWORD64), ("Rbx", DWORD64), ("Rsp", DWORD64), ("Rbp", DWORD64), ("Rsi", DWORD64), ("Rdi", DWORD64), ("R8", DWORD64), ("R9", DWORD64), ("R10", DWORD64), ("R11", DWORD64), ("R12", DWORD64), ("R13", DWORD64), ("R14", DWORD64), ("R15", DWORD64), ("Rip", DWORD64), ("DebugControl", DWORD64), ("LastBranchToRip", DWORD64), ("LastBranchFromRip", DWORD64), ("LastExceptionToRip", DWORD64), ("LastExceptionFromRip", DWORD64), ("DUMMYUNIONNAME", DUMMYUNIONNAME), ("VectorRegister", M128A * 26), ("VectorControl", DWORD64) ] class DUMMYUNIONNAME(Union): _fields_=[ ("FltSave", XMM_SAVE_AREA32), ("DummyStruct", DUMMYSTRUCTNAME) ] class DUMMYSTRUCTNAME(Structure): _fields_=[ ("Header", M128A * 2), ("Legacy", M128A * 8), ("Xmm0", M128A), ("Xmm1", M128A), ("Xmm2", M128A), ("Xmm3", M128A), ("Xmm4", M128A), ("Xmm5", M128A), ("Xmm6", M128A), ("Xmm7", M128A), ("Xmm8", M128A), ("Xmm9", M128A), ("Xmm10", M128A), ("Xmm11", M128A), ("Xmm12", M128A), ("Xmm13", M128A), ("Xmm14", M128A), ("Xmm15", M128A) ] class XMM_SAVE_AREA32(Structure): _pack_ = 1 _fields_ = [ ('ControlWord', WORD), ('StatusWord', WORD), ('TagWord', BYTE), ('Reserved1', BYTE), ('ErrorOpcode', WORD), ('ErrorOffset', DWORD), ('ErrorSelector', WORD), ('Reserved2', WORD), ('DataOffset', DWORD), ('DataSelector', WORD), ('Reserved3', WORD), ('MxCsr', DWORD), ('MxCsr_Mask', DWORD), ('FloatRegisters', M128A * 8), ('XmmRegisters', M128A * 16), ('Reserved4', BYTE * 96) ] class M128A(Structure): _fields_ = [ ("Low", DWORD64), ("High", DWORD64) ]
注意,在《Gay hat python》一書中所使用的線程上下文是32位的,如果你在64位平台下使用32位的結構體來保存線程上下文,將會得到一個寄存器值全為0的空的線程上下文。
接下來編寫一個用來獲取線程上下文的函數,根據MSDN,在調用kernel32.GetThreadContext前需要對結構體進行初始化:
1 # Context flags for GetThreadContext() 2 CONTEXT_FULL = 0x00010007 3 CONTEXT_DEBUG_REGISTERS = 0x00010010 4 5 #get thread context 6 def get_thread_context(self, thread_id): 7 8 #64-bit context 9 context64 = WOW64_CONTEXT() 10 context64.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS 11 12 self.h_thread = self.open_thread(thread_id) 13 if kernel32.GetThreadContext(self.h_thread, byref(context64)): 14 kernel32.CloseHandle(self.h_thread) 15 return context64 16 else: 17 print '[*] Get thread context error. Error code: %d' % kernel32.GetLastError() 18 return False
雖然是64位,但是我用kernel32.GetThreadContext來獲取線程上下文卻並沒任何問題,反倒是調用kernel32.Wow64GetThreadContext卻會發生87號錯誤(參數不正確),我試了很久也沒弄清楚為什么,如果有大神知道這是什么情況請聯系我,謝謝!
通常硬件斷點都是針對整個進程的,因此我們需要對目標進程中的所有線程逐一修改線程上下文,這就涉及到一個枚舉線程的問題,kernel32仍然提供API幫助我們做這件事。每個進程都保存了一張線程快照表來保存所有線程的狀態信息,有了這張表我們可以利用kernel32.Thread32First獲取到第一個線程,先后調用kernel32.Thread32Next就能繼續遍歷線程了。
保存線程信息的結構體和獲取線程快照表所需的常量參數如下所示:
1 class THREADENTRY32(Structure): 2 _fields_ = [ 3 ("dwSize", DWORD), 4 ("cntUsage", DWORD), 5 ("th32ThreadID", DWORD), 6 ("th32OwnerProcessID", DWORD), 7 ("tpBasePri", DWORD), 8 ("tpDeltaPri", DWORD), 9 ("dwFlags", DWORD), 10 ] 11 12 TH32CS_SNAPTHREAD = 0x00000004
枚舉線程的函數如下:
1 # enumerate threads 2 def enumerate_threads(self): 3 thread_entry = THREADENTRY32() 4 thread_list = [] 5 snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, self.pid) 6 if snapshot is not None: 7 thread_entry.dwSize = sizeof(thread_entry) 8 success = kernel32.Thread32First(snapshot, byref(thread_entry)) 9 while success: 10 if thread_entry.th32OwnerProcessID == self.pid: 11 thread_list.append(thread_entry.th32ThreadID) 12 kernel32.CloseHandle(snapshot) 13 success = kernel32.Thread32Next(snapshot, byref(thread_entry)) 14 return thread_list 15 else: 16 return False
注意,《Gay hat python》源碼中此處有bug,以上代碼已將其修復。
現在我們就可以開始編寫打硬件斷點的函數了。我們用self.hardware_breakpoints來保存硬件斷點,然后枚舉線程,逐一修改調試寄存器。
怎么修改調試寄存器呢?對於DR0-DR3,我們可以簡單地找一個空閑的寄存器,將我們要打斷點的地址寫進去即可。對於DR7,需要仔細研究標志位構造。注意,我們是在64位平台下,因此寄存器是64位的,《Gay hat python》中修改DR7的代碼是32位的,那么我們的代碼是否應與此書不同呢?實際上,64位的DR6和DR7的高32位是用不到的,低32位構造與32位寄存器完全一致,因此此書上的代碼在64位環境下兼容。
我們看看64位的DR6與DR7的構造:
DR7的0、2、4、6位代表DR0、DR1、DR2、DR3,置1表示此寄存器被打上了硬件斷點。16、20、24、28位分別保存DR0、DR1、DR2、DR3的硬件斷點的條件,18、22、26、30位分別保存DR0、DR1、DR2、DR3的硬件斷點的長度。
舉個例子,如果我們要打一個0x77284地址的內存長度為1,條件為執行的硬件斷點,該怎么修改寄存器呢。首先在DR0-DR3中找一個空閑的寄存器(假設是DR2)賦值為0x77284,接着將DR7的第24位賦值為HW_EXCUTE,將26位賦值為0(長度減1)。
現在可以開始寫硬件斷點了:
1 def bp_set_hw(self, address, length, condition): 2 if length not in (1,2,4): 3 return False 4 else: 5 length -= 1 6 if condition not in (HW_ACCESS, HW_EXECUTE, HW_WRITE): 7 return False 8 if not self.hardware_breakpoints.has_key(0): 9 available = 0 10 elif not self.hardware_breakpoints.has_key(1): 11 available = 1 12 elif not self.hardware_breakpoints.has_key(2): 13 available = 2 14 elif not self.hardware_breakpoints.has_key(3): 15 available = 3 16 else: 17 return False 18 for thread_id in self.enumerate_threads(): 19 context64 = self.get_thread_context(thread_id) 20 context64.Dr7 |= 1 << (available * 2) 21 if available == 0: 22 context64.Dr0 = address 23 elif available == 1: 24 context64.Dr1 = address 25 elif available == 2: 26 context64.Dr2 = address 27 elif available == 3: 28 context64.Dr3 = address 29 #set condition 30 context64.Dr7 |= condition << ((available * 4) + 16) 31 #set length 32 context64.Dr7 |= length << ((available * 4) + 18) 33 #update context 34 h_thread = self.open_thread(thread_id) 35 if not kernel32.SetThreadContext(h_thread, byref(context64)): 36 print '[*] Set thread context error.' 37 38 #update breakpoint list 39 self.hardware_breakpoints[available] = (address, length, condition) 40 return True 41
移除硬件斷點就只需要將調試寄存器改回來就可以了,下面只提供代碼:
1 def bp_del_hw(self, slot): 2 for thread_id in self.enumerate_threads(): 3 context = self.get_thread_context(thread_id) 4 context.Dr7 &= ~(1 << (slot * 2)) 5 if slot == 0: 6 context.Dr0 = 0x00000000 7 elif slot == 1: 8 context.Dr1 = 0x00000000 9 elif slot == 2: 10 context.Dr2 = 0x00000000 11 elif slot == 3: 12 context.Dr3 = 0x00000000 13 #condition 14 context.Dr7 &= ~(3 << ((slot * 4) + 16)) 15 #length 16 context.Dr7 &= ~(3 << ((slot * 4) + 18)) 17 18 h_thread = self.open_thread(thread_id) 19 kernel32.SetThreadContext(h_thread,byref(context)) 20 del self.hardware_breakpoints[slot] 21 return True 22
內存斷點
內存斷點是最特殊的一類斷點,它實際上並不是被設計來作為斷點使用的,不過我們可以利用它能產生中斷的特性當成斷點來使用。
內存在操作系統中是分頁管理的,每個內存頁都有讀和寫的權限,如果試圖對一個內存頁做權限之外的事情就會觸發一個異常導致中斷,因此打內存斷點就是修改該內存所在內存頁的權限。
我們可以調用kernel32.VirtualQueryEx獲取目標進程指定內存地址的內存頁的基址,然后調用kernel32.VirtualProtectEx修改權限。
這一部分代碼與《Gray hat python》上一樣,《Gray hat python》上有詳盡解釋這里只給出代碼:
映射所需的結構體:
class MEMORY_BASIC_INFORMATION(Structure): _fields_ = [ ("BaseAddress", PVOID), ("AllocationBase", PVOID), ("AllocationProtect", DWORD), ("RegionSize", SIZE_T), ("State", DWORD), ("Protect", DWORD), ("Type", DWORD), ]
權限常量:
1 # Memory page permissions, used by VirtualProtect() 2 PAGE_NOACCESS = 0x00000001 3 PAGE_READONLY = 0x00000002 4 PAGE_READWRITE = 0x00000004 5 PAGE_WRITECOPY = 0x00000008 6 PAGE_EXECUTE = 0x00000010 7 PAGE_EXECUTE_READ = 0x00000020 8 PAGE_EXECUTE_READWRITE = 0x00000040 9 PAGE_EXECUTE_WRITECOPY = 0x00000080 10 PAGE_GUARD = 0x00000100 11 PAGE_NOCACHE = 0x00000200 12 PAGE_WRITECOMBINE = 0x00000400
斷點代碼:
1 def bp_set_mem(self, address, size): 2 mbi = MEMORY_BASIC_INFORMATION() 3 4 if kernel32.VirtualQueryEx(self.h_process, address, byref(mbi), sizeof(mbi)) < sizeof(mbi): 5 return False 6 7 current_page = mbi.BaseAddress 8 9 while current_page <= address + size: 10 self.guarded_pages.append(current_page) 11 old_protection = c_ulong(0) 12 if not kernel32.VirtualProtectEx(self.h_process, current_page,size,mbi.Protect | PAGE_GUARD,byref(old_protection)): 13 return False 14 current_page += self.page_size 15 self.memory_breakpoints[address] = (address, size, mbi) 16 return True 17
至此,我們實現了調試器打斷點的需求,接下來我們要監聽異常,在斷點觸發的時候中斷,並調用相應例程進行處理。
斷點例程
我們最開始寫的get_debug_event只是在監聽到調試事件后簡單的讓目標進程繼續運行,並沒有做任何事情,現在我們修改這個函數如下:
1 def get_debug_event(self): 2 3 debug_event = DEBUG_EVENT() 4 continue_status = DBG_CONTINUE 5 bpflag = False 6 7 if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE): 8 9 self.thread_id = debug_event.dwThreadId 10 self.h_thread = self.open_thread(self.thread_id) 11 self.context = self.get_thread_context(self.thread_id) 12 13 print 'Event code: %s Thread ID: %d' % (EVENTCODE_MAP[debug_event.dwDebugEventCode], 14 debug_event.dwThreadId) 15 16 if debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT: 17 18 self.exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode 19 self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress 20 21 if self.exception == EXCEPTION_ACCESS_VIOLATION: 22 print 'Access Violation Detected.' 23 24 elif self.exception == EXCEPTION_BREAKPOINT: 25 print 'EXCEPTION_BREAKPOINT' 26 bpflag = not self.first_breakpoint 27 continue_status = self.exception_handler_breakpoint() 28 29 30 elif self.exception == EXCEPTION_GUARD_PAGE: 31 print 'Guard Page Access Detected.' 32 continue_status == self.exception_handler_guard_page() 33 34 elif self.exception == EXCEPTION_SINGLE_STEP: 35 print 'Single Stepping.' 36 continue_status = self.exception_handler_single_step() 37 38 kernel32.ContinueDebugEvent(debug_event.dwProcessId,debug_event.dwThreadId, continue_status ) 39 40 #if it is int3 breakpoint 41 if bpflag == True: 42 self.write_process_memory(self.exception_address,'\xCC')
值得一提的是,《Gray hat python》在int3斷點中斷后是直接將‘\xCC’修改回原字節,並沒有在恢復目標進程執行后重新將int3
斷點打回去,因此斷點只生效一次,筆者的代碼則做了相應處理,使斷點能夠繼續生效。
相應斷點例程如下:
1 2 3 #deal with memory breakpoint exception 4 def exception_handler_guard_page(self): 5 print '[*] Hit the memory breakpoint.' 6 print '[**] Exception address: 0x%08x' % self.exception_address 7 return DBG_CONTINUE 8 9 #deal with breakpoint exception 10 def exception_handler_breakpoint(self): 11 print '[*] Inside the int3 breakpoint handler' 12 print '[**] Exception address: 0x%08x' % self.exception_address 13 if self.first_breakpoint == True: 14 self.first_breakpoint = False 15 print '[**] Hit the first breakpoint.' 16 else: 17 print '[**] Hit the user defined breakpoint.' 18 # put the original byte back 19 self.write_process_memory(self.exception_address, 20 self.breakpoints[self.exception_address] 21 ) 22 return DBG_CONTINUE 23 24 #deal with single step exception 25 def exception_handler_single_step(self): 26 27 if self.context == False: 28 print '[*] Exception_handler_single_step get context error.' 29 else: 30 if self.context.Dr6 & 0x1 and self.hardware_breakpoints.has_key(0): 31 slot = 0 32 elif self.context.Dr6 & 0x2 and self.hardware_breakpoints.has_key(1): 33 slot = 1 34 elif self.context.Dr6 & 0x4 and self.hardware_breakpoints.has_key(2): 35 slot = 2 36 elif self.context.Dr6 & 0x8 and self.hardware_breakpoints.has_key(3): 37 slot = 3 38 else: 39 continue_status = DBG_EXCEPTION_NOT_HANDLED 40 # remove this hardware breakpoint 41 if self.bp_del_hw(slot): 42 continue_status = DBG_CONTINUE 43 print '[*] Hardware breakpoint removed.' 44 else : 45 print '[*] Hardware breakpoint remove failed.' 46 #raw_input('[*] Press any key to continue.') 47 return continue_status 48
總結
本文對《Gray hat python》一書中第三章做了歸納,並且修改了源代碼為64位版本,目前在筆者的電腦上運行沒有任何問題。筆者初學Python和Windows內核編程,在StackOverflow查了很多問題都是由於書中的代碼在64位環境下不兼容導致的,又沒在網上找到一份64位版本的代碼,因此寫了本文。在學會編寫調試器后,對一些像hook和進程注入這樣的底層黑客技術也有了自己的思路,將來找時間將其實現。本文也是作為筆者的心得體會而寫的,如果有大神發現代碼中的問題歡迎指正,也可以與我交流討論,我的QQ是83488773。