【问题标题】:Anti-debugging: gdb does not write 0xcc byte for breakpoints. Any idea why?反调试:gdb 不会为断点写入 0xcc 字节。知道为什么吗?
【发布时间】:2014-05-13 03:40:50
【问题描述】:

我正在学习一些Linux上的反调试技术,并找到了一个sn-p代码,用于检查内存中的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 字节。有谁知道发生了什么或我可能错在哪里?谢谢。

【问题讨论】:

标签: c linux debugging gdb


【解决方案1】:

第二部分很容易解释(正如 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) 中断主要
0x400585 处的断点 1:文件 ../bp.c,第 6 行。
(gdb) 信息中断
Num 类型 Disp Enb 地址 什么
1 个断点将 y 0x0000000000400585 保留在 ../bp.c:6 处
(gdb) disas/r main,+32
从 0x40057d 到 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
汇编程序转储结束。

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

(gdb) 续
继续。
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 中。

【讨论】:

  • 为什么gdb在断点命中后没有删除0xCC字节?
  • 这是设计使然。 GDB 不会删除它们被命中的“标准”断点。但是,临时断点(用 tbreak 插入)一旦命中就会被永久删除。
【解决方案2】:

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

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

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

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

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

不过,为了反调试,希望大家不要灰飞烟灭!在 Windows 上,GetThreadContext API 允许您为(停止的)线程获取(副本)CONTEXT。该结构包括DRx 寄存器的内容。 This question 是关于如何在 Linux 上实现相同的。

【讨论】:

  • 我认为你说错了:While I have not confirmed this via any official docs, this page indicates that。此页面指示它用于观察点,而不是断点。相反,user1726119 询问断点。顺便说一句,GDB 的官方文档也谈到了观察点:gdb sets a hardware watchpoint if possible.sourceware.org/gdb/current/onlinedocs/gdb/…
【解决方案3】:

这也可能是 GDB 告诉你的一个善意的谎言...... RAM 中可能有一个断点,但 GDB 已经事先记录了那里的内容(因此它可以稍后恢复它)并向你展示,而不是RAM 的真实内容。

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

【讨论】:

  • 虽然您的第一段可能解释了他在通过 GDB 观察内存时看到的行为(起初我也这么认为),但它并没有解释程序观察本身。
  • 不,最初的问题没有说他曾经在 GDB 下运行过设置断点的程序并且让它观察自己。该问题描述了编写一个观察自身的程序,然后在 GDB 中设置断点,然后使用 GDB 在内存中查找断点。仅限。
  • @dbrank0 给出了最佳答案,显示了一个程序在观察自己同时在 GDB 下运行设置了断点。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-04-05
  • 1970-01-01
  • 1970-01-01
  • 2020-01-10
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多