要想调试一个程序, 就需要与目标程序建立某种联系, 为此, 我们的调试器应该具备两种基本能力:
使用 CreateProcessA() 来实现就可以了.
这个函数其实已经很熟悉了, 回顾一下吧:
BOOL CreateProcess (
LPCTSTR lpApplicationName, // 程序路径
LPTSTR lpCommandLine, // 命令行参数
LPSECURITY_ATTRIBUTES lpProcessAttributes。
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags, // 创建的标志
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo, // 指向一个用于决定新进程的主窗体如何显示的 STARTUPINFO 结构体
LPPROCESS_INFORMATION lpProcessInformation // 指向一个用来接收新进程的识别信息的 PROCESS_INFORMATION 结构体
);
基本上, 我们只要关心 lpApplicationName、lpCommandLine、dwCreationFlags、lpStartupInfo 和 lpProcessInformation 这五个参数, 其它的设为 NULL即可.
其中通过给 dwCreationFlags 参数设定一个指定的值, 那么跑起来的程序就有了可调试权限.
现在我们来通过 python 来运行一个新进程.
首先新建两个 py 文件: my_debugger.py 和 my_debugger_defines.py
my_debugger_defines.py 保存的是程序常量及结构定义的地方.
my_debugger.py 是核心代码实现的地方.
先来定义我们程序所需要的数据结构:
# -*- coding: utf-8 -*-
from ctypes import *
# 统一命名风格
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte)
LPSTR = c_char_p
LPWSTR = c_wchar_p
HANDLE = c_void_p
DEBUG_PROCESS = 0x00000001 # 与父进程共用一个控制台(以子进程的方式运行)
CREATE_NEW_CONSOLE = 0x00000010 # 独占一个新控制台(以单独的进程运行)
# 定义 CreateProcessA() 所需的结构体
class STARTUPINFOA(Structure):
_fields_ = [
('cb', DWORD),
('lpReserved', LPSTR),
('lpDesktop', LPSTR),
('lpTitle', LPSTR),
('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_INFORMATIONA(Structure):
_fields_ = [
('hProcess', HANDLE),
('hThread', HANDLE),
('dwProcessId', DWORD),
('dwThreadId', DWORD),
]
再来书写核心代码:
# -*- coding: utf-8 -*-
from ctypes import *
from my_debugger_defines import *
kernel32 = windll.kernel32
class debugger():
def __init__(self):
pass
def load(self, path_to_exe):
creation_flags = DEBUG_PROCESS # 指定新进程打开方式(这里选的是调试模式)
startupinfo = STARTUPINFOA()
process_information = PROCESS_INFORMATIONA()
startupinfo.cb = sizeof(startupinfo)
# 下面的设置让新进程在一个单独窗体中被显示
startupinfo.dwFlags = 0x1
startupinfo.wShowWindow = 0x0
if kernel32.CreateProcessA(path_to_exe,
None,
None,
None,
None,
creation_flags,
None,
None,
byref(startupinfo),
byref(process_information)):
print 'We have successfully launched the process!'
print 'PID: %d' % process_information.dwProcessId
else:
print 'Error: 0x%08x' % kernel32.GetLastError()
现在就可以来编写测试文件了, 新建一个 my_test.py 文件:
# -*- coding: utf-8 -*-
import my_debugger
debugger = my_debugger.debugger()
debugger.load('c:/windows/system32/calc.exe')
运行脚本后, 打印出了运行成功的结果, 但是我们并没有看到 calc.exe 的窗口, 这是因为我们使用的是 DEBUG_PROCESS 方式新建进程, 当我们脚本执行完毕, python 进程退出, 那么我们创建的子进程 calc.exe 也就被销毁了.
如果改成 CREATE_NEW_CONSOLE 方式就能看到我们的计算器窗口了.
该功能涉及到的 api 有:
OpenProcess 用来获取进程句柄, 原型如下:
HANDLE OpenProcess(
DWORD dwDesiredAccess, //渴望得到的访问权限(标志)应该为 PROCESS_ALL_ACCESS
BOOL bInheritHandle, // 是否继承句柄, 总为 FALSE
DWORD dwProcessId// 进程标示符
);
DebugActiveProcess 用来实现进程的附加, 原型如下:
BOOL WINAPI DebugActiveProcess(
__in DWORD dwProcessId
);
成功附加后, 目标进程会立即将自身的控制权转交给调试程序(这就是为什么首次附加时, 目标进程会断下的原因), 然后调试程序可以循环地调用 WaitForDebugEvent 等待目标进程触发调试事件:
WaiteForDebugEvent(
LPDEBUG_EVENT _DEBUG_EVENT, // 描述具体的调试事件
DWORD dwMilliseconds // 可设为 INFINITE
);
在处理完调试事件后, 通过调用 ContinueDebugEvent 返回权限给目标进程:
BOOL ContinueDebugEvent(
DWORD pid, // 来源于 WaitForDebugEvent 中的第一个参数
DWORD tid, // 来源于 WaitForDebugEvent 中的第一个参数
DWORD status // 指定目标进程下一步动作, DBG_CONTINUE 是继续执行, DBG_EXCEPTION_NOT_HANDLED 继续处理所捕获的事件
)
当需要退出调试的时候, 调试 DebugActiveProcessStop 即可:
BOOL WINAPI DebugActiveProcessStop(
__in DWORD dwProcessId
);
好, 接下来上代码:
首先是 my_debugger_defines.py 数据结构定义:
# -*- coding: utf-8 -*-
from ctypes import *
# 统一命名风格
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte)
LPSTR = c_char_p
LPWSTR = c_wchar_p
HANDLE = c_void_p
PVOID = c_void_p
UINT_PTR = c_ulong
DEBUG_PROCESS = 0x00000001 # 与父进程共用一个控制台(以子进程的方式运行)
CREATE_NEW_CONSOLE = 0x00000010 # 独占一个新控制台(以单独的进程运行)
PROCESS_ALL_ACCESS = 0x001F0FFF
INFINITE = 0xFFFFFFFF
DBG_CONTINUE = 0x00010002
# 定义 CreateProcessA() 所需的结构体
class STARTUPINFOA(Structure):
_fields_ = [
('cb', DWORD),
('lpReserved', LPSTR),
('lpDesktop', LPSTR),
('lpTitle', LPSTR),
('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_INFORMATIONA(Structure):
_fields_ = [
('hProcess', HANDLE),
('hThread', HANDLE),
('dwProcessId', DWORD),
('dwThreadId', DWORD),
]
########################################################################
class EXCEPTION_RECORD(Structure):
pass
EXCEPTION_RECORD._fields_ = [ # 这里之所以要这么设计, 是因为 ExceptionRecord 调用了 EXCEPTION_RECORD, 所以要提前声明
('ExceptionCode', DWORD),
('ExceptionFlags', DWORD),
('ExceptionRecord', POINTER(EXCEPTION_RECORD)),
('ExceptionAddress', PVOID),
('NumberParameters', DWORD),
('ExceptionInformation', UINT_PTR * 15),
]
class EXCEPTION_DEBUG_INFO(Structure):
_fields_ = [
('ExceptionRecord', EXCEPTION_RECORD),
('dwFirstChance', DWORD),
]
class U_DEBUG_EVENT(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', U_DEBUG_EVENT),
]
然后是核心代码:
# -*- coding: utf-8 -*-
from ctypes import *
from my_debugger_defines import *
kernel32 = windll.kernel32
class debugger():
def __init__(self):
self.h_process = None
self.pid = None
self.debugger_active = False # 用来控制循环的开关
def load(self, path_to_exe):
creation_flags = DEBUG_PROCESS
startupinfo = STARTUPINFOA()
process_information = PROCESS_INFORMATIONA()
startupinfo.cb = sizeof(startupinfo)
# 下面的设置让新进程在一个单独窗体中被显示
startupinfo.dwFlags = 0x1
startupinfo.wShowWindow = 0x0
if kernel32.CreateProcessA(path_to_exe,
None,
None,
None,
None,
creation_flags,
None,
None,
byref(startupinfo),
byref(process_information)):
print 'We have successfully launched the process!'
print 'PID: %d' % process_information.dwProcessId
# 成功, 保存句柄
self.h_process = self.open_process(process_information.dwProcessId)
else:
print 'Error: 0x%08x' % kernel32.GetLastError()
def open_process(self, pid):
h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS, False, pid)
return h_process
def attach(self, pid):
self.h_process = self.open_process(pid)
if kernel32.DebugActiveProcess(pid):
self.debugger_active = True
self.pid = int(pid)
self.run()
else:
print 'Unable to attach to the process.'
def run(self):
while self.debugger_active == True:
self.get_debug_event()
def get_debug_event(self):
debug_event = DEBUG_EVENT()
continue_status = DBG_CONTINUE
if kernel32.WaitForDebugEvent(byref(debug_event), INFINITE):
# 这里没有对调试事件做任何处理, 只是简单的返回到目标程序
raw_input('press a key to continue ...')
self.debugger_active = False
kernel32.ContinueDebugEvent(debug_event.dwProcessId,
debug_event.dwThreadId,
continue_status)
def detach(self):
if kernel32.DebugActiveProcessStop(self.pid):
print 'Finished debugging. Exiting ...'
return True
else:
print 'There was an error'
return False
最后, 更改我们的测试文件:
# -*- coding: utf-8 -*-
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input('Enter the pid: ')
debugger.attach(int(pid))
debugger.detach()