何谓内存安全

最近忙于把一些 软件安全方面的网络公开课资源整合到一起,这项工作将于今年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 BergerBen 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这两篇文章具有阅读价值,想想他们说的内存安全的含义是什么?
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值