WIN32汇编语言教程:第13章 进程控制 · 13.3 进程调试(8)
这两个函数的用法是:
invoke GetThreadContext,hThread,lpContext
invoke SetThreadContext,hThread,lpContext
hThread指定目标线程句柄,lpContext指向一个CONTEXT结构,GetThreadContext函数会将目标线程的环境返回到这个结构中,SetThreadContext函数将结构中的寄存器设置到目标线程中。为了执行这两个函数,程序必须对目标线程拥有THREAD_GET_CONTEXT和THREAD_SET_CONTEXT权限。
在执行函数前,必须设置CONTEXT结构中的ContextFlags字段,这个字段表示需要操作的寄存器的范围。访问通用寄存器可以指定CONTEXT_INTEGER;访问段寄存器可以指定CONTEXT_SEGMENTS;要访问全部寄存器则指定为CONTEXT_FULL。
另外,在定义CONTEXT结构的时候,应该将它定义为双字对齐,否则,在NT下将得到奇怪的结果,可以在定义前加上“align dword”关键字。
在执行GetThreadContext函数前,最好使用SuspendThread函数将目标线程挂起,防止函数执行到一半的时候被Windows切换走了,在执行SetThreadContext以后可以再使用ResumeThread函数将目标线程恢复运行。但是在调试事件中就没有这个必要,因为在ContinueDebugEvent函数返回之前,目标线程不会恢复运行。
现在回过头来看例子程序,在发生异常事件的逻辑分支中,当检测到断点中断的时候,程序使用GetThreadContext函数获取线程环境,并比较eip是否到达我们设置的断点+1处(因为断点指令执行以后才发生异常,这时候eip已经指向了下一条指令),如果到达,则用WriteProcessMemory函数将断点处原来的代码恢复回去,并将eip寄存器减1以便目标线程能够重新执行这条指令,同时,程序将eflags标志的单步标志置1,这样以后每执行一条指令,就会发生单步中断回到调试器中,就可以一步步跟踪,直到壳代码将Test.exe的内容完全解压缩为止。完成了对线程环境的修改以后,使用SetThreadContext函数将线程环境设置回去,现在就等单步中断发生了。
当单步中断发生的时候,程序同样使用GetThreadContext函数获取线程环境,并比较eip是否到达Test.exe程序原来的入口处(00401000h),如果没有到达,程序继续设置单步标志,这是因为Windows执行一条语句以后会自动清除单步标志,为了继续进行单步跟踪,必须每次重新设置单步标志;如果发现已经执行到00401000h处了,程序开始打内存补丁,这时程序不必再设置单步标志,因为要做的工作全部完成了!
运行程序,在经过几秒的等待后,“感谢您使用正版软件”的消息框终于出现了,这就意味着我们成功地突破了壳代码的封锁。
但是单步中断的开销是很大的,毕竟在原来的一条指令之间要多执行成千上万条指令,效率也就相应低了很多,这就是前面要等待几秒的原因。实际上有时候可以不用单步中断,而采用分步进行断点中断的办法;有时候,一个合适的断点就可以解决全部问题,比如对于用做例子的Test.exe程序来说,观察反汇编后的代码:
:0040526E 61 popad
:0040526F E98CBDFFFF jmp 00401000
可以在0040526Eh处设置断点,这样就可以在壳代码完成解压缩操作以后再将它中断,而且只要中断一次就可以了,按这种方法编写的补丁程序放在所附光盘的Chapter13\Patch3目录中,这个程序执行起来没有一点延时,有兴趣的读者自己分析一下。实际上,Pathc2程序舍近求远用了单步中断的原因仅是为了给读者做处理单步中断的示范。
当然,使用调试API编写补丁程序的时候,采用的方案是建立在对目标程序的分析上的,并没有一个很确定的方案,本节的例子给出了Pathc1到Pathc3总共3个程序,就是为了给读者演示同一种问题的不同解决方法,在具体的使用中,读者还应该根据实际情况灵活应用。