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报告所有的编译警告。