C/C++安全编码-字符串
1 字符串
1.1 字符串基础
字符串提供命令行参数、环境变量、控制台输入、文本文件及网络连
接,提供外部输入方法来影响程序的行为和输出,这也是程序容易出错的地方。字符串是一个概念,并不是C/C++内置类型,标准C语言库支持类型为char的字符串和类型为wchar_t的宽字符串。
字符串由一个以第一个空(null)字符作为结束的连续字符序列组成,并
包含此空字符(所以sizeof和strlen会差1)。一个指向字符串的指针实际指向该字符串的起始字符。目标大小,指sizeof(array)大小,注意与元素个数区分。
数组大小。数组带来的问题之一是确定其元素数量,例如下面的例子:
void clear(int array[])
{
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); ++i)
{
array[i] = 0;
}
}
void dowork()
{
int dis[12];
clear(dis);
/* ... */
}
array是一个参数,所以它的类型是指针。因此,sizeof(array)等于sizeof(int*),在x86 32机中,sizeof(array) / sizeof(array[0])计算结果都是1。
字符串字面值:简而言之就是在双引号中的值,在C中,字符串字面值的类型是一个char数组,但在C++中,它是一个const char数组。所以在C中可以修改字面值,但是程序如果试图去修改,该行为是未定义的。不要试图修改字符串字面值,编译器有时会把多个相同的字符串字面值存储在相同位置,例如只读存储器(ROM)中,看下面例子:
const char *s1 = "abc";
const char *s2 = "abc";
char *s3 = "abc";
char *s4 = "abc";
char s5[] = "abc";
char s6[] = "abc";
比较地址会发现s1,s2,s3,s4相同,用这4个指针去改变字符串字面值是会出问题的。s5,s6值不同
字符数组初始化:不要指定一个用字符串字面值初始化的字符数组的界限
const char s[3] = "abc"; //不安全写法,少一个'\0'
const char s[] = "abc"; //推荐初始化方式
1.2 C++中的字符串
C++标准类模板std::basic_string。简单来说就是string(basic_string<char>)
和wstring(basic_string<wchar_t>),basic_string的类的模版特化更不容易出现错误和安全漏洞,需要强调的是大多数C++字符串对象被视为不可分割的整体(通常按值传递和引用传递),内部字符串不一定是以空字符结束(大多数实现是以空字符结尾),C的库函数都接受以空字符结尾的字符序列指针。
1.3 字符类型
char 是 signed char 还是 unsigned char 可由编译器的配置项设定
当char有符号时,由unsigned char[]转换为const char *
当char无符号时,由singned char[] 转换为const char *
如果不强制转换会有警告,建议使用普通的char
1.4 字符串的长度
混淆概念容易在C和C++中导致严重的错误,
wchar_t wide_str1[] = L"0123456789";
wchar_t *wide_str2 = (wchar_t*)malloc(strlen(wide_str1) + 1);
if(wide_str2 == NULL)
{
/*处理错误*/
}
free(wide_str2);
wide_str2 = NULL;
对一个以空字符结尾的字节字符串,strlen()统计终止空字节前面的字符数量。然而,宽字符可以包含空字节,所以计算结果会出问题。
使用wcslen可以计算宽字符串的大小
wchar_t wide_str1[] = L"0123456789";
wchar_t *wide_str2 = (wchar_t*)malloc(wcslen(wide_str1) + 1);
if(wide_str2 == NULL)
{
/*处理错误*/
}
free(wide_str2);
wide_str2 = NULL;
注意此长度没有乘sizeof(wchar_t),所以还是不对,下面值最终正确写法:
wchar_t wide_str1[] = L"0123456789";
wchar_t *wide_str2 = (wchar_t*)malloc((wcslen(wide_str1)+1)*sizeof(wchar_t));
if(wide_str2 == NULL)
{
/*处理错误*/
}
free(wide_str2);
wide_str2 = NULL;
2 常见的字符串操作错误
2.1 无界字符串复制
void get_y_or_n()
{
char response[8];
puts("Continue? [y] n:");
gets(response);
if(response[0] == 'n')
exit(0);
return;
}
其实gets()函数在C99中以废弃并在C11中淘汰。它没有提供方法指定读入的字符数的限制。这种限制在此函数的如下一致实现中是显而易见的:
char *gets(char *dest)
{
int c = getchar();
char *p = dest;
while(c != EOF && c != '\n')
{
*p++ = c;
c = getchar();
}
*p = '\0';
return dest;
}
如果输入超出8个字符,那么会导致未定义的行为。不要从一个无界源复制数据到定长数组中,禁止这种方法。
2.1.1 复制和连接字符串
例如strcpy(), strcat(), sprintf(), 容易执行无界操作。例如:
int main(int argc, char *argv[])
{
/*argc参数个数,argv参数数组*/
}
当argc大于0,按照惯例,argv[0]指向的字符串是程序名。若argc > 1,则argv[0]~argv[argc-1]引用的就是实际程序参数。
当分配的空间不足以复制一个程序的输入,就会产生漏洞。攻击者可以控制argv[0]的内容
int main(int argc, char *argv[])
{
/*argc参数个数,argv参数数组*/
char prog_name[128];
strcpy(prog_name, argv[0]);
/* ... */
}
输入一个大于128个字节的字符,栈溢出,即缓冲区溢出漏洞。
标准的写法应该是:
int main(int argc, char *argv[])
{
/* 不要假设argv[0]不许为空 */
const char *const name = argv[0]? argv[0] : "";
char *prog_name = (char*)malloc(strlen(name)+1);
if(prog_name != NULL)
{
strcpy(prog_name, name);
}
else
{
/* 复原 */
}
}
其实还有一种方法可以避免溢出,通过设置域宽可以消除gets()的缺陷
char buf[12];
std::cin::width(12);
std::cin >> buf;
std::cout << buf << std::endl;
2.2 差一错误
简而言之就是从源字符串拷贝内容到目的字符串,刚好最后的'\0'没有
拷贝到目的字符串中,在这之后对目的串调用C语言库的函数可能会出问题,即空字符结尾错误,其余的还有字符串阶截断误差,越界操作等。
2.3 字符串漏洞及其利用
大体上就是缓冲区溢出(详细的可以自己网上查,有很多资料详细介
绍),栈溢出的话,可以把目标代码或者数据覆盖到栈里面,关于栈为什么会溢出,其实是因为在编译后,栈的大小就固定了。这种攻击方式也称注入,这里涉及到汇编以及底层的结构,不做详细解释,不过解决方法也有很多,要么做边界检查,要么动态的分配内存,还有更简单的那就是直接使用std::basic_string。当然使用string也会出问题,例如迭代器失效。
char input[];
string email;
string::iterator loc = email.begin();
//复制到string对象,同时把";" 转换成" "
for (size_t i = 0; i < strlen(input); ++i)
{
if(input[i] != ";")
email.insert(loc++, input[i]);
else
email.insert(loc++, ' ');
}
第一次insert之后,loc就已经失效,后面的insert都将产生未定义行为。正确的写法应该是
char input[];
string email;
string::iterator loc = email.begin();
//复制到string对象,同时把";" 转换成" "
for (size_t i = 0; i < strlen(input); ++i)
{
if(input[i] != ";")
loc = email.insert(loc, input[i]);
else
loc = email.insert(loc, ' ');
++loc;
}
当然在编程的时候引用边界之外的元素会抛出一个异常std::out_of _range。另外std::string.c_str()函数可以返回一个以空字符结尾的字符,const值,所以调用free()或者delete()会出错,需要修改则只能修改副本。