GCC中的堆疊保護機制
阿新 • • 發佈:2019-02-09
以堆疊溢位為代表的緩衝區溢位已成為最為普遍的安全漏洞,由此引發的安全問題比比皆是。我們知道攻擊者利用堆疊溢位漏洞時,通常會破壞當前的函式棧。在gcc中,通過編譯選項可以新增 函式棧的保護機制,通過重新對區域性變數進行佈局來實現,達到監測函式棧是否非破壞的目的。
gcc中有3個與堆疊保護相關的編譯選項
-fstack-protector:啟用堆疊保護,不過只為區域性變數中含有 char 陣列的函式插入保護程式碼。
-fstack-protector-all:啟用堆疊保護,為所有函式插入保護程式碼。
-fno-stack-protector:禁用堆疊保護。
1.1 編譯選項為 gcc -fstack-protector-all main.c
輸出結果為:#include <stdio.h> int main(void) { int i; /* 此時i的宣告在陣列a之前 */ int a[4]; //int i; printf("&a=%p,&i=%p\n",&a,&i); //a[4] = 0; printf("%d\n", a[4]); //a[5] = 0; //a[6] = 0; //a[7] = 0; //a[8] = 0; for(i=0;i<=3;i++) /* 當把3改為4的話,程式執行就會出錯,因為開啟了堆疊保護,在a[4]的地方編譯器放置了一個保護變數,當該變數被修改時,程式 * 認為該函式棧被破壞,於是丟擲錯誤 */ a[i]=0; printf("hello\n"); return 0; }
&a=0xbf9b7ebc,&i=0xbf9b7eb8
1425164544
hello
1.2 編譯選項為 gcc -fstack-protector-all main.c
輸出結果為:#include <stdio.h> int main(void) { //int i; int a[4]; int i; /* 此時i的宣告在陣列a之後 */ printf("&a=%p,&i=%p\n",&a,&i); //a[4] = 0; printf("%d\n", a[4]); //a[5] = 0; //a[6] = 0; //a[7] = 0; //a[8] = 0; for(i=0;i<=3;i++)/* 當把3改為4的話,程式執行就會出錯,因為開啟了堆疊保護,在a[4]的地方編譯器放置了一個保護變數,當該變數被修改時,程式 * 認為該函式棧被破壞,於是丟擲錯誤 */ a[i]=0; printf("hello\n"); return 0; }
&a=0xbfe0cd3c,&i=0xbfe0cd38
1166871552
hello
1.3 結論
在gcc -fstack-protector-all的編譯選項下,編譯器會對區域性變數的組織方式進行重新佈局,在本例中,無論i和a變數的宣告前後順序怎麼樣,它們的佈局都是如上圖中右邊的佈局方式,a陣列始終在高地址,變數i始終在低地址。並且在a[4]的地址位置,編譯器會放置一個保護變數,當該變數被修改時會丟擲錯誤。
2.1 編譯選項為 gcc -fno-stack-protector main.c
#include <stdio.h>
int main(void)
{
int i; /* 此時i的宣告在陣列a之前 */
int a[4];
//int i;
printf("&a=%p,&i=%p\n",&a,&i);
//a[4] = 0;
printf("%d\n", a[4]);
//a[5] = 0;
//a[6] = 0;
//a[7] = 0;
//a[8] = 0;
for(i=0;i<=4;i++)
a[i]=0;
printf("hello\n");
return 0;
}
輸出結果:(程式陷入無限迴圈之中)
2.2 編譯選項為 gcc -fno-stack-protector main.c
#include <stdio.h>
int main(void)
{
//int i;
int a[4];
int i; /* 此時i的宣告在陣列a之後 */
printf("&a=%p,&i=%p\n",&a,&i);
//a[4] = 0;
printf("%d\n", a[4]);
//a[5] = 0;
//a[6] = 0;
//a[7] = 0;
//a[8] = 0;
for(i=0;i<=4;i++)
a[i]=0;
printf("hello\n");
return 0;
}
輸出結果:
&a=0xbfda2350,&i=0xbfda234c
134513824
hello
2.3 結論
在gcc -fno-stack-protector的編譯選項下,編譯器不會對區域性變數的組織方式進行重新佈局,在本例中,當i的宣告在a之前時,記憶體佈局如上圖中的左邊的那個一樣;當i的宣告在a之後時,記憶體佈局如上圖中的右邊的那個一樣。
3.1 編譯選項為 gcc main.c (預設情況下)
#include <stdio.h>
int main(void)
{
int i; /* 此時i的宣告在陣列a之前 */
int a[4];
//int i;
printf("&a=%p,&i=%p\n",&a,&i);
//a[4] = 0;
printf("%d\n", a[4]);
//a[5] = 0;
//a[6] = 0;
//a[7] = 0;
//a[8] = 0; /* 當把這行程式碼取消註釋後,執行程式會出現段錯誤,把改行以上的3行取消註釋沒有問題 */
for(i=0;i<=4;i++)
a[i]=0;
printf("hello\n");
return 0;
}
輸出結果:(程式陷入無限迴圈之中)
3.2 編譯選項為 gcc main.c (預設情況下)
#include <stdio.h>
int main(void)
{
//int i;
int a[4];
int i;
printf("&a=%p,&i=%p\n",&a,&i);
//a[4] = 0;
printf("%d\n", a[4]);
//a[5] = 0;
//a[6] = 0;
//a[7] = 0;
//a[8] = 0;
for(i=0;i<=4;i++)
a[i]=0;
printf("hello\n");
return 0;
}
輸出結果:(程式陷入無限迴圈之中)
3.3 結論
預設情況下,編譯器會對區域性變數的組織方式進行重新佈局,在本例中,無論i和a變數的宣告前後順序怎麼樣,它們的佈局都是如上圖中左邊的佈局方式,變數i始終在低地址,a陣列始終在高地址。
參考資料: