概述
缓冲区溢出问题日益严重,根据 CERT/CC的统计,在2005 年公布的漏洞中,有 2/3 属于缓冲区溢出漏洞。现有缓冲区溢出静态检测方法包括:模式匹配,词法分析,约束求解等。 FlawFinder 和 ITS4 采用模式匹配方法。 SPLINT使用轻量级数据流分析验证程序中的错误,它要求程序员在源码中添加注释。Bugscam是一个用于检测可执行文件中缓冲区溢出问题的工具,以 IDA PRO 支持的 IDC 脚本形式实现。该工具采用模式匹配方法,定位不安全的 API 函数,并通过回溯寄存器的值来判定 Buffer 的大小和类型(堆或栈)。其分析没有涉及控制流和数据流。 Bugscam 的另一个缺点是没有通用性,只能对 x86 进行分析,无法用于对其他硬件体系结构的汇编。在很多情况下,类似 strcpy, memcpy 的函数调用在汇编层消失,此时采用硬编码完成相应函数的功能。例如 MS03-020, MS04-011, MS06-055 等漏洞都属于该类型,上述缓冲区溢出漏洞都发生在写内存的循环中1。
缓冲区溢出原理分析我们知道,计算机程序是由函数或过程组成的。 当程序运行时, 过程 /函数调用时需保存的信息用堆栈中一块连续的存储区来管理,这块存储区称为活动记录或帧(它在程序执行时在进程的栈中分配)。
由此可知当在程序中对活动记录中的局部变量进行赋值时,若不对缓冲区的边界进行检测,那么对局部缓冲区的赋值操作就可能越过缓冲区边界而覆盖活动记录中的 EBP 和返回地址,这样就造成了缓冲区溢出漏洞,一般后果就会造成程序异常终止,严重的就会被一些恶意的攻击者利用其漏洞, 从而使程序的执行路径改变,转至攻击者精心设计的代码, 达到攻击者的目的,给系统造成非常严重的后果。
缓冲区溢出漏洞这方面的研究,国外的一些大学和公司在这方面做了大量的工作,但效果都不是很理想。比如:一些静态检测工具和静态检测方法, 比如佛罗理达科技学院 TerryBruce Gillette 提出的二进制代码检测技术和由加利福尼亚大学的 Eric Haugh 和 Matt Bishop 提出的 C 程序缓冲区漏洞测试方法; 以及动态检测工具动态检测方法,比如由哥伦比亚大学的 Stelios Sidiroglou 和 Angelos D.Keromytis 提出的利用执行处理从缓冲区溢出漏洞中恢复的方法。 这其中针对缓冲区溢出的动态检测工具和方法,是通过对 C 编译器进行改进之后来达到检测的目的的,这样源程序经过编译之后会增加可执行程序的大小,同时对程序的运行效率有一定的影响; 而针对 C源程序的静态检测工具和方法需要程序员在源程序中添加注释来辅助完成对缓冲区溢出漏洞的检测,这样就会给测试人员造成了诸多不便,同时这种检测方法也是不完全的,尤其是针对大型的源程序而言,很容易造成遗漏2。
溢出检测模型的实现静态检测模型针对缓冲区溢出原理,这里提出一种对缓冲区溢出漏洞进行静态检测的模型。 利用该模型通过对 C源程序的扫描,检测出其中对缓冲区的没有进行边界检测而造成缓冲区越界的操作。
模型实现利用编译技术中的词法分析和语法分析, 使用flex 生成词法分析器,使用 yacc 生成语法分析器。 在系统中使用两张 hash 表来保存在扫描过程中检测到的函数和变量,每个语法元素的综合属性和继承属性在语法分析过程中保存至记录的不同属性域中。 对于标准 C 函数库中需要检测的函数,在系统初始化时将它们插入函数 hash 表中。 整个实现过程由以下步骤组成:
(1)利用 flex 生成词法分析程序,识别出源程序中标记。 比如标准 C 语言保留的关键字、变量名、函数名等等标准 C 的语法单元。 然后将这些识别出的语法单元作为语法分析程序的输入;
(2)由 yacc 生成语法分析程序,语法分析程序利用词法分析的输出作为输入,根据标准 C 的语法识别出变量的声明、定义以及对变量的操作;函数的声明、定义以及函数调用;并在基本语句(比如条件语句、赋值语句等等)的语法中嵌入相应的动作;
(3)根据第二步中识别出的不同对象,如果识别出的是变量的定义单元,就在模型中生成一个该变量的记录并将该变量记录插入变量记录 hash 表中;如果是函数的定义单元(记录函数的定义信息), 就在模块中相应的生成一个函数定义记录并将其加入函数定义记录 hash 表中。 变量的属性域中有专门记录变量属性值的域,通过这些属性反应变量的状态;
(4)对于一般的 C 语句,利用语法分析程序在对语句分析的过程中模拟其语句动作。这样就可以反应变量和缓冲区的经过语句操作后的状态, 从而检测出源程序中对缓冲区越界的操作;
(5)对于函数的定义,需要分析其中语句的语义,根据不同的语句编写不同的语义动作函数来实现对函数定义中语句的分析;
(6)对于每个识别的变量,在变量的属性记录域中用一个域来记录变量的值,如果是缓冲区,根据其初始长度动态分配内存块用来记录缓冲区的每个值。
(7)对于函数的嵌套调用,通过生成函数的嵌套调用树(程序执行的顺序性), 在扫描时, 通过后序遍历嵌套函数调用树,根据函数定义时的形参的约定和函数调用时实参的具体值以及函数体内对部局部变量的操作的分析来得到结果。
本检测模型可以检测 C 源程序中的栈和堆溢出2。
基于中间汇编的检测模型这里提出的检测模型需要将汇编格式的可执行代码翻译成中间汇编形式,并对翻译后的代码进行检测。采用中间汇编形式的优点如下: (1)算法对硬件平台透明,由于检测算法建立在中间汇编形式上,因此针对各种不同硬件平台,只要完成语言翻译,无须重写算法; (2)代码可阅读性好,采用RISC 设计和 3-操作数体系,精简了部分指令; (3)易于检测,例如,为了方便溢出的检测,对内存读、写指令进行区分1。
其实现步骤如下:
1.漏洞检测模型
将可执行文件输入 IDA PRO中, IDA PRO 识别二进制文件编译的机器语言,将其反汇编成对应的 X86, SPARC 或 Alpha 格式。中间汇编翻译模块将反汇编代码转换成统一形式的中间汇编语言。溢出检测模块对不安全函数和写内存循环进行定位,并对这些位置进行相应的检测,产生分析结果报表1。
2.中间汇编语言的设计
由于不同 CPU架构采用不同汇编指令集, 因此需要采用中间汇编语言,否则就要针对不同硬件体系结构编写不同检测程序,不便于统一处理1。
3.设计原则与 X86 汇编语言的翻译
中间汇编语言设计的主要原则如下: (1)精简指令集的设计思想; (2)足够多的寄存器数量以适应各种硬件体系结构;(3)尽量简单的寻址方式,去除不利于阅读的复杂寻址方式;(4)3-操作数的指令格式1。
4.寻址方式的设计与翻译
保留以下 4 种寻址方式: (1)立即寻址,操作数直接存放在指令中; (2)寄存器寻址,操作数存放在寄存器中; (3)直接寻址,操作数的有效地址直接存放在指令中; (4)寄存器间接寻址,操作数的有效地址存放在寄存器中。取消基址变址寻址,中间运算结果采用临时寄存器来存储1。
5.指令系统的设计
下文从指令格式、参数传递方式、访存指令设计等方面对指令系统进行介绍。
(1)指令格式。由于 X86 汇编语言指令集是 CISC,不利于阅读,因此这里统一指令格式为“3-操作数”表示方式,即(op, arg1, arg2, result),其中,arg1, arg2 表示指令参数;result存储指令运算结果。当 op 为一元或零元运算符(如无条件转移)时,指令格式表示为(op, arg1, -, result)或(op, -, -, result)。
(2)寄存器传递参数。仅采用输入/输出寄存器传递函数调用的参数并返回函数值,此时存在函数参数识别问题。
(3)明晰的访存指令。 X86 汇编在访存指令上,对读/写的区分不够明晰。例如, mov 指令可以表示读内存或写内存。而循环读内存不会造成缓冲区溢出,循环写内存则可能造成缓冲区溢出。ldm(load memory)指令表示读内存, stm(store memory)指令表示写内存。
(4)精简部分指令。取消冗余指令,例如 INC, DEC 可以用 ADD, SUB 指令代替。
(5)删除重复的转移指令。 无条件跳转指令,采用 branchaddr, -, -表示。条件跳转指令采用 br_cc addr, [%fxx], -格式表示,是否跳转根据%fxx 寄存器的值确定。
(6)清晰的循环拷贝指令。 4.2.1 节给出的循环发现算法无法检测到类似 rep movsd 内存拷贝指令的指令。因此,为了便于统一处理,需要对 rep movsd 做适当翻译,使其在控制流图上形成循环1。
6.缓冲区溢出漏洞的发现
根据字符串拷贝操作的不同,可以将缓冲区溢出漏洞分为以下 2 种情况: (1)调用不安全的函数(例如调用 strcpy 函数将源字符串拷贝到目的缓冲区)导致出错; (2)循环写内存出错,指程序对目的缓冲区进行循环拷贝时,超出缓冲区边界导致溢出1。
7.变量发现算法
本文直接利用 IDA PRO 的变量发现算法。该算法思想如下:内存的分布在编译时已分配好,对程序变量的直接访问通过绝对地址或栈帧指针的相对偏移来实现。通过分析代码对内存的访问,找出其绝对或相对地址的偏移,从而确定程序变量的起始地址和大小1。
采用上述漏洞检测模型,检测微软 MS03-020, MS04-011,MS06-055 等漏洞,能发现其缓冲区溢出问题。