二、纯 python 实现一个调试器

要想调试一个程序, 就需要与目标程序建立某种联系, 为此, 我们的调试器应该具备两种基本能力:

  • 打开一个程序, 并使它以自身子进程的形式运行起来
  • 附加到一个正在进去的程序上

1. 创建子进程

使用 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 方式就能看到我们的计算器窗口了.

2. 附加现有进程

该功能涉及到的 api 有:

  • OpenProcess()
  • DebugActiveProcess()
  • WaitForDebugEvent()
  • ContinueDebugEvent()
  • DebugActiveProcessStop()

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()

你可能感兴趣的:(★Python,灰帽★)