纯 C 的安全 memcpy

信息安全 应用安全 源代码 linux 缓冲区溢出 C
2021-09-03 03:29:30

缓冲区溢出并不是什么新鲜事。然而它们仍然经常出现,尤其是在本机(即非托管)代码中......

部分根本原因是“不安全”函数的使用,包括 C++ 主食,如 memcpy、strcpy、strncpy 等。这些函数被认为是不安全的,因为它们直接处理不受约束的缓冲区,并且如果没有密集的、仔细的边界检查,通常会直接溢出任何目标缓冲区。

Microsoft 通过 SDL 禁止使用那些“不安全”的函数,并为 C++ 提供替代函数 - 例如 strcpy_s 用于 strcpy,memcpy_s 用于 memcpy 等(取决于环境)。最新版本的 Visual Studio 甚至可以让您自动执行此操作...

但是“纯”C(即不是 C++)呢?
尤其是非 MS 平台——包括 Linux 甚至是 Windows 上的非 VS 编译器……
有没有人有这些更安全的替换功能?任何推荐的解决方法(除了简单地做更多的边界检查......)?
还是我们都注定要继续重复我们对 memcpy 的使用?

4个回答

让我们在这里说清楚。

  1. 不安全上下文问题,而不是“使用”特定功能的情况。如果我在作为用户运行的进程上不安全地使用 memcpy,几乎没有丢失,没有 setuid 或任何此类标志,我能做的“最糟糕的”是为该用户获取一个 shell 并从那里开始。为了实现特权升级,您需要攻击一些您可以欺骗的东西,从而为您提供更高的特权。
  2. C/C++ 不是“危险工具”。它们是工具。插入关于危险的 DIY 工具的适当比喻。

事实是,正如 Rook 已经说过的,C/C++ 和其他类似的其他机器编译语言在世界上占有一席之地。它们用于构建快速系统、操作系统、系统服务等。它们使您能够根据需要管理自己的内存。你在掌控之中。

除非您引入某种形式的自动内存管理,否则如果您已经超过了分配的内存,就无法真正解决问题。所以,好吧,让我们介绍一个容器。现在需要检查每个内存访问调用。它是否在该特定功能的范围内?有多少物体指向它?在哪里,他们的范围是什么?我可以有原始参考范围之外的指针吗?如果可以,我该如何跟踪它?很快,您就拥有了一台虚拟机,因此您拥有了一种托管语言。

此外,正如在 StackOverflow 上所展示memcpy_s的那样,无论如何都可以以不安全的方式调用它并没有真正解决根本问题,只是让犯错变得更加困难。

这就是区别。C/C++ 是花哨的汇编程序,具有执行任何操作所需的所有功能。Java/Python 等以一定的代价保护您免受伤害:速度和功能。

您在今天的过程中反复说过(我也遵循了 SO 版本),您仍然没有得到关于如何在 Linux 等其他系统上使用 C/C++ 安全开发的答案。首先,我几乎习惯性地使用 VS2005/2008 设置CRT_SECURE_NO_DEPRECATE,但无论如何,这里有一些你可以做的事情:

  1. 你问 StackOverflow你记笔记、学习、阅读和重读。
  2. 您的编译器(大部分)是您的朋友。听这个。使用警告。把它们变成错误。所以cl /W4gcc -Wall -Werror -pedantic -std=c99是的,就错误消息而言,它是 OTT。但是如果你不能解释每一个并证明你为什么忽略它,你就不会理解你自己的代码。
  3. 检查您的内存分配。valgrind的默认调用检查您的分配和释放意味着您不会丢失内存。如果您有内存泄漏,那么您对代码的考虑还不够。这是一个好兆头,你有一个错误,无效的边界检查等。
  4. 再次使用valgrind我刚刚拿起了这个,但是看,valgrind 中有一个实验性的过度/不足检查器。会告诉您,例如,您是否在堆栈末尾崩溃(可能,因为这是分配大多数局部变量的地方)或在brk()'d 堆之外。
  5. 使用splint又名安全 lint,记下它的输出。同样,如果您无法解释它给出的任何输出,那么您就无法理解您的代码。
  6. 内心消化 C 安全编码标准。这里特别相关:

    • STR31-C。保证字符串的存储有足够的空间用于字符数据和空终止符。

      将数据复制到不足以容纳该数据的缓冲区会导致缓冲区溢出。虽然不限于空终止字节字符串 (NTBS),但在处理 NTBS 数据时经常会发生缓冲区溢出。为防止此类错误,请通过截断来限制副本,或者最好确保目标有足够的大小来保存要复制的字符数据和空终止字符。

    • STR35-C。不要将数据从无界源复制到固定长度数组

      执行无界副本的函数通常依赖于外部输入来获得合理的大小。这样的假设可能被证明是错误的,导致发生缓冲区溢出。因此,在使用可能执行无限复制的函数时必须小心。

      Splint 会为您提供一些类似于 CERT 的指导。请注意,这在 STR35-C 的第一个示例中memcpy被认为是合规解决方案

  7. 如果您使用的是 C++,请使用std::stringand boost::shared_ptr(以及相关的;使用适当的)。除了与 C 的交互之外,在 C++中绝对没有参数是malloc'ing和ing 字符串。即便如此,请将操作留给 C++。memcpystring.c_str()
  8. 尽可能动态链接。如果您已将任何第 3 方代码静态链接到您的应用程序中并且结果存在安全问题,那么您也遇到了问题,您也必须重新分发您的图像。共享对象不仅通常更方便,而且默认情况下您还可以获得安全更新。我知道有些情况你不能,这就是为什么我说“在可能的情况下”。
  9. 将此构建到您的开发周期中。
  10. 希望你没有错过什么。
  11. 跟上可能相关的相关安全社区更新。
  12. 善待安全社区。确认安全漏洞,采取措施修复它们。如果值得运行,所有代码都有(或有)错误。就那么简单。

归根结底,我认为“安全”功能无法解决问题。我认为问题可以通过使用一些不错的工具、一个适当的开发过程来解决,该过程拒绝来自关键版本(后期测试版和发布候选版本)的糟糕代码,最后是对良好实践/当前问题的认识。

最后,memcpy_s它不是 C99 标准的一部分。它是 C 库的扩展(不是核心的一部分),因此不能保证在我正在使用的平台上。memcpy是。对于需要跨平台编译的软件项目,这可能是使用哪个功能的决定因素。

尤其是非 MS 平台——包括 Linux 甚至是 Windows 上的非 VS 编译器呢?

作为 Apple 核心操作系统的一部分,有一个名为CoreFoundation Lite的跨平台开源项目,它提供 C 类型用于安全操作字节块、字符串和其他基本数据类型。与本次讨论相关,它们实现了类型检查、边界检查、内存管理,并区分了可变对象和不可变对象。

任何推荐的解决方法(除了简单地做更多的边界检查......)?

顺便说一句,微软unsafe.h包括 GCC 支持。这样做的方法是poison在您使用不安全函数时使用 GCC 的 pragma 导致错误。我还在我写的一本书中写到了这一点(免责声明:我写的)。

还是我们都注定要继续重复我们对 memcpy 的使用?

从根本上说,解决方案是“不要那样做”。所谓的安全实现可以阻止您在不属于您的内存上乱涂乱画,但仍然会留下很多“滥用案例”。边界检查只是问题的一小部分。例如,假设你malloc(1024)决定创建一个指向 56 字节的指针,并将其视为对 16 字节长的特定结构的引用。如果您随后(“安全地”)将 58 个字节复制到原始指针,则复制操作将成功,但您的结构已损坏。你能检测到破损吗?也许不是:它可能仍然看起来像一个有效的结构(特别是如果你在末尾放了一个幻数,或者没有幻数)。

“堆栈粉碎”和“堆损坏”问题实际上是“将内存视为大的无类型包 o' 字节”问题的特例。此问题的其他一些示例包括:

  • STR30-C。不要尝试修改字符串文字虽然从“big bag o' bytes”的角度来看,可变和不可变的 C 字符串看起来相同,但实际上尝试就地编辑字符串文字会导致未定义的行为。但是代码无法判断 a 是char *代表字符数组还是字符串字面量,因此确保正确行为的所有痛苦都必须通过调用代码来处理。
  • MEM01-C。之后立即在指针中存储一个新值free()因为可能会引用不再属于您的内存块,并且通常很难判断它是否有效。
  • MEM05-C。避免大的堆栈分配当你与声称你只需要让你的界限正确并且其余的都到位的人打交道时,这一点特别有趣。有了这个问题,您可以拥有完全内部一致且正确的代码,但仍会破坏堆栈。

该列表还在继续:请参阅CERT C 安全编码标准的其余部分

综合答案是:停止使用 C(或 C++)!这是一种古老而彻底的危险工具。切换到具有集成安全性的语言(Java、C#、Scheme、OCaml、Python ......选择范围很大)。

缓冲区溢出是一种错误,属于被称为“程序员没有完全意识到自己在做什么”的一类错误。具有边界检查(甚至使用memcpy_s())的语言不会消除此类错误;它只会使后果不那么可怕(例如抛出异常,通常意味着立即终止线程)。它就像安全带:安全带不能防止车祸,它只是在你的汽车被遗忘时试图让你活着。因此,您应该使用绑定检查(特别是以透明方式包含此类检查的语言),原因与您应该始终系好安全带的原因相同。

至于 C:memcpy_s()及其同类实际上是在 ISO/IEC TR 24731(2005 年的标准)中定义的,因此它们应该在某些时候渗透到 C 和 C++ 编译器中。但现在似乎只有微软在推动它们。对于“字符串”函数(strcpy()strcat()),有标准(C99)函数(strncpy()strncat()),并且一些 BSD 操作系统(主要是 OpenBSD)正在使用具有更易于使用的 API(strlcpy()strlcat())的变体然而,这些函数都没有纠正这个错误,即程序员试图将数据放入一个太小的缓冲区中;它们只是让程序员意识到他的缓冲区可能没有足够的大小。

但是“纯”C(即不是 C++)呢?

我不认为像 memcpy_s 这样的函数真的有资格作为 memcpy 的“安全”替代品,但无论哪种方式,它们都同样适用于纯“C”,关于它们的“C++”并不多。它们也不是特定于平台的(从某种意义上说,编写它们的可移植版本并不难,即使您不能依赖它们成为平台 C 库的一部分)。

就安全而言,memcpy & co 被定义为允许在内存上任意涂鸦。这就是它们的用途,无论添加多少尺寸参数都不会改变这一点。

同时,我认为说“不要使用‘C’/C++”是不合理的(目前),因为仍然有很多系统,其中类似“C”的语言是唯一实用的选择。

我猜真正的问题是,是否有比 memcpy 更安全的缓冲区管理替代方案,并且可以在 C 和 C++ 中使用。嗯,当然。可以定义一个“C”API 或 C++ 类来分配、管理和访问经过边界检查的缓冲区和数组,这正是 Java VM 等人的解释器正在访问的那种东西。

还有一些像 Objective-C 这样的语言还不是托管代码,它们具有托管代码的许多安全方面,尽管事实上 memcpy 等可用。这是因为 Cocoa 框架倾向于鼓励并且至少允许使用更安全的替代方案。