1. 程式人生 > >GCC中的堆疊保護機制

GCC中的堆疊保護機制

        以堆疊溢位為代表的緩衝區溢位已成為最為普遍的安全漏洞,由此引發的安全問題比比皆是。我們知道攻擊者利用堆疊溢位漏洞時,通常會破壞當前的函式棧。在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陣列始終在高地址。

參考資料: