用Ollydbg调试以下代码,可以展示出其F8单步步过所存在的一个Bug:
#include <windows.h> int main() { CONTEXT ctx; ctx.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS; GetThreadContext(GetCurrentThread(), &ctx); ctx.Dr7 = 0x101; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; SetThreadContext(GetCurrentThread(), &ctx); return 0; }
代码中会调用SetThreadContext设置调试寄存器dr0有效(并未指定dr0的值)。
如果你用Od在call SetThreadContext处F8,程序基本上将会跑飞,不会停在下一条指令处。
这和Od的F8的原理有关。
重启程序,来到call GetThreadContext后按F8,如下图:
内存区0x0012FC80是局部变量ctx的地址,内容是刚刚从GetThreadContext获得的。右边是当前调试寄存器的值。
可以看到内存偏移0x0012FC84处,即ctx.Dr0的值为0x00401023,与右边寄存器窗口中DR0的值相等。
Od的F8原理,若当前指令为call,则会在call指令的下一条指令即返回指令处设置一个一次性的硬件断点。
也就说,cCall GetThreadContext即0x0040101D处按F8,Od会在下一条指令即0x00401023处设置一个硬件断点,当前使用dr0存放地址。
设置硬件断点的方法是调用SetThreadContext,使得右边寄存器窗口的DR0变成0x00401023。
之后被调试程序调用GetThreadContext,得到的其实是用SetThreadContext设置过的内容,局部变量ctx.Dr0也就变成了0x00401023。
基于上面的道理,下面F8步过 call esi 就会导致右边窗口DR0的值发生改变,这里不要让它改变,让它依旧保持为0x00401023。
直接F9来到SetThreadContext处,先别按F8,而是按回车进入SetThreadContext内部,在第一条指令处下断。
这样做的目的是为了在SetThreadContext处按F8后能先进入其内部,而非直接运行函数。
按F8断在SetThreadContext的开头处:
打开硬件断点列表,发现Od记录了一个一次性硬件断点,地址为0x00401041,与右边调试寄存器内DR0的值相同。
这正好是call SetThreadContext的下一条指令的地址(call SetThreadContext的返回地址)。
这印证了前面所说在call处F8会在返回地址处设硬件断点。
关键的来了。
如果接着在call NtSetContextThread处F8,或者不在SetThreadContext内部下断而直接在call SetThreadContext处F8,程序就会跑飞。
原因是这样的:
在call SetThreadContext按F8时,根据前面所述原理,Od会在下一条指令即0x00401041处设硬件断点,以期望程序运行到那里断下来。
那样就会形成我们平常见到的效果,在call处F8,接着断在下一行。
Od是这样想的,但是此时调用的函数是SetThreadContext,该函数也会设置一遍寄存器的值,会把Od设置过的内容覆盖掉。
本来Od设置了DR0为0x00401041,而被调试程序调用SetThreadContext,将DR0设置成了局部变量ctx中的内容,即0x00401023。
这使得0x00401041断点无效,程序之后将不能断在0x00401041,如果后面没有其他断点,程序将一直跑下去。
结果就是跑飞了,完全没有平常F8见到的效果。
但是Od很傻,它的硬件断点列表中还记录着自己设置的内容,并不是调试寄存器的真实值。
重启程序,在call SetThreadContext之后的 pop esi 处下断,然后在call SetThreadContext处按F8,会发现程序直接断在了 pop esi 处。
就像按了F9一样。程序没有停在call SetThreadContext的下一条指令xor eax, eax处,可见Od设置的DR0失效了。
可是Od的硬件断点列表中依然有指令xor eax, eax的地址0x00401041,和右边寄存器窗口中的真实值不符。
接着再按F8,会发现Od又将自己硬件断点列表中的值写回调试寄存器了。
这个Bug不仅Od原版存在,而且各种加插件版本的Od也有。
归根结底,这是Od内部源码设计的问题,没有考虑周全,最终变成了一种反调试的行为。