avatar

Catalog
单步步入与单步步过

前言

本篇将是学习调试相关的最后一篇,也是Windows(XP)内核基础的最后一篇了。下一篇开始将会进入到网络安全领域的基础学习。本篇介绍调试相关最后的内容,单步步入与单步步过。对于经常用OD的人来说,其实就是相当于F7与F8的功能,F7可以单步执行每一行指令,F8可以跳过call指令,接下来就学习一下它们的原理和简单的实现。

单步步入

实现原理

想要实现单步步入,有很多手段,可以通过不断的下软件断点,每执行一行,就下一个INT3,然后恢复再重新执行。虽然这是可行的办法,但是过于复杂,因此Intel在设计CPU时考虑到了这一点,调试程序是必不可少的手段,因而在Eflags里设置了一个TF位。

单步步入的实现就是利用了这个TF位。在前一篇学习硬件断点时提到过,有两种情况会导致单步异常,一种情况是触发了硬件断点,另一种情况则是Eflags的TF位被置为了1。因此调试寄存器Dr6被专门用来判断当前的单步异常属于哪一类。

当在调试器中执行单步操作时,就会将Eflags的TF位设置为1,从而产生单步异常,接下来的步骤,与硬件断点的执行流程是一样的,查找1号中断处理函数。并最终将该类型调试事件发送给调试器。

单步步入函数

单步步入的实现,就是将Context记录的Eflags的TF置1。

c
1
2
3
4
5
6
7
8
9
VOID SetSingleStep()
{
CONTEXT Context;
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread, &Context);
//将Eflags的TF置1
Context.EFlags |= 0x100;
SetThreadContext(hDebugeeThread, &Context);
}

处理函数

由于单步步入与硬件断点触发的异常都属于单步异常,因此这两种异常使用同一个处理函数。仅需判断一下Dr6的值即可。

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
BOOL SingleStepExceptionProc(EXCEPTION_DEBUG_INFO * pExceptionInfo)
{
CONTEXT Context;
BOOL bRet = FALSE;

//1.获取线程上下文
Context.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
GetThreadContext(hDebugeeThread,&Context);

//2.判断是否是硬件断点导致的异常
if(Context.Dr6&0xF) //B0~B3不为空
{
//显示断点信息
printf("硬件断点 %d 0x%p\n",Context.Dr7 & 0x00030000,Context.Dr0);

//将断点去除
Context.Dr0 = 0;
Context.Dr7 &= 0xfffffffe;
}
else
{
//显示断点信息
printf("单步异常 0x%p\n",Context.Eip);

//去除单步异常
Context.EFlags &= 0xfffffeff;
}

//3.等待用户命令
while(bRet == FALSE)
{
bRet = WaitForUserCommand();
}
return bRet;
}

除此之外,有了单步步入后,也可以进一步补充WaitForUserCommand函数

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
BOOL WaitForUserCommand()
{
BOOL bRet = FALSE;
BYTE CMD;

printf("COMMAND>");

CMD = getchar();
fflush(stdin);

switch(CMD)
{
//单步步入
case't':
bRet = TRUE;
SetSingleStep();
break;
//继续执行
case'g':
bRet = TRUE;
break;
}
}

单步步过

实现原理

单步步过对应OD的F8,也就是遇到call指令的时候不跟进去,直接跳过。实现的原理是遇到call指令(好几种)后,计算当前指令的长度,然后在(当前EIP+当前指令长度)的地方下断(也就是call指令的下一行),然后执行CPU,便可以实现单步步过。实现过程有一点小复杂,所以这里没有下断过程与处理函数。

反调试技巧

针对单步步过也有一种反调试技巧,可以设置多段call调用,而逆向的人不可能会跟进每一个函数里,就会使用单步步过,这时可以修改call函数里堆栈的返回地址,让函数返回的别的位置,这样逆向的人再尝试单步步过,就会跟丢了,从而达到反调试的目的。

总结

至此,调试章节告一段落,Windows内核也完结了。之后会更新一篇调试器的简单实现,完成对调试与异常章节的收束。这一篇不会一次更新完,每天调试一点,保持对内核的熟悉,并在之后从头开始复习内核的内容,也必然会对之前的博客有些许修改。接下来就到网安篇了,希望2020年能搞定鸭!

参考资料

参考笔记:

参考教程:

  • 海哥逆向中级预习班
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/09/20/%E5%8D%95%E6%AD%A5%E6%AD%A5%E5%85%A5%E4%B8%8E%E5%8D%95%E6%AD%A5%E6%AD%A5%E8%BF%87/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶