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