使C免受缓冲区溢出和其他错误的选项?

时间:2014-09-29 09:04:45

标签: c security buffer-overflow

是否有任何使C可证明对缓冲区溢出免疫的东西(编辑:和其他错误,这些错误是由于C通常编译未经检查,即可能是某种边界检查)?并且兼容性足以用于大型生产代码(编辑:用于所有内容)?

我用mudflap尝试了gcc,它允许它运行没有错误。

#include <stdio.h>
int main()
{
    int a[2];
    a[-1] = 5;
    printf("%u\n", a[-1]);
    return 0;
}

所以看起来mudflap像我尝试的其他人一样不完整,只会使漏洞利用的可能性降低。它似乎也旨在调试而非生产用途。我想知道可证明的无法开发。可以办到。对于为什么它到处都没有和普遍使用的任何想法?轻微的性能损失(甚至慢了10倍,但可能慢了2倍)似乎是一个很小的代价,如果没有数万亿美元的损失,这类漏洞利用允许的数十亿美元。

编辑:澄清:

通过'缓冲区溢出',我的意思是程序员不允许代码允许溢出,但编译器允许目标变量/ array /(m)allocblock之外的内存被它写入(或读取)(如:int a ,b; *(&amp; b-1)应由编译器捕获,而不仅仅是a)。

通过“证明”,我的意思是俗称“旧的简单Pascal不允许缓冲区溢出,足够接近100%确定我们可以说它已被证明”,尽管它可能使用不安全的系统功能,如果他们写在一个检查Pascal的边界,那么他们也不会超支。我用“证明”这个词来区分各种不完善的硬化工具。

“可利用性”是指“缓冲区溢出可利用性”,这是一个简单的问题,在其他语言中以牺牲速度和内存为代价来解决。

“你是认真的吗?如果它存在,我们已经做到了。” - 这是我很好奇的。技术在这里 - 胖指针(C标准允许编译器使任何大小的指针)具有完整的每指针边界检查。但是我找不到概念,讨论和论文的证据,当我想要的是一个完整的C编译器来做这个以及用它构建的整个Linux发行版。没有人会在不久的将来用更安全的语言重写所有内容(Linux,Apache等等)(遗憾的是他们不断在C中编写新内容),但我们可以使C / C ++更安全并重新编译所有内容。至少对于首先需要安全性的用途。

3 个答案:

答案 0 :(得分:1)

这个问题有各种各样的解决方案,使C语言基本上孤立无援。他们所做的主要是跟踪“危险”指针访问(那些静态分析无法证明是安全的),但需要花费一些运行时间。

当然,你可以争辩说,所有这些都是编译C“已检查”的解决方案,你似乎在你的问题中反对。在最坏的情况下,我认为这只是构建过程中的另一个步骤。

真正的问题是这些解决方案在时间和空间上都有可衡量的开销。在构建嵌入式系统时,额外的成本显示为花费在获得更昂贵的处理器以在分配的时间内完成工作所花费的真实美元,和/或用于跟踪麻烦指针的额外内存。大多数制造商在选择不良程序的低概率(或者更黑,“没有人会注意到我卖掉所有这些!”)和绝对真实的额外成本之间进行选择时,往往会优化成本,现在你是回到编译原始C程序而不进行运行时检查。 “便宜”会损害“质量”或“时间表”。我们在航空座椅舒适性和软件安全性方面看到它。

答案 1 :(得分:0)

当然,只要你愿意放弃一些事情:

  • 指针数学
  • 未经检查的第三方图书馆(有点破坏重点,没有?)
  • 以null结尾的C字符串

您只需要使用自己的allocator和deallocator将每个分配的大小写入标头,然后使用宏进行指针/数组查找以检查该分配大小:

void *myalloc(size_t size) {
    if (size == 0) return NULL;
    void *data = malloc(size + sizeof(size_t));
    if (data == NULL) return NULL;
    *((size_t *)data) = size;
    return data + sizeof(size_t);
}

void mydealloc(void *data) {
    free(data - sizeof(size_t));
}

#define item(data, index) ({ \
    __typeof__(data) item_data = data; \
    __typeof__(index) item_index = index; \
    assert(item_index >= 0 && (item_index + 1) * sizeof(*item_data) <= *((size_t *)((void *)item_data - sizeof(size_t)))); \
    *(item_data + item_index); \
})

任何字符串函数都需要使用写入标头的size_t值,而不是查找空字节。无论如何,你应该这样做,因为null终结符在Unicode世界中毫无意义。也可以在同一个item()宏中检查堆栈分配,但我认为它必须是平台和实现相关的。

你不经常看到这种情况的原因是因为如果你愿意接受重大的性能影响,使用具有这些保证的更高级语言更有意义内置 以及大量其他漂亮的语言功能。

答案 2 :(得分:0)

我们无法修复C,因此要处理已知的危险,这是专业程序员的责任。 C的优势在于它永远存在,所以所有的弱点,陷阱和定义不明确的行为都是众所周知的,并且有文档记载。

你可以做些什么来防止这些错误:

  • 将编译器设置为尽可能挑剔。对于这个特定错误的海湾合作委员会的情况,它似乎没有帮助...据说有一些选项叫做'Warray-bounds和-fbounds-check,但这些似乎不起作用至少在GCC 4.9.1 / Mingw64中没有。无论如何,通过始终使用std=cxx -pedantic-errors -Wall -Wextra进行编译,您可以捕获许多其他常见错误。
  • 使用静态分析工具。这些应该至少能够找到与边界检查相关的所有编译时错误。
  • 更便宜的外部工具替代方案是使用防御性编程。在这种情况下,您可能已经捕获了断言的错误。例如:

    #include <stdio.h>
    
    #define ARR_SIZE 2
    #define INDEX -1
    
    int main()
    {
        _Static_assert(INDEX < ARR_SIZE, "Array index too large.");
        _Static_assert(INDEX >= 0,       "Array index too small.");
    
        int a[ARR_SIZE];
        a[INDEX] = 5;
        printf("%u\n", a[-1]);
        return 0;
    }
    

    您甚至可以在项目的调试版本中使用运行时assert()来捕获运行时错误。这也节省了程序开发过程中的大量时间,因为您可以更快地发现错误。

  • 提高代码质量的一般措施:代码审查,编码标准等。