为什么攻击者可以控制 `memcpy()` 的 `n` 参数是危险的?

信息安全 应用安全 记忆 C
2021-08-14 10:18:57

我在看一篇论文,看到这段代码存在信息泄露漏洞。据说下面的代码会将内存布局信息泄露给攻击者

有人可以解释一下这是如何泄露信息的吗?

struct userInfo{
    char username[16];
    void* (*printName)(char*);
} user;
...
user.printName = publicFunction.
...
n = attacker_controllable_value; //20
memcpy(buf, user.username, n);   //get function ptr
SendToServer(buf);

我可以看到memcpy会给出异常,但为什么它应该将内存地址返回给攻击者(或者它返回的任何东西)?

提前致谢

3个回答

假设buf的大小由 n 控制或大于 16,攻击者可以将 n 设为他想要的任何数字,并使用它来读取任意数量的内存。 memcpyC 通常不会抛出异常或阻止这种情况发生。只要您不违反任何类型的页面保护或访问无效地址,memcpy 就会继续愉快地进行,直到它复制所请求的内存量。

我假设user这个易受攻击的代码块在某个函数中。这可能意味着它驻留在堆栈上。所有局部函数变量、返回地址和其他信息都包含在堆栈中。下图显示了它在使用英特尔汇编的系统中的结构(大多数平台都使用,我假设您的计算机也使用)。

堆栈帧

如果要使 n 足够大以使 memcpy 在堆栈帧中向前移动,则可以使用此方法获取返回地址。 user将在此图中标记为“本地声明的变量”的部分中。EBP 是一个 4 字节的值,所以如果我们读过去,然后他们用 memcpy 复制接下来的 4 个字节,我们最终会复制返回地址。

请注意,上述内容取决于程序运行的架构。这篇论文是关于 iOS 的,由于我对 ARM 一无所知,所以这些信息的细节可能有些不准确。

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) 互连总线,但原理仍然完全相同。

就您的问题而言,需要注意的重要部分是没有边界检查。这是设计使然!之所以会这样,有三个重要原因:

  1. C 经常被用作操作系统编程语言,它被设计为“便携式汇编程序”。因此,对于许多旧库函数(memcpy() 就是其中之一)和一般语言的一般方法是,如果您可以在汇编程序中执行,那么它也应该在 C 中可行。很少您可以在汇编程序中执行但在 C 中无法执行的操作。
  2. 给定一个指向内存位置的指针,没有办法知道在该位置正确分配了多少内存,或者即使指针指向的内存被分配了!(在早期的 x86 系统和 DOS 的旧时代,加速软件的一个常见技巧是直接写入图形内存以将文本显示在屏幕上。显然,图形内存从未由程序本身分配;它只是已知可以在特定的内存地址访问。)真正确定它是否有效的唯一方法是读取或写入内存并查看会发生什么(即使这样我相信访问未初始化的内存会调用未定义的行为,所以基本上,C语言标准允许任何事情发生)。
  3. 基本上,数组退化为指针,其中未索引的数组变量与指向数组开头的指针相同。并非在所有情况下都严格如此,但现在对我们来说已经足够了。

从 (1) 可以看出,您应该能够将任何您想要的内存从任何地方复制到任何地方。内存保护是别人的问题具体来说,现在它是操作系统和MMU的责任(现在通常是 CPU 的一部分);操作系统本身的相关部分可能是用 C 语言编写的......

从 (2) 可以看出,memcpy() 和朋友需要被告知要复制多少数据,并且他们必须相信目标处的缓冲区(或目标指针指向的地址处的任何其他内容)是足够大以容纳该数据。内存分配是程序员的问题

从 (3) 可以得出,我们无法确定复制多少数据是安全的。确保内存分配(源和目标)足够程序员的问题

当攻击者可以使用 memcpy() 控制要复制的字节数时,(2) 和 (3) 就会崩溃。如果目标缓冲区太小,后面的任何内容都将被覆盖。如果幸运的话,这将导致内存访问冲突,但C 语言或其标准库不保证会发生这种情况。(您要求它复制内存内容,它要么这样做,要么尝试死掉,但它不知道复制什么。)如果您传递的源数组小于您要求的字节数对于 memcpy() 进行复制,memcpy() 没有可靠的方法来检测这种情况,只要从源位置读取并写入目标,它就会很高兴地在源数组的末尾进行攻击位置有效。

通过允许攻击者n在您的示例代码中以n大于副本源端数组的最大大小的方式进行控制,memcpy() 将由于上述几点而愉快地继续复制超出预期的源数组。简而言之,这基本上是Heartbleed攻击。

这就是代码泄漏数据的原因。 究竟哪些数据被泄露取决于n编译器在内存中布局机器语言代码和数据的值和方式。sasha 的答案中的图表提供了一个很好的概述,每个架构都相似但不同。

根据您的变量buf在内存中的声明、分配和布局方式,您可能还会遇到所谓的堆栈粉碎攻击,在这种攻击中,程序正常运行所需的数据会被覆盖,而数据会覆盖所有存在的数据后面提到。在普通情况下,这会导致崩溃或几乎不可能调试的错误;在严重的、有针对性的情况下,它可能导致完全在攻击者控制下的任意代码执行。

我发布了另一个答案,因为这里的两个答案虽然都是正确的,但在我的观点中错过了问题的一个重要点。问题是关于内存布局的信息泄漏。

呈现的 memcpy 可能始终具有正确大小的输出缓冲区,因此即使攻击者控制了大小,此时也可能没有堆栈粉碎的风险。泄漏信息(如在心脏出血中,正如 Linuxios 已经提到的)是一个潜在的问题,具体取决于泄漏的信息。在此示例中,您正在泄漏publicFunction. 这是一个真正的问题,因为它打败了 Address Space Layout RandomizationASLR 是ASLR 和 DEP 如何工作?. 只要你公布地址publicFunction,同一模块(DLL 或 EXE 文件)中的所有其他函数的地址都被公开,可用于 return-to-libc 或 return-oriented-programming 攻击。不过,对于这些攻击,您需要一个与此处介绍的不同的漏洞。