最近又遇到了一個崩潰,棧回溯非常怪異。
/lib/i386-linux-gnu/libc.so.6(gsignal+0x4f) [0xb2b751df] /lib/i386-linux-gnu/libc.so.6(abort+0x175) [0xb2b78825] /lib/i386-linux-gnu/libc.so.6(+0x6b39a) [0xb2bb239a] /lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x45) [0xb2c4b0e5] /lib/i386-linux-gnu/libc.so.6(+0x102eba) [0xb2c49eba] /ramdisk/xxxxxx() [0x8467639] /ramdisk/xxxxxx() [0x849a802] /ramdisk/xxxxxx() [0x84b75da] /ramdisk/xxxxxx(xxxxxxxxxxxx+0x444) [0x84b9224]
其中的xxxxx是公司的模塊和函數,故隱藏,對接下去的分析沒有影響。
一開始,因為沒有接觸過__fortify_fail這個函數,另外加上因為有一部分棧回溯沒有對應的符號,我以為是數組溢出把棧信息破壞了。但實際上想想不對,如果是棧信息被破壞了,不出意外的話,應該是回溯不到某些很有序的函數的,這些函數我沒上。
後來同事無意的一句話,說__fortify_fail是內存檢測,我才百度了一下這個__fortify_fail函數,那麼這個函數是什麼情況下會被調用的呢?
一。gcc編譯選項-fstack-protector和-fstack-protector-all
正是我在前面猜測的錯誤原因,牛人Stack Guard 就想出了保護棧信息的方式,在ebp和ip等信息的地址下面放一個保護數,如果棧溢出,那麼這個8位數會被修改,就會導致函數進入棧溢出錯誤處理函數,也就是導致了上面的棧。
二。比較加選項前後的反匯編代碼
源碼:
#include <stdio.h> int main() { char a; int i; memcpy(&a,"ss",2); printf("1\n"); memcpy(&i,"sssss",4); printf("2\n"); return 0; }
使用gdb調試該程序,首先查看a和i的地址,
(gdb) p &a $1 = 0xbffff69b "\b\364\037\374\267\220\204\004\b" (gdb) p &i $2 = (int *) 0xbffff694
顯然變量a的地址要高,更接近棧頂。可以證明i的溢出並不一定能被檢測到,而a的檢測一定會被檢測到。
看下匯編代碼的對比。
movw $0x7373那句話就是往a裡面拷貝ss,所以整個程序前後的差異在於插入兩段代碼,這兩段的代碼就是用來檢測局部變量。
運行溢出時的棧
#0 0xb7fdd424 in __kernel_vsyscall () #1 0xb7e4f1ef in raise () from /lib/i386-linux-gnu/libc.so.6 #2 0xb7e52835 in abort () from /lib/i386-linux-gnu/libc.so.6 #3 0xb7e8a2fa in ?? () from /lib/i386-linux-gnu/libc.so.6 #4 0xb7f20dd5 in __fortify_fail () from /lib/i386-linux-gnu/libc.so.6 #5 0xb7f20d8a in __stack_chk_fail () from /lib/i386-linux-gnu/libc.so.6 #6 0x08048485 in main ()
與本文最前面的錯誤是一致的
三。走讀代碼修改錯誤。
四。總結
當然這個舉措並不能夠完全的抑制棧溢出,如果跳過了保護數,那麼還是檢測不到棧溢出的,並且對其他的局部變量溢出沒有保護。當然每個變量都保護會大大增加程序復雜度。