假设我有一个程序,它声明了一个char buffer[size]
和另一个变量,并使用gets(buffer);
将数据写入buffer
。如果gets
的输入太长,那么它将从缓冲区溢出,进入下一个变量(假设该变量位于buffer
之后的下一个地址):
void f(){
char str[12] = "hello_world";
char buffer[1];
gets(buffer); // provided with a random char and then "hello_kitty"
printf("str = %sn", str); // no crash. Just prints "hello_kitty" as expected
}
当用"合法输入"(意思是没有溢出第二个缓冲区)运行这个时,这还可以。即使我溢出了一点缓冲区,也可以,但输入太多后,程序崩溃了。
据我所知,这(意思是不溢出第二个缓冲区)不应该导致任何崩溃。可能导致崩溃的原因是损坏了保存指令指针的内存,因此它现在将指向无效地址(这是页面错误吗?)。
这是正确的吗?一个不影响堆栈/帧/指令指针的错误写入会导致崩溃吗?
要简洁地回答您的问题,请不要。请继续阅读以获得更长的答案。
缓冲区溢出导致分段错误的最典型方式是溢出的缓冲区位于堆栈上,溢出覆盖返回指针。当返回指针在函数返回时弹回到指令指针时,通常会发生分段故障,因为处理器试图读取您无法访问的内存。
最后一句话非常重要。覆盖返回指针只是分段错误发生的一种方式。事实上,任何覆盖以后用于访问内存的内存地址的缓冲区溢出都可能导致分段错误,或者,如果是试图写入内存,则可能导致访问违规。
例如,假设您在堆上分配了一个结构。该结构采用以下形式:
struct sample_struct {
char bytes[20];
struct sample_struct *next;
};
如果在没有正确验证边界的情况下将数据复制到bytes
成员中,那么内存中的下一项将是next
指针。如果该指针被覆盖,随后尝试从中读取,则很可能会发生分段错误,假设现在的值代表您无法控制的内存地址。如果你碰巧在内存空间中找到了一个地址,结果将是试图将位于那里的字节解释为struct sample_struct
,这可能会导致其他问题。
请注意,不要假设覆盖上面示例结构中的指针只需要21到24个字节;除非您指示编译器打包该结构,否则该结构的内存分配可能会包括用于对齐目的的额外字节。
缓冲区溢出是否仅在重要指针为是否被覆盖?
没有必要,让我们从头开始。在大多数处理器中,内存可以分为大块或小块。大块通常称为段。一小块通常被称为页面。
若您在缓冲区后面写入(缓冲区溢出),并且在该缓冲区后面覆盖的"东西"属于同一进程,则不会立即发生故障。
SEGMENT n | SEGMENT n
buffer1 | buffer2
+-----+-----+-----+-----+-----+-----+-----+-----+
| 'a' | 'a' | 'a' | 'a' | 'a' | 'a' | 'a' | 'a' |
+-----+-----+-----+-----+-----+-----+-----+-----+
^-----^-----^-----^-----^-----^-----^-----^-------- Writing to variables of same process (UB)
SEGFAULT发生在具有溢出的缓冲区在内存段末尾结束时。无论如何,在这个处理器的当前状态下,该段之外的内容是当前未定义的。因此,在该空隙中的存储器访问将导致总线故障。处理器没有可用的内存内容。它不知道该去哪里。因此,在这种情况下,错误是一场直接的灾难。
在x86或ARM上,当写入不在任何映射区域中的页、位于只读映射的内存区域中的页面,或从不在任何已映射区域的地址读取时,会得到SIGSEGV
。
END OF SEGMENT
|
SEGMENT n v SEGMENT n+1
+-----+-----+-----+-----+-----+-----+-----+-----+
| 'a' | 'a' | 'a' | 'a' | | | | |
+-----+-----+-----+-----+-----+-----+-----+-----+
^-----^-----^-----^-----^--------------------------- Writing to another segment
|
Writing here will cause SEGFAULT
SEGFAULT通常是在程序计数器返回地址(位于参数后面的堆栈上)被覆盖时引起的,并且在函数后弹出继续后,程序计数器跳到某个位置(取决于写入指针的值),并且无法访问该位置。
即使我溢出缓冲区一点点,也没问题
您在同一段中只覆盖了属于您的进程的内存(没有返回地址),但它仍然是UB。
但输入过多后程序崩溃。
是的,您可能已将写入另一个段或覆盖了返回地址。
如果你读到Aleph One为了好玩和盈利而砸堆栈,你就会知道计算机会被溢出缓冲区(并覆盖重要指针)入侵。。。
正如本文所述,没有segfault,因为部分利用代码通常是exit(0);
,前提是攻击者无法以更好的方式恢复程序。
因此,为了回答您的问题:
这是页面错误吗?
不一定。在C.的世界里不必存在这样的概念
这是正确的吗?一个不影响堆栈/帧/指令指针的错误写入会导致崩溃吗?
当然,但这是不可依赖的。这就是为什么人们认为未定义的行为,如缓冲区溢出会导致真正激烈的事情发生(如龙和核浩劫)。
我可以从其他评论中看出这个问题背后的意图:
-
众所周知。试图了解如何利用它。
-
。。。重写ret操作想要返回到的地址
在这种情况下,你走在了正确的轨道上。。。您缺少的一件事是,您提到的随机字符是机器代码,需要覆盖指令指针以指向该机器代码。Aleph One更详细地介绍了这一点,尽管你的里程肯定会有所不同,因为这是一份古老的文件。
如果不是因为这个有许多未定义行为的问题,所有的C专家都可能会拦截信号以毫无问题地恢复他们的软件,但遗憾的是,C不是Java。。。最好遵循专家的榜样,避免未定义的行为。。。
假设一个体面的导游会尽可能地帮助你避免不明确的行为。至少有一本像样的指南,我们称之为形式的"圣经",部分原因是它是由一些权威人士撰写的。它上面有一个蓝色的大"C",现在"第二版"是用红色写的…
找到那本书对你们来说是一种锻炼吗。我有信心。