何谓内存安全

2024-06-12 12:38
文章标签 内存 安全 何谓

本文主要是介绍何谓内存安全,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

最近忙于把一些 软件安全方面的网络公开课资源整合到一起,这项工作将于今年10月份上线。目前为止,我已经完成了 缓冲区溢出, 格式化字符串攻击和其他一些C语言的软肋方面的材料收集。给出这些材料之后,我想问,“这些错误有什么共同点?”,答案是他们都违背了内存安全的原则。接下来我会阐释内存安全的定义是什么,为什么上面提到的这些C语言的软肋违背了内存安全的原则,反过来说,Java等内存安全的语言是怎么保证内存安全的。

考虑到内存安全是一个常见术语,一开始我以为找到它的定义并不困难。可事实上,比我想象的要难的多。

写作本文的目的是找出C语言中内存安全的准确定义,这个定义要语义明确,更重要的是,要能够区分内存安全的和不安全的代码。定义越简单完整越好。我最终给出的定义基于两个概念:定义/未定义内存(defined/undefined memory)和能力指针(pointers as capabilities)。如果你有更好的方法,请告知我。

五条禁忌:

Systematization of Knowledge(SoK)上的最新文章, Eternal War in Memory,采用了一个常见的方式来定义内存安全。即一个程序只要不出现 内存访问错误(memory access errors),就说这个程序是内存安全的。内存访问错误具体包含以下几个方面的内容:

  • buffer overflow
  • null pointer dereference
  • use after free
  • use of uninitialized memory
  • illegal free (of an already-freed pointer, or a non-malloced pointer)

维基百科上关于内存安全的页面给出了相似的定义。这个列表本身是没有问题的,但是用它来作为内存安全的定义还有失偏颇。严格来说,这个列表应该是由内存安全的定义导出来的,而不是内存安全的实质。那么,究竟该怎么定义内存安全呢?

不访问未定义内存

下面是内存安全一个更合理的定义:内存安全就是不访问任何未定义的内存。未定义内存就是没有分配给程序的内存,可能是堆,也可能是栈,还可能是静态数据区。 George Necula和他的学生,在完成 CCured项目的过程中,这是一个以加强C程序内存安全为目标的项目,提出一个内存安全的程序从不访问未定义的内存。我们可以假设,从概念上来说,内存是无限多的,并且分配出去的内存即使收回也不重复使用(内存无限)。这样,收回之后的内存永久处于未定义状态。

显然,这个定义包含上面列表中第2条和第3条中的内容,如果把未初始化的内存也视为未定义内存,第4条也可以包含在内,如果再假定free操作只可以在定义内存上操作,第5条也能包含在内。

不幸的是,这个定义并不能把第1条,也就是缓冲区溢出也包含在内。比如说下面这段程序,根据上面的定义,程序1是内存安全的,因为第四行写入的内存是已经分配过的内存,甚至写入的数据类型也对。问题是在向buf[5]写入数据时,因为缓冲溢出,实际上是给变量x写入了数据,这显然不是内存安全的。

/* Program 1 */
int x;
int buf[4];
buf[5] = 3; /* overwrites x */

注:译者在翻译的过程中发现,上面程度的第2行和第3行应该交换位置,这应该是作者的笔误。

无限空间

我们可以通过扩展定义来修正上面出现的这种错误。我们假设在分配内存的时候,不是连续分配的,而是间隔无限远。

拿上面的程序1来说,因为buf和x的内存区域不连续,很显然,根据扩展后的定义,在访问buf[5]时,访问到的是未定义内存,所以不是内存安全的。上面的例子说的是栈中的溢出,堆和静态数据区中的溢出可以做相似的处理。请注意加上这个扩展之后,就不能将int类型的数据转换成指针了,这是因为指针要求是无限的,而int类型的数据是有限的,所以很有可能你选择的int数据并没有对应一个合法的内存区域。 Emery Berger和 Ben Zorn,在他们的 DieHard memory allocator工作中,希望通过随机分配内存来将这个定义变成现实。

虽然我们离正确的定义越来越近,但是还没有得出正确的定义。来看第2个程序,这个程序在程序1上稍有变动。

/* Program 2 */
struct foo {int buf[4];int x;
};
struct foo *pf = malloc(sizeof(struct foo));
pf->buf[5] = 3; /* overwrites pf->x */

在程序2中,缓冲溢出发生在结构体中。当然我们可以假设不同的field之间是间隔无限远的,这样就可以像程序1一样说程序2也不是内存安全的。但是这样的假设和实际情况并不符合。C标准中允许编译器自行决定不同的fields之间到底间隔多远。另一方面,C标准还建议一条记录对应一个object。许多程序将一个结构体转换成另外一个,或者假定一个确定的间隔距离,而许多编译器都支持这两个操作。所以目前的定义还不够好,因为它依赖于和现实情况不符合的假设。

能力指针

我现在要说的是我认为更好的一个定义,这个定义受到 SoftBound的启发,SoftBound由 Santosh Nagarakatte和另外的几位合作者共同完成,这项工作致力于使C语言编写的程序更加内存安全。

在之前的定义中,我们提到了定义内存和未定义内存,定义内存就是已经被分配的内存,未定义内存就是没有被分配或者分配之后被回收的内存,回收之后的内存不再重复使用,如果一段程序访问未定义内存,我们就说它不是内存安全的。在这个定义的基础上,我们增加 能力指针的概念,就是说和每个指针相关的存在一个这个指针能够访问的内存区域。有了这个概念之后,可以将指针看成是一个三元组(p,b,e),p代表指针本身,指针能够合法访问的内存区域由b和e来决定。在程序中,真正操作的只有p,b和e只是概念上存在,而它们的存在是为了判断某个操作是不是内存安全的。

下面以程序3为例来阐释这个概念,画出下面这张内存图是为了是问题更加清晰。

/* Program 3 */
struct foo {int x;int y;char *pc;
};
struct foo *pf = malloc(...);
pf->x = 5;
pf->y = 256;
pf->pc = "before";
pf->pc += 3;
int *px = &pf->x;

程序3的最后两行是关键所在。对指针进行加减操作来创建新的指针是和合法的。在上面的例子中,增加pc的p值,但是在b和e所指定的范围之内,这时,执行 *(pf->pc)就是合法的。如果先执行 pf ->pc += 10,因为已经超出了b和e指定的范围,再执行 *(pf->pc)就违反了内存安全的原则,即使pf->pc指向的是一个已经分配的内存。

程序3的最后一行创建指针px指向pf的第一个field,因为pf的第一个field是一个int类型的变量,px的b,e将px可访问的内存区域限制在那个field上。这样就避免了程序2中所述的缓冲溢出。如果我们将pf的范围直接复制到px上,就可以通过px访问到本field之外的field,这显然是不合理的。

将整数转换成指针的操作是不合法的,不管是直接的转换还是间接的转换。直接的转换,比如说p = (int *)5,间接的转换,比如说p = (int **)pf。根据定义,这种转换操作将直接被忽视,只有合法的指针可以被解引用,指针的范围在其创建的时候就已经确定了。根据我们的定义,下面这段程序是合法的。

/* Program 4 */
int x;
int *p = &x;
int y = (int)p;
int *q = (int *)y
*q = 5;
当p转换成int类型的整数(第4行),p在创建时确定的b和e仍然被保留下来,所以在执行第5行之后,p的b和e又传给了q。如果在上面程序3的后面加上这一行,然后执行*p = malloc(sizeof(int)),再执行 **p, printf(“%d\n”,pf->x)  这都是合法的。也就是说,以前是一个整数的内存区域可以被改造成包含一个指针,这个指针可以被解引用,把指针当成一个整数来对待是安全的,但是反过来不行。

在某种程度上,上面说的关于内存安全的定义是类型安全的一种形式,类型只有两种,指针类型和非指针类型。上面的定义无非是说明以下三个方面的问题。1,指针仅仅以安全的方式创建,所谓安全的方式即在创建指针时定义指针能够访问的内存区域;2,只有指针在其允许访问的内存区域时才能被解引用;3,这个定义将上面五种类型的错误全部考虑在内。

你怎么看?我给出的这个定义能否在书写内存安全的C程序方面给你帮助?是否还有按照定义是内存安全实际上却不安全的例子,或者按照定义是内存不安全而实际上是安全的例子,如果有,我们怎么在保证定义简洁的情况下扩展定义,使它的适应范围更广。

结论:

正在看这篇文章的你可能在想,这是一个学术练习吗?定义内存安全究竟有何意义?
对我来说,一个好的定义关乎科学的发展。当我阅读一篇声称可以保证内存安全的科技文献时,如果我所知道的内存安全的定义和作者理解的不是一回事,那么我就根本不会明白作者到底在说些什么。从大的方面来说,这些含混不清的定义实际上阻碍了科学的进步。比如说,有一篇文章声称发明了解决内存安全的技术,如果不知道内存安全的定义,验证或是反驳这个观点就将无从谈起。 Static Error Detection using Semantic Inconsistency Inference   和  Large-Scale Analysis of Format String Vulnerabilities in Debian Linux这两篇文章具有阅读价值,想想他们说的内存安全的含义是什么?

这篇关于何谓内存安全的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1054242

相关文章

Python从零打造高安全密码管理器

《Python从零打造高安全密码管理器》在数字化时代,每人平均需要管理近百个账号密码,本文将带大家深入剖析一个基于Python的高安全性密码管理器实现方案,感兴趣的小伙伴可以参考一下... 目录一、前言:为什么我们需要专属密码管理器二、系统架构设计2.1 安全加密体系2.2 密码强度策略三、核心功能实现详解

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

最新Spring Security实战教程之Spring Security安全框架指南

《最新SpringSecurity实战教程之SpringSecurity安全框架指南》SpringSecurity是Spring生态系统中的核心组件,提供认证、授权和防护机制,以保护应用免受各种安... 目录前言什么是Spring Security?同类框架对比Spring Security典型应用场景传统

Redis 内存淘汰策略深度解析(最新推荐)

《Redis内存淘汰策略深度解析(最新推荐)》本文详细探讨了Redis的内存淘汰策略、实现原理、适用场景及最佳实践,介绍了八种内存淘汰策略,包括noeviction、LRU、LFU、TTL、Rand... 目录一、 内存淘汰策略概述二、内存淘汰策略详解2.1 ​noeviction(不淘汰)​2.2 ​LR

Golang基于内存的键值存储缓存库go-cache

《Golang基于内存的键值存储缓存库go-cache》go-cache是一个内存中的key:valuestore/cache库,适用于单机应用程序,本文主要介绍了Golang基于内存的键值存储缓存库... 目录文档安装方法示例1示例2使用注意点优点缺点go-cache 和 Redis 缓存对比1)功能特性

Go使用pprof进行CPU,内存和阻塞情况分析

《Go使用pprof进行CPU,内存和阻塞情况分析》Go语言提供了强大的pprof工具,用于分析CPU、内存、Goroutine阻塞等性能问题,帮助开发者优化程序,提高运行效率,下面我们就来深入了解下... 目录1. pprof 介绍2. 快速上手:启用 pprof3. CPU Profiling:分析 C

golang内存对齐的项目实践

《golang内存对齐的项目实践》本文主要介绍了golang内存对齐的项目实践,内存对齐不仅有助于提高内存访问效率,还确保了与硬件接口的兼容性,是Go语言编程中不可忽视的重要优化手段,下面就来介绍一下... 目录一、结构体中的字段顺序与内存对齐二、内存对齐的原理与规则三、调整结构体字段顺序优化内存对齐四、内

Linux内存泄露的原因排查和解决方案(内存管理方法)

《Linux内存泄露的原因排查和解决方案(内存管理方法)》文章主要介绍了运维团队在Linux处理LB服务内存暴涨、内存报警问题的过程,从发现问题、排查原因到制定解决方案,并从中学习了Linux内存管理... 目录一、问题二、排查过程三、解决方案四、内存管理方法1)linux内存寻址2)Linux分页机制3)

Java循环创建对象内存溢出的解决方法

《Java循环创建对象内存溢出的解决方法》在Java中,如果在循环中不当地创建大量对象而不及时释放内存,很容易导致内存溢出(OutOfMemoryError),所以本文给大家介绍了Java循环创建对象... 目录问题1. 解决方案2. 示例代码2.1 原始版本(可能导致内存溢出)2.2 修改后的版本问题在

浅析Rust多线程中如何安全的使用变量

《浅析Rust多线程中如何安全的使用变量》这篇文章主要为大家详细介绍了Rust如何在线程的闭包中安全的使用变量,包括共享变量和修改变量,文中的示例代码讲解详细,有需要的小伙伴可以参考下... 目录1. 向线程传递变量2. 多线程共享变量引用3. 多线程中修改变量4. 总结在Rust语言中,一个既引人入胜又可