手把手教你写Windows 64位平台调试器

Posted on 2014-07-20 00:29  ArayzBuffer  阅读(6011)  评论(3编辑  收藏  举报

本文网页排版有些差,已上传了doc,可以下载阅读。本文中的所有代码已打包,下载地址在此

--------------------------------------------------------------------------------------------------------------------------------------------------------------

 

手写一个调试器有助于我们理解hook、进程注入等底层黑客技术具体实现,在编写过程中需要涉及大量Windows内核编程知识,因此手写调试器也可以作为启发式学习内核编程的任务驱动。(本文中代码大量参考《Gray hat python》(Python灰帽子),此书中详细讲解了调试器的基本原理,因此本文假设读者已具备基本调试技能,能理解调试器大致原理以及断点原理,读者在不理解本文内容时可参考此书,不过《Gray hat python》中的代码是32位的,并且有不少错误,笔者花了一周的时间调试修复了书中的bug,并且将其修改成了64位版本,本文旨在总结该书中第三章的内容并提供一份64位版本的代码,该代码在本人64Win7i3-2310M CPU主机上能成功运行)

首先来看看一个调试器需要实现哪些基本需求:

  1. 以调试级状态启动目标进程,或附加在目标进程上,监听调试事件;
  2. 为程序打断点,包括int3断点(软断点),硬件断点,内存断点;
  3. 遇到断点后挂起目标进程,并能做相应处理;
  4. 挂起目标进程后获取上下文,并能修改上下文

 完成这些需求都需要调用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需要用到两个关键参数:

  1. STARTUPINFOPROCESS_INFORMATION两个结构体,在MSDN里同样可以找到它们的文档,我们需要把它们映射到my_debugger_defines.py里面来;
  2. 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位的DR6DR7的高32位是用不到的,低32位构造与32位寄存器完全一致,因此此书上的代码在64位环境下兼容。

我们看看64位的DR6DR7的构造:

 

DR70246位代表DR0DR1DR2DR3,置1表示此寄存器被打上了硬件断点。16202428位分别保存DR0DR1DR2DR3的硬件断点的条件,18222630位分别保存DR0DR1DR2DR3的硬件断点的长度。

举个例子,如果我们要打一个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位版本,目前在笔者的电脑上运行没有任何问题。笔者初学PythonWindows内核编程,在StackOverflow查了很多问题都是由于书中的代码在64位环境下不兼容导致的,又没在网上找到一份64位版本的代码,因此写了本文。在学会编写调试器后,对一些像hook和进程注入这样的底层黑客技术也有了自己的思路,将来找时间将其实现。本文也是作为笔者的心得体会而写的,如果有大神发现代码中的问题欢迎指正,也可以与我交流讨论,我的QQ83488773