本文網(wǎng)頁(yè)排版有些差,已上傳了doc,可以下載閱讀。本文中的所有代碼已打包,下載地址在此。
--------------------------------------------------------------------------------------------------------------------------------------------------------------
手寫(xiě)一個(gè)調(diào)試器有助于我們理解hook、進(jìn)程注入等底層黑客技術(shù)具體實(shí)現(xiàn),在編寫(xiě)過(guò)程中需要涉及大量Windows內(nèi)核編程知識(shí),因此手寫(xiě)調(diào)試器也可以作為啟發(fā)式學(xué)習(xí)內(nèi)核編程的任務(wù)驅(qū)動(dòng)。(本文中代碼大量參考《Gray hat python》(Python灰帽子),此書(shū)中詳細(xì)講解了調(diào)試器的基本原理,因此本文假設(shè)讀者已具備基本調(diào)試技能,能理解調(diào)試器大致原理以及斷點(diǎn)原理,讀者在不理解本文內(nèi)容時(shí)可參考此書(shū),不過(guò)《Gray hat python》中的代碼是32位的,并且有不少錯(cuò)誤,筆者花了一周的時(shí)間調(diào)試修復(fù)了書(shū)中的bug,并且將其修改成了64位版本,本文旨在總結(jié)該書(shū)中第三章的內(nèi)容并提供一份64位版本的代碼,該代碼在本人64位Win7,i3-2310M CPU主機(jī)上能成功運(yùn)行)
首先來(lái)看看一個(gè)調(diào)試器需要實(shí)現(xiàn)哪些基本需求:
完成這些需求都需要調(diào)用kernel32.dll里的函數(shù),這些函數(shù)都是用C寫(xiě)成的,在MSDN里可以查到這些函數(shù)的官方文檔, 因此用C語(yǔ)言調(diào)用它們會(huì)更方便,但是C語(yǔ)言的開(kāi)發(fā)效率不高,我們選擇用Python來(lái)編寫(xiě)調(diào)試器。
映射C語(yǔ)言數(shù)據(jù)類(lèi)型
在調(diào)用kernel32.dll里的函數(shù)時(shí)需要傳入C語(yǔ)言中的數(shù)據(jù)類(lèi)型聲明的變量和結(jié)構(gòu)體,Python提供ctypes模塊來(lái)與C語(yǔ)言對(duì)接,C語(yǔ)言里的數(shù)據(jù)類(lèi)型都可以映射到Python里,因此先讓我們從映射數(shù)據(jù)類(lèi)型著手開(kāi)始我們的調(diào)試器編寫(xiě)。

上圖展示了C語(yǔ)言中各數(shù)據(jù)類(lèi)型在ctypes中對(duì)應(yīng)的類(lèi)型,而調(diào)用kernel32.dll需要使用微軟自己宏定義的數(shù)據(jù)類(lèi)型,這些數(shù)據(jù)類(lèi)型實(shí)際上就是C語(yǔ)言中的基本數(shù)據(jù)類(lèi)型。
建立Python工程my_debugger,再創(chuàng)建一個(gè)包main_package,在main_package下新建文件my_debugger_defines.py,該文件主要用來(lái)儲(chǔ)存數(shù)據(jù)類(lèi)型和結(jié)構(gòu)體的定義,輸入以下代碼:
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_p10 ULONG_PTR = POINTER(c_ulong)11 LPVOID = c_void_p12 UINT_PTR = c_ulong13 SIZE_T = c_ulong14 DWORD64 = c_uint64
初步構(gòu)建調(diào)試器
新建文件my_debugger.py,我們用該文件實(shí)現(xiàn)調(diào)試器。輸入以下代碼:
1 from ctypes import *2 from main_package.my_debugger_defines import *3 4 kernel32 = windll.LoadLibrary("kernel32.dll")
我們調(diào)用LoadLibrary函數(shù)將kernel32.dll裝載進(jìn)來(lái),然后就可以用kernel32來(lái)引用它了。
接下來(lái)我們要開(kāi)始構(gòu)造調(diào)試器,我們將調(diào)試器的功能封裝進(jìn)一個(gè)類(lèi),用h_PRocess來(lái)保存調(diào)試器附加的進(jìn)程的進(jìn)程句柄,用pid來(lái)保存目標(biāo)進(jìn)程的pid,用debugger_active來(lái)作為調(diào)試器是否啟動(dòng)的標(biāo)志,我們?cè)?span style="font-family: Consolas;">__init__里聲明這三個(gè)變量:
1 class debugger():2 3 def __init__(self):4 self.h_process = None5 self.pid = None6 self.debugger_active = False
接下來(lái)考慮我們的第一個(gè)需求:以調(diào)試級(jí)狀態(tài)啟動(dòng)目標(biāo)進(jìn)程,或附加在目標(biāo)進(jìn)程上,監(jiān)聽(tīng)調(diào)試事件。
以調(diào)試級(jí)狀態(tài)啟動(dòng)目標(biāo)進(jìn)程
進(jìn)程運(yùn)行分調(diào)試級(jí)狀態(tài)和非調(diào)試級(jí)狀態(tài),當(dāng)進(jìn)程為調(diào)試級(jí)狀態(tài),觸發(fā)調(diào)試事件或是拋出異常時(shí),操作系統(tǒng)會(huì)將該進(jìn)程掛起,并通知附加它的調(diào)試進(jìn)程。
我們編寫(xiě)一個(gè)load函數(shù)來(lái)讓我們的調(diào)試器以調(diào)試級(jí)狀態(tài)啟動(dòng)目標(biāo)進(jìn)程,這樣我們就能用debugger監(jiān)聽(tīng)目標(biāo)進(jìn)程的調(diào)試事件了。kernel32.dll提供CreateProcessA函數(shù)來(lái)創(chuàng)建進(jìn)程,有關(guān)該函數(shù)的信息請(qǐng)自行查閱MSDN。調(diào)用CreateProcessA需要用到兩個(gè)關(guān)鍵參數(shù):
在my_debugger_defines.py里添加DEBUG_PROCESS的聲明:
1 DEBUG_PROCESS = 0X00000001
繼續(xù)映射兩個(gè)結(jié)構(gòu)體:
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來(lái)為指定的pid打開(kāi)進(jìn)程,返回句柄,我們寫(xiě)一個(gè)函數(shù)來(lái)封裝它:
首先定義PROCESS_ALL_access:
1 PROCESS_ALL_ACCESS = 0X1F0FFF
接下來(lái)編寫(xiě)open_process:
1 1 #get process handle2 2 def open_process(self,pid):3 3 h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS,False,pid)4 4 return h_process
接下來(lái)在my_debugger.py中編寫(xiě)load函數(shù):
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 = 0X010 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.dwProcessId24 self.h_process = self.open_process(process_information.dwProcessId) #keep a process handle25 self.debugger_active = True26 else:27 print "[*] Error: 0x%08x." % kernel32.GetLastError()
該代碼調(diào)用CreateProcessA創(chuàng)建一個(gè)進(jìn)程,并且成功后打印進(jìn)程的pid,調(diào)用open_process打開(kāi)該進(jìn)程的句柄并保存。至于下面三行代碼的意思請(qǐng)自行MSDN:
1 startupinfo.dwFlags = 0X12 startupinfo.wShowWindow = 0X03 startupinfo.cb = sizeof(startupinfo)
至此我們初步實(shí)現(xiàn)了需求1的第一個(gè)功能:以調(diào)試狀態(tài)啟動(dòng)目標(biāo)進(jìn)程。你可以創(chuàng)建一個(gè)debugger,像這樣來(lái)測(cè)試它debugger.load(‘C:/Windows/System32/calc.exe’)。
有時(shí)我們需要將調(diào)試器附加在已啟動(dòng)的進(jìn)程上,因此我們還需要編寫(xiě)一個(gè)attach(pid)。
kernel32提供DebugActiveProcess來(lái)將本進(jìn)程附加在指定pid的進(jìn)程上:
1 def attach(self,pid):2 self.h_process = self.open_process(pid)3 if kernel32.DebugActiveProcess(pid):4 self.debugger_active = True5 self.pid = int(pid)6 else:7 print "[*] Unable to attach the process."
值得注意的是,pid必須是整數(shù),如果你用raw_input來(lái)輸入pid的話(huà),記得把它轉(zhuǎn)換成整數(shù)。
我們現(xiàn)在只剩下最后一個(gè)功能(監(jiān)聽(tīng)調(diào)試事件)就能完成需求1了。
監(jiān)聽(tīng)調(diào)試事件
Kernel32.dll提供WaitForDebugEvent來(lái)監(jiān)聽(tīng)調(diào)試事件,當(dāng)目標(biāo)進(jìn)程發(fā)生調(diào)試事件時(shí)會(huì)通知我們的調(diào)試器進(jìn)行處理,我們用一個(gè)循環(huán)不斷調(diào)用此函數(shù)來(lái)在處理完一個(gè)調(diào)試事件后立即監(jiān)聽(tīng)下一個(gè)調(diào)試事件。
1 def run(self):2 while self.debugger_active == True:3 self.get_debug_event()
只要調(diào)試器是啟動(dòng)的(self.debugger_active == True),則不斷獲取調(diào)試事件。
編寫(xiě)get_debug_event(),調(diào)用WaitForDebugEvent需要用DEBUG_EVENT結(jié)構(gòu)體來(lái)保存調(diào)試事件信息,我們把它映射進(jìn)來(lái):
1 class DEBUG_EVENT(Structure):2 _fields_ = [3 ("dwDebugEventCode", DWORD),4 ("dwProcessId", DWORD),5 ("dwThreadId", DWORD),6 ("u", _DEBUG_EVENT_UNION)7 ]
該結(jié)構(gòu)體需要用到聯(lián)合體_DEBUG_EVENT_UNION,我們也把它映射進(jìn)來(lái):
1 class _DEBUG_EVENT_UNION(Union):2 _fields_ = [3 ("Exception", EXCEPTION_DEBUG_INFO),4 ]
該聯(lián)合體實(shí)際上包含許多成員,但我們的調(diào)試器只需要用到EXCEPTION_DEBUG_INFO一個(gè)就足夠了。
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 ]
接下來(lái)就可以開(kāi)始編寫(xiě)get_debug_event了:
1 def get_debug_event(self):2 3 debug_event = DEBUG_EVENT()4 continue_status = DBG_CONTINUE5 if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):6 kernel32.ContinueDebugEvent(debug_event.dwProcessId,debug_event.dwThreadId, continue_status )
其中:
1 INFINITE = 0xFFFFFFFF2 DBG_CONTINUE = 0X00010002
我們?cè)讷@取到調(diào)試事件后直接調(diào)用ContinueDebugEvent來(lái)使掛起的目標(biāo)繼續(xù)執(zhí)行。
筆者在WaitForDebugEvent監(jiān)聽(tīng)到一個(gè)調(diào)試事件后曾將debug_event保存下來(lái),結(jié)果導(dǎo)致程序出現(xiàn)了一個(gè)非常詭異的bug,調(diào)試了很久才發(fā)現(xiàn)WaitForDebugEvent并不會(huì)等debug_event的所有成員變量釋放鎖后才通知調(diào)試進(jìn)程,因此千萬(wàn)不要對(duì)debug_event進(jìn)行操作,否則會(huì)導(dǎo)致死鎖發(fā)生。
現(xiàn)在我們的調(diào)試器已完成了需求1,你可以在監(jiān)測(cè)到調(diào)試事件后添加輸出一些信息的代碼,attach到一個(gè)計(jì)算器上測(cè)試一下。
設(shè)置斷點(diǎn)
作為一個(gè)調(diào)試器,最重要的功能就是打斷點(diǎn)。首先來(lái)實(shí)現(xiàn)最簡(jiǎn)單的軟斷點(diǎn)。
軟斷點(diǎn)
軟斷點(diǎn)就是int3斷點(diǎn),當(dāng)程序執(zhí)行到int3指令時(shí)會(huì)觸發(fā)一個(gè)異常中斷下來(lái),并查看是否有調(diào)試器附加在該程序上,如果有,則交給調(diào)試進(jìn)程處理。因此打軟斷點(diǎn)就是將我們要中斷的地址的字節(jié)修改為’/xCC’(int3指令),在斷下來(lái)后將該字節(jié)改回去,讓程序正常執(zhí)行。
首先要實(shí)現(xiàn)的是讀取內(nèi)存和修改內(nèi)存:
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 False11 else:12 data += read_buf.raw13 return data14 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 False25 else:26 return True
這樣我們就能將要打斷點(diǎn)的地址的內(nèi)容修改成’/xCC’了。
我們用self.breakpoints來(lái)保存軟斷點(diǎn),在__init__中添加:
1 self.breakpoints = {}
然后編寫(xiě)bp_set來(lái)打軟斷點(diǎn):
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 False10 return True
該函數(shù)先檢查斷點(diǎn)字典中是否有該地址,如果沒(méi)有,則記錄該地址首字節(jié),并修改成’/xCC’,將其添加進(jìn)self.breakpoints字典。
硬件斷點(diǎn)
軟斷點(diǎn)最大的缺點(diǎn)是需要修改進(jìn)程內(nèi)存,這會(huì)破壞CRC,因此軟斷點(diǎn)是極其容易被反調(diào)試技術(shù)Anti的。硬件斷點(diǎn)由于只修改寄存器,因此不容易被目標(biāo)進(jìn)程察覺(jué)。
硬件斷點(diǎn)是用8個(gè)調(diào)試寄存器DR0-DR7實(shí)現(xiàn)的。其中DR0-DR3用來(lái)儲(chǔ)存斷點(diǎn)地址,DR4-DR5保留,DR6是調(diào)試狀態(tài)寄存器,在進(jìn)程觸發(fā)硬件斷點(diǎn)時(shí)返回觸發(fā)的是哪一個(gè)斷點(diǎn)給調(diào)試器,DR7是調(diào)試控制寄存器。
打硬件斷點(diǎn)首先需要在DR0-DR3中找一個(gè)空閑的寄存器,將斷點(diǎn)地址寫(xiě)進(jìn)去,然后修改DR7相應(yīng)標(biāo)志位來(lái)設(shè)置斷點(diǎn)長(zhǎng)度和斷點(diǎn)條件。
斷點(diǎn)條件有三個(gè):讀、寫(xiě)、執(zhí)行。分別表示在讀該地址、寫(xiě)該地址、執(zhí)行該地址的時(shí)候中斷:
1 HW_ACCESS = 0x000000032 HW_EXECUTE = 0x000000003 HW_WRITE = 0x00000001
我們還需要知道斷點(diǎn)的長(zhǎng)度才能判斷是否應(yīng)該中斷,斷點(diǎn)長(zhǎng)度也有三個(gè)選擇:1字節(jié)、2字節(jié)、4字節(jié)。
如何修改寄存器呢?操作系統(tǒng)為每一個(gè)線程維護(hù)了一個(gè)結(jié)構(gòu)體來(lái)保存上下文,當(dāng)線程中斷時(shí),操作系統(tǒng)會(huì)將所有寄存器放進(jìn)該結(jié)構(gòu)體里保存起來(lái),當(dāng)線程恢復(fù)執(zhí)行時(shí)將該結(jié)構(gòu)體取出,恢復(fù)寄存器的值。因此我們可以通過(guò)修改這個(gè)結(jié)構(gòu)體來(lái)實(shí)現(xiàn)修改寄存器的目的。
線程上下文結(jié)構(gòu)體如下:
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》一書(shū)中所使用的線程上下文是32位的,如果你在64位平臺(tái)下使用32位的結(jié)構(gòu)體來(lái)保存線程上下文,將會(huì)得到一個(gè)寄存器值全為0的空的線程上下文。
接下來(lái)編寫(xiě)一個(gè)用來(lái)獲取線程上下文的函數(shù),根據(jù)MSDN,在調(diào)用kernel32.GetThreadContext前需要對(duì)結(jié)構(gòu)體進(jìn)行初始化:
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_REGISTERS11 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 context6416 else:17 print '[*] Get thread context error. Error code: %d' % kernel32.GetLastError()18 return False
雖然是64位,但是我用kernel32.GetThreadContext來(lái)獲取線程上下文卻并沒(méi)任何問(wèn)題,反倒是調(diào)用kernel32.Wow64GetThreadContext卻會(huì)發(fā)生87號(hào)錯(cuò)誤(參數(shù)不正確),我試了很久也沒(méi)弄清楚為什么,如果有大神知道這是什么情況請(qǐng)聯(lián)系我,謝謝!
通常硬件斷點(diǎn)都是針對(duì)整個(gè)進(jìn)程的,因此我們需要對(duì)目標(biāo)進(jìn)程中的所有線程逐一修改線程上下文,這就涉及到一個(gè)枚舉線程的問(wèn)題,kernel32仍然提供API幫助我們做這件事。每個(gè)進(jìn)程都保存了一張線程快照表來(lái)保存所有線程的狀態(tài)信息,有了這張表我們可以利用kernel32.Thread32First獲取到第一個(gè)線程,先后調(diào)用kernel32.Thread32Next就能繼續(xù)遍歷線程了。
保存線程信息的結(jié)構(gòu)體和獲取線程快照表所需的常量參數(shù)如下所示:
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
枚舉線程的函數(shù)如下:
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_list15 else:16 return False
注意,《Gay hat python》源碼中此處有bug,以上代碼已將其修復(fù)。
現(xiàn)在我們就可以開(kāi)始編寫(xiě)打硬件斷點(diǎn)的函數(shù)了。我們用self.hardware_breakpoints來(lái)保存硬件斷點(diǎn),然后枚舉線程,逐一修改調(diào)試寄存器。
怎么修改調(diào)試寄存器呢?對(duì)于DR0-DR3,我們可以簡(jiǎn)單地找一個(gè)空閑的寄存器,將我們要打斷點(diǎn)的地址寫(xiě)進(jìn)去即可。對(duì)于DR7,需要仔細(xì)研究標(biāo)志位構(gòu)造。注意,我們是在64位平臺(tái)下,因此寄存器是64位的,《Gay hat python》中修改DR7的代碼是32位的,那么我們的代碼是否應(yīng)與此書(shū)不同呢?實(shí)際上,64位的DR6和DR7的高32位是用不到的,低32位構(gòu)造與32位寄存器完全一致,因此此書(shū)上的代碼在64位環(huán)境下兼容。
我們看看64位的DR6與DR7的構(gòu)造:

DR7的0、2、4、6位代表DR0、DR1、DR2、DR3,置1表示此寄存器被打上了硬件斷點(diǎn)。16、20、24、28位分別保存DR0、DR1、DR2、DR3的硬件斷點(diǎn)的條件,18、22、26、30位分別保存DR0、DR1、DR2、DR3的硬件斷點(diǎn)的長(zhǎng)度。
舉個(gè)例子,如果我們要打一個(gè)0x77284地址的內(nèi)存長(zhǎng)度為1,條件為執(zhí)行的硬件斷點(diǎn),該怎么修改寄存器呢。首先在DR0-DR3中找一個(gè)空閑的寄存器(假設(shè)是DR2)賦值為0x77284,接著將DR7的第24位賦值為HW_EXCUTE,將26位賦值為0(長(zhǎng)度減1)。
現(xiàn)在可以開(kāi)始寫(xiě)硬件斷點(diǎn)了:
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 = 010 elif not self.hardware_breakpoints.has_key(1):11 available = 112 elif not self.hardware_breakpoints.has_key(2):13 available = 214 elif not self.hardware_breakpoints.has_key(3):15 available = 316 else:17 return False18 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 = address25 elif available == 2:26 context64.Dr2 = address27 elif available == 3:28 context64.Dr3 = address29 #set condition30 context64.Dr7 |= condition << ((available * 4) + 16)31 #set length32 context64.Dr7 |= length << ((available * 4) + 18)33 #update context34 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 list39 self.hardware_breakpoints[available] = (address, length, condition)40 return True41
移除硬件斷點(diǎn)就只需要將調(diào)試寄存器改回來(lái)就可以了,下面只提供代碼:
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 = 0x0000000011 elif slot == 3:12 context.Dr3 = 0x0000000013 #condition14 context.Dr7 &= ~(3 << ((slot * 4) + 16))15 #length16 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 True22
內(nèi)存斷點(diǎn)
內(nèi)存斷點(diǎn)是最特殊的一類(lèi)斷點(diǎn),它實(shí)際上并不是被設(shè)計(jì)來(lái)作為斷點(diǎn)使用的,不過(guò)我們可以利用它能產(chǎn)生中斷的特性當(dāng)成斷點(diǎn)來(lái)使用。
內(nèi)存在操作系統(tǒng)中是分頁(yè)管理的,每個(gè)內(nèi)存頁(yè)都有讀和寫(xiě)的權(quán)限,如果試圖對(duì)一個(gè)內(nèi)存頁(yè)做權(quán)限之外的事情就會(huì)觸發(fā)一個(gè)異常導(dǎo)致中斷,因此打內(nèi)存斷點(diǎn)就是修改該內(nèi)存所在內(nèi)存頁(yè)的權(quán)限。
我們可以調(diào)用kernel32.VirtualQueryEx獲取目標(biāo)進(jìn)程指定內(nèi)存地址的內(nèi)存頁(yè)的基址,然后調(diào)用kernel32.VirtualProtectEx修改權(quán)限。
這一部分代碼與《Gray hat python》上一樣,《Gray hat python》上有詳盡解釋這里只給出代碼:
映射所需的結(jié)構(gòu)體:
class MEMORY_BASIC_INFORMATION(Structure): _fields_ = [ ("BaseAddress", PVOID), ("AllocationBase", PVOID), ("AllocationProtect", DWORD), ("RegionSize", SIZE_T), ("State", DWORD), ("Protect", DWORD), ("Type", DWORD),]
權(quán)限常量:
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 = 0x0000008010 PAGE_GUARD = 0x0000010011 PAGE_NOCACHE = 0x0000020012 PAGE_WRITECOMBINE = 0x00000400
斷點(diǎn)代碼:
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 False14 current_page += self.page_size15 self.memory_breakpoints[address] = (address, size, mbi)16 return True17
至此,我們實(shí)現(xiàn)了調(diào)試器打斷點(diǎn)的需求,接下來(lái)我們要監(jiān)聽(tīng)異常,在斷點(diǎn)觸發(fā)的時(shí)候中斷,并調(diào)用相應(yīng)例程進(jìn)行處理。
斷點(diǎn)例程
我們最開(kāi)始寫(xiě)的get_debug_event只是在監(jiān)聽(tīng)到調(diào)試事件后簡(jiǎn)單的讓目標(biāo)進(jìn)程繼續(xù)運(yùn)行,并沒(méi)有做任何事情,現(xiàn)在我們修改這個(gè)函數(shù)如下:
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.dwThreadId10 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.ExceptionCode19 self.exception_address = debug_event.u.Exception.ExceptionRecord.ExceptionAddress20 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_breakpoint27 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斷點(diǎn)中斷后是直接將‘/xCC’修改回原字節(jié),并沒(méi)有在恢復(fù)目標(biāo)進(jìn)程執(zhí)行后重新將int3
斷點(diǎn)打回去,因此斷點(diǎn)只生效一次,筆者的代碼則做了相應(yīng)處理,使斷點(diǎn)能夠繼續(xù)生效。
相應(yīng)斷點(diǎn)例程如下:
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 exception10 def exception_handler_breakpoint(self):11 print '[*] Inside the int3 breakpoint handler'12 print '[**] Exception address: 0x%08x' % self.exception_address13 if self.first_breakpoint == True:14 self.first_breakpoint = False15 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_CONTINUE23 24 #deal with single step exception25 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 = 032 elif self.context.Dr6 & 0x2 and self.hardware_breakpoints.has_key(1):33 slot = 134 elif self.context.Dr6 & 0x4 and self.hardware_breakpoints.has_key(2):35 slot = 236 elif self.context.Dr6 & 0x8 and self.hardware_breakpoints.has_key(3):37 slot = 338 else:39 continue_status = DBG_EXCEPTION_NOT_HANDLED40 # remove this hardware breakpoint41 if self.bp_del_hw(slot):42 continue_status = DBG_CONTINUE43 print '[*] Hardware breakpoint removed.'44 else :45 print '[*] Hardware breakpoint remove failed.'46 #raw_input('[*] Press any key to continue.')47 return continue_status48
總結(jié)
本文對(duì)《Gray hat python》一書(shū)中第三章做了歸納,并且修改了源代碼為64位版本,目前在筆者的電腦上運(yùn)行沒(méi)有任何問(wèn)題。筆者初學(xué)Python和Windows內(nèi)核編程,在StackOverflow查了很多問(wèn)題都是由于書(shū)中的代碼在64位環(huán)境下不兼容導(dǎo)致的,又沒(méi)在網(wǎng)上找到一份64位版本的代碼,因此寫(xiě)了本文。在學(xué)會(huì)編寫(xiě)調(diào)試器后,對(duì)一些像hook和進(jìn)程注入這樣的底層黑客技術(shù)也有了自己的思路,將來(lái)找時(shí)間將其實(shí)現(xiàn)。本文也是作為筆者的心得體會(huì)而寫(xiě)的,如果有大神發(fā)現(xiàn)代碼中的問(wèn)題歡迎指正,也可以與我交流討論,我的QQ是83488773。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注