3.2获得 CPU 寄存器状态

一个调试器必须能够在任何时候都搜集到 CPU 的各个寄存器的状态。当异常发生的时 候这能让我们确定栈的状态,目前正在执行的指令是什么,以及其他一些非常有用的信息。 要实现这个目的,首先要获取被调试目标内部的线程句柄,这个功能由 OpenThread()实现. 函数原型如下:

HANDLE WINAPI OpenThread( 
    DWORD dwDesiredAccess, 
    BOOL bInheritHandle, 
    DWORD dwThreadId
);

这看起来非常像 OpenProcess()的姐妹函数,除了这次是用线程标识符(thread identifier TID) 提到了进程标识符(PID)。

我们必须先获得一个执行着的程序内部所有线程的一个列表,然后选择我们想要的,再 用 OpenThread() 获 取 它 的 句 柄 。 让 我 研 究 下 如 何 在 一 个 系 统 里 枚 举 线 程 ( enumerate threads)。

3.2.1 枚举线程

为了得到一个进程里寄存器的状态,我们必须枚举进程内部所有正在运行的线程。线程 是进程中真正的执行体(大部分活都是线程干的),即使一个程序不是多线程的,它也至少 有一个线程,主线程。实现这一功能的是一个强大的函数 CreateToolhelp32Snapshot(),它由 kernel32.dll 导出。这个函数能枚举出一个进程内部所有线程的列表,以加载的模块(DLLs) 的列表,以及进程所拥有的堆的列表。函数原型如下:

HANDLE WINAPI CreateToolhelp32Snapshot( 
    DWORD dwFlags,
    DWORD th32ProcessID
);

dwFlags 参数标志了我们需要收集的数据类型(线程,进程,模块,或者堆 )。这里我 们把它设置成 TH32CS_SNAPTHREAD,也就是 0x00000004,表示我们要搜集快照 snapshot 中 所 有 已 经 注 册 了 的 线 程 。 th32ProcessID 传 入 我 们 要 快 照 的 进 程 , 不 过 它 只 对 TH32CS_SNAPMODULE, TH32CS_SNAPMODULE32, TH32CS_SNAPHEAPLIST, and TH32CS_SNAPALL 这几个模块有用,对 TH32CS_SNAPTHREAD 可是没什么用的哦(后面 有说明)。当 CreateToolhelp32Snapshot()调用成功,就会返回一个快照对象的句柄,被接下 来的函数调以便搜集更多的数据。

一旦我们从快照中获得了线程的列表,我们就能用 Thread32First()枚举它们了。函数原型如下:

BOOL WINAPI Thread32First( 
    HANDLE hSnapshot, 
    LPTHREADENTRY32 lpte
);

hSnapshot 就 是 上 面 通 过 CreateToolhelp32Snapshot() 获 得 镜 像 句 柄 , lpte 指 向 一 个 THREADENTRY32 结构(必须初始化过)。这个结构在 Thread32First()在调用成功后自动填 充,其中包含了被发现的第一个线程的相关信息。结构定义如下:

typedef struct THREADENTRY32{ 
    DWORD dwSize;
    DWORD cntUsage; 
    DWORD th32ThreadID;
    DWORD th32OwnerProcessID; 
    LONG tpBasePri;
    LONG tpDeltaPri; 
    DWORD dwFlags;
};

在这个结构中我们感兴趣的是 dwSize, th32ThreadID, 和 th32OwnerProcessID 3 个参数。 dwSize 必须在 Thread32First()调用之前初始化,只要把值设置成 THREADENTRY32 结构的 大小就可以了。th32ThreadID 是我们当前发现的这个线程的 TID,这个参数可以被前面说过 的 OpenThread() 函数调用以打开此线程,进行别的操作。 th32OwnerProcessID 填充了当前 线 程 所 属 进 程 的 PID 。 为 了 确 定 线 程 是 否 属 于 我 们 调 试 的 目 标 进 程 , 需 要 将 th32OwnerProcessID 的值和目标进程对比,相等则说明这个线程是我们正在调试的。一旦我 们获得了第一个线程的信息,我们就能通过调用 Thread32Next()获取快照中的下一个线程条 目。它的参数和 Thread32First()一样。循环调用 Thread32Next()直到列表的末端。

3.2.2 把所有的组合起来

现在我们已经获得了一个线程的有效句柄,最后一步就是获取所有寄存器的值。这就需 要通过 GetThreadContext()来实现。同样我们也能用 SetThreadContext()改变它们。

BOOL WINAPI GetThreadContext( 
    HANDLE hThread, 
    LPCONTEXT lpContext
);

BOOL WINAPI SetThreadContext( 
    HANDLE hThread, 
    LPCONTEXT lpContext
);

hThread 参数是从 OpenThread() 返回的线程句柄,lpContext 指向一个 CONTEXT 结构, 其中存储了所有寄存器的值。CONTEXT 非常重要,定义如下:

typedef struct CONTEXT { 
    DWORD ContextFlags; 
    DWORD Dr0;
    DWORD Dr1;
    DWORD Dr2;
    DWORD Dr3;
    DWORD Dr6;
    DWORD Dr7;
    FLOATING_SAVE_AREA FloatSave;
    DWORD  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; 
    BYTE  ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; 
};

如你说见所有的寄存器都在这个列表中了,包括调试寄存器和段寄存器。在我们剩下的 工作中,将大量的使用到这个结构,所以尽快的实习起来。

让我们回来看看我们的老朋友 my_debugger.py 继续扩展它,增加枚举线程和获取寄存 器的功能。

#my_debugger.py
class debugger():
    ...
    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 "[*] Could not obtain a valid thread handle." 
            return False
    def enumerate_threads(self):
        thread_entry = THREADENTRY32()
        36 Chapter 3
        thread_list = []
        snapshot = kernel32.CreateToolhelp32Snapshot(TH32CS
        _SNAPTHREAD, self.pid)
        if snapshot is not None:
            # You have to set the size of the struct
            # or the call will fail
            thread_entry.dwSize = sizeof(thread_entry) success = kernel32.Thread32First(snapshot,
            byref(thread_entry))
            byref(thread_entry))
            while success:
                if thread_entry.th32OwnerProcessID == self.pid: 
                    thread_list.append(thread_entry.th32ThreadID)
                success = kernel32.Thread32Next(snapshot,
                kernel32.CloseHandle(snapshot) return thread_list
        else:
            return False
    def get_thread_context (self, thread_id): 
        context = CONTEXT()
        context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS
        # Obtain a handle to the thread 
        h_thread = self.open_thread(thread_id)
        if kernel32.GetThreadContext(h_thread, byref(context)): 
            kernel32.CloseHandle(h_thread)
            return context
        else:
            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))
list = debugger.enumerate_threads()
# For each thread in the list we want to
# grab the value of each of the registers Building a Windows Debugger 37
for thread in list:
    thread_context = debugger.get_thread_context(thread)
    # Now let's output the contents of some of the registers
    print "[*] Dumping registers for thread ID: 0x%08x" % thread 
    print "[**] EIP: 0x%08x" % thread_context.Eip
    print "[**] ESP: 0x%08x" % thread_context.Esp 
    print "[**] EBP: 0x%08x" % thread_context.Ebp 
    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 "[*] END DUMP"
debugger.detach()

当你运行测试代码,你将看到如清单 3-1 显示的数据。

Enter the PID of the process to attach to: 4028
[*] Dumping registers for thread ID: 0x00000550 
[**] EIP: 0x7c90eb94
[**] ESP: 0x0007fde0 
[**] EBP: 0x0007fdfc 
[**] EAX: 0x006ee208 
[**] EBX: 0x00000000
[**] ECX: 0x0007fdd8 
[**] EDX: 0x7c90eb94 
[*] END DUMP
[*] Dumping registers for thread ID: 0x000005c0 
[**] EIP: 0x7c95077b
[**] ESP: 0x0094fff8 
[**] EBP: 0x00000000 
[**] EAX: 0x00000000 
[**] EBX: 0x00000001 
[**] ECX: 0x00000002 
[**] EDX: 0x00000003 
[*] END DUMP
[*] Finished debugging. Exiting...

Listing 3-1:每个线程的 CPU 寄存器值

太酷了 ! 我们现在能够在任何时候查询所有寄存器的状态了。试验下不同的进程 ,看看能得到什么结果。到此为止我们已经完成了我们调试器的核心部分,是时间实现一些基础 调试事件的处理函数了。