3 自己动手写一个 windows 调试器

现在我们已经讲解完了基础知识,是时候实现一个真正的的 调试器的时候了。当微软开发 windows 的时候,他们增加了一 大堆的令人惊喜的调试函数以帮助开发者们保证产品的质量。我 们将大量的使用这些函数创建你自己的纯 python 调试器。有一点很重要,我们本质上是在深入的学习 PyDbg(Pedram Amini’s )的使用,这是目前能找到的最简洁的 Windows 平台下的 Python 调试器 。拜 Pedram 所赐,我尽可能用 PyDbg 完成了我的代码(包括函数名,变 量,等等),同时你也可以更容易的用 PyDbg 实现你的调试器。

为了对一个进程进行调试,你首先必须用一些方法把调试器和进程连接起来。所以,我 们的调试器要不然就是装载一个可执行程序然后运行它,要不然就是动态的附加到一个运行 的进程。Windows 的调试接口(Windows debugging API)提供了一个非常简单的方法完成 这两点。

运行一个程序和附加到一个程序有细微的差别。打开一个程序的优点在于他能在程序运 行任何代码之前完全的控制程序。这在分析病毒或者恶意代码的时候非常有用。附加到一个 进程,仅仅是强行的进入一个已经运行了的进程内部,它允许你跳过启动部分的代码,分析 你感兴趣的代码。你正在分析的地方也就是程序目前正在执行的地方。

第一种方法,其实就是从调试器本身调用这个程序(调试器就是父进程,对被调试进程 的控制权限更大)。在 Windows 上创建一个进程用 CreateProcessA()函数。将特定的标志传 进这个函数,使得目标进程能够被调试。一个 CreateProcessA()调用看起来像这样:

BOOL WINAPI CreateProcessA( 
    LPCSTR lpApplicationName, 
    LPTSTR lpCommandLine,
    LPSECURITY_ATTRIBUTES lpProcessAttributes, 
    LPSECURITY_ATTRIBUTES lpThreadAttributes, 
    BOOL bInheritHandles,
    DWORD dwCreationFlags, 
    LPVOID lpEnvironment, 
    LPCTSTR lpCurrentDirectory, 
    LPSTARTUPINFO lpStartupInfo,
    LPPROCESS_INFORMATION lpProcessInformation
);

初看这个调用相当恐怖,不过,在逆向工程中我们必须把大的部分分解成小的部分以便理解。这里我们只关心在调试器中创建一个进程需要注意的参数。这些参数是 lpApplicationName,lpCommandLine,dwCreationFlags,lpStartupInfo, 和 lpProcessInformation。 剩余的参数可以设置成空值(NULL)。关于这个函数的详细解释可以查看 MSDN(微软之葵 花宝典)。最前面的两个参数用于设置,需要执行的程序的路径和我们希望传递给程序的参 数。dwCreationFlags (创建标记)参数接受一个特定值,表示我们希望程序以被调试的状 态 启 动 。 最 后 两 个 参 数 分 别 分 别 指 向 2 个 结 构 (STARTUPINFO and PROCESS_INFORMATION) ,不仅包含了进程如何启动,以及启动后的许多重要信息 。( lpStartupInfo : STARTUPINFO 结 构 , 用 于 在 创 建 子 进 程 时 设 置 各 种 属 性 , lpProcessInformation:PROCESS_INFORMATION 结构,用来在进程创建后接收相关信息 , 该结构由系统填写。)

创建两个 Python 文件 my_debugger.py 和 my_debugger_defines.py。我们将创建一个父类 debugger() 接 着 逐 渐 的 增 加 各 种 调 试 函 数 。 另 外 , 把 所 有 的 结 构 , 联 合 , 常 量 放 到 my_debugger_defines.py 方便以后维护。

# my_debugger_defines.py
from ctypes import *
# Let's map the Microsoft types to ctypes for clarity 
WORD = c_ushort
DWORD = c_ulong
LPBYTE = POINTER(c_ubyte) LPTSTR = POINTER(c_char)
HANDLE = c_void_p
# Constants
DEBUG_PROCESS = 0x00000001 
CREATE_NEW_CONSOLE = 0x00000010
# Structures for CreateProcessA() function 
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),
    ]
# my_debugger.py
from ctypes import *
from my_debugger_defines import * 
kernel32 = windll.kernel32
class debugger():
    def init (self): 
        pass
    def load(self,path_to_exe):
        # dwCreation flag determines how to create the process
        # set creation_flags = CREATE_NEW_CONSOLE if you want
        # to see the calculator GUI 
        creation_flags = DEBUG_PROCESS
        # instantiate the structs
        startupinfo = STARTUPINFO() 
        process_information = PROCESS_INFORMATION()
        # The following two options allow the started process
        # to be shown as a separate window. This also illustrates
        # how different settings in the STARTUPINFO struct can affect
        # the debuggee. 
        startupinfo.dwFlags = 0x1 
        startupinfo.wShowWindow = 0x0
        # We then initialize the cb variable in the STARTUPINFO struct
        # which is just the size of the struct itself 
        startupinfo.cb = sizeof(startupinfo)
        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,保 证前面的文件都在同一个目录下。

#my_test.py
import my_debugger
debugger = my_debugger.debugger() debugger.load("C:\\WINDOWS\\system32\\calc.exe")

如果你是通过命令行或者 IDE 手动输入上面的代码,将会新产生一个进程也就是你键 入程序名,然后返回进程 ID(PID),最后结束。如果你用上面的例子 calc.exe,你将看不到 计算器的图形界面出现。因为进程没有把界面绘画到屏幕上,它在等待调试器继续执行的命 令。很快我们就能让他继续执行下去了。不过在这之前,我们已经找到了如何产生一个进程 用于调试,现在让我们实现另一个功能,附加到一个正在运行的进程。

为了附加到指定的进程,就必须先得到它的句柄。许多后面将用到的函数都需要句柄做 参数,同时我们也能在调试之前确认是否有权限调试它(如果附加都不行,就别提调试了)。 这个任务由 OpenProcess()完成,此函数由 kernel32.dll 库倒出,原型如下:

HANDLE WINAPI OpenProcess( 
    DWORD dwDesiredAccess, 
    BOOL bInheritHandle 
    DWORD dwProcessId
);

dwDesiredAccess 参数决定了我们希望对将要打开的进程拥有什么样的权限(当然是 越 大 越 好 root is hack )。 因 为 要 执 行 调 试 , 我 们 设 置 成 PROCESS_ALL_ACCESS 。 bInheritHandle 参数设置成 False,dwProcessId 参数设置成我们希望获得句柄的进程 ID ,也就是前面获得的 PID。如果函数成功执行,将返回一个目标进程的句柄。

接下来用 DebugActiveProcess()函数附加到目标进程:

BOOL WINAPI DebugActiveProcess(
    DWORD dwProcessId
);

把需要 a 附加的 PID 传入。一旦系统认为我们有权限访问目标进程,目标进程就假定 我们的调试器已经准备好处理调试事件,然后把进程的控制权转移给调试器。调试器接着循 环调用 WaitForDebugEvent()以便俘获调试事件。函数原型如下:

BOOL WINAPI WaitForDebugEvent(
    LPDEBUG_EVENT lpDebugEvent, 
    DWORD dwMilliseconds
);

第一个参数指向 DEBUG_EVENT 结构,这个结构描述了一个调试事件。第二个参数设 置成 INFINITE(无限等待),这样 WaitForDebugEvent() 就不用返回,一直等待直到一个事 件产生。

调试器捕捉的每一个事件都有相关联的事件处理函数,在程序继续执行前可以完成不同 的 操 作 。 当 处 理 函 数 完 成 了 操 作 , 我 们 希 望 进 程 继 续 执 行 用 , 这 时 候 再 调 用 ContinueDebugEvent()。原型如下:

BOOL WINAPI ContinueDebugEvent( 
    DWORD dwProcessId,
    DWORD dwThreadId, 
    DWORD dwContinueStatus
);

dwProcessId 和 dwThreadId 参数由 DEBUG_EVENT 结构里的数据填充,当调试器捕捉 到调试事件的时候,也就是 WaitForDebugEvent()成功执行的时候,进程 ID 和线程 ID 就以 及初始化好了。dwContinueStatus 参数告诉进程是继续执行(DBG_CONTINUE),还是产生异 常(DBG_EXCEPTION_NOT_HANDLED)。

还剩下一件事没做,从进程分离出来:把进程 ID 传递给 DebugActiveProcessStop()。 现在我们把这些全合在一起,扩展我们的 my_debugger 类,让他拥有附加和分离一个进程的功能。同时加上打开一个进程和获得进程句柄的能力。最后在我们的主循环里完成事 件处理函数。打开 my_debugger.py 键入以下代码。

提示:所有需要的结构,联合和常量都定义在了 debugger_defines.py 文件里,完整的代码可 以从 http://www.nostarch.com/ghpython.htm 下载。

#my_debugger.py
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):
        ...
        print "[*] We have successfully launched the process!" 
        print "[*] PID: %d" % process_information.dwProcessId
        # Obtain a valid handle to the newly created process
        # and store it for future access
        self.h_process = self.open_process(process_information.dwProcessId)
        ...
    def open_process(self,pid):
        h_process = kernel32.OpenProcess(PROCESS_ALL_ACCESS,pid,False) 
        return h_process
    def attach(self,pid):
        self.h_process = self.open_process(pid)
        # We attempt to attach to the process
        # if this fails we exit the call
        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):
        # Now we have to poll the debuggee for
        # debugging events
        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):
            # We aren't going to build any event handlers
            # just yet. Let's just resume the process for now. 
            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

现在让我们修改下测试套件以便使用新创建的函数。

#my_test.py
import my_debugger
debugger = my_debugger.debugger()
pid = raw_input("Enter the PID of the process to attach to: ") 
debugger.attach(int(pid))
debugger.detach()

按以下的步骤进行测试(windows 下):

  1. 选择 开始->运行->所有程序->附件->计算器

  2. 右击桌面低端的任务栏,从退出的菜单中选择任务管理器。

  3. 选择进程面板.

  4. 如果你没看到 PID 栏,选择 查看->选择列

  5. 确保进程标识符(PID)前面的确认框是选中的,然后单击 OK。

  6. 找到 calc.exe 相关联的 PID

  7. 执行 my_test.py 同时前面找到的 PID 传递给它。

  8. 当 Press a key to continue...打印在屏幕上的时候,试着操作计算器的界面。你应该什么键都 按不了。这是因为进程被调试器挂起来了,等待进一步的指示。

  9. 在你的 Python 控制台里按任何的键,脚本将输出别的信息,热爱后结束。

  10. 现在你能够操作计算器了。

如果一切都如描绘的一样正常工作,把下面两行从 my_debugger.py 中注释掉:

# raw_input("Press any key to continue...")
# self.debugger_active = False

现在我们已经讲解了获取进程句柄的基础知识,以及如何创建一个进程,附加一个运行 的进程,接下来让我们给调试器加入更多高级的功能。