xfocus logo xfocus title
首页 焦点原创 安全文摘 安全工具 安全漏洞 焦点项目 焦点论坛 关于我们
添加文章 English Version

介绍Propolice怎样保护stack-smashing的攻击


创建时间:2001-03-02
文章属性:整理
文章提交:xundi (xundi_at_xfocus.org)

介绍Propolice怎样保护stack-smashing的攻击

整理:XUNDI
            
        低级    ...                    ...        
               |-----------------------------------|  
               | 如果你觉得这中文乱七八糟,JMP 0xAA|  
               |-----------------------------------|  
               | ....................              |
               |-----------------------------------|
    0xAA ------>   |请看文章最后提供原文,        |
               |如果你觉得还是看不懂原文,JMP 0xBB |
               |-----------------------------------|    
               |............                       |  
               |-----------------------------------|  
    0xBB------->   | 请去请教backend,warning3,scz等他们|
               |如果你请教以后还是不懂,  JMP 0xCC |
               |-----------------------------------|  
               |..............               |
               |-----------------------------------|    
    0xCC------->   |JMP 0x黄河                         |  
               |-----------------------------------|  
        高级    ...            ...
        

stack-smashing是一种引起设备停止并且导致允许攻击者侵入系统的攻击方法,
一般它被使用在叫缓冲溢出的应用程序漏洞上。stack-smashing攻击是在UNIX
安全新闻报道上最常见最普通的攻击方式。在1999年,IIS4.0的那个由EEYE发现
的缓冲溢出,使将近1500000系统遭受此漏洞的侵害。

多数应用程序使用C写的时候使用缓冲区(buffer),这个缓冲区是一个保存了一些
同样数据类型的数据,通常是字符数组,在堆栈里临时保存这些字符串操作的临时
值。stack-smashing攻击通常是提供一个超过实际BUFFER大小的字节数到缓冲区,
这就导致了破坏了BUFFER中的内容,这些内容可能就是调用函数的返回地址和函数
指针。


这文章提供了系统的解决缓冲溢出攻击的方法,这文章提供的一种叫ProPolice
保护方法能自动在应用程序编译的时候插入保护代码.它的主要特征是在对数字
的操作下具有比较少的效率支出,保护多种stack-smashing攻击,支持多种处理器。

下面讲讲攻击说明和它们的类别

缓冲溢出漏洞通常在应用程序需要读取外部信息如字符串时,接收缓冲区
相对与实际的输入字符串小而且应用程序又不正确检查这些操作时产生。
缓冲区在实时运行时分配空间到堆栈,这时候堆栈中保存了一些可执行函数
的信息:如本地变量,参数变量,和返回地址。而超长的字符串改动了这些
信息,如攻击者把一系列机器语言命令作为字符串插入到堆栈,并使这些字符
串覆盖在堆栈中的返回地址为这些命令的地址(我们通常所说的SHELLCODE),
这样到函数返回的时候就执行了这些代码。通常这些方法的最终目标是获得有
一定权利的SHELL。

图1描述了在一函数调用的时候典型的堆栈结构,在图的在最底下是堆栈指针在
堆栈的顶,C语言程序在堆栈中从堆栈顶(就是内存低端)使用下面的顺序来排列:
本地变量,前栈帧指针 (previous frame pointer--前栈帧指针),这对被调函
数而言不可见,也就是被压栈保存的%ebp,返回地址,和被调函数的入口参数。
其中下面所示的帧栈指针(frame pointer)定位了本帧以及前栈帧的帧指针存储在
调用者的栈帧中。


        高址    ...                    ...        
               |-----------------------------------|  
               | 传递给函数的参数                  |   向上是字符
               |-----------------------------------|   串增长方向
               | 函数的返回地址 (RET)              |
               |-----------------------------------|
    帧栈指针-->    | 保存的前栈帧指针           |
    (frame pointer)|(previous frame pointer)           |
               |-----------------------------------|    
               | 局部变量                          |   向下是堆栈
               |-----------------------------------|   增加方向
               | buffer                       |
               |-----------------------------------|  
    堆栈指针-->    |-----------------------------------|  
            ...                    ...
        低址

            图 1 -- 堆栈结构



图2的函数foo是一个有漏洞的函数,它产生的堆栈结构就如图1所示。这函数读
取了环境变量'HOME'的内容到大小为128字节的'buffer'中,但是由于strcpy
函数没有正确检查输入的大小,它就能拷贝超过128字节的数据到"buffer"中,
想象下如果"HOME"变量有这样的字符串:128字节的41,1, 1, 1, 1, 2, 2, 2,
2, 3, 3, 3, 3。这就分配了128个A字符,和0x01010101, 0x02020202, 和
0x03030303分别到"buffer","lvar"和前栈帧指针(previous frame pointer)
及返回地址(ret)中.(我们假定使用了默认的32位变量和C语言符号.)当foo函数
完成它的操作并返回调用foo函数的函数返回地址,也就是覆盖了刚才堆栈中的
返回地址,而变为了0x03030303地址,这样如果有恶意代码存放在上面这个
0x03030303地址中的话,这恶意代码将以此函数原来的运行级别运行。


            void foo()
            {
                 long *lvar;
                 char buffer[128];
                 ........
                     strcpy (buffer,getenv("HOME"));
                 ........
            }
            图 2:一个简单的有缓冲溢出的示例


我们下面介绍下一种典型的攻击方式,攻击者怎样获得应用程序的控制。在第一
类中攻击的目标在堆栈中,下面列出了存储在堆栈中的数据并描述攻击模式的使用:

--返回地址
  通过改变返回地址的值到恶意代码地址是最流行的攻击方式。
  
--本地变量

--入口参数变量

这个函数的变量是另一个需要控制的目标,把入口参数或者本地变量指定为攻击
代码也是攻击的一种方式,在这种情况下,漏洞一般是通过检查源程序发现的。

第三种函数指针重定向(Function Pointers)
   “void (* foo)()”声明了一个返回值为void函数指针的变量foo。函数指针
可以用来定位任何地址空间,所以攻击者只需在任何空间内的函数指针附近找到
一个能够溢出的缓冲区,然后溢出这个缓冲区来改变函数指针。在某一时刻,当
程序通过函数指针调用函数时,程序的流程就按攻击者的意图实现了!它的一个攻
击范例就是在Linux系统下的superprobe程序。
  

改变指针变量而不是指向函数的指针也是有可能获得应用程序的控制,在这种
情况下,如图2函数foo,指针变量'lvar'可以改变它指向返回地址的地址。如果
在'strcpy'声明(statement)后有一个声明(statement)通过'lvar'修改值的指向,
就存在了改变其值到返回地址的可能。

--前栈帧指针(previous frame pointer)  

对于对前栈帧指针(previous frame pointer)的攻击也可以获得对应用程序的
控制,前栈帧指针(previous frame pointer)和返回地址的联系是基于下面的
条件下:
    --返回地址的位置由栈帧指针(the frame pointer)决定
    --在函数返回时候栈帧指针(the frame pointer)指派了前栈帧指针
    (previous frame pointer)的值。
这样一个攻击者可以建立一个含有指向攻击代码的返回地址的伪造帧。他也可以
改变前栈帧指针(previous frame pointer)的值而指向伪造帧地址。当函数返回
到调用函数时,栈帧指针(frame pointer)会根据上面所述的第二个条件而指向
攻击者伪造的帧。也就是说攻击者改变了返回地址。



一些目前存在的相关技术


目前对于缓冲问题的保护已经有不少方案(project)发表,一种方法是下面参考中的
[10]:从源程序中检查排除有漏洞代码并从发现的问题中帮助应用程序变的更安全。
如在参考[10]中所示地址中有安全审核工具来自动检查源码,它主要是检查和排除
一些危险的函数:如strcpy,gets等等,但工具的受限之处是它不检查指针变量的边界。

另一个方案是提供对程序代码潜在的漏洞进行保护的方式,我们可以根据他们怎样
保护动机分为四个类别:

    1,避免数组的溢出问题
    参考中的[7]是对数组边界进行检查方法和参考中的[6]是对内存访问进行
    检查的方法,这两种方法防止了对分配给数组边界之外访问。因此,这些
    方法是很安全的一种方法,但是,这种保护的开销比不保护的代码昂贵的很,
    经过这样优化的代码相对于普通的代码速度慢。

    2,禁止攻击代码的执行
    ``Solar Designer'' 开发的LINUX补丁就是使堆栈区域的不可执行性,因此
    存储在堆栈中的攻击代码就不能执行了。Solar Designer提供的kernel
    security patch中是通过减少代码段的长度,来区分堆栈段和代码段的,
    由于堆栈段的增长方向是从高地址到低地址的,因此堆栈段和代码段地址
    范围通常是不会重叠的。这样可以有效的避免在堆栈中安排溢出代码,并
    返回到堆栈中执行的攻击手段。这种方法的好处就是没有额外的开销,不
    需要应用程序源程序。

    这种方法的缺点就是依赖特别的操作系统和处理器。而且特别是把堆栈段
    标识为不可执行,它不能保护每个区域不可执行,通过把攻击代码放置到
    别的某处,仍旧有可能让攻击者获得对程序的控制,如把攻击代码静态的
    插入到已经分配的缓冲区并改变起返回地址,让其指向攻击代码来实现。

    参考[5]中-Janus通过限制程序访问操作系统的方法设计了应用程序的安全
    环境。它保护了一些特权操作,如对于攻击代码不在堆栈里的情况下,但
    在其他静态区域里怎样保护在特权模式下执行/bin/sh等。它也是依靠操作
    系统的某些特点,如必须提供一些如strace的调试工具(注:对于strace的
    用法你可以使用man,但在THC的LKM文章里也解释了不少用法,具体文章可以
    看http://focus.silversand.net里面的文章)。

    3,保护执行控制不传递到已经被攻击的代码

    Snarskii已经开发了一个关于FreeBSD的补丁来实现堆栈完整性检查来探测
    缓冲溢出,见参考[9]。这种方法是采用libc嵌入的方法,不具可移植性。

    StackGuard--参考[2], StackShield--参考[11], 和libsafe参考[1]提供
    可移植的,通用的保护方式.Libsafe提供的方法是基于在软件中层中截获
    所有一些有漏洞的库函数调用,如gets,strcpy等等。

    StackGuard是探测是否有攻击者对返回地址的攻击,它将一个"canary"值
    (一个单字)放到返回地址的前面,如果当函数返回时,发现这个canary的
    值被改变了,就证明可能有人正在试图进行缓冲区溢出攻击,程序会立刻
    响应,发送一则入侵警告消息给syslogd,然后停止工作。

    StackShield是做法是创建一个特别的堆栈--(不能缓冲溢出的地方)用来储存
    函数返回地址的一份拷贝。它在受保护的函数的开头和结尾分别增加一段代码,
    开头处的代码用来将函数返回地址拷贝到一个特殊的表中,而结尾处的代码用
    来将返回地址从表中拷贝回堆栈。因此函数执行流程不会改变,将总是正确返
    回到主调函数中。由于没有比较堆栈中的返回地址与保存的是否相同,因此并
    不能得知是否发生了堆栈溢出。

    但这三种方法都有其破坏的地方,有关资料可以参考warning3的'绕过StackGuard
    和StackShield保护',至于Libsafe,就象上面描述的,光光对函数的检查是远
    远不能很好保护的。

    在下面的图7中会对Propolice和这些方法做一次比较。


堆栈保护模式


    按照上面所说的,针对stack-smashing攻击我们要有四个区域需要保护:
    本地入口参数,返回地址,前栈帧指针(previous frame pointer)和本地
    变量。

    下面介绍的guard变量是为了防止改变前三个区域,这个技术源自StackGuard
    方案--参考[2]的设计,它设计在返回地址之后立即插入guard变量,在
    ProPolice里采用的方法与Stackguard不同的是guard的位置和函数指针的
    保护,ProPolice中guard将被插入到next to the previous frame pointer
    并且居于攻击者能开始破坏堆栈之前的数组。

    这里将使用源代码的方式简明的阐述,提供一个函数的源代码,一个预处理
    步骤会把下面的代码片段插入到适当的位置:本地变量声明部分,入口点,
    和退出点,如:

    --本地变量声明部分(declaration part of local variables )

      volatile int guard;

    --入口点(the entry point )

      guard = guard_value;

    --退出点(the exit point )

      if (guard != guard_value) {
            /* output error log */
            /* halt execution */
      }


    修改后的源代码程序如图3所示,注意guard变量的位置是在字符串buffer
    之前,入口点存储了一个攻击者不知道的值,并在退出的时候校验它,如果
    不相等就会报警到系统并记录信息到数据库,还会停止代码的执行。

            void foo()
            {
                volatile int guard;
                char buf[128]

                guard=guard_value;
                ................
                if (guard!=guard_value) {
                   /*output error log*/
                   /*halt execution*/
                }        
            }
            图3:增加了保护代码后的程序

    上面简单的解释了用源码转化进行保护,但在事实上的实现比较困难,
    因为函数通常有很多退出点和guard必须定位在堆栈中字符缓冲之前。
    而ProPolice的实现是采用了GCC编译器的intermediate code translator
    (这个我查了不少地方,感觉还是模糊,所以请大家赐教!),根据
    intermediate language的说明:对于每个函数只有一个退出点并且每个
    变量的位置已经确定。

    这里的guard值是一个不能让攻击者知道的值,如果他知道这个值,攻击者
    就可以在buffer和前栈帧指针(previous frame pointer)之间填充此值,就
    导致对guard值检验正确,接着就修改返回地址。

    ProPolice的实现方法是为guard值挑选一个随机数,这个随机数是由应用程序
    初始化时计算而得,而且不能用普通用户发现。这个数字必须是一个不能预测
    的随机数,LINUX有一个随机数发生器,它的实现是采用一个叫/dev/urandom和
    /dev/random的设备,这个设备使用环境噪音来生成数字,所以可以提供一个
    不能预测的随机数。


安全函数模型


    下面是图4介绍的一个安全函数模型,它能采用下面的方式处理堆栈中的使用限制:
    
    --位置(A) 没有数组和指针变量
    --位置(B) 有数组或者包含结构的数组
    --位置(C) 没有数组


            高址    ...                    ...        
               |-----------------------------------|  
               | 传递给函数的参数   (A)            |  
               |-----------------------------------|  
               | 函数的返回地址 (RET)              |
               |-----------------------------------|
    帧栈指针-->    | 保存的前栈帧指针           |
    (frame pointer)|(previous frame pointer)           |
               |-----------------------------------|    
               | guard                             |   向下是堆栈
               |-----------------------------------|   增加方向
               | 数组(B)                      |
               |-----------------------------------|  
               | 本地变量(C)                 |    
    堆栈指针-->    |-----------------------------------|  
            ...                    ...
        低址

            图4:安全帧结构

    这个模型具有下面的特性:
    
    1,当函数返回的时候函数帧之外的内存区域不会被破坏。

       只有位置(B)是攻击者可以开始破坏堆栈的有漏洞的位置,对于超过
       帧函数后的区域(如想破坏返回地址),都将在校验guard值时被检测
       到,如果出现这种破坏,程序的执行将停止。

    2,一个在函数帧之外的指针变量上的攻击将不会成功。

       攻击能成功的话就需要下面的条件:1)攻击者改变了函数指针的值;
       2)他使用函数指针调用一个函数,为了获得这第二个条件,函数指针
       必须是可见的,但我们的假设是说这个信息是超越函数的范围之外的,
       所以,第二个条件不能成立,所以攻击往往失败。

    3,在函数帧中对于指针变量的攻击将不会成功。

       位置(B)是唯一可以遭受stack-smashing攻击的位置,文章说位置(C)
       是安全的位置,我的理解是,位置(B)这个数组假如缓冲溢出,字符增
       长是在堆栈中向高地址的走向,所以是安全的。



指针保护


    下面图5示例的是另一个有漏洞的函数,攻击者可以通过替代函数指针为
    攻击代码的地址而获得对应用程序的控制:
        
            void bar( void (*func1)() )
            {
                void (*func2)();
                char buf[128];
                .........
                strcpy(buf,getenv("HOME"));
                (*func1)();(*func2)();
            }
            图5:会被攻击函数指针方法攻击的示例

    为了保护对函数指针的stack-smashing攻击,我们改变每个变量在堆栈中
    的位置来建立安全的函数模型。

    C语言中没有限制本地变量的顺序,但它不允许改变入口参数的位置,对入
    口参数的限制可以通过建立一个新的本地变量声明,拷贝参数"func1"给这
    新的变量,使用新的本地变量来改变"func1"的参考,图6显示了转化的结果:
    
            
            void bar( void (*tmpfunc1)() )
            {    
                char buf[128];
                void (*func2)();
                void (*func1); func1=tmpfunc1;
                .........
                strcpy(buf,getenv("HOME"));
                (*func1)();(*func2)();
            }
            图5:能保护攻击函数指针方法的示例

一些优化


    下面是讨论了为减少使用guard产生的额外支出效率的优化方法。下面的
    假设介绍知识为了这个优化目的,当碰到下面这些假设的时候,一些
    保护代码可以安全的去掉。

    假设 1
    
    源代码正确的类型声明及遵循类型转换规则。

    如:一个整数变量永远是存储整数值,不能存储字符值。不正确的操作将
    在程序执行的时候出现内存保护错误。

    假设 2

    只有字符数组会引起缓冲溢出

    缓冲溢出出现在对数组进行分配操作的时候没有进行边界检查。分配操作
    通常时候终止符来检查边界问题。多数终止符仅在字符函数中使用,如在
    gets()函数中使用null字符或者换行字符。

    如果除设想1之外碰到设想2,除了函数声明字符缓冲为本地变量或者参数,
    guard的保护就会忽略。这是为了减少运行的开销,就想最开头说的,尤其
    对数字处理更有利,这是因为这些函数通常使用数字数组而且会在短时间
    内多次调用。


局限性
    
    堆栈保护的方法是通过程序转化获得的主要是转化一些有漏洞的函数为
    没有漏洞的函数。但这些转化在某些情况下不是永远有效。

    如果一个结构包含了指针变量和字符数组,指针就得不到保护,因为改变
    结构成员的顺序是被禁止的。

    其他还有对保护指针变量的局限性。当参数声明为变量参数,即函数参数
    数目可变和类型可变。这样的用法指针变量在编译时候就不能被判断,而
    是在执行阶段来判断。


各种技术的比较


    图7显示了各种保护技术的比较:
    ---------------------------------------------------------------------------------
    |Description               |None | ProPolice | libsafe | StackGuard | StackShield |
    ---------------------------------------------------------------------------------
    ---------------------------------------------------------------------------------
    |Protection Effectiveness                            |
    ---------------------------------------------------------------------------------
    |return  address      | NO  |    Yes    |    Yes  |      Yes      |    Yes1    |    
    |pre. frame pointer      | NO  |    Yes    |    Yes  |      No      |    Yes1    |
    |argument          | NO  |    Yes    |    Yes  |      No      |    No    |    
    |local variable          | NO  |    Yes2   |    No   |      No      |    No    |
    |string operation coverage| NO  |    ALL    |Not all |      All      |    All    |
    ---------------------------------------------------------------------------------
    ---------------------------------------------------------------------------------
    |Implementation characteristics                            |
    ---------------------------------------------------------------------------------
    |OS independence      | --  |    Yes    |    No3  |      Maybe4  |    Maybe4    |    
    |Processor independence      | --  |    Yes    |    Yes  |      No      |    No    |
    ---------------------------------------------------------------------------------
    ---------------------------------------------------------------------------------
    |Other characteristics                                    |
    ---------------------------------------------------------------------------------
    |Performance overhead      | none| Very low  |Very low|      Low     |    Low    |    
    |source code needed      | --  |    Yes    |    No   |      Yes      |    Yes    |
    ---------------------------------------------------------------------------------

    1,Protects only the function within a limited depth of function calls
    2,It cannot protect a pointer variable in a structure that contains a vulnerable string
    3,Needs dynamic link library
    4,Intel processor only

            图7:各种技术的特点比较

    protection effectiveness 描述了对堆栈中所保护的区域的覆盖的保护(有点拗口),
    及针对字符函数引起的缓冲溢出的覆盖。保护区域其中标为"No"的意思是有些特别
    的区域不能被保护,如针对于SuperProbe2.11的EXPLOIT程序攻击是从参数变量中攻击
    函数指针变量的指向,因此STACKGUARD就不能很好的阻止攻击而导致获得ROOT SHELL。

    Libsafe不能保护所有字符函数,也不能保护字符操作直接使用指针,如针对于``xterm
    distributed with RedHat5.2''的EXPLOIT程序使用了libtermcap库中的tgetent()函数
    的一个缓冲溢出。

    implementation characteristics 表示了ProPolice进行独立于多个系统和处理器的
    保护机制。


测试评价


    图8显示了攻击的测试结果,其中前三个是通过攻击返回地址获得ROOT SHELL
    而最后一个是攻击指向带函数指针的结构参数获得ROOT SHELL的,从下面的
    表中可以看到,这些被保护后都有效的终止执行。

    虽然没有提供更多更全面的EXPLOIT列表,但足可以校验保护方式工作正常,
    受保护后都回显示"a stack-smashing attack had been detected"的信息,
    并且没有获得ROOT SHELL。

    
    -----------------------------------------------------------------------------
    |   EXPLOIT程序       |        描述              | 攻击结果   | 保护后结果  |
    -----------------------------------------------------------------------------
    |  xlockkmore3.10     |Lock an X windows display | ROOT SHELL | 终止        |
    -----------------------------------------------------------------------------
    |   Elm 2.003          |ELM mail user agent       | ROOT SHELL | 终止        |
    -----------------------------------------------------------------------------
    |   Perl 5.003        |Perl script language      | ROOT SHELL | 终止        |
    -----------------------------------------------------------------------------
    |   SuperProbe 2.11   |Probes video hardware     | ROOT SHELL | 终止        |
    -----------------------------------------------------------------------------

                图8:测试评价


    下面是文章对效率额外支出的评估,时间的关系就没有进行整理。有兴趣朋友就去看原文-参考[12]

参考:

    1    A. Baratloo, N. Singh, and T. Tsai.
        Transparent Run-Time Defense Against Stack Smashing Attacks.
        In Proceedings of the USENIX Annual Technical Conference, June 2000.
        to be appeard.

    2    C. Cowan, C. Pu, D. Maier, H. Hinton, J. Walpole, P. Bakke, A. G. Steve Beattie, P. Wagle, and Q. Zhang.
        StackGuard: Automatic Adaptive Detection and Prevention of Buffer-Overflow Attacks.
        In Proceedings in the 7th USENIX Security Symposium, January 1998.

    3    S. Designer".
        Non-executable user stack.
        http://www.false.com/security/linux/.

    4     "eEye-Digital Security Team".
        Iis4.0 remote exploit.
        http://www.eeye.com/, 1999.

    5     I. Goldberg, D. Wagner, R. Thomas, and E. A. Brewer.
        A secure environment for untrusted helper applications.
        In In Proceedings of the 6th USENIX Security Symposium, 1996.

    6     R. Hastings and B. Joyce.
        Purify: Fast Detection of Memory Leaks and Access Errors.
        In Proceedings of the Winter USENIX Conference. 1992.

    7    R. Jones and P. Kelly.
        Bounds Checking for C.
        http://www-ala.doc.ic.ac.uk/ phjk/BoundsChecking.html, July 1995.

    8    Perlbench.
        http://www.metacard.com/perlbench.html.

    9     A. Snarskii.
        FreeBSD stack integrity patch.
        ftp://ftp.lucky.net/pub/unix/local/libc-letter, 1997.

    10     "The Software Security Group.
        Its4: Open source software security tool.
        http://www.rstcorp.com/its4/.

    11
        "Vendicator".
        Stack shield: A "stack smashing" technique protection tool for linux.
        http://www.angelfire.com/sk/stackshield/.

    12    Hiroaki Etoh and Kunikazu Yoda
        IBM Research Division, Tokyo Research Laboratory,
        1623-14 Shimotsuruma, Yamato, Kanagawa 242-8502, Japan
        {etoh,yoda}@jp.ibm.com
        http://www.trl.ibm.co.jp/projects/security/propolice/

    13    关于缓冲溢出的中文资料请看http://www.nsfocus.com中的资料

          最后语:很奇怪我没有找到Propolice这个project,不知道是不是要EMAIL想作者要,或者
      是我没有找到.:(  有人获得的时候不要忘了MAIL我一份。

         xundi@xfocus.org 2000-08-15
     http://focus.silversand.net
     http://www.xfocus.org