绿盟于旸:让阿里安娜火箭爆炸的缓冲区溢出

来源:腾讯

腾讯科技讯 3月18日,由中国最大的互联网综合服务提供商腾讯发起和组织的互联网安全峰会在深圳召开。包括微软、盛大、新浪等互联网界各大巨头的技术专家,学者和专业人士参与了此次的交流。此次峰会是今年以来首场由中国互联网各顶尖企业共同参与的大型网络安全专业盛会。

来自绿盟的于旸,在现场发表演讲。以下为文字实录:

于旸:我首先做个自我介绍,我叫于旸,目前主要从事信息技术安全有关的研究工作。腾讯虽然不是一家安全公司,但是腾讯科技对于信息安全问题的重视程度和在对待上的专业态度,一直以来都给我留下了很深刻的印象。举个例子,我们可能会注意到,在腾讯的网站上有一个专门的安全中心的页面,会对腾讯的软件中的一些安全问题及时在这里通知用户,并且FIX。在中国仅此一家,如果把微软中国也算上的话就是两家。

所以,我非常荣幸今天在这里和来自腾讯以及全国各地的各位安全专家们一起,就一些信息安全方面的问题进行讨论。我演讲的题目是《缓冲溢出历史、八卦和娱乐》。用缓冲区溢出这个题目对在座各位朋友并不陌生。我可能无法用短短的一个小时让大家了解很多的技术,但是我会尽量让大家听得愉快。

如果大家想进一步地了解相关的技术,可以关注一下明天禇诚云和吴石的演讲。其中一定会有更加详细和精彩的内容。

首先,我们来简单看一下什么是缓冲区溢出以及缓冲区溢出的历史,然后我会花一点时间给大家介绍在二十年当中,我们和缓冲区溢出以及它的诸多“亲戚”所做的搏斗。第二部分内容我会给大家介绍一些和缓冲区溢出相关的小例子,这些东西可能没有很多的技术含量,但是可能会比较有趣。

首先我们来看什么是缓冲区溢出,严格来说,我们去查相关的专业资料可以看到一个很精确的定义,但实际上在广义上我们把所有的内存读写类漏洞,包括很多,基本上通俗的我们都称之为缓冲区溢出。今天在我的演讲之中用的缓冲区溢出的概念是最广泛意义上的,并不是精确地去指一个具体的缓冲区溢出问题。

到底什么是缓冲区溢出呢?我们看这张图上面(图),我找了一个油漆桶的图。简单说,我们在200毫升的咖啡杯里倒了205毫升的咖啡。不管什么原因,结果都是咖啡倒多了。咖啡倒多了杯子承纳不下,它会流到别的地方。流到什么地方去呢?会流到什么东西上面去呢?根据情况不同可能导致不同的危害。简单来说,这就是缓冲区溢出。

在这里我给大家举一个简单的例子。这是一台Windows 2000,在Windows2000当中有一个程序叫MRinFo,这个程序是Windows自带的网络程序,这个程序它在处理命令行参数的时候是存在程序溢出问题的。不会导致安全风险,但是我想可以用这个程序作为一个例子,让大家大概对缓冲区溢出有一个概念。首先我们给这个程序敲入参数,这个参数并不是程序设计的时候,它是做预计的输入,预计的输入应该是IP地址或者域名,返回之后地址不能被解析。如果我们把这个参数输得相当长,大家看它很快就返回了,根本就没有出现这些(图)。为什么呢?因为在程序设计的时候,正常情况下如果一个地址不能被解析,它就会转入打印的流程。由于在这个地方我们传递的相当长的参数,导致了缓冲区溢出的发生,程序就直接崩溃,或者可能转入了其它流程,最后就没有打印出信息来。

我再给大家看另外一个例子。刚才一瞬间非常的迅速,可能没有看清楚,我们再来一遍。大家看一看,我给它传递了一个很长的参数,但是在末尾我加了两个汉字(图),在座各位有认识这俩字的吗?这不是太上老君画的符,这是两个我们在字库里可以找到的汉字。这两个汉字对应的内码恰好是Windows当中参数的地址。这两个汉字是什么?就是我刚才说的200毫升的咖啡杯我们倒了205毫升,这就是那5毫升。这5毫升咖啡流到了不该流的地方,而恰好它的内容我们又把它设定为一个特殊的值,就是我说的函数的溢值。这个程序在转入到下一流程的时候就会被两个神奇的汉字所欺骗。这就是一个最简单的缓冲区溢出的例子。

所以,我们可以用一个原本用于网络操作的程序来完成一个锁屏的工作。简单来说,我们几乎可以对任何安全漏洞下这样一个定义,什么叫安全漏洞?就是可以让程序执行没有设定的功能,你如果能实现这个,那它就是一个安全问题。我们先把这个MRinFo程序放到一边,一会儿还会用上它。

以前大家如果对安全漏洞没有太多了解,或者大家想去了解和学习我给大家推荐一个我的朋友也是我的同事,他写的一个小小的文档。一般来说,我们除了刚才的这种非常典型的急于备战的程序溢出之外,把堆溢出以及覆盖BSS区等等问题都在广义上称之为容易溢出的问题。

导致缓冲区溢出的原因是什么呢?在历史上,当然我说的历史不是很长的时间,可能十几年前甚至于十年以内的时间。最早是由于我们使用了一些不安全函数,或者是没有正确地使用好一些函数导致的。这是导致缓冲区溢出比较简单的方式。后来又出现了类似于整数问题导致的一些缓冲区溢出,以及近两年又出现一些新的情况。我在这里就不给大家一一介绍。

我们把其中的一条我认为比较有意思的导致缓冲区溢出的方式给大家看一下,整数问题(图)。对于我们的计算机来说,它有一个非常有意思的特性,我们知道一个数是正数还是负数,为什么?因为写阿拉伯数字的时候下面可以加一个减号表示负,在脑子里面我们也可以形成这样一个逻辑概念,但计算机没有这个功能。为了表示负数在计算机当中是用了一个特殊的方式,在这里我也不进行介绍了。总的来说,大家知道在计算机处理数字的时候,两个数字相加它的值可能小于那两个数字,两个数字相乘可能得出来的值也小于那两个数字。就是加和乘不一定使结果变大。反过来减和除也不一定使结果变小。另外,内存当中的一段数字它既可以认为它是一个正数也可以认为是一个负数,这都是在程序当中所决定的,如果在这个当中没有处理好也会出现一些问题。

整数问题的确是非常让人在这个地方出问题的。但是,像这种只是表示温度上出一点小的差错可能无伤大雅,不会出现什么严重的后果。但是也有出现严重后果的时候,现在我就要开始八卦了,这是阿里安娜五星火箭,1996年5月4日,失事四颗卫星同时被毁,这是什么原因导致的呢?因为火箭的控制程序是拉非人写的,程序员把一个长整形变量写成了短整形导致了溢出。就是这么一个小小的错误,导致阿里安娜五星火箭失事的非常重要的原因。当然整体来说还有别的原因,比如本身这段代码在阿里安娜五型火箭没有用的,这其实也是整个开发管理方面的问题,因为大并不适用于五型火箭。在四型火箭里可能没有用好的性能做这样一个负值,到了五型火箭就会出问题。我们看到给火箭写程序的人也会出这样一个错误。

还有一个例子,代码本身找不出什么问题来,实际上代码也没有问题,但是这段代码如果拿VC2003去编译的话,在一些特殊的情况下就会出问题。为什么呢?在VR2003里面对于new的处理没有考虑到整型,如果我们传递一个大的参数,比如Lx4然后后面七个零,在这个时候,因为一个整形是多少?四、五十节,这时候会把Lx4后面七个零这个数字乘以4。在分辨内存的时候会乘4,一乘4就导致的32位整型的上溢,算出来的一个值是多少呢?是零。或者说我们用稍微再大一点的数字,实际上真正分配的内存是一个比较小的值。

顺便说一下,这个安全问题在VC2005里面已经得到了解决。接下来我们再用一点时间说一下缓冲区溢出的一段小小历史。今天当我们谈到历史两个字的时候,可能脑子里面浮现出来的是巨大的时间跨度和厚重的文化。但是对于信息技术领域,因为它本身年龄就不是很大,没有很长的历史,对于信息安全技术它就更加年轻了,在这个领域里面可能十年就是很长的时间了。那我们的缓冲区溢出到现在多长时间呢?也就二十多年时间。这个技术第一次登上历史舞台,被公众所了解、所看到是在1988年11月2日,就是非常著名的Morris蠕虫事件。Morris蠕虫是罗伯特Morris Junior编译,他为什么叫这个名字呢?因为洋人取名字跟中国人不一样,在座各位不可能跟父母用一样的名字,但是外国人觉得这个名字不错就可以给自己儿子用。所以我们用Junior来表示区别,很显然他的父亲也叫Robert Morris。

我们想象一下,1988年,我对这段情况不了解,中国可能还没有这种互联网,甚至连实验性的互联网都没有。你会发现这里面闪耀着很多的智慧,是你在今天的这些木马病毒当中看不到的。举一个例子,那时候主要是VIX,这些机器各自是不一样的,甚至硬件价格都不一样,Morris蠕虫是由原代码和脚本组成的,它感染一台机器之后会利用这台机器上的编译器去编译,所以它可以跨平台,它本身是已原代码方式存在的,它感染下一代的时候也是把原代码传过去。

Robert Morris也就因为这件事情成了世界上第一个因为计算机被正式地被计算机法所判决的一个人,他被判了400小区的社区服务和一万美元的罚款。同时Morris蠕虫事件之所以这么有名,就是因为它直接导致了CERT这个组织的成立。就是1988年发生的蠕虫事件之后美国就赶紧成立了CERT,有了CERT之后中国有成立了CERT。如果没有蠕虫事件可能我们在座各位很多都不会从事这个行业。Morris蠕虫事件是互联网非常重要的一个事件。老Robert Morris实际上是我们现代unix系统的设计者,这就很难怪他儿子为什么这么厉害了。另外,老Robert Morris也是美国国家安全局的首席科学家。这就是Robert Morris(图)。

但是Morris蠕虫出现的时候,实际上公众对其一无所知,只是非常小范围的传播,即使是搞技术的人对它也不是很了解,只是知道有风险,具体怎么样都不清楚。包括Morris出现之后大家还是不清楚。这个时候另外一个人走上了历史舞台,Spaftord,这个老头对Morris蠕虫进行了分析,并且编写了INTERNET蠕虫分析。他在分析蠕虫的时候也同时把缓冲区溢出的知识带给公众,但是这一次介绍仍然可以说不是非常透彻和详细的。真正意义上的一篇专门用来论述缓冲区溢出技术的文章,实际上是1996年才发表,发表在PHRACK 49的杂志,他的本名叫ALEGH ONE,在这篇文章当中,可以说是非常详细地把缓冲区溢出这个知识介绍给公众。ALEGH ONE是谁呢?大家可能都看过邮件列表,他管理过五年,在1996-2001年之间,大部分时间都是他在管理这个邮件列表。

我们知道,今天来说缓冲区溢出威胁最多的系统可能是Windows系统。在1999年之前,可能这种情况都不是这样。当时很多人都认为缓冲区溢出这个技术在Windows系统上是难以实施的,而在unix或者是Sun是比较容易实施的。1999年,Spyrit写了一篇非常有里程碑意义的文章,也是发表在PHRACK55期杂志上。这篇文章可以说是近十年来信息安全灾难的一个开端。当然我不能说没有这篇文章现在我们就不会有这么多木马、不会有这么多蠕虫,但至少可以说它是我们今天所有做Windows安全技术研究的最早的一篇。

从Morris蠕虫算起,我们和缓冲区溢出已经搏斗了差不多整整二十年。在这二十年当中,防守的一方几乎总是落于下风,直到这两年情况才有所改观。除了在开发过程当中实施安全编程原则和流程等之外,我们还找到了一些其它更全局、更宏观的方法,这些方法有软件的、有硬件的,因为时间有限,我今天就简单给大家介绍一些。

一个非常简单而且有效的技术,Stack Cookie,这个系统最早是在Linux系统上提出来的,并且做了实现。但是这个技术真正让广大的程序员们了解,功劳还是微软的,因为微软在Visual Studio2003里面内置了Stack Cookie代码。这个技术说起来也非常简单,大家还记得我刚才做的例子吗?(图),在这两个神奇的汉字之前是我们输入的一段东西,如果没有这两个东西,这个缓冲区溢出毫无意义,最多只能导致程序崩溃。如果我们没有写这么长的话就不会发生缓冲区溢出。

StackCookie技术的核心在什么地方呢?就是对我们编译器所编译的每一个程序进行小小的修改,让堆栈的地方,也就是这两个汉字所占用的地方前面放上。这是被调函数的局部变量(图),我们在这里放上StackCookie,在每此将要到返回地址里去找代码执行之前,让程序先检测一下它的前面,因为在函数开始的时候我们放进去StackCookie

我们知道值是多少,当需要调用它的时候,我检测一下这个StackCookie和开始一样不一样,如果不一样的话就说明什么?已经发生了溢出,这个地方被修改了,因为我们要改这个地方就必然要改到它,因为是顺着下来的,用这个简单的方法就可以比较准确地检测到覆盖返回地址的行为。这的确是一个非常简单同时也非常有效的方法。

在VC2003的编译当中,默认开这个选项。研究相关技术的人也找到了对抗StackCookie的方法,做编译器这方面同时大家也找到了另外一些反过来克制的方法,双方的搏斗一直到今天仍然在继续,但是最基本的包括思路上都还是基于StackCookie区域当中,具体的技术细节我今天在这里就不详细介绍。

刚才说的是软件上面,在硬件上,今天在座各位大家用的电脑,或者说你们今天去买一个新的CPU,几乎这个CPU里面一定有这样一个技术,叫“内存不可执行”,或者叫NX技术。什么意思呢?我们知道计算机的内存在过去我们可以给它加上一个标志,它是否可读、是否可写。但是没有这样一个标志,我标志这段代码、这段数据是否可执行,它是否可作为代码来执行,没有这样一个标志。为什么呢?因为过去可能是因为CPU本身不够快,我让它在处理每一个内存的时候还要额外判断,会降低运行性能。但现在CPU已经足够强了,我可以牺牲百分之几的性能来做这件事情。所以这个技术实际上并不是这两年出现的。在很早以前,SUN就已经有了,只不过是因为我刚才说的性能问题近两年才在X86个人电脑上出现。

实际上,在X86处理器也不是这两年出现的,最早在2001年的时候InteL就已经在自己用于服务器的技术上处理这个技术。只不过还是我刚才说的由于性能问题没有在电脑上用,因为对于服务器来说安全的权重要更大一些,我可以为了安全牺牲一点性能。我们现在在一个Windows系统上,大家在系统设置界面里可以看到“数据执行保护”,英文叫DEP,它实际上就是使用了CPU的这个功能。我们传递进去的数据实际上可能是存放在一个数据区的,通过NX技术,我们可以把这种专门用来存储数据区的地方就标志为数据,不能执行,CPU就会拒绝执行这个区域的东西。简单来说,NX技术就是解决这样一个问题。

反过来,从攻击者这边来说,现在也有人研究出来了一系列的如何对抗NX技术的方法。举一个简单的例子,我的这个例子当中,如果这个CPU设置了内存不可执行的,不受影响,为什么呢?我传递的是代码。RETURN这个技术就是传递进去一系列地址,用这一系列地址来描述。当然得非常精巧地研究如何用一些特殊的地址去构成这样一个序列,让它们去实现一些功能。在Windows上实施这个技术的时候有一个便利条件,因为Windows特别是在2003及其以前的版本上有这样一个系统的这样一个技术,它可以决定是否启用。我们调用一次它,把当前进程的DISABLE不用它,然后就可以正常地按照这个流程来了。

但是总的来说,这些技术实施之后,还是大大地增加了攻击者的成本,大大增加了攻击者的难度,安全就是这样,不可能百分之百的解决这个问题,但这是一场博弈,就像刚才李总说的,我们要尽可能地增加这些坏人的成本。

这是另外一个非常有效的技术(图)。Safe C Library。我们刚才说了缓冲区溢出是伴随着C语言的东西,如果完全用C++来写代码,基本上不会出现缓冲区溢出的问题。但是很多时候我们完全不能放弃C语言,来用C++。在这里我给大家介绍Safe C library。使用Safe C library来取代传统的这些C函数,可以非常有效地避免我刚才说的各种缓冲区溢出的问题,像大家看到的这个(见图),这是传统的写法。这是用Safe C library的写法。大家可以看出,两者之间的改动并不是很大,但是用这种方法基本上可以解决缓冲区溢出的问题。即使在这上面出现一些异常情况也不会有什么危害。而且总体来说实施起来也不是很难。

还有一个可能是更年轻一点也是更有效一些的技术,就是SAL,什么是SAL呢?它实际上是一种注释技术,我不知道我的解释准不准确,但是我看相关文档之后我觉得它就是注释技术,就是为编码添加一些注释,但是这个注释更多地是让程序去看的,是让编译器去看的,而不是让人去看的。为编码添加注释,帮助那些不易被发现的问题。

举个例子,这是一个函数定义,它接收了Buffer和BufferLength,我们如何让程序知道它是一个缓冲区和缓冲区长度呢?利用SAL我们可以把它标出来,让这两个本来没有什么关系的变量发生关系,做这样一个标记(图)。现在就把它们两个联系起来了,大家看这个难度也不是很大,我刚才说的这些都是非常有效的技术。

我刚才的科普工作就告一段落,现在还有一点儿时间,我给大家简单地就一些缓冲区溢出类的安全问题做一些小小的演示。

第一、像内存不可执行类等等的方法,可以在程序运行的时候检测出这些发生的溢出问题。接下来我给大家演示一种情况,这种情况是不会被检测出来的。当然,这种情况它可能出现的概率也非常小,但非常有意思。这是我写的一个小小的程序,它模拟了一个软件的登录界面,我们只需要输入一个任意长的数值就可以登录界面。这是为什么呢?类似的安全问题在历史上是实际出现过的。在2001年就出现过类似的问题,只需要用一个小小的溢出来标识,你不需要密码也可以以任意用户登录成功,实际上在这个例子中描述的就是那样一个情况。有一个用来表示是否登录成功的标志,虽然我们在前面把它标志为了FALSE,但是因为发生了缓冲区溢出,把局部变量覆盖了一点点,因为不管拿什么值覆盖,本来是零,你只要让它不是零,这个验证就通过了。因为这个地方覆盖的是局部变量。覆盖的是这个区域(图),不涉及执行的问题,并不需要执行什么代码,同时也不涉及覆盖返回地址的问题,可以根本用不着硼着这个地方的StackCookie。千万不要认为这种现象会消失。

我这里还有另外一个例子,在这个例子当中,它们俩的基本代码是差不多的,只不过我在这个地方用了一个安全函数,所以在这里就不会出现缓冲区溢出问题,它不会让写进去的程度超过。但是它犯了另外一个错误,Printf,在这里因为错误的使用了Printf,导致了一个格式串的问题,我个人认为这是最有意思的,它有意思是在哪里呢?刚才我们看了可以饶过验证。

接下来我要演示的问题,我们可以看到密码,因为这个程序不再有我们刚才说的缓冲区溢出问题了,我已经限制了最多只拷贝这么长(图),所以无论如何它都不可能再发生溢出了。但是由于它存在格式串的问题,我输入的是百分号X,但打印出来的并不是百分号X。这个数字是什么呢?这是当前堆栈里面的数据,具体格式串漏洞是怎么回事以及它的原理,如果大家有兴趣的可以去找一些资料来看。我这里只是给大家简单展示一下它的现象。

我们回过头来看这个代码(图),Password是作为一个局部变量放在这里。在我们输入一个格式串的时候,会作为第一个参数处理。我们看到内存当中的这一串数字(图),我们把它复制下来。这些都是十六进制的数据,我们现在把它还原成字符,大家看到是这样几个(图),后四个字母,7465,现在我们反过来,改成6574。现在我们就得到了一个完整的密码。这个例子当中我们就是用格式串问题去窃取内存当中的数据。其实格式化漏洞在Windows系统上非常罕见,因为Windows程序员可能不太喜欢用Pranff函数。但是也有例外,如果大家用Windows2000就会发现(图)。类似这样的问题还出现过在国外非常有名的邮件系统里,当时我们发现的时候就觉得非常有意思,因为我们今天展示的是我自己写的一个小程序,我把密码放在局部变量当中,那是一个邮件系统,它的程序处理流程的时候是这样,你登录,用户名、密码输入进去,然后把密码从它的数据库里面取出来进行比较,因为设计者可能没有考虑很多的安全,这个密码是能保存在数据库里面,它能放在内存里进行比较。如果你输入的我们刚才说的格式串问题,就可以用刚才的方法直接把前面拿出来比较过的这些残存在内存中的密码看到,几乎是利用这个方法可以非常稳定、可靠地拿到任何用户的登录帐号。

这些问题虽然看上去很有意思或者觉得匪夷所思,真的会犯这种错误吗?会有。DIR真的能溢出几乎不可能造成什么安全风险。我们可以在这个地方看一下(图):“找不到文件”。在处理一些特殊情况的时候,和一些为了兼容老的系统所保留下来的代码之间,在它们之间的配合上,在处理这种特殊情况的时候没有处理好,所以会引发一个堆栈溢出。

我们来看一下情况是怎么样的(图)。现在我敲完之后大家看到cmd不见了,为什么?崩溃了。这个问题是我的一个同事在2003年初的时候发现的,因为的确这个东西虽然有趣但它不能说是一个安全漏洞。现在再用这两个神奇的汉字,现在已经不一样了,因为在内存当中会做一个转换,所以我们必须找一个被转换之后恰好是函数地址的汉字,后来发现它们正好是吻合的,它被转换之后也恰好是我们刚才说的函数。我们现在就用它来锁屏试试看。

接下来我们来看今天的最后一个演示。我们还回到刚才的MRINFOR,我们没办法把一串代码交给它。实际上现在已经有技术完全解决这个问题。大家看到的这一串奇怪的字母(图),它们就是由字母编写的代码,它们既是字母,也是代码,本身它们完全可以被CPU执行,执行的结果就是进行计算器程序。同时,我又找了一个可以表示为字母的跳转地址。这四个字母恰好在我当前的平台上是一个可用的跳转地址,所以它们俩结合在一起就是长长的字符串,同时这个字符串可以执行一个功能,就是运行计算器。

刚才是我做的一些科普和基于技术的娱乐和八卦,希望能够引起大家对于这方面相关知识的兴趣,也算是给明天吴石和禇诚云的演讲做一个引子吧,谢谢大家!

相关日志

发表评论