sasha 已经给出了一个很好的答案,但是我想从另一个角度看这个;具体来说,memcpy 实际做了什么(就执行的代码而言)。
考虑到在这个快速而肮脏的实现中可能存在小错误memcpy()
,满足 C89/C99/POSIX 函数签名和合约的简单实现可能与以下内容并不完全不同:
/* copy n bytes starting at source+0, to target+0 through target+(n-1), all inclusive */
void memcpy (void* target, void* source, size_t n)
{
for (size_t i = 0; i < n; i++)
{
*target++ = *source++;
/* or possibly the here equivalent: target[i] = source[i]; */
}
}
现在,一个真正的实现可能会一次以大于一个字节的块进行复制,以利用当今的宽内存 (RAM) 互连总线,但原理仍然完全相同。
就您的问题而言,需要注意的重要部分是没有边界检查。这是设计使然!之所以会这样,有三个重要原因:
- C 经常被用作操作系统编程语言,它被设计为“便携式汇编程序”。因此,对于许多旧库函数(memcpy() 就是其中之一)和一般语言的一般方法是,如果您可以在汇编程序中执行,那么它也应该在 C 中可行。很少您可以在汇编程序中执行但在 C 中无法执行的操作。
- 给定一个指向内存位置的指针,没有办法知道在该位置正确分配了多少内存,或者即使指针指向的内存被分配了!(在早期的 x86 系统和 DOS 的旧时代,加速软件的一个常见技巧是直接写入图形内存以将文本显示在屏幕上。显然,图形内存从未由程序本身分配;它只是已知可以在特定的内存地址访问。)真正确定它是否有效的唯一方法是读取或写入内存并查看会发生什么(即使这样我相信访问未初始化的内存会调用未定义的行为,所以基本上,C语言标准允许任何事情发生)。
- 基本上,数组退化为指针,其中未索引的数组变量与指向数组开头的指针相同。并非在所有情况下都严格如此,但现在对我们来说已经足够了。
从 (1) 可以看出,您应该能够将任何您想要的内存从任何地方复制到任何地方。内存保护是别人的问题。具体来说,现在它是操作系统和MMU的责任(现在通常是 CPU 的一部分);操作系统本身的相关部分可能是用 C 语言编写的......
从 (2) 可以看出,memcpy() 和朋友需要被告知要复制多少数据,并且他们必须相信目标处的缓冲区(或目标指针指向的地址处的任何其他内容)是足够大以容纳该数据。内存分配是程序员的问题。
从 (3) 可以得出,我们无法确定复制多少数据是安全的。确保内存分配(源和目标)足够是程序员的问题。
当攻击者可以使用 memcpy() 控制要复制的字节数时,(2) 和 (3) 就会崩溃。如果目标缓冲区太小,后面的任何内容都将被覆盖。如果幸运的话,这将导致内存访问冲突,但C 语言或其标准库不保证会发生这种情况。(您要求它复制内存内容,它要么这样做,要么尝试死掉,但它不知道要复制什么。)如果您传递的源数组小于您要求的字节数对于 memcpy() 进行复制,memcpy() 没有可靠的方法来检测这种情况,只要从源位置读取并写入目标,它就会很高兴地在源数组的末尾进行攻击位置有效。
通过允许攻击者n
在您的示例代码中以n
大于副本源端数组的最大大小的方式进行控制,memcpy() 将由于上述几点而愉快地继续复制超出预期的源数组。简而言之,这基本上是Heartbleed攻击。
这就是代码泄漏数据的原因。 究竟哪些数据被泄露取决于n
编译器在内存中布局机器语言代码和数据的值和方式。sasha 的答案中的图表提供了一个很好的概述,每个架构都相似但不同。
根据您的变量buf
在内存中的声明、分配和布局方式,您可能还会遇到所谓的堆栈粉碎攻击,在这种攻击中,程序正常运行所需的数据会被覆盖,而数据会覆盖所有存在的数据后面提到。在普通情况下,这会导致崩溃或几乎不可能调试的错误;在严重的、有针对性的情况下,它可能导致完全在攻击者控制下的任意代码执行。