C和C++安全编码笔记:字符串

1. 安全概念

计算机安全(computer security):指的是阻止攻击者通过未授权访问或未授权使用计算机和网络达到目的。安全包含开发和配置两方面的元素。开发安全要求具有安全的设计和无瑕疵的实现;配置安全则要求系统和网络被安全地予以部署以免遭攻击。

安全策略(security policy):指系统管理员或网络管理员为使系统免遭威胁而设定的一些规则和操作。

安全缺陷(security flaw):指会导致潜在安全风险的软件瑕疵(software defect)。软件瑕疵是人类思维错误(包括疏忽)被编码到软件中的结果。并非所有软件瑕疵都有安全风险,只有那些确实有安全风险的软件瑕疵才称为安全缺陷。

漏洞(vulnerability):指允许攻击者违反显示或隐式的安全策略的一组条件。并非所有的安全缺陷都会导致漏洞。然而,如果一个安全缺陷会导致输入数据(例如,命令行参数)越过安全界限进入到程序中,那该安全缺陷就会导致软件漏洞。

软件中的漏洞是可以利用(exploitation)的。利用的形式多种多样,包括蠕虫、病毒和木马等。

利用(exploit):指借助软件漏洞来违反一个显式或隐式的安全策略的软件或技术。

缓解措施(mitigation):指能够保护或者限制对漏洞进行利用的方法、技术、过程、工具或运行库。缓解措施是指针对软件缺陷的解决方案,或者用于防止软件漏洞被利用的应急方案。缓解措施也可称为对策(countermeasure)或规避策略(avoidance strategy)。

2. 字符串

2.1 字符串:由一个以第一个空(null)字符作为结束的连续字符序列组成,并包含此空字符。一个指向字符串的指针实际上指向该字符串的起始字符。字符串长度指空字符之前的字节数,字符串的值则是它所包含的按顺序排列的字符值的序列。

void calculate_array_size(int arr1[], char arr3[])
{
	int arr2[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
	fprintf(stdout, "arr2 element count: %d\n", sizeof(arr2) / sizeof(arr2[0])); // 8

	// 此处arr1是一个参数,所以它的类型是指针,在x64中,sizeof(int*)=8,在x86中,sizeof(int*)=4
	// 数组名作为函数参数会被C语言转换为指针,而不是sizeof的"参数",因为sizeof不是函数而是运算符
	// sizeof运算符在应用于声明为数组或函数类型的参数时,它产生调整后的(即指针)类型大小
	fprintf(stdout, "arr1 element count: %d\n", sizeof(arr1) / sizeof(arr1[0])); // 2, error
	fprintf(stdout, "sizeof(int*): %d\n", sizeof(int*)); // 8 // note: x64, not x86

	fprintf(stdout, "arr3 byte count: %d\n", strlen(arr3)); // 10
}

void string_literal()
{
	const char s1[4] = "abc"; // 不推荐,任何随后将数组作为一个空字节结尾的字符串的使用都会导致漏洞,因为s1没有正确地以空字符结尾
	const char s2[] = "abc";  // 推荐,对于一个用字符串字面值初始化的字符串,不指定它的界限,因为编译器会自动为整个字符串字面值分配足够的空间,包括终止的空字符
	fprintf(stdout, "s1 length: %d, s2 length: %d\n", strlen(s1), strlen(s2)); // 3, 3
}

void string_size()
{
	wchar_t wide_str1[] = L"0123456789";
	// 计算容纳宽字符串的一个副本所需的字节数(包括终止字符)
	wchar_t* wide_str2 = (wchar_t*)malloc((wcslen(wide_str1) + 1) * sizeof(wchar_t));
	free(wide_str2);
}

int test_secure_coding_2_1()
{
	int arr1[] = { 1, 2, 3, 4, 5, 6, 7, 8 };
	char arr3[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', '\0' };
	calculate_array_size(arr1, arr3);

	string_literal();

	char x = 'a';
	fprintf(stdout, "sizeof('a'): %d, sizeof(x): %d\n", sizeof('a'), sizeof(x)); // 1, 1

	string_size();

	return 0;
}

字符串并不是C或C++的内置类型。标准C语言库支持的类型为char的字符串和类型为wchar_t的宽字符串。

字符串实现为字符数组而且容易遭受与数组同样的问题。

C标准允许创建指向数组对象的末元素之后加1位置的指针,虽然这些指针无法在不产生未定义行为的状况下解引用。

strlen()函数可以用来确定一个正确地以空字符结尾的字符串的长度,但不能用来确定一个数组的可用空间。在获取一个数组的大小时,不要对一个指针应用sizeof运算符

执行字符集:一个字符串中的字符都属于在执行环境中解释的字符集。这些字符由C标准定义的一个基本字符集和一组零个或多个扩展字符(它们不是基本字符集的成员)组成。执行字符集的成员的值是具体实现定义的,但可能(例如)是美国7位ASCII字符集的值。

C使用一个语言环境(locale)的概念,它可以由setlocale()函数改变,用来跟踪各种约定,如具体实现支持的语言和标点符号。当前语言环境确定哪些字符可用作可扩展字符。

基本执行字符集中必须存在一个字节的所有位都设置为0的字符,称为空字符(null),它是用来终止字符串的。

执行字符集可能包含大量的字符,因此需要多个字节来表示扩展字符集中的一些单个字符。这就是所谓的多字节(multibyte)字符集。一个字符串,可能有时会称为一个多字节字符串,以强调它可能会存在多字节字符。这些与宽字符串不同,因为在宽字符串中每个字符都具有相同的长度。

UTF-8:是一个多字节字符集,它可以表示在Unicode字符集中的每个字符,而且与美国7位ASCII字符集向后兼容。每个UTF-8字符由1~4个字节表示

UTF-8的解码器有时会成为一个安全漏洞。在某些情况下,攻击者可以通过向它发送UTF-8语法不允许的一个八位字节序列,来利用一个不谨慎的UTF-8解码器。

宽字符串:若要处理大字符集的字节,程序可以将每个字符都表示为一个宽字符。大多数实现选择16位或32位来表示一个宽字符。一个宽字符串是一个连续的宽字符序列,它包括并由第一个null宽字符终止。

字符串字面值:是一个包围在双引号中的零个或更多个字符的序列。宽字符串字面值除了以字面L作为前缀外,其它的表示方式与字符串字面值相同。

在C中,字符串字面值的类型是一个char数组,但在C++中,它是一个const char数组。因此,一个字符串字面值在C中是可修改的。然后,如果程序试图修改这样的一个数组,该行为是未定义的,因此这种行为是禁止的。

不要试图修改字符串字面值。原因:(1).编译器有时会把多个相同的字符串字面值存储在相同的地址中,这样导致修改一个这样的字面值可能也会改变其它字面值。(2).字符串字面值经常存储在只读存储器(ROM)中。

对于字符串,一个字符串字面值指定的大小是字面值中的字符数再加上1(用于终止的空字符)。

不要指定一个用字符串字面值初始化的字符数组的界限。因为编译器会自动为整个字符串字面值分配足够的空间,包括终止的空字符。

C++中的字符串:C++标准的类模板std::basic_string,该模板代表一个字符序列。它支持序列存取操作也支持字符串操作(如搜索和串连),并且是由字符类型参数化的:

string是模板特化basic_string的一个typedef。

wstring是模板特化basic_string的一个typedef。

在标准C++的string类中,其内部表示并不一定非得是以空字符结尾的,虽然所有常见的实现都是以空字符结尾的。

字符类型:char、signed char和unsigned char统称为字符类型。编译器可以自由地定义char,使它与signed char或unsigned char具有相同的范围、表示方式和行为。C标准选择字符类型遵从如下一致的理念:(1).signed char和unsigned char适用于小整数值;(2).普通的char用于一个字符串字面值的每个元素的类型,也用于与整数的数据相对的字符数据(其中符号没有意义)。

存储在unsigned char类型对象中的值,保证会当作一个纯粹的二进制表示法来表示属性值。C标准定义的纯粹二进制表示法为”一种使用二进制数字0和1的整数的位置表示法,其中,其值用连续二进制位乘以从1开始的2个连续整数次幂之和表示,除非此二进制位是最高位”。unsigned char类型的对象都保证没有填充位并因此没有表示形式的陷阱。所以,任何类型的非二进制位域(non-bit-field)的对象都可以复制到一个unsigned char数组中(例如,通过memcpy()),并每次1个字节地检查它们的表示形式。

计算字符串大小:对一个以空字符结尾的字节字符串,strlen()函数对终止空字节前面的字符数量进行计数(长度)。然而,宽字符可以包含空字节,尤其是从ASCII字符集获取时。

保证字符串的存储空间具有容纳字符数据和空终结符的足够空间

2.2 常见的字符串操作错误:在C和C++中,操作字符串最常见的错误有4种,分别是无界字符串复制(unbounded string copy)、差一错误(off-by-one error)、空结尾错误(null termination error)以及字符串截断(string truncation)。

void test_unbounded_string_copy()
{
	char buf[12];
	std::cin >> buf; // 如果用户输入多于11个字符,会导致写越界
	std::cout << "echo: " << buf << std::endl;

	std::cin.width(12); // 通过将域宽成员设置为字符数组的长度消除了溢出
	std::cin >> buf;
	std::cout << "echo: " << buf << std::endl;
}

void test_off_by_one_error()
{
#ifdef _MSC_VER
	char s1[] = "012345678";
	char s2[] = "0123456789";

	strcpy_s(s1, sizeof(s2), s2); // error
	//char* s3 = (char*)malloc(strlen(s2) + 1); // note: when free, it will crash
	char s4[20];
	int r = strcpy_s(s4, _countof(s4), s2); // gcc version > 5.0
	fprintf(stdout, "s4: %s\n", s4);

	//char* dest = (char*)malloc(strlen(s1)); // error
	char* dest = (char*)malloc(strlen(s1) + 1);
	int i = 0;
	//for (i = 1; i <= 11; i++) // error
	for (i = 0; i < strlen(s1); ++i) {
		dest[i] = s1[i];
	}
	dest[i] = '\0';

	fprintf(stdout, "dest: %s\n", dest);
	free(dest);
#endif
}

void test_null_termination_error()
{
	char a[16], b[16], c[16];
	// No null-character is implicitly appended at the end of destination if source is longer than num.
	// Thus, in this case, destination shall not be considered a null terminated C string (reading it as such would overflow)
	//strncpy(a, "0123456789abcdef", sizeof(a)); // error, a并未以空字符结尾
	//fprintf(stdout, "a: %s\n", a); // a并未以空字符结尾,导致无法正常打印a
	strncpy(a, "0123456789abcde", sizeof(a));
	//strncpy(b, "0123456789abcdef", sizeof(b)); // error, b并未以空字符结尾
	//fprintf(stdout, "b: %s\n", b); // b并未以空字符结尾,导致无法正常打印b
	strncpy(b, "0123456789abcde", sizeof(b));
	// To avoid overflows, the size of the array pointed by destination shall be long enough to contain the 
	// same C string as source (including the terminating null character)
	strcpy(c, a); // 若a并未以空字符结尾,那么c也未以空字符结尾,而且c可能写得远远超出了数组界限,导致无法正常打印c
	fprintf(stdout, "a: %s, b: %s, c: %s\n", a, b, c);

	char d[16];
	strncpy(d, "0123456789abcdefghijk", sizeof(d) - 1);
	d[sizeof(d) - 1] = '\0';
	fprintf(stdout, "d: %s\n", d);
}

int test_secure_coding_2_2()
{
	//test_unbounded_string_copy();
	//test_off_by_one_error();
	test_null_termination_error();

	return 0;
}

无界字符串复制:发生于从源数据复制数据到一个定长的字符数组时。从无界数据源(例如stdin)读入数据,由于事先无法得知用户将会输入多少个字符,因此不可能预先分配一个长度足够的数组。常见的解决方案是静态分配一个认为长度远远大于所需的数组。不要从一个无界源复制数据到定长数组

不要使用废弃或过时的函数

当分配的空间不足以复制一个程序的输入(比如一个命令行参数)时,就会产生漏洞。如strcpy()、strcat()和sprintf()函数,执行无界复制操作。

snprintf()函数是一个相对安全的函数,但像其它格式的输出函数一样,它也容易产生格式化字符串漏洞。需要对snprintf()的返回值进行检查,因为函数可能会失败,这不仅是因为缓冲区空间不足,还有其它原因,如在函数执行过程中发生内存不足的状况。检测和处理输入和输出错误。检测和处理导致未定义行为的输入输出错误。

差一错误:与无界字符串复制有相似之处,即都涉及对数组的越界写问题。

空字符结尾错误:一个字符串正确地以空字符结尾,是指在数组最后一个元素处或在它之前存在一个空终结符。如果一个字符串没有以空字符结尾,程序可能会被欺骗,导致在数组边界之外读取或写入数据

字符串必须在数组的最后一个元素的地址处或在它之前包含一个空终止字符,才可以安全地作为标准字符串处理函数如strcpy()函数或strlen()函数的参数被传递。空终止字符之所以是必要的,是因为前面这些函数以及其它由C标准定义的字符串处理函数,都依赖于它的存在来标记字符串的结尾。同样,如果程序对一个字符数组迭代循环的终止条件取决于为字符串分配的内存是否存在一个空终止字符,字符串也必须以空字符结尾。按要求提供空字节结尾的字符串

字符串截断:当目标字符数组的长度不足以容纳一个字符串的内容时,就会发生字符串截断。截断通常发生于读取用户输入或字符串复制时,通常是程序员试图防止缓冲区溢出的结果。尽管没有缓冲区溢出危害那么大,但字符串截断会丢失数据,有时也会导致软件漏洞。

与函数无关的字符串错误:大部分在标准字符串处理库中定义的函数都非常容易出错,包括strcpy、strcat、strncpy、strncat、strtok等。例如,微软Visual Studio已经废弃了许多这样的函数。空字符结尾的字符串是用字符数组实现的

2.3 字符串漏洞及其利用:

缓冲区溢出(buffer overflow):实际上是一个运行时事件。当向为某特定数据结构分配的内存空间的边界之外写入数据时,即会发生缓冲区溢出。

进程内存组织:进程:已载入内存并受操作系统管理的程序实例。

进程的内存一般分为code(代码段)、data(数据段)、heap(堆)以及stack(栈)。code和data段包含了程序的指令和只读数据。它们可以被标记为只读,从而当试图对其对应的内存进行修改时,就会引发错误。(把内存标记为只读有两种方法,一是使用支持该功能的计算机硬件平台的内存管理硬件,二是安排内存,使可写的数据和只读数据存储在不同的页面。)data段包含了初始化数据、未初始化数据、静态变量以及全局变量。heap则用于动态地分配进程内存。stack是一个后进先出(last-in, first-out, LIFO)数据结构,用于支持进程的执行。

进程内存的精确组织形式依赖于操作系统、编译器、链接器以及载入器----换言之,依赖于编程语言的实现。

栈管理:栈通过维护自动的进程状态数据来支持程序的执行。要做到将程序控制返回到正确的位置,就需要将返回地址的序列存储起来。栈很适合做这项工作。除了返回地址以外,栈还被用来保存子例程的参数以及局部(或自动)变量。帧(frame)指由函数调用引发的压入栈的数据。当前帧的地址被存储到帧或者基址寄存器中。帧指针在栈中是一个定点的引用。当调用一个子例程时,调用端函数的帧指针同样被压入栈,这样当被调用子例程退出时,帧指针能被重新恢复。

栈溢出:当缓冲区溢出覆写分配给执行栈内存中的数据时,就会导致栈溢出(stack smashing)。这种情况会对程序的可靠性和安全性造成严重的后果。stack段的缓冲区溢出使得攻击者能够修改自动变量的值或执行任意的代码。

代码注入:如果由于一个软件缺陷导致(函数的)返回地址被覆写,那么被覆写后的地址很少会指向有效的指令。结果,将控制转移到该地址通常会引发异常并导致栈混乱。然而,攻击者也有可能蓄意构造出一个字符串,其中包含一个指向某些恶意代码的指针,该代码也由攻击者提供。当子例程返回时,控制就被转移到了那段(恶意的)代码。这样,恶意代码就会以与具有该漏洞的程序相同的权限执行。恶意代码可以执行以其它任何形式编程所能执行的功能,不过它们通常只是简单地在受害机器上开一个远程shell。鉴于此,被注入的恶意代码通常也被称为外壳代码(shellcode)。

弧注入:通过修改函数返回地址的方式改变了程序的控制流。这种技术称为弧注入(arc injection,有时也称为return-into-libc),它将控制转移到已经存在于程序内存空间中的代码中。弧注入的利用方式是在程序的控制流”图”中插入一段新的”弧”(表示控制流转移),而不是进行代码注入。

返回导向编程:该攻击技术与弧注入是类似的,但漏洞利用代码不是返回函数,而是返回跟在return指令后的指令序列。任何这样的可使用的指令序列都称为小工具(gadget)。返回导向编程语言,由一组小工具组成。每个小工具指定要放置在栈中的某些值,供代码段中的一个或多个指令序列使用。小工具执行良好定义的操作,如卸载、加法或跳转。

2.4 字符串漏洞缓解策略:

void test_basic_string()
{
	std::string str;
	std::cin >> str;
	std::cout << "str: " << str << std::endl;

	// 使用迭代器编译一个字符串的内容
	for (std::string::const_iterator it = str.cbegin(); it != str.cend(); ++it) {
		std::cout << *it;
	}
	std::cout << std::endl;
}

void test_string_reference_invalid()
{
	char input[] = "feng;bing;chun;email";
	std::string email;
	std::string::iterator loc = email.begin();
	for (int i = 0; i < strlen(input); ++i) {
		if (input[i] != ';') {
			//email.insert(loc++, input[i]); // 非法迭代器
			loc = email.insert(loc, input[i]);
		} else {
			//email.insert(loc++, ' '); // 非法迭代器
			loc = email.insert(loc, ' ');
		}
		++loc;
	}
	fprintf(stdout, "email: %s\n", email.c_str());
}

int test_secure_coding_2_4()
{
	//test_basic_string();
	test_string_reference_invalid();

	return 0;
}

字符串处理:”采用并实现一个管理字符串的一致计划”,建议选择一种方法来处理字符串并在项目中始终如一地执行。否则,决定权就落到了单个程序员身上,他们很可能采取不同、不一致的方法。

C11附录K边界检查接口:设计目的主要是实现现有函数的更安全的替代品。例如,C11附录K定义了strcpy_s、strcat_s、strncpy_s和strncat_s函数,分别作为strcpy、strcat、strncpy和strncat的替代品,适用于源字符串长度未知的或保证小于已知目标缓冲区大小的情况。

C11附录K函数是微软为响应许多众所周知的安全事故而创建的,以帮助改变其现有的遗留代码库。这些函数已作为ISO/IEC TR 24731-1公布,后来又合并入C11中。(预期这样的实现定义了__STDC_LIB_EXT1__宏。)C11附录K是一个规范性的,但可选的附录,你应该确保它在自己所有的目标平台上可用。

大多数边界检查函数,在检测到错误,如参数无效或输出缓冲区没有足够的可用字节时,会调用一种特殊的运行时约束处理程序(runtime-constraint-handler)函数。此函数可能会打印一个错误信息和中止程序。程序员可以通过set_constraint_handler_s()函数控制哪个处理函数被调用,并可以使处理程序简单地返回,如果需要返回的话。如果处理简单地返回,调用处理程序的函数,用它的返回值提示它的调用者处理失败。当调用TR24731-1定义的函数时,使用运行约束处理程序。如果把运行时约束处理程序设置为ignore_handler_s()函数,那么任何库函数违反运行时约束时,都将返回到它的调用者。调用者可以在库函数的规格说明的基础上确定是否发生运行约束违反。约束处理程序设置在main(),以便在整个应用程序中保持一致的错误处理策略。

动态分配函数:由被调用者分配,由调用者释放。ISO/IEC TR 24731-2定义了许多标准C字符串处理函数的替代品,这些替代品使用动态分配的内存,即自动调整缓冲区大小以容纳所需的数据,以确保不会发生缓冲区溢出。使用这样的函数需要引入随后的释放缓冲区的额外调用。

C++ std::basic_string:此类代表一个字符序列。它支持序列操作以及字符串操作,并由字符类型参数化。basic_string类使用一种动态方法,按需要为字符串分配内存,缓冲区总是自动调整大小以容纳所需的数据,通常是通过调用realloc函数。basic_string类实现了”由被调用者分配,由被调用者释放”的内存管理策略。basic_string类比以空字符结尾的字节串更不容易有安全漏洞,但编码错误仍然可能导致安全漏洞。

使字符串对象的引用失效:修改字符串的操作会使引用、指针和引用字符串对象的迭代器失效,这可能会导致错误。使用无效的迭代器是未定义的行为,并可能会导致安全漏洞。虽然当操作引用字符串的范围之外的内存时,C++一般抛出一个std::out_of_range类型的异常,但是为了使效率最高,下标成员std::string::operator[](不执行边界检查)不会抛出异常。c_str()方法可以用来生成一个与字符串对象内容相同的以空字符结尾的字符序列,并把它作为一个字符数组的指针返回。

使用basic_string的其它常见错误:(1).使用无效或者未初始化的迭代器;(2).传递出界的索引;(3).使用实际上不是一个区间的迭代器区间;(4).传递一个无效的迭代器位置。

2.5 字符串处理函数:

void test_fgets()
{
	char buf[10];

	if (fgets(buf, sizeof(buf), stdin)) {
		// fgets成功,扫描查找换行符
		char* p = strchr(buf, '\n');
		if (p) {
			*p = '\0';
		} else {
			// 未找到换行符,刷新stdin到行尾
			int ch;
			while (((ch = getchar()) != '\n') && !feof(stdin) && !ferror(stdin));
		}
	} else { // fgets失败
		fprintf(stderr, "fail to fgets\n");
	}

	fprintf(stdout, "buf: %s\n", buf);
}

void test_getchar()
{
	const int BUFSIZE = 10;
	char buf[BUFSIZE];
	int ch;
	int index = 0;
	int chars_read = 0;

	while (((ch = getchar()) != '\n') && !feof(stdin) && !ferror(stdin)) {
		if (index < BUFSIZE - 1) {
			buf[index++] = (unsigned char)ch;
		}
		++chars_read;
	}

	buf[index] = '\0'; // 空终结符

	if (feof(stdin)) { fprintf(stderr, "EOF\n"); } // 处理EOF
	if (ferror(stdin)) { fprintf(stderr, "ERROR\n"); } // 处理错误
	if (chars_read > index) { fprintf(stderr, "truncated\n"); } // 处理截断

	fprintf(stdout, "buf: %s\n", buf);
}

void test_gets_s()
{
#ifdef _MSC_VER
	char buf[10];
	if (gets_s(buf, sizeof(buf)) == NULL) { // 处理错误
		fprintf(stderr, "fail to gets_s\n");
	}

	fprintf(stdout, "buf: %s\n", buf);
#endif
}

void test_strncpy()
{
	char source[] = "http://blog.csdn.net/fengbingchun";
	char* dest = (char*)malloc(sizeof(source) + 1);
	size_t dest_size = strlen(dest);
	strncpy(dest, source, dest_size - 1);
	dest[dest_size - 1] = '\0';
	fprintf(stdout, "dest: %s\n", dest);
	free(dest);
}

void test_strncpy_s()
{
#ifdef _MSC_VER
	char src1[100] = "hello";
	char src2[7] = { 'g', 'o', 'o', 'd', 'b', 'y', 'e' };
	char dst1[6], dst2[5], dst3[5];
	errno_t r1, r2, r3;

	r1 = strncpy_s(dst1, sizeof(dst1), src1, sizeof(src1));
	fprintf(stdout, "dst1: %s, r1: %d\n", dst1, r1); // hello\0
	r2 = strncpy_s(dst2, sizeof(dst2), src2, 4);
	fprintf(stdout, "dst2: %s, r2: %d\n", dst2, r2); // good\0
	//r3 = strncpy_s(dst3, sizeof(dst3), src1, sizeof(src1)); // crash, r3并没有返回非零值,原因应该是没有开启运行时约束
	//fprintf(stdout, "dst3: %s, r3: %d\n", dst3, r3);
#endif
}

int test_secure_coding_2_5()
{
	//test_fgets();
	//test_getchar();
	//test_gets_s();
	//test_strncpy();
	test_strncpy_s();

	return 0;
}

gets():永远不要使用gets(),它不对缓冲区溢出进行任何检测

C99:用fgets()或getchar()取代gets()。

fgets()函数:接受两个额外的参数:期望读入的字符数和输入流。与gets()不同,fgets()函数保留换行符。当使用fgets()时,可能只读取了一行的部分数据,然而,可以确定用户的输入是否被截断了,因为那样的话,输入缓冲区内将不会包含一个换行符。fget()函数从流中最多读入比指定数量少1个的字符到一个数组中。如果遇到换行符或者EOF标志,则不会继续读取。在最后一个字符读入数组中后,一个空字符随即被写入缓冲区的结尾处。

getchar()函数:返回stdin指向的输入流中的下一个字符。如果流在EOF处,则该流的EOF标记就会被设置,且getchar()返回EOF。如果发生读取错误,则该流的错误标记就会被设置,且getchar()返回EOF。

C11的gets_s()函数是gets()的一个兼容且更安全的版本。它只从stdin指向的流中读取,且不保留换行符。gets_s()函数接受一个额外的参数rsize_t,用于指定输入的最大字符数。如果这个参数等于0或者比RSIZE_MAX更大,或者目标字符数组指针为NULL,将产生一个错误条件。如果产生了错误条件,那么将不会有任何的输入动作,并且目标字符数组将不会被更改。否则,该函数最多读入比指定数量少1的字符,并且在最后一个字符读入数组后立即在其后加上空字符。如果gets_s()函数执行成功,则返回一个指向字符数组的指针,否则返回一个空指针。如果指定的输入字符数超过目标缓冲区的长度,那么gets_s()函数仍然可能导致缓冲区溢出。

strcpy()和strcat()函数:是缓冲区溢出的频繁来源,因为它们不允许调用者指定目标数组的大小,许多预防策略都建议使用这些函数的更安全的变种。

在C11附录K中,strcpy_s()和strcat_s()函数被定义为与strcpy()和strcat()函数非常接近的替代函数。strcpy_s()函数有一个额外的参数,用于给定目标数组的大小,来防止缓冲区溢出。strcpy_s()仅在源字符串可被完全复制到目标缓冲区且不引起目标缓冲区溢出的情况下才会调用成功。strcpy_s()函数执行各种运行时约束。

strcat_s()函数将源字符串中的字符追加到目标字符串的末尾,直至遇到空结束符为止,并且追加的字符包含结尾的空字符。

在没有正确指定目标缓冲区的最大长度的情况下,strcpy_s()和strcat_s()仍然可能会引起缓冲区溢出的问题。

strncpy()和strncat()函数:与strcpy()和strcat()函数类似,但每个函数都有一个额外的size_t类型的参数n用于限制要被复制的字符数量。这些函数可以被认为是截断型的复制和拼接函数。因为strncpy()函数不能保证用空字符终止目标字符串,所有程序员必须小心,以确保目标字符串是正确地以空字符终止的,并且没有覆盖最后一个字符。C标准的strncpy()函数经常被推荐为strcpy()函数”更安全”的替代品,然而,strncpy()容易发生串终止错误。

strncat(char* s1, const char* s2, size_t n)函数:从s2指向的数组追加不超过n个字符(空字符和它后面的字符不追加)到s1指向的字符串结尾。s2最初的字符覆盖了s1末尾的空字符。终止空字符总是被附加到结果字符串。因此,在s1指向的数组中的最大字符数量是strlen(s1)+n+1。

必须谨慎使用strncpy()和strncat()函数,或根本不使用它们,尤其是在有更不易出错的替代品的时候。这两个函数都要求指定剩余的长度而不是缓冲区的总长度。由于剩余的长度在每次添加或删除数据时都会改变,因此程序员必须跟踪这些改变或重新计算剩余长度。这个过程很容易出错,并且可能会导致漏洞。

C11附录K指定strncpy_s()和strncat_s()函数作为strncpy()和strncat()很接近的替代品。

strncpy_s()函数有一个额外的参数用于给出目标数组的大小,以防止缓冲区溢出。如果发生运行时约束违反,则目标数组被设置为空字符串,以增加问题的能见度。strncpy_s()函数返回0表示成功。

strncat_s()函数从源字符串附加不超过指定数目的连续字符(空字符后面的字符将不会被复制)到目标字符数组中。源字符串的首字符会覆盖目标数组原来结尾的空字符。如果没有从源字符串复制空字符,则在附加后的字符串结尾写入一个空字符。

memcpy()和memmove():在C11附录K中定义的memcpy_s()和memmove_s()函数,与相应的安全性较低的memcpy()和memmove()函数类似,但提供了一些额外的保障。为了防止缓冲区溢出,memcpy_s()和memmove_s()函数具有额外的参数来指定目标数组的大小。

strlen()函数:没有特别的缺陷,但由于底层字符串表示的弱点,它的操作可能被破坏。strlen()函数接受一个指向一个字符数组的指针,并返回终止空字符之前的字符数量。如果字符数组不是正确地以空字符结尾的,strlen()函数可能会返回一个错误的超大的数值,使用它时,就可能会导致漏洞。在将字符串传递给strlen()函数之前,有必要确保它们是正确地以空值结尾的,从而使函数的结果在预期范围内。C11提供了一种替代strlen()的函数----带边界检查的strnlen_s()函数。

2.6 运行时保护策略:

检测和恢复的缓解策略通常要求对运行时环境做出一定的改变,以便可以在缓冲区溢出发生时对其进行检测,以便应用程序或操作系统可以从错误中恢复(或者至少”安全地”失效)。

输入验证:任何到达某个跨越信任边界的程序接口的数据都需要验证。这类数据的例子包括main()函数的argv和argc参数、环境变量,以及从套接字、管道、文件、信号、共享内存和设备中读取的数据。

对象大小检查:GNU C编译器(GCC)推出了__builtin_object_size()函数来获取指针指向的对象的大小。定义了_FORTIFY_SOURCE时,GCC把strcpy()实现为调用__builtin___strcpy_chk()的内联函数。

Visual Studio中编译器生成的运行时检查:/RTCs编译器标志。

栈探测仪(canary):是另一种用来检测和阻止栈溢出攻击的机制。探测仪用于保护栈上的返回地址免遭通过内存的连续写操作(例如,调用strcpy()所导致的结果),而不是执行一般化的边界检查。GCC的栈溢出保护器(Stack-Smashing Protector也被称为ProPolice),以及微软的Visual C++ .NET编译器中作为缓冲区溢出检测能力的那部分,都实现了探测仪。

栈溢出保护器:GCC引入了栈溢出保护(SSP)的功能,它实现了来自StackGuard的探测仪。SSP也被称为ProPolice,它是GCC的一个扩展,用以保护用C编写的应用程序免遭大多数常见形式的栈缓冲区溢出的漏洞利用,它以GCC的中间语言翻译器的形式实现。SSP特性通过GCC的命令行参数启用,-fstack-protector和-fno-stack-protector选项可以为带有易受攻击的对象(如数组)的函数打开或关闭栈溢出保护。-fstack-protector-all和-fno-stack-protector-all选项可以打开或关闭对每一个函数的保护,而不仅仅局限于对具有字符数组的函数的保护。

检测和恢复:地址空间布局随机化(Address Space Layout Randomization, ASLR)是许多操作系统的一项安全功能,其目的是为了防止执行任意代码。该功能对程序所使用的内存页的地址随机化。

不可执行栈:是一种针对缓冲区溢出的运行时解决方案,设计它的目的在于防止在栈段(stack segment)内运行可执行代码。很多操作系统都可以被配置为使用不可执行栈。

W^X:多种操作系统,包括OpenBSD、Windows、Linux和OS X,在内核强制减少权限,使得进程地址空间中的任何部分都不能同时既可写又可执行。这一策略被称为W xor X,或更为简洁的W^X,并通过使用多种CPU的不执行(No eXecute, NX)位来支持这种功能。NX位使内存页可以标记为数据,以禁用这些页面上的代码的执行。

2.8 小结:

当在为一个特定数据结构分配的内存的边界之外写入数据时,就会发生缓冲区溢出。缓冲区溢出在C和C++程序中司空见惯,因为这两种语言:(1).将字符串定义为以空字符结尾的字符数组;(2).不进行隐式的边界检查;(3).提供了不执行边界检查的标准字符串调用库。

常用的C和C++编译器在编译时并不识别可能的缓冲区溢出情形,在运行时也不会报告缓冲区溢出异常。只有当测试数据能够引发一个可侦测的溢出时,才能使用动态分析工具来探查缓冲区溢出。

并非所有的缓冲区溢出都会导致可利用的软件漏洞。然而,如果程序的输入数据是由(可能怀有恶意的)用户控制的,那么缓冲区溢出就很可能会造成程序漏洞,从而易受攻击。

常见的环境策略就是采用新的、提供更多安全保障的字符串操作库。例如,C11附录K边界检查接口就是为既有函数调用设计的方便的替代品。因此,这些函数可以作为预防性的维护措施,以减少现有遗留代码库产生漏洞的可能性。

运行时解决方案,例如边界检查、探测仪和安全库,都具有运行时性能开销并且可能发生冲突。例如将探测仪与安全库一起使用并无意义,因为它们多是以不同的方式执行或多或少相同的功能。

GitHub:https://github.com/fengbingchun/Messy_Test

你可能感兴趣的:(C/C++/C++11,c++)