为什么计算机不检查某些内存空间中是否有内存内容?

信息安全 记忆 缓冲区溢出
2021-08-16 00:54:22

发生缓冲区溢出是因为它写入程序的其他部分使用或将要使用的内存空间。

计算机程序通常会写入已被其他先前执行占用的内存位置;例如,如果我们通过接收标准输入(来自用户输入)数据来写入 char a[64],那么它写入的空间并不总是一个空白空间;这些空格是不必要的,可以被覆盖。

但是,如果我们强制计算机/编译器写入空的空间,我们将能够检查内存内容,如果内存空间不空,则停止执行,防止许多安全漏洞。

那么,为什么这没有发生呢?我弄错了吗?

这是由于 RAM 架构造成的吗?(比如可能无法轻松访问内存,我们只能通过寄存器访问内存等等?)

4个回答

记忆中总有一些东西。每个位始终包含 0 或 1。如果内存中的同一空间用于两个不同的目的,计算机(编译器或运行时系统)无法分辨:目的是人类的概念,而不是机器的概念。

在当今大多数您认为是计算机的系统上(PC、服务器、手机、路由器等,但不是大多数电子元件中普遍存在的微处理器和微控制器),都有一个内存管理单元 (MMU) . MMU 在程序使用的内存地址(虚拟地址)和实际内存库中的内存地址(物理地址)之间进行转换。在这个转换层,可以将一些虚拟内存区域标记为不可访问。如果一个程序试图使用不可访问的内存,该程序将被杀死(Windows 将此称为一般保护错误,Unix 将此称为分段错误)。

可以使用 MMU 进行内存保护。事实上,这就是多任务操作系统如何保护应用程序不访问彼此的内存。但是,在程序内部,应用程序是有限的。您不能将程序的每个变量都放在被不可访问区域包围的自己的区域中。嗯,你可以,并且一些调试程序会这样做,但这非常低效,因为地址转换的粒度很粗(在大多数处理器架构上为 4kB)。它并不能解决所有错误。如果您有一个 64 字节的缓冲区,后跟 4032 字节的未使用内存,然后是 4096 个不可访问的字节,则不会检测到第 65 字节的溢出(您可以确保它不会破坏程序的其他变量); 溢出到第 4097 个字节将覆盖其他内容。如果您释放一个对象并将其内存地址重用于另一个对象,但程序的某些部分仍然引用旧对象,则也不会被检测到。(你不能让旧对象的地址永远无效,因为你最终会用完地址。)

可以自动检测许多类型的内存滥用,但运行时并不是一个好方法。有时您可以捕获错误——除了访问不可访问的内存之外,您还可以在程序或运行时环境中进行显式检查。例如,您可以尝试通过将已知值放在堆栈底部(金丝雀) 并不时检查。如果在损坏发生之前检查金丝雀值,这确实有助于防止意外错误。当涉及恶意攻击时,金丝雀值会使编写漏洞利用变得更加困难,但它们并不能阻止一切。还有更复杂的运行时检查方法,例如获取部分内存的校验和,如果这些校验和稍后不匹配,则安排中止;这些主要用于敌对(“灰盒”)环境中,攻击者有一些有限的手段来扰乱程序的执行。

检测内存滥用的正确时间是在编译时。在那里,可以通过确保对于访问数组元素的每条指令来检测缓冲区溢出:

  • 编译器可以证明索引始终在数组的范围内;
  • 或者在访问数组元素之前立即对索引进行范围检查。

缓冲区溢出只是问题之一。另一个常见问题是程序释放分配给对象的内存,但保留指向该对象的指针并稍后尝试访问该对象。有一个解决方案,但它对语言设计有很大的影响。这意味着该语言必须禁止程序员以不受控制的方式存储指针。有几种方法:

  • 当对象的内存被释放时,编程环境使所有指向对象的指针无效;
  • 当周围仍有指向该对象的指针时,编译器拒绝编译释放该对象的程序;
  • 编译器推迟释放对象的请求,直到它可以满意地证明没有指向该对象的指针。

大多数编程语言通过某种形式的自动内存管理来解决这些问题,其中释放对象(如果它们存在的话)的请求不会立即处理,并且所有指针都在编译器的控制之下,因此它可以知道这是什么对象指针正在访问。例如,编译器看到一条指令正在访问某个对象 A 的第 42 个字节,因此它发出一条指令来检查 A 是否至少有那么多字节,并将 A 记录为在该点之前正在使用,以便 A不会提前释放。自动内存管理最常用的技术是垃圾收集:运行时环境有一种方法可以探索程序的所有指针(这意味着程序员不能以某种隐藏的方式构成指针),并且它只会在程序没有指向它们的指针时释放对象可以访问。

大多数编程语言都使用垃圾收集和强制边界检查(两者通常一起使用):C#、Java、JavaScript、Perl、PHP、Python、Ruby……。但例外情况包括一些非常常见的语言:C 和 C++。这些语言旨在为程序员提供大量控制权。它们与禁止程序员编写指针的方法不兼容。

为什么人们用这种容易出错的语言编程?其中一些原因包括:

  • 历史的重量:程序是用 C 编写的,并且它们在 C 中不断发展。
  • 表现。真实的或感知的。自动内存管理有运行时成本(但是,手动内存管理也是如此)。
  • 历史的重量:程序员懂 C,所以他们用 C 写的程序更多。
  • 过度自信。许多程序员认为,由于他们比计算机更聪明,计算机不应该凌驾于他们的决定之上。(我比阳台上的护栏聪明,为什么它阻止我从10楼走下来?)
  • 历史的重要性:每个平台都有一个 C 编译器。其他语言往往不太便携。

程序员无法编写指针的语言会将由于缓冲区溢出或访问已释放对象而导致任意代码执行的编程错误转变为拒绝服务:程序可能由于未捕获的越界异常而中止,或者它可能会耗尽内存,但至少它不会按照攻击者的指示行事。所以他们并没有完全解决问题——没有灵丹妙药——他们只是减少了它的影响。

虽然具有自动内存管理的语言更上一层楼,但它们并没有完全解决最初无法解决的问题,即内存用于与预期不同的目的。事实上,它们只是一个进步。攻击者可能会滥用字符串,而不是滥用内存中的字节(这是缓冲区溢出可以做的):本应仅被解释为供人类使用的文本的字符串,反而被解释为某些编程语言中的指令。例如,SQL 注入攻击包括在字符串中插入单引号这导致该字符串的一部分被解释为 SQL 而不是某人的名字。有一些方法可以依靠编译器来检测这种误用,但是大多数流行的语言都没有实现它们,而且大多数程序员甚至不知道这种技术甚至存在(请参阅打印函数中处理不受信任的字符串输入并避免缓冲区溢出)它是如何完成的)。

在许多编程语言中,都会检查内存访问:通过构造,无论程序员多么不称职,以及恶意用户输入的数据多么不正常,用这些语言编写的程序都不能将数据写入缓冲区之外,或者覆盖带有整数或类似东西的指针。作为现实生活中的例子,Java、Javascript、所有 .NET 语言(C#、F#、VB.NET ......)......都是这样的语言。

出于传统和对所谓性能的错误认识,许多程序员仍然坚持使用一些包括这种不可避免的检查的语言,特别是 C 和 C++。这些语言与某些情况相关,但比通常认为的要少得多。基本上,用 C 编写安全的代码需要一个能力很强的C 程序员,而人类不轻易承认自己的不足是人类的一个众所周知的事实;因此,许多 C 程序员认为自己比实际更擅长 C 编程。就好像驾驶执照被禁止了,只要他认为自己可以,每个人都可以开车。

但是请注意,具有强大语言的编程语言并不能防止错误。在缓冲区溢出的情况下,主要问题是程序试图做一些无意义的事情,即在缓冲区中存储的字节数超出了可能容纳的范围。再多的边界检查都无法纠正这一点。在 Java 程序中,ArrayIndexOutOfBoundsException这至少避免了攻击者获得远程代码执行的可怕后果;但错误仍然存​​在。

许多非 C 语言的真正避免错误的特性不是边界检查,而是自动内存管理(通常使用垃圾收集器),因为它允许不可变的字符串,可以轻松地传递和附加,而无需执行通过将字节复制到缓冲区(strcpy()...)的模糊业务:经验表明,在 C 中,大多数缓冲区溢出潜伏在字符串处理代码中。

没错,许多语言在空闲时不会写入内存空间。对于不需要的语言,如 C 和 C++,语言期望如果程序员需要它,程序员将手动执行此操作。

https://stackoverflow.com/questions/1287180/why-doesnt-free-zero-out-the-memory-prior-to-releasing-it

如果您正在使用其中一种语言进行开发,则应该使用为您完成工作的库。有些库甚至会进行垃圾收集并提供其他“高级”功能。这里的好处是,如果您在性能方面处于紧张状态,您可以将其关闭以手动调整。

也就是说,我同意其他一些帖子,即这些低级语言经常被过度使用,但并非随意:

  • 高级语言中也存在安全漏洞,
  • 性能是一个真正的问题,它可能会影响您在市场上的竞争力
  • 您现有的代码库是一项巨大的投资。当您的软件最初被开发时,其中许多语言并不成熟。
  • 您不应该仅仅因为您现有的代码库是用低级语言编写的,就认为您的安全实践是松懈的并且您的代码是不安全的。

安全就是风险管理。如果用另一种语言重写代码的成本可能会让你破产,新语言甚至没有任何安全保证,而且发生重大、无法修补的安全漏洞的可能性极低,那么很明显用“更安全”的语言重写你的应用程序只会增加你的风险。

我们的确是。堆栈金丝雀就是一个例子。