字符串操作安全实验
实验目的
了解C&C++字符串操作
理解C&C++字符串操作安全规则
掌握安全的字符串操作
实验原理
1.字符串
各种字符串,例如命令行参数、环境变量、控制台输入、文本文件以及网络连接,提供了外部输入的方法来影响程序的行为和输出,因此安全编程对其给予了特别的关注。例如,图形化和基于Web的应用广泛使用了文本输入域,此外,由于XML这样的标准的存在,程序之间的数据交换也越来越多地采用字符串的形式。结果,因字符串表示法、字符串管理以及字符串操作方面的缺陷已经导致广泛的软件漏洞和漏洞利用。
在软件工程中,字符串是一个基本的概念,但它并不是C或C++的内置类型。标准C语言库支持的类型为char的字符串和类型为wchar_t的宽字符串。
2.字符串数据类型
字符串由一个以第一个空(null) 字符作为结束的连续字符序列组成,并包含此空字符。一个指向字符串的指针实际上指向该字符串的起始字符。字符串长度指空字符之前的字节数, 字符串的值则是它所包含的按顺序排列的字符值的序列。
3.C++字符串
C++ 提供了以下两种类型的字符串表示形式:
C 风格字符串
C++ 引入的 string 类类型
C 风格的字符串起源于 C 语言,并在 C++ 中继续得到支持。字符串实际上是使用 null 字符 '\0' 终止的一维字符数组。因此,一个以 null 结尾的字符串,包含了组成字符串的字符。
实验步骤
步骤1:确保有足够的空间存储字符串的字符数据和'\0'结束符
在分配内存或者在执行字符串复制操作时,除了要保证足够的空间可以容纳字符数据,还要预留'\0'结束符的空间,否则会造成缓冲区溢出。
1.1 错误示例1:拷贝字符串时,源字符串长度可能大于目标数组空间。
void main(int argc, char *argv[])
{
char dst[128];
if ( argc > 1 )
{
strcpy(dst, argv[1]); // 源字符串长度可能大于目标数组空间,造成缓冲区溢出
}
/*…*/
}
推荐做法:根据源字符串长度来为目标字符串分配空间。
void main(int argc, char *argv[])
{
char *dst = NULL;
if ( argc > 1 )
{
dst = (char *)malloc(strlen(argv[1]) + 1); /* 【修改】确保字符串空间足够容纳argv[1] */
if( dst != NULL )
{
strncpy(dst, argv[1], strlen(argv[1]));
dst[strlen(argv[1])] = '\0'; //【修改】dst以'\0'结尾
}
}
/*...dst使用后free...*/
}
1.2 典型的差一错误,未考虑'\0'结束符写入数组的位置,造成缓冲区溢出和内存改写。
void NoCompliant()
{
char dst[ARRAY_SIZE + 1];
char src[ARRAY_SIZE + 1];
unsigned int i = 0;
memset(src, '@', sizeof(dst));
for(i=0; src[i] != '\0' && (i < sizeof(dst)); ++i )
dst[i] = src[i];
dst[i] = '\0';
/*…*/
}
推荐做法:
void Compliant()
{
char dst[ARRAY_SIZE + 1];
char src[ARRAY_SIZE + 1];
unsigned int i = 0;
memset(src, '@', sizeof(dst));
for(i=0; src[i]!='\0' && (i < sizeof(dst) - 1 ); ++i) /*【修改】考虑'\0'结束符 */
dst[i] = src[i];
dst[i] = '\0';
/*…*/
}
步骤2:字符串操作过程中确保字符串有'\0'结束符
2.1 字符串结束与否是以'\0'作为标志的。没有正确地使用'\0'结束字符串可能导致字符串操作时发生缓冲区溢出。因此对于字符串或字符数组的定义、设置、复制等操作,要给'\0'预留空间,并保证字符串有'\0'结束符。
注意:strncpy、strncat等带n版本的字符串操作函数在源字符串长度超出n标识的长度时,会将包括'\0'结束符在内的超长字符串截断,导致'\0'结束符丢失。这时需要手动为目标字符串设置'\0'结束符。
错误示例1:strlen()不会将'\0'结束符算入长度,配合memcpy使用时会丢失'\0'结束符。
void Noncompliant()
{
char dst[11];
char src[] = "0123456789";
char *tmp = NULL;
memset(dst, '@', sizeof(dst));
memcpy(dst, src, strlen(src));
printf("src: %s \r\n", src);
tmp = dst; //到此,dst还没有以'\0'结尾
do
{
putchar(*tmp);
}while (*tmp++); // 访问越界
return;
}
推荐做法: 为目标字符串设置'\0'结束符
void Compliant()
{
char dst[11];
char src[] = "0123456789";
char *tmp = NULL;
memset(dst, '@', sizeof(dst));
memcpy(dst, src, strlen(src));
dst[sizeof(dst) - 1] = '\0'; //【修改】dst以'\0'结尾
printf("src: %s \r\n", src);
tmp = dst;
do
{
putchar(*tmp);
} while (*tmp++);
return;
}
错误示例2:strncpy()拷贝限长字符串,截断了'\0'结束符。
void Noncompliant()
{
char dst[5];
char src[] = "0123456789";
strncpy(dst, src, sizeof(dst));
printf(dst); //访问越界,dst没有'\0'结束符
return;
}
推荐做法:
void Compliant()
{
char dst[5];
char src[] = "0123456789";
strncpy(dst, src, sizeof(dst));
dst[sizeof(dst)-1] = '\0'; // 【修改】最后字节置为'\0'
printf(dst);
return;
}
步骤3:把数据复制到固定长度的内存前必须检查边界
3.1 将未知长度的数据复制到固定长度的内存空间可能会造成缓冲区溢出,因此在进行复制之前应首先获取并检查数据长度。典型的如来自gets()、getenv()、scanf()的字符串。
错误示例:输入消息长度不可预测,不加检查的复制会造成缓冲区溢出。
void Noncompliant()
{
char dst[16];
char * temp = getInputMsg();
if(temp != NULL)
{
strcpy(dst,temp); // temp长度可能超过dst的大小
}
return;
}
推荐做法:
void Compliant()
{
char dst[16];
char *temp = getInputMsg();
if(temp != NULL)
{
strncpy(dst, temp, sizeof(dst)); /* 【修改】只复制不超过数组dst大小的数据 */
}
dst[sizeof(dst) -1] = '\0'; //【修改】copy以'\0'结尾
return;
}
步骤4:避免字符串/内存操作函数的源指针和目标指针指向内存重叠区
4.1 内存重叠区是指一段确定大小及地址的内存区,该内存区被多个地址指针指向或引用,这些指针介于首地址和尾地址之间。
在使用像memcpy、strcpy、strncpy、sscanf()、sprintf()、snprintf()和wcstombs()这样的函数时,复制重叠对象会存在未定义的行为,这种行为可能破坏数据的完整性。
错误示例1:snprintf的参数使用存在问题
void Noncompliant()
{
#define MAX_LEN 1024
char cBuf[MAX_LEN + 1] = {0};
int nPid = 0;
strncpy(cBuf, "Hello World!", strlen("Hello World!"));
snprintf(cBuf, MAX_LEN, "%d: %s", nPid, cBuf); /* cBuf既是源又是目标,函数使用不安全 */
return;
}
推荐做法:使用不同源和目标缓冲区来实现复制功能。
void Compliant()
{
#define MAX_LEN 1024
char cBuf[MAX_LEN + 1] = {0};
char cDesc[MAX_LEN + 1] = {0}; //【修改】另起一个缓冲区,防止缓冲区重叠出错
int nPid = 0;
strncpy(cDesc, "Hello World!", strlen("Hello World!")); /* 【修改】防止缓冲区重叠出错 */
snprintf(cBuf, MAX_LEN, "%d: %s", nPid, cDesc); /* 【修改】防止缓冲区重叠出错 */
return;
}
错误示例2:
#define MSG_OFFSET 3
#define MSG_SIZE 6
void NoCompliant ()
{
char str[] = "test string";
char *ptr1 = str;
char *ptr2;
ptr2 = ptr1+MSG_OFFSET;
memcpy(ptr2, ptr1, MSG_SIZE);
return;
}
推荐做法:使用memmove函数,源字符串和目标字符串所指内存区域可以重叠,但复制后目标字符串内容会被更改,该函数将返回指向目标字符串的指针。
#define MSG_OFFSET 3
#define MSG_SIZE 6
void Compliant ()
{
char str[] = "test string";
char *ptr1 = str;
char *ptr2;
ptr2 = ptr1 + MSG_OFFSET;
memmove(ptr2, ptr1, MSG_SIZE); /*【修改】使用memmove代替memcpy,防止缓冲区重叠出错 */
return;
}
memcpy与memmove的目的都是将N个字节的源内存地址的内容拷贝到目标内存地址中。
但当源内存和目标内存存在重叠时,memcpy会出现错误,而memmove能正确地实施拷贝,但这也增加了一点点开销。
memmove的处理措施:
当源内存的首地址等于目标内存的首地址时,不进行任何拷贝
当源内存的首地址大于目标内存的首地址时,实行正向拷贝
当源内存的首地址小于目标内存的首地址时,实行反向拷贝