反调试:gdb不为断点写入0xcc字节。知道为什么吗?

时间:2014-05-13 03:40:50

标签: c linux debugging gdb

我正在学习Linux上的一些反调试技术,并发现了一段代码,用于检查内存中的0xcc字节以检测gdb中的断点。这是代码:

     if ((*(volatile unsigned *)((unsigned)foo + 3) & 0xff) == 0xcc)   
     {
                    printf("BREAKPOINT\n");
                    exit(1);
      }

     foo();

但它不起作用。我甚至试图在foo()函数上设置断点并观察内存中的内容,但没有看到为断点写的任何0xcc字节。这是我做的:

(gdb) b foo
Breakpoint 1 at 0x804846a: file p4.c, line 8.
(gdb) x/x 0x804846a
0x804846a <foo+6>:  0xe02404c7
(gdb) x/16x 0x8048460
0x8048460 <frame_dummy+32>: 0x90c3c9d0  0x83e58955  0x04c718ec  0x0485e024
0x8048470 <foo+12>: 0xfefae808  0xc3c9ffff  .....

如您所见,似乎没有在foo()函数的入口点上写入0xcc字节。有谁知道发生了什么或我可能错在哪里?感谢。

3 个答案:

答案 0 :(得分:8)

第二部分很容易解释(正如Flortify正确陈述的那样): GDB显示原始内存内容,而不是断点“字节”。在默认模式下,它实际上甚至会在调试器挂起时删除断点,并在继续之前重新插入它们。用户通常希望看到他们的代码,而不是用于断点的奇怪的修改指令。

使用您的C代码,您错过了几个字节的断点。 GDB在 function prologue之后设置断点,因为函数序言通常不是gdb用户想要看到的。因此,如果你把break放到foo,那么实际的断点通常会在那之后位于几个字节之后(取决于与函数有关的序言代码本身,因为它可能或者可能不需要保存堆栈指针,帧指针等)。但很容易检查。我用了这段代码:

#include <stdio.h>
int main()
{
    int i,j;
    unsigned char *p = (unsigned char*)main;

    for (j=0; j<4; j++) {
        printf("%p: ",p);
        for (i=0; i<16; i++)
            printf("%.2x ", *p++);
        printf("\n");
    }
    return 0;
}

如果我们单独运行该程序,则打印:

0x40057d: 55 48 89 e5 48 83 ec 10 48 c7 45 f8 7d 05 40 00
0x40058d: c7 45 f4 00 00 00 00 eb 5a 48 8b 45 f8 48 89 c6
0x40059d: bf 84 06 40 00 b8 00 00 00 00 e8 b4 fe ff ff c7
0x4005ad: 45 f0 00 00 00 00 eb 27 48 8b 45 f8 48 8d 50 01

现在我们在gdb中运行它(输出重新格式化为SO)。

(gdb) break main
Breakpoint 1 at 0x400585: file ../bp.c, line 6.
(gdb) info break
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0x0000000000400585 in main at ../bp.c:6
(gdb) disas/r main,+32
Dump of assembler code from 0x40057d to 0x40059d:
  0x000000000040057d (main+0):  55                        push %rbp
  0x000000000040057e (main+1):  48 89 e5                  mov %rsp,%rbp
  0x0000000000400581 (main+4):  48 83 ec 10               sub $0x10,%rsp
  0x0000000000400585 (main+8):  48 c7 45 f8 7d 05 40 00   movq $0x40057d,-0x8(%rbp)
  0x000000000040058d (main+16): c7 45 f4 00 00 00 00      movl $0x0,-0xc(%rbp)
  0x0000000000400594 (main+23): eb 5a                     jmp 0x4005f0 
  0x0000000000400596 (main+25): 48 8b 45 f8               mov -0x8(%rbp),%rax
  0x000000000040059a (main+29): 48 89 c6                  mov %rax,%rsi
End of assembler dump.

通过这个我们验证,该程序正在打印正确的字节。但这也表明断点已插入0x400585(即在函数序言之后),而不是在函数的第一个指令处。 如果我们现在在gdb(运行)下运行程序,然后在命中断点后“继续”,我们得到这个输出:

(gdb) cont
Continuing.
0x40057d: 55 48 89 e5 48 83 ec 10 cc c7 45 f8 7d 05 40 00 
0x40058d: c7 45 f4 00 00 00 00 eb 5a 48 8b 45 f8 48 89 c6 
0x40059d: bf 84 06 40 00 b8 00 00 00 00 e8 b4 fe ff ff c7 
0x4005ad: 45 f0 00 00 00 00 eb 27 48 8b 45 f8 48 8d 50 01 

现在显示0xcc被打印为地址9字节到main。

答案 1 :(得分:4)

如果你的硬件支持它,GDB可能正在使用Hardware Breakpoints,它不会修补代码。

虽然我没有通过任何官方文档证实这一点,但this page表示

  

默认情况下,gdb会尝试使用硬件辅助断点。

由于您指示期望0xCC字节,我假设您在x86硬件上运行,因为int3操作码是0xCC。 x86处理器有一组debug registers DR0 - DR3,您可以在其中编程数据地址以导致断点异常。 DR7是一个控制断点行为的位域,DR6表示状态。

调试寄存器只能从Ring 0(内核模式)读/写。这意味着内核会为您管理这些寄存器(我相信,通过ptrace API。)

然而,为了反调试,所有的希望都不会丢失!在Windows上,GetThreadContext API允许您获取(已停止)线程的CONTEXT(副本)。该结构包括DRx寄存器的内容。 This question是关于如何在Linux上实现相同的。

答案 2 :(得分:2)

这也可能是GDB告诉你的一个白色谎言...... RAM中可能有一个断点但是GDB事先已经注意到了它(所以它可以在以后恢复它)并向你展示,而不是RAM的真实内容。

当然,它也可以使用硬件断点,这是某些处理器上可用的工具。设置h / w断点是通过告诉处理器应该注意的地址来完成的(如果在执行代码时它被程序计数器命中,则触发断点中断)。