zoukankan      html  css  js  c++  java
  • 堆栈溢出检测机制

    堆栈溢出问题总结

    栈溢出所带来的问题往往十分隐蔽,有时很难复现问题,问题出现的现象可能也不一样,导致问题排查十分困难,遇到一些莫名其妙的问题时,我们会倾向于怀疑堆栈溢出,但是却又不能准确地找出问题的根源。

    问题现象

    最近遇到了两个死机问题,问题排查也比较困难

    • 长时间运行死机:

      能够定位问题的信息有死机时候的内核打印crashinfo以及coredump,crashinfo显示有有两种死机原因:一个是由于发生SP Alignment exception异常导致系统崩溃,另一个是unhandled level 1 translation fault (11) at 0x7f8d0347, esr 0x92000005导致系统死机。
      coredump显示是死在其他组提供的so库内部函数,但是coredump的函数栈帧被破坏,无法显示完整的调用栈帧链,查看当前函数的局部变量和函数入参,发现局部变量有被破坏的痕迹。
      上述现象让我们怀疑是堆栈溢出,导致局部变量和栈帧被破坏从而出现死机。
    • 更换库后启动时死机

      根据coredump显示死机位置在mod算法内部函数的memset函数,且函数调用栈显示不完整。

      查看代码,memset的变量为局部变量,且该局部变量的结构体大小较大,怀疑是栈大小不足导致栈溢出

    上述第一个问题是由于字符串拷贝导致堆栈溢出,第二个问题是局部变量太大导致,这些堆栈溢出问题往往不易排查。如果在代码中加入栈溢出检测机制,在运行时大部分的栈溢出就可以第一时间被发现,不会让问题潜伏。

    栈溢出保护机制

    gcc提供了栈保护机制stack-protector,开启了栈保护机制后,可检测运行时栈溢出,不过该选项并不是万能的,不是所有的栈溢出都能被检测到。我们平时还是需要注意不要使用体积较大的局部变量,结构体参数尽量使用指针传递,数组拷贝检查溢出,字符串拷贝检查字符串是否有''结尾,尽量使用strncpy等较为安全的拷贝函数等等来避免堆栈溢出问题。

    • stack-protector:保护函数中通过alloca()分配缓存以及存在大于8字节的缓存。缺点是保护能力有限。
    • stack-protector-all:保护所有函数的栈。缺点是增加很多额外栈空间,增加程序体积。
    • stack-protector-strong:在stack-protector基础上,增加本地数组、指向本地帧栈地址空间保护。
    • stack-protector-explicit:在stack-protector基础上,增加程序中显式属性"stack_protect"空间。

    stack-protector测试

        #include <stdio.h>   
        int main()  
        {
            char name[10] = {0};
            strcpy(name, "stack overflowooooooooooooooooooo");
            printf("%s", name);
            return 0;
        }
    

    上述代码在执行时,如果不加stack-protector选项,程序能正常执行完成,加了stack-protector-all 选项后,执行会报错

    *** stack smashing detected ***: <unknown> terminated<br>
    stackoverfloooooooooooooooooooooooooooooooooooooooooooooooooooAborted
    

    分析加和不加编译选项的反汇编结果

    不加栈保护选项:

    0000000000400590 <main>:
      400590:       a9be7bfd        stp     x29, x30, [sp, #-32]! //sp-32位置开始依次存放x29(sp) x30(pc),保存caller函数的返回地址和sp指针,并将sp = sp-32(分配栈空间)
      400594:       910003fd        mov     x29, sp               //保存当前栈顶sp到x29寄存器
      400598:       f9000bbf        str     xzr, [x29, #16]
      40059c:       790033bf        strh    wzr, [x29, #24]
      4005a0:       910043a2        add     x2, x29, #0x10
      4005a4:       90000000        adrp    x0, 400000 <_init-0x3c8>
      4005a8:       911a2001        add     x1, x0, #0x688
      4005ac:       aa0203e0        mov     x0, x2
      4005b0:       a9400c22        ldp     x2, x3, [x1]
      4005b4:       a9000c02        stp     x2, x3, [x0]
      4005b8:       a9410c22        ldp     x2, x3, [x1, #16]
      4005bc:       a9010c02        stp     x2, x3, [x0, #16]
      4005c0:       f9401022        ldr     x2, [x1, #32]
      4005c4:       f9001002        str     x2, [x0, #32]
      4005c8:       b9402821        ldr     w1, [x1, #40]
      4005cc:       b9002801        str     w1, [x0, #40]
      4005d0:       910043a1        add     x1, x29, #0x10
      4005d4:       90000000        adrp    x0, 400000 <_init-0x3c8>
      4005d8:       911ae000        add     x0, x0, #0x6b8
      4005dc:       97ffff95        bl      400430 <printf@plt>
      4005e0:       52800000        mov     w0, #0x0                        // #0
      4005e4:       a8c27bfd        ldp     x29, x30, [sp], #32
      4005e8:       d65f03c0        ret
      4005ec:       00000000        .inst   0x00000000 ; undefined
    

    加了栈保护选项的反汇编结果

    0000000000400670 <main>:
      400670:       a9bd7bfd        stp     x29, x30, [sp, #-48]!  //sp-32位置开始依次存放x29(sp) x30(pc),保存caller函数的返回地址和sp指针,并将sp = sp-48(分配栈空间)
      400674:       910003fd        mov     x29, sp                //保存当前栈顶sp到x29寄存器
      //diff:增加的部分
      400678:       90000080        adrp    x0, 410000 <__FRAME_END__+0xf834> //获取保护数所在的页的基址,装入寄存器x0
      40067c:       9137c000        add     x0, x0, #0xdf0   //将x0+0xdf0,获得保护数的地址,偏移量为0xdf0
      400680:       f9400001        ldr     x1, [x0]         //将x0指向的保护数存入x1
      400684:       f90017a1        str     x1, [x29, #40]   //将保护数放入sp+40的位置,该位置就是返回地址的前一个字节
      400688:       d2800001        mov     x1, #0x0                        // #0
    
      40068c:       f9000fbf        str     xzr, [x29, #24]
      400690:       790043bf        strh    wzr, [x29, #32]
      400694:       910063a2        add     x2, x29, #0x18
      400698:       90000000        adrp    x0, 400000 <_init-0x490>
      40069c:       911e6001        add     x1, x0, #0x798
      4006a0:       aa0203e0        mov     x0, x2
      4006a4:       a9400c22        ldp     x2, x3, [x1]
      4006a8:       a9000c02        stp     x2, x3, [x0]
      4006ac:       a9410c22        ldp     x2, x3, [x1, #16]
      4006b0:       a9010c02        stp     x2, x3, [x0, #16]
      4006b4:       f9401022        ldr     x2, [x1, #32]
      4006b8:       f9001002        str     x2, [x0, #32]
      4006bc:       b9402821        ldr     w1, [x1, #40]
      4006c0:       b9002801        str     w1, [x0, #40]
      4006c4:       910063a1        add     x1, x29, #0x18
      4006c8:       90000000        adrp    x0, 400000 <_init-0x490>
      4006cc:       911f2000        add     x0, x0, #0x7c8
      4006d0:       97ffff90        bl      400510 <printf@plt>
      4006d4:       52800000        mov     w0, #0x0                        // #0
      //增加的部分
      4006d8:       90000081        adrp    x1, 410000 <__FRAME_END__+0xf834> //找到保护数的所在页的基址
      4006dc:       9137c021        add     x1, x1, #0xdf0                    //获取保护数的地址,存入x1
      4006e0:       f94017a2        ldr     x2, [x29, #40]                    //取出sp+40位置上的值
      4006e4:       f9400021        ldr     x1, [x1]                          //保护数放入x1
      4006e8:       ca010041        eor     x1, x2, x1                        //将 取出的值x2和x1异或,将结果存入x1
      4006ec:       f100003f        cmp     x1, #0x0                          //检查x1是否为0--即检查堆栈上的保护数是否被篡改
      4006f0:       54000040        b.eq    4006f8 <main+0x88>  // b.none     //相等则正常返回返回
      4006f4:       97ffff7b        bl      4004e0 <__stack_chk_fail@plt>     //不等则说明堆栈有溢出,跳转执行__stack_chk_fail,进程退出
    
      4006f8:       a8c37bfd        ldp     x29, x30, [sp], #48
      4006fc:       d65f03c0        ret
    
    

    通过对比加了堆栈保护选项和没加保护选项的汇编结果,可以看出在函数的开头和结尾处分别多了几条汇编语句,上述汇编结果中对于多出来的汇编语句进行了标注和注释,通过这几句汇编代码就在函数栈框中插入了一个 Canary,并实现了通过这个 canary 来检测函数栈是否被破坏。

    函数栈的局部变量布局

    我们来看下下面C代码的输出结果

    int main() 
    {
       int i = 0;
       char name[10] = {0};
       i = 11;
       strcpy(name, "stack over");
       printf("%s %p, %p", name,&i, name);
       return 0;
    }
    

    不加堆栈保护编译选项:stack over 0x7fc80b9d9c, 0x7fc80b9d90

    加了堆栈保护编译选项:stack over 0x7ff14e66c4, 0x7ff14e66c8


    可以看出,加了编译保护选项后影响函数内的局部变量布局。堆栈的增长方向是高地址->低地址,不加编译选项的时候,变量i的地址大于变量name的地址,说明变量i在name上方;加了编译选项后,变量i变成了在name的下方。这样的内存布局在一定程度上可以减轻堆栈溢出带来的风险,因为有时候局部数组的溢出长度短的话,并不一定会触发堆栈检测,但是局部变量有可能被数组溢出篡改值,这会导致程序存在一定风险。

    总结

    建议在开发过程中增加堆栈溢出保护编译选项-fstack-protector-all,虽然会稍稍增加程序体积,但是带来的收益确是很客观的,很大一部分栈溢出问题就会被探测到,通过结合coredump的函数栈帧信息可以定位发生溢出的函数,这样可以大大缩小问题的范围。

  • 相关阅读:
    「PHP」使用 Homestead 作为 thinkphp5 的开发环境
    「PHP」Homestead 安装 swoole
    「PHP」Homestead
    存储过程
    Windows不能用鼠标双击运行jar文件怎么办?
    spring事务管理
    xml页面开头报错Multiple annotations found at this line
    修行
    jsp页面get和post不同导致的乱码问题
    VC执行Cmd命令,并获取结果
  • 原文地址:https://www.cnblogs.com/qinghaowusu/p/14522279.html
Copyright © 2011-2022 走看看