堆棧溢出檢測機制


堆棧溢出問題總結

棧溢出所帶來的問題往往十分隱蔽,有時很難復現問題,問題出現的現象可能也不一樣,導致問題排查十分困難,遇到一些莫名其妙的問題時,我們會傾向於懷疑堆棧溢出,但是卻又不能准確地找出問題的根源。

問題現象

最近遇到了兩個死機問題,問題排查也比較困難

  • 長時間運行死機:

    能夠定位問題的信息有死機時候的內核打印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,開啟了棧保護機制后,可檢測運行時棧溢出,不過該選項並不是萬能的,不是所有的棧溢出都能被檢測到。我們平時還是需要注意不要使用體積較大的局部變量,結構體參數盡量使用指針傳遞,數組拷貝檢查溢出,字符串拷貝檢查字符串是否有'\0'結尾,盡量使用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的函數棧幀信息可以定位發生溢出的函數,這樣可以大大縮小問題的范圍。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM