python的系统底层操作5—软断点 ∞
python进(线)程中的数据可能是时刻变化的,为了能得到它们内部的数据,或者你想知道当代码运行到某些地方时,内存或CPU里面都有些什么数据,这时你就要先让进(线)程停下来,这个在前面的博客中已经提到,更进一步地,我们要它在我们想要的地方停下来,这就要涉及到断点了。平时经常写代码调代码的朋友对断点应该很熟悉了,下文会简单地分析这些断点的分类及原理。
1、软断点
假如我们要访问的指令地址为0x748cc5b9,指令8BC3(mov eax, ebx),为了使CPU执行到此地址的命令时,停下来,我们把8BC3的前一个字节替换成0xCC,它是INT3中断指令,它告诉CPU暂停执行当前的进程,替换后,变成CCC3,把原来的8B保存到另一个变量中,要在中断后继续执行就把8B替换回去。
根据这个原理,我们可以给出一个指令的具体地址,让线程跑到这里的时候停下来。当然,随便说一个地址也不一定会起作用,因为它可能是数据地址甚至不可访问的地址等等,所以这种情况比较少,当然你也可以用一个其它的工具去得到某个指令所在的内存地址,不过我们一般是给出一个函数的首地址,让CPU在这个函数要执行时,停下来。GetProcAddress,这个API可以帮我们得到某个函数的首地址:
FARPROC GetProcAddress(
HMODULE hModule, // DLL模块句柄
LPCSTR lpProcName // 函数名
);
在MSDN中搜不到具体的定义,不过它很好理解,它带两个参数,第一个是模块的句柄,第二个是函数名,字符串。对于模块的句柄,可以用GetModuleHandle来取得。它只有一个参数,就是模块名,很简单。
接下来的事就是从得到的这个函数首地址中,取得第一个字节,把它存起来,然后替换成0xCC。之后再把它替换回去。
原理很简单,下面的例子,在系统的printf函数首地址下个断点,直接上代码:
具体实现,debugger.py
# -*- coding: utf-8 -*-
from ctypes import *
from debug_define import *
kernel32 = windll.kernel32
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False
self.h_process = None
self.context = None
self.breakpoints = {} # 所有的断点数
self.first_breakpoint = True
def loadExe(self, exe_path):
creation_flags = DEBUG_PROCESS
startupinfo = STARTUPINFO() #相当于c的alloc这么大一个空间,初始化为0或NULL
processinfo = PROCESS_INFORMATION() #同上
startupinfo.dwFlags = STARTF_USESHOWWINDOW
startupinfo.wShowWindow = SW_HIDE
startupinfo.cb = sizeof(startupinfo)
# 开始创建
if kernel32.CreateProcessA(exe_path, #lpApplicationName
None, #lpCommandLine
None, #lpProcessAttributes
None, #lpThreadAttributes
None, #bInheritHandles
creation_flags, #dwCreationFlags
None, #lpEnvironment
None, #lpCurrentDirectory
byref(startupinfo), #lpStartupInfo
byref(processinfo)): #lpProcessInformation
print "进程ID:%d" % processinfo.dwProcessId
else:
print "进程创建失败。"
# 打开这个进程
self.h_process = self.open_process(processinfo.dwProcessId)
# 由id取得进程
def open_process(self, pid):
h_progress = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
return h_progress
# 附加进程
def attach(self, pid):
self.h_process = self.open_process(pid)
# 调用DebugActiveProcess,附加到pid进程
if kernel32.DebugActiveProcess(pid):
self.debugger_active = True # 可以开始调试了
self.pid = int(pid)
# self.run()
else:
print "进程附加失败!"
def run(self):
while self.debugger_active == True:
print '--- *** ---'
self.fire_debug_event()
# 等待触发调试事件。。
def fire_debug_event(self):
debug_event = DEBUG_EVENT()
continue_status = DBG_CONTINUE
# 接下去就是等待调试的事件被触发
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE): #一直等
# 到这里,说明调试的条件被触发了
# 。。。(在这里做些调试相关的事情)
self.debug_event = debug_event
self.h_thread = self.open_thread(debug_event.dwThreadId)
self.context = self.get_thread_context(h_thread=self.h_thread)
# print "被调试的进程:%d,线程:%d" % (debug_event.dwProcessId,debug_event.dwThreadId)
print "0x%08x" % debug_event.dwDebugEventCode
if debug_event.dwDebugEventCode == EXCEPTION_DEBUG_EVENT:
# 获取异常的信息
exception = debug_event.u.Exception.ExceptionRecord.ExceptionCode
self.exception_addr = debug_event.u.Exception.ExceptionRecord.ExceptionAddress
print "异常代码:%d,地址0x%08x" % (exception,self.exception_addr)
if exception == EXCEPTION_ACCESS_VIOLATION:
print "EXCEPTION_ACCESS_VIOLATION"
elif exception == EXCEPTION_BREAKPOINT:
continue_status = self.exception_handle_breakpoint()
# print "EXCEPTION_BREAKPOINT"
elif exception == EXCEPTION_GUARD_PAGE:
print "EXCEPTION_GUARD_PAGE"
elif exception == EXCEPTION_SINGLE_STEP:
print "EXCEPTION_SINGLE_STEP"
# 这个调试事件处理结束,继续
# self.debugger_active = False
kernel32.ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
continue_status)
# 控制断点事件
def exception_handle_breakpoint(self):
print "断点地址:0x%08x" % self.exception_addr
# 打印出CPU中的数据
self.print_cur_context()
if not self.breakpoints.has_key(self.exception_addr):
if self.first_breakpoint:
self.first_breakpoint = False
print "第一个断点地址"
return DBG_CONTINUE
else:
print "用户定义的断点"
# 把INT3断点去掉
self.write_process_memory(self.exception_addr,
self.breakpoints[self.exception_addr])
# 把EIP往回调动一个字节,因为它多读了一个INT3中断0xCC
self.context.Eip -= 1
kernel32.SetThreadContext(self.h_thread,byref(self.context))
continue_status = DBG_CONTINUE
return continue_status
# 解除对进程的附加
def detach(self):
if kernel32.DebugActiveProcessStop(self.pid):
print "完成调试,退出..."
return True
else:
print "调试过程中出错..."
return False
# 打开线程
def open_thread(self, thread_id):
h_thread = kernel32.OpenThread(THREAD_ALL_ACCESS,
None,
thread_id)
if h_thread is not None:
return h_thread
else:
print "获取线程ID时出错!"
return False
# 得到线程的CONTEXT信息
def get_thread_context(self, thread_id=None, h_thread=None):
context = CONTEXT()
context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS
# 获得线程句柄
if h_thread is None:
self.h_thread = self.open_thread(thread_id)
if kernel32.GetThreadContext(h_thread, byref(context)):
# kernel32.CloseHandle(h_thread)
return context
else:
return False
# 打印寄存器信息(CONTEXT)
def print_cur_context(self):
thread_context = self.context
if thread_context:
# 打印信息
print "====================================="
print "[**] 线程ID: %d" % self.h_process
print "[**] EAX: 0x%08x" % thread_context.Eax
print "[**] EBX: 0x%08x" % thread_context.Ebx
print "[**] ECX: 0x%08x" % thread_context.Ecx
print "[**] EDX: 0x%08x" % thread_context.Edx
print "===="
print "[**] ESI: 0x%08x" % thread_context.Esi
print "[**] EDI: 0x%08x" % thread_context.Edi
print "===="
print "[**] EBP: 0x%08x" % thread_context.Ebp
print "[**] ESP: 0x%08x" % thread_context.Esp
print "[**] EIP: 0x%08x" % thread_context.Eip
print "====================================="
# 读取线程内存中的数据
def read_process_memory(self, addr, length):
data = ""
read_buf = create_string_buffer(length)
cnt = c_ulong(0)
if kernel32.ReadProcessMemory(self.h_process,
addr,
read_buf,
length,
byref(cnt)):
data += read_buf.raw
return data
else:
return False
# 往线程内存中写数据
def write_process_memory(self, addr, data):
cnt = c_ulong(0)
length = len(data)
c_data = c_char_p(data[cnt.value])
if kernel32.WriteProcessMemory(self.h_process,
addr,
c_data,
length,
byref(cnt)):
return True
else:
return False
# 设置断点
def set_breakpoint(self, addr):
if not self.breakpoints.has_key(addr): # 在这个地址上还没加过断点
try:
# 把该地址当前的字节数据备份一下
ori_byte = self.read_process_memory(addr, 1)
# 写入INT3中断,0xCC
self.write_process_memory(addr,"\xCC")
# 记录到breakpoints字典里面
self.breakpoints[addr] = ori_byte
except:
return False
return True
# 要下断点,得找一个地址,一般是一个函数的首地址
# 获取函数首地址的方法:
def get_func_addr(self, dllName, # dllName函数所在dll模块的名称
funcName): # funcName函数名称
handle = kernel32.GetModuleHandleA(dllName)
addr = kernel32.GetProcAddress(handle, funcName)
kernel32.CloseHandle(handle)
print "得到的地址:0x%08x" % addr
return addr
然后我们自己写个简单的循环打印的程序,里面调用系统的printf函数,专用来调试测试
from ctypes import *
import time
msvcrt = cdll.msvcrt
cnt = 0
while True:
msvcrt.printf("going...%d\n" , cnt)
time.sleep(3)
cnt += 1
测试文件,testMyDebugger.py:
# -*- coding: utf-8 -*-
import debugger.debugger as debugger
deb = debugger.debugger()
pid = raw_input("请输入要调试(附加)的进程ID:")
deb.attach(int(pid))
printf_addr = deb.get_func_addr("msvcrt.dll", "printf")
deb.set_breakpoint(printf_addr)
deb.run()
deb.detach()
还有结构体、联合体、常量等的定义,在debug_define.py里面:
# -*- coding: utf-8 -*-
from ctypes import *
# 定义window编程中常用的类型
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte)
LPTSTR = POINTER(c_char)
HANDLE = c_void_p
PVOID = c_void_p
LPVOID = c_void_p
ULONG_PTR = c_ulong
SIZE_T = c_ulong
BYTE = c_ubyte
# 常量
DEBUG_PROCESS = 0x0001
CREATE_NEW_CONSOLE = 0x0010
PROCESS_ALL_ACCESS = 0x001F0FFF
THREAD_ALL_ACCESS = 0x001F03FF
INFINITE = 0xFFFFFFFF
DBG_CONTINUE = 0x00010002
STARTF_USESHOWWINDOW = 0x0001
SW_HIDE = 0
TH32CS_SNAPTHREAD = 0x00000004
CONTEXT_FULL = 0x00010007 # 得到所有的CPU寄存器
CONTEXT_DEBUG_REGISTERS = 0x00010010 # CPU的调试寄存器
# Exception
EXCEPTION_DEBUG_EVENT = 0x0001
EXCEPTION_ACCESS_VIOLATION = 0xC0000005
EXCEPTION_BREAKPOINT = 0x80000003
EXCEPTION_GUARD_PAGE = 0x80000001
EXCEPTION_SINGLE_STEP = 0x80000004
# CreateProcessA函数所需结构体
class STARTUPINFO(Structure):
_fields_ = [
("cb",DWORD),
("lpReserved",LPTSTR),
("lpDesktop",LPTSTR),
("lpTitle",LPTSTR),
("dwX",DWORD),
("dwY",DWORD),
("dwXSize",DWORD),
("dwYSize",DWORD),
("dwXCountChars",DWORD),
("dwYCountChars",DWORD),
("dwFillAttribute",DWORD),
("dwFlags",DWORD),
("wShowWindow",WORD),
("cbReserved2",WORD),
("lpReserved2",LPBYTE),
("hStdInput",HANDLE),
("hStdOutput",HANDLE),
("hStdError",HANDLE)
]
class PROCESS_INFORMATION(Structure):
_fields_ = [
("hProcess",HANDLE),
("hThread",HANDLE),
("dwProcessId",DWORD),
("dwThreadId",DWORD)
]
class EXCEPTION_RECORD(Structure):
pass
EXCEPTION_RECORD._fields_ = [
("ExceptionCode",DWORD),
("ExceptionFlags",DWORD),
("ExceptionRecord",POINTER(EXCEPTION_RECORD)),
("ExceptionAddress",PVOID),
("NumberParameters",DWORD),
("ExceptionInformation",ULONG_PTR*15) # 最多允许15条错误信息
]
class EXCEPTION_DEBUG_INFO (Structure):
_fields_ = [
("ExceptionRecord", EXCEPTION_RECORD),
("dwFirstChance",DWORD)
]
class DEBUG_EVENT_UNION(Union):
_fields_ = [
("Exception",EXCEPTION_DEBUG_INFO), #只用到这一个,下面的不用定义了
# ("CreateThread",CREATE_THREAD_DEBUG_INFO),
# ("CreateProcessInfo",CREATE_PROCESS_DEBUG_INFO),
# ("ExitThread",EXIT_THREAD_DEBUG_INFO),
# ("ExitProcess",EXIT_PROCESS_DEBUG_INFO),
# ("LoadDll",LOAD_DLL_DEBUG_INFO),
# ("UnloadDll",UNLOAD_DLL_DEBUG_INFO),
# ("DebugString",OUTPUT_DEBUG_STRING_INFO),
# ("RipInfo",RIP_INFO)
]
class DEBUG_EVENT(Structure):
_fields_ = [
("dwDebugEventCode",DWORD),
("dwProcessId",DWORD),
("dwThreadId",DWORD),
("u",DEBUG_EVENT_UNION)
]
class THREADENTRY32(Structure):
_fields_ = [
("dwSize",DWORD),
("cntUsage",DWORD),
("th32ThreadID",DWORD),
("th32OwnerProcessID",DWORD),
("tpBasePri",DWORD), # 本来是LONG,它和DWORD一样的位数
("tpDeltaPri",DWORD), # LONG
("dwFlags",DWORD)
]
# Used by the CONTEXT structure
class FLOATING_SAVE_AREA(Structure):
_fields_ = [
("ControlWord", DWORD),
("StatusWord", DWORD),
("TagWord", DWORD),
("ErrorOffset", DWORD),
("ErrorSelector", DWORD),
("DataOffset", DWORD),
("DataSelector", DWORD),
("RegisterArea", BYTE * 80),
("Cr0NpxState", DWORD),
]
class CONTEXT(Structure):
_fields_ = [
("ContextFlags", DWORD),
("Dr0", DWORD),
("Dr1", DWORD),
("Dr2", DWORD),
("Dr3", DWORD),
("Dr6", DWORD),
("Dr7", DWORD),
("FloatSave", FLOATING_SAVE_AREA),
("SegGs", DWORD),
("SegFs", DWORD),
("SegEs", DWORD),
("SegDs", DWORD),
("Edi", DWORD),
("Esi", DWORD),
("Ebx", DWORD),
("Edx", DWORD),
("Ecx", DWORD),
("Eax", DWORD),
("Ebp", DWORD),
("Eip", DWORD),
("SegCs", DWORD),
("EFlags", DWORD),
("Esp", DWORD),
("SegSs", DWORD),
("ExtendedRegisters", BYTE * 512)
]
结果:
请输入要调试(附加)的进程ID:8516
得到的地址:0x748cc5b9
— * —
0x00000003
— *** —
0x00000006
— *** —
0x00000006
— *** —
………
………
… #这里一共有22个0x00000006
— *** —
0x00000002
— *** —
0x00000001
异常代码:2147483651,地址0x76fd000c
断点地址:0x76fd000c
=====================================
[] 线程ID: 148
[] EAX: 0x7efda000
[] EBX: 0x00000000
[] ECX: 0x00000000
[] EDX: 0x7705f85a
====
[] ESI: 0x00000000
[] EDI: 0x00000000
====
[] EBP: 0x0258ff88
[] ESP: 0x0258ff5c
[] EIP: 0x76fd000d
=====================================
第一个断点地址
— *** —
0x00000004
— *** —
0x00000001
异常代码:2147483651,地址0x748cc5b9
断点地址:0x748cc5b9
=====================================
[] 线程ID: 148
[] EAX: 0x0027fb6c
[] EBX: 0x0027fb01
[] ECX: 0x00000001
[] EDX: 0x0027facc
====
[] ESI: 0x0027fad4
[] EDI: 0x00378ad8
====
[] EBP: 0x0027fad8
[] ESP: 0x0027fac8
[**] EIP: 0x748cc5ba
=====================================
用户定义的断点
— ** —
打印出来的下面这种值,
— ** —
0x00000003
是DEBUG_EVENT的dwDebugEventCode字段,它的值有:
#define EXCEPTION_DEBUG_EVENT 1
#define CREATE_THREAD_DEBUG_EVENT 2
#define CREATE_PROCESS_DEBUG_EVENT 3
#define EXIT_THREAD_DEBUG_EVENT 4
#define EXIT_PROCESS_DEBUG_EVENT 5
#define LOAD_DLL_DEBUG_EVENT 6
#define UNLOAD_DLL_DEBUG_EVENT 7
#define OUTPUT_DEBUG_STRING_EVENT 8
#define RIP_EVENT 9
它的定义在WinBase.h里面,和WinNT.h同一目录,具体地址C:\Program Files\Microsoft SDKs\Windows\v7.0A\Include(我的机上上的路径,各大机子不尽相同),找不到的话,我上传了:金山快盘附件:WinBase.h(333.15KB)
大家可以对应上面这些值去看程序运行的结果,先创建调试进程,再加载相应的模块,再创建调试线程,然后触发异常,结束线程,最后再触发一次断点。这里为什么会有两个断点触发呢?在上面的exception_handle_breakpoint函数中,对第一次触发事件直接跳过,第二次触发时,再把原字节写回INT3中断0xCC所在的地方,所以就有两次触发,当然你也可以在第一次触发时把INT3断点信息替换掉。上面提到过“异常”这两个字,断点也是一种异常,这些异常的定义在WinBase.h里面,从78行开始。而它们的值则在WinNT.h里面定义,从2005行开始,两个文件综合起来查询来写这个程序。