C/C++安全编码-字符串

时间:2023-03-08 17:19:48
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()会出错,需要修改则只能修改副本。