tonybai在他的網站上寫了一個本書的知識點綱要
安全問題與指針的不恰當使用
本章從三個方面來討論安全問題
1.指針聲明與初始化
2.指針的不當使用
3.內存回收問題(deallocation problems)
第一部分:指針的聲明與初始化
指針聲明不當。如:
int* ptr1,ptr2;
作者本意是聲明兩個int指針,但是實際結果是隻有ptr1被聲明爲指針,ptr2沒有被聲明爲指針,所以應該這樣聲明int *ptr1,*ptr2;
#define PINT int*
PINT ptr1,ptr2; //宏替換是代碼的替換,所以依然只有ptr1被聲明爲指針,ptr2沒有被聲明爲指針
typedef int* PINT;
PINT ptr1,ptr2; //使用typedef這樣就將ptr1和ptr2都聲明爲指針了
指針使用前沒有初始化使用未經初始化的指針可能會導致運行時錯誤。有時也稱爲野指針
int *p1;
...
printf("%d\n",*pi); //pi在使用時根本沒有初始化,裏面存的是垃圾
處理未初始化的指針
1.初始化指針時將其置爲NULL
2.使用assert函數
3.使用第三方工具
如:
int *pi = NULL; //置爲NULL
...
if(pi == NULL) //判斷是否爲NULL
{
}
else
{
}
也可以使用assert,調試時非常有用
assert(pi != NULL);
第二部分:指針的使用問題
該部分會討論有關字符串、結構和函數指針等等,許多安全問題都是以緩衝區溢出這個概念爲中心。
緩衝區溢出可通過以下幾種方式發生
1.數組中使用索引訪問元素時不檢查索引的值
2.使用數組指針進行指針運算時粗心大意
3.使用像gets之類的函數從標準輸入中讀取字符串
4.strcpy和strcat的不當使用
檢查是否爲NULL(Test for NULL)
對使用malloc函數返回的值一定要進行防空判斷如:
float *vector = malloc(sizeof(float) * 20);
if(vector == NULL){
//內存分配失敗
} else {
//處理vector
}
(誤用解引用操作符*)Misuse of the Deference Operator
正確情況
int num;
int *pi = &m; /*這裏的*是用來聲明指針*/
錯誤情況:
int num;
int *pi; /*聲明瞭一個指針*/
*pi = # /*這裏的*是解引用,應該改爲pi = #*/
懸掛指針(Dangling Poiners第二章說過)
數組訪問越界(Accessing Memory Outside the Bounds of an Array)
char firstName[8] = "1234567";
char middleName[8] = "1234567";
char lastName[8] = "1234567";
middleName[-2] = 'X';
middleName[0] = 'X';
middleName[10] = 'X';
printf("%p %s\n",firstName,firstName);
printf("%p %s\n",middleName,middleName);
printf("%p %s\n",lastName,lastName);
其一種可能的內存分配如下圖:數組大小計算錯誤(Calculating the Array Size Incorrectly)
#include <stdio.h>
#include <ctype.h>
#include <string.h>
void replace(char buffer[],char replacement,size_t size);
main()
{
char name[8];
strcpy(name,"Alexander");/* 越界了,因爲name數組只能有8個char,減去一個NUL字符,實際只能有7個char,但是Alexander有8個字符 */
replace(name,'+',sizeof(name));/* 這裏數組元素個數應該用sizeof(name) / sizeof(char),這樣做圖方便,因爲sizeof(char)就是1 */
printf("%s\n",name); /* 輸出++++++++r */
}
void replace(char buffer[],char replacement,size_t size)
{
size_t count = 0;
while(*buffer != '\0' && count++ < size)
{
*buffer = replacement;
buffer++;
}
}
誤用sizeof操作符(Misusing the sizeof Operator)
int buffer[20];
int *pbuffer = buffer;
int i;
for(i = 0; i < sizeof(buffer); i++) /* sizeof(buffer)的使用造成越界,應該用sizeof(buffer) / sizeof(int) */
{
*(pbuffer++) = 0;
}
永遠匹配指針類型(Always Match Pointer Types)
It is a good idea to always use the appropriate pointer type for the data.
/* 這個例子非常適合在計算機對不同數值的表示中講,且涉及到了機器的大小端法表設計,非常好 */
int num = 2147483647;
int *pi = #
short *ps = (short*)pi;
printf("pi: %p,Value(16):%x,Value(10):%d\n",pi,*pi,*pi);
printf("ps: %p,Value(16):%hx,Value(10):%hd\n",ps,(unsigned short)*ps,(unsigned short)*ps);
小端法如下圖:有界限的指針(Bounded Pointers)
C語言並沒有提供直接的支持,但是可以像下面一樣由程序員控制
#define SIZE 32
char name[SIZE];
char *p = name;
if(name != NULL)
{
if(p > name && p < name + SIZE)
{
//合法指針
}
else
{
//不合法指針
}
}
使用這種方法很繁瑣(tedious),可以使用靜態分析等等字符串相關的安全問題(String Security Issues)
strcpy和strcat可能會導致緩衝溢出,C11中新引入的strcpy_s和strcat_s目前只有微軟Visual C++支持,這兩個新函數在發生緩衝區溢出時返回錯誤
同樣還有新引入的scanf_s和wscanf_s
char firstName[8];
int result;
result = strcpy_s(firstName,sizeof(firstName),"Alexander");
The use of some functions can result in an attacker accessing memory using a technique known as format string attacks.作者在這裏舉了一個printf的例子
int main(int argc,char** argv)
{
printf(argv[1]); /* 這裏就可能會造成字符串攻擊 */
...
}
更多這方面的信息請參閱hackerproof.org指針運算與結構(Pointer Arithmetic and Structures)
對於數組可以放心使用指針運算,因爲數組保證內存分配是連續的,而對於結構其位域可能不在連續的內存上(as the structure's fields may not be allocated in consecutive regions of memory)
比如下面結構:
typedef struct _employee
{
char name[10];
int age;
} Employee;
name分配了10個字節,其後跟着一個int,但是int要對齊到能被4整除的位置,所以name和age之間就有位填補(gap)。如圖:Even if the memory within a structure is contiguous, it is not a good practice to use pointer arithmetic with the structure’s fields.
即使結構的內存是連續的,最好也不要使用指針運算
typedef struct _item
{
int partNumber;
int quantity;
int binNumber;
} Item;
Item part = {12345, 35, 107};
int *pi = &part.partNumber;
printf("Part number: %d\n",*pi);
pi++;
printf("Quantity: %d\n",*pi);
pi++;
printf("Bin number: %d\n",*pi);
正常情況下輸出結果是我們期待的,但它並不能保證總是起作用(but it is not guaranteed to work)下面是一種較好的辦法
int *pi = &part.partNumber;
printf("Part number: %d\n",*pi);
pi = &part.quantity;
printf("Quantity: %d\n",*pi);
pi = &part.binNumber;
printf("Bin number: %d\n",*pi);
更好的辦法是:
printf("Part number: %d\n",part.partNumber);
printf("Quantity: %d\n",part.quantity);
printf("Bin number: %d\n",part.binNumber);
函數指針問題(Function Pointers Issues)
函數指針誤用與濫用會導致不可預測的結果,考慮如下函數:
int getSystemStatus()
{
int status;
...
return status;
}
判斷系統狀態是否爲0的最佳方法如下:if(getSystemStatus() == 0)
{
printf("Status is 0\n");
}
else
{
printf("Status is not 0\n");
}
但如果忘了寫()的話,代碼運行就不正常了
if(getSystemStatus == 0)
{
printf("Status is 0\n");
}
else
{
printf("Status is not 0\n"); /* 總會執行着這句 */
}
這樣寫的話也不科學:
if(getSystemStatus) /*函數指針,肯定不爲0*/
{
//總會執行這段代碼
}
Do not assign a function to a function pointer when their signatures differ.This can result in undefined behaviour.例子如下:
int (*fptrCompute)(int,int);
int add(int n1,int n2,int n3)
{
return n1 + n2 + n3;
}
fptrCompute = add;
fptrCompute(2,5); /* 實際需要3個參數,但卻只傳了2個 ,代碼會編譯通過gcc給出了警告,但是結果卻很難預料了 */
第三部分:內存回收問題(Memory Deallocation Issues)
多次釋放(Double free)
char *name = (char*) malloc(...);
...
free(name); /* 第一次釋放 */
...
free(name); /* 第二次釋放 */
cert.org上有有關這方面的更多資料,一種簡單的避免雙重釋放就是釋放完後即置爲NULL,也可以寫自己的free函數
char *name = (char *) malloc(...);
...
free(name);
name = NULL;
清除敏感數據(Clearing Sensitive Data)
程序退出時清除敏感數據有助於隱私保存,像陳老師那樣把照片沒徹底刪除再跑去修電腦實在是太危險了。如:
char name[32];
int userID;
char *securityQuestion;
//assign values
...
//delete sensitive information
memset(name,0,sizeof(name));
userID = 0;
memset(securityQuestion,0,strlen(securityQuestion));
如果name被聲明爲指針,那麼在回收之前要清除其內存如:
char *name = (char*) malloc(...);
...
memset(name,0,sizeof(name));
free(name);
使用靜態分析工具(Using Static Analysis Tools)
有許多靜態分析工具可用於檢測指針的不當使用。GCC編譯器使用-Wall報告所有的編譯警告。