承接上文 进程调试基础2
一个调试器必须能够在任何时候都搜集到CPU的各个寄存器的状态。
当异常发生的时候这能让我们确定栈的状态,目前正在执行的指令是什么,以及其他一些非常有用的信息。
理论基础
1 - 要实现这个目标,首先要获取被调试目标内部的线程句柄。
该功能由OpenThread()
实现,函数原型:
HANDLE WINAPI OpenThread(
DWORD dwDesiredAccess,
BOOL bInheritHandle,
DWORD dwThreadId
);
要获取线程id(TID),必须先获取目标程序内部所有线程的一个列表,然后选择我们想要的,再用OpenThread()
获取它的句柄。下面讲解如何在一个系统里枚举线程。
2 - 枚举线程
(1)为了得到一个进程里寄存器的状态,我们必须枚举进程内部所有正在运行的线程。
实现这一功能的函数为CreateToolhelp32Snapshot()
,它由kernel32.dl
l导出。
这个函数能枚举出一个进程内部所有线程的列表,已加载模块的列表,以及进程所有用的堆的列表。
函数原型:
HANDLE WINAPI CreateToolhelp32Snapshot(
DWORD dwFlags,
DWORD th32ProcessID
);
dwFlags
参数标志了我们收集的数据类型(线程,进程,模块,或者堆)。
我们设置为TH32CS_SNAPTHREAD,也就是0x00000004,表示要手机快照snapshot中所有已经注册了的线程。
th32ProcessID
传入我们要快照的进程PID
当函数调用成功,就会返回一个快照对象句柄,被接下来的函数调用以便收集更多的数据。
(2)一旦从快照中获取了线程的列表,就可以用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;
};
这里我们关注结构体中3个属性:
dwSize
:该参数 必须在 Thread32First()
调用之前初始化,只要把值设置成 THREADENTRY32 结构的大小就可以了。
th32ThreadID
:是我们当前发现的这个线程的TID,这个参数可以被文章开头处介绍的OpenThread()
函数调用以打开此线程,进行别的操作。
th32OwerProcessID
:填充了当前线程所属进程的PID。
(3)一旦我们获得了第一条线程的信息,就可以通过调用Thread32Next()
获取快照中下一个线程条目。
它的参数和 Thread32First()
一样。循环调用 Thread32Next()
直到列表的末端
3 - 获取寄存器状态
通过以上过程,我们即获取到了一个进程中所有线程的有效句柄,接下来就可以获取所有寄存器的值了。这需要GetThreadContext()
来实现。同样我们也可以使用SetThreadContext()
改变它们。
函数原型:
BOOL WINAPI GetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);
BOOL WINAPI SetThreadContext(
HANDLE hThread,
LPCONTEXT lpContext
);
hThread
:参数是从 OpenThread() 返回的线程句柄
lpContext
:指向一个 CONTEXT 结构 ,
其中存储了所有寄存器的值。CONTEXT 非常重要,定义如下:
#64位系统CONTEXT结构体
class WOW64_CONTEXT(Structure):
_pack_=16
_fields_=[
("P1Home",DWORD64),
("P2Home",DWORD64),
("P3Home",DWORD64),
("P4Home",DWORD64),
("P5Home",DWORD64),
("P6Home",DWORD64),
("ContextFlags",DWORD),
("MxCsr",DWORD),
("SegCs",WORD),
("SegDs",WORD),
("SegEs",WORD),
("SegFs",WORD),
("SegGs",WORD),
("SegSs",WORD),
("EFlags",DWORD),
("Dr0",DWORD64),
("Dr1",DWORD64),
("Dr2",DWORD64),
("Dr3",DWORD64),
("Dr6",DWORD64),
("Dr7",DWORD64),
("Rax",DWORD64),
("Rcx",DWORD64),
("Rdx",DWORD64),
("Rbx",DWORD64),
("Rsp",DWORD64),
("Rbp",DWORD64),
("Rsi",DWORD64),
("Rdi",DWORD64),
("R8",DWORD64),
("R9",DWORD64),
("R10",DWORD64),
("R11", DWORD64),
("R12", DWORD64),
("R13", DWORD64),
("R14", DWORD64),
("R15", DWORD64),
("Rip",DWORD64),
("DebugControl",DWORD64),
("LastBranchToRip", DWORD64),
("LastBranchFromRip", DWORD64),
("LastExceptionToRip", DWORD64),
("LastExceptionFromRip", DWORD64),
("DUMMYUNIONNAME",DUMMYUNIONNAME),
("VectorRegister",M128A * 26),
("VectorControl",DWORD64)
]
注意:这个是64位系统的CONTEXT结构体定义,如果是32位的会有一些区别,具体请看WINDOWS常量结构体定义
如上所见,所有的寄存器都在这个结构体中了,包括调试寄存器和段寄存器。
接下来,在my_debugger.py中增加枚举线程和获取寄存器的功能
4 - 总结
总结一下以上的流程:
(1)获取进程ID,PID。
(2)以PID作为参数,通过CreateToolhelp32Snapshot()
获取快照对象句柄,其实就是指定进程下所有线程的列表
(3)以快照对象句柄作为参数,通过Thread32First()
函数获取快照下第一个条目的THREADENTRY32结构体实例,该结构体中的th32ThreadID属性的值即是当前线程的ID(TID)
(4)将TID作为参数,传递给OpenThread()
函数,获取该线程的句柄
(5)通过Thread32Next()
获取下一个条目的THREADENTRY32结构体实例,并重复第4步,获取线程句柄。
(6)将各线程的句柄作为参数,调用GetThreadContext()
函数获取对应线程的CONTEXT结构体实例,即得到了各线程CPU寄存器的值
程序编写
常量和结构体定义在这里
#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"[*]Counld not obtain a valid thread handle"
return False
#枚举线程,获取线程ID列表
def enumerate_threads(self):
thread_entry = THREADENTRY32()
thread_list = []
#获取进程快照
snapshot = kernel32.CreateToolhelp32Snapshot(
TH32CS_SNAPTHREAD,
self.pid)
if snapshot is not None:
#设置结构体大小
thread_entry.dwSize = sizeof(thread_entry)
success = kernel32.Thread32First(snapshot,byref(thread_entry))
while success:
if thread_entry.th32OwnerProcessID == self.pid:
thread_list.append(thread_entry.th32ThreadID)
success = kernel32.Thread32Next(snapshot,byref(thread_entry))
kernel32.CloseHandle(snapshot)
return thread_list
else:
return False
#获取寄存器的值(64位寄存器)
def get_thread_context(self,thread_id):
context = WOW64CONTEXT()
context.ContextFlag = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS
h_thread = self.open_thread(thread_id)
if kernel32.GetTheadContext(h_thread,byref(context)):
kernel32.CloseHandle(h_thread)
return context
else:
return False
下面编写测试程序:
这里我的CPU是64位的,如果你的CPU是32位的,记得修改get_thread_context()
函数中关于context变量的声明。
#get_cpu_reg_value.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 thread in list:
thread_context = debugger.get_thread_context(thread)
print "[*] Dumping registers for thread ID: 0x%08x" % thread
print "[**] RDI: 0x%08x" % thread_context.Rdi
print "[**] RSP: 0x%08x" % thread_context.Rsp
print "[**] RBP: 0x%08x" % thread_context.Rbp
print "[**] RAX: 0x%08x" % thread_context.Rax
print "[**] RBX: 0x%08x" % thread_context.Rbx
print "[**] RCX: 0x%08x" % thread_context.Rcx
print "[**] RDX: 0x%08x" % thread_context.Rdx
print "[*] END DUMP"
debugger.detach()
还是以计算器程序为例,打开计算器,获得计算器的PID。
然后运行上面的测试程序.终端中输入计算器的PID。
窗口中应该会显示出该进程下所有的线程context的结果。像下面这样