如何防止memcpy缓冲区溢出?

13

在一个程序中,有一些固定大小的二进制缓冲区用于存储数据,并使用 memcpy 将缓冲区从一个复制到另一个。由于源缓冲区可能比目标缓冲区大,因此如何检测是否存在缓冲区溢出?


1
检测?你知道目标缓冲区大小吗?那么可以编写如下代码:memcpy(src, dst, sizeof(dst))。 - BSen
比较源缓冲区和目标缓冲区的大小,看哪个更大? - SingerOfTheFall
1
@BSen,sizeof只会给出指针的大小。 - juanchopanza
将其视为伪代码。它可以根据声明的方式给出缓冲区的大小。我确实知道,sizeof(dst *)给出指针大小,而sizeof(dst [123])给出缓冲区大小,至少当1个元素= 1字节时是这样。 - BSen
如果dst是一个指针,那么它将返回指针所指向的内存块的大小。如果dst是其他类型的变量(例如数组),那么它将返回该变量的大小。 - James Kanze
显示剩余2条评论
4个回答

11

如何检测是否存在缓冲区溢出?

我的看法是你有三到四个选择(多少不等)。


第一选择是为memcpy提供一个“安全”的函数。这是我在我的代码审查中要求的,我经常进行审核。我还要求验证所有参数,并断言所有参数。
断言创建自我调试代码。我希望开发人员编写代码; 我不希望他们浪费时间进行调试。因此,我要求他们编写可以自行调试的代码。ASSERTs也很好地记录了事情,因此他们可以节省文档的成本。在发布版本中,ASSERTs将通过预处理器宏来删除。
errno_t safe_memcpy(void* dest, size_t dsize, void* src, size_t ssize, size_t cnt)
{
    ASSERT(dest != NULL);
    ASSERT(src != NULL);
    ASSERT(dsize != 0);
    ASSERT(ssize != 0);
    ASSERT(cnt != 0);

    // What was the point of this call?
    if(cnt == 0)
        retrn 0;

    if(dest == NULL || src == NULL)
        return EINVALID;

    if(dsize == 0 || ssize == 0)
        return EINVALID;

    ASSERT(dsize <= RSIZE_MAX);
    ASSERT(ssize <= RSIZE_MAX);
    ASSERT(cnt <= RSIZE_MAX);

    if(dsize > RSIZE_MAX || ssize > RSIZE_MAX || cnt > RSIZE_MAX)
        return EINVALID;

    size_t cc = min(min(dsize, ssize), cnt);
    memmove(dest, src, cc);

    if(cc != cnt)
        return ETRUNCATE;

    return 0;
}

如果你的safe_memcpy返回非0值,那么就意味着有错误,比如说坏的参数或者潜在的缓冲区溢出。
第二个选择是使用 C 标准提供的“更安全”的函数。通过 ISO/IEC TR 24731-1, Bounds Checking Interfaces,C 提供了“更安全”的函数。在符合要求的平台上,您可以简单地调用 gets_ssprintf_s。它们提供一致的行为(例如始终确保字符串以 NULL 结尾)和一致的返回值(例如成功时返回 0 或一个 errno_t)。
errno_t  err = memcpy_s(dest, dsize, src, cnt);
...

很遗憾,gcc和glibc不符合C标准。其中一位glibc维护者Ulrich Drepper称边界检查接口为"可怕低效的BSD垃圾",并且它们从未被添加。


第三种选择是使用平台上的“更安全”的接口(如果有的话)。在Windows上,这恰好与 ISO/IEC TR 24731-1,边界检查接口中的相同。您还可以使用String Safe库。
在Apple和BSD上,您没有一个“更安全”的memcpy函数。但您确实拥有更安全的字符串函数,例如strlcpystrlcat等。
在Linux上,第四种选择是使用FORTIFY_SOURCE。 FORTIFY_SOURCE使用“更安全”的高风险函数变体,例如memcpystrcpygets。编译器在可以推断目标缓冲区大小时使用更安全的变体。如果复制超过目标缓冲区大小,则程序调用abort()。如果编译器无法推断目标缓冲区大小,则不使用“更安全”的变体。
为了进行测试而禁用FORTIFY_SOURCE,您应该使用-U_FORTIFY_SOURCE-D_FORTIFY_SOURCE=0编译程序。

10

您需要知道源缓冲区中有多少数据,以及目标缓冲区中有多少可用空间。

如果目标缓冲区没有足够的空间来存放要从源缓冲区复制的所有数据,请不要调用memcpy()。(您必须决定如果源比目标大时截断数据是否可行。)

如果您不知道有多少空间,则应重新编写代码以便了解有多少可用空间;否则,这样做是不安全的。

请注意,如果存在源和目标缓冲区重叠的可能性,则应使用memmove()而不是memcpy()

在C++中,首先考虑不使用memcpy();因为它是一种C风格的操作,而不是C++风格。


谢谢。在C++中,进行内存复制的正确方式是什么? - Michael D
1
@MichaelD:将您的数据存储在std::vector<>中,然后只需使用vector2 = vector1即可。 - MSalters
如何将数据插入向量?使用push_back逐字节插入数据? - Michael D
@MichaelD:vector有一个名为insert的成员函数。 - Steve Jessop
如果二进制数据来自网络,例如TCP,如何在不牺牲太多性能的情况下插入到向量中? - Michael D
@MichaelD:如果你想避免insert执行的额外复制操作,那么你可以将向量中的一个元素的指针作为缓冲区传递给套接字的recv函数。但是你必须确保向量的大小足够大。对向量进行resize操作会清除字节,因此与写入未初始化数组相比仍然存在一些小的开销。 - Steve Jessop

7

您应该始终知道并检查源和目标缓冲区的大小!

void *memcpy(void *dest, const void *src, size_t n);

n不应大于srcdest的大小。


1
如果您有以下内容:
目标 4 字节大小
源 5 字节大小
您可以确保将最多 4 个字节复制到目标缓冲区:
size_t getCopySize(size_t sourceSize, size_t destSize)
{
    return (destSize <= sourceSize ? destSize : sourceSize);
}
memcpy(destination, source, getCopySize(sizeof(source),sizeof(destination)));

根据您的应用程序,您还可以确保剩余数据将在稍后复制,或者如果某些数据可以忽略,则可以跳过它。

不幸的是,似乎你不能立即将它用作嵌套函数 - 你会得到一个“隐式声明”错误。 - Gerd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接