《C和C++安全编码》读书笔记(一)

第一章 夹缝求生

1.1 衡量危险
  • 生产不安全软件系统的风险可以从历史风险和潜在的未来风险两方面进行评估。
  • 威胁的来源:黑客、内部人员、罪犯、竞争情报从业者、恐怖分子、信息战士。
  • CERT/CC(美国计算机紧急事件响应小组协调中心)监控漏洞信息的公开来源,同时也经常会接到漏洞报告。漏洞信息是以CERT漏洞备忘录和US-CERT漏洞备忘录的形式公布的。许多其他组织,包括赛门铁克(Symantec)和MITRE也报告漏洞数据。目前,漏洞信息的最佳来源之一是美国国家标准与技术研究所(NIST)的全美国漏洞数据库(NVD)。
1.2 安全概念
  • 安全包含开发和配置两方面的元素。开发安全要求具有安全的设计和无瑕疵的实现;配置安全则要求系统和网络被安全地予以部署以免遭攻击。
  • 安全策略:一套规则和操作,规定了系统或自足如何提供安全的服务以便保护其敏感和关键的系统资源。安全策略可以是显式的,也可以是隐式的。
  • 安全缺陷:指会导致潜在安全风险的软件瑕疵。并非所有软件瑕疵都有安全风险,只有那些确实有安全风险的软件瑕疵才称为安全缺陷。
  • 漏洞:指允许攻击者违反显式或隐式的安全策略的一组条件。在没有安全缺陷的情况下,同样可能存在漏洞。因为安全知识质量指标中的一个属性,它还需要与其他质量属性如性能和易用性进行权衡。在这个权衡的过程中,软件设计者可能会故意选择将产品设计成附带漏洞的形式(可以某种形式利用)。故意忽略软件中的安全漏洞并不意味着软件就是安全的,只是说明软件的设计者代表软件的使用者接受了可能由安全问题所带来的风险而已。
  • 实际的漏洞通常是在软件被配置成使其本身固有的漏洞可被利用的情况下而被发现的。
  • 利用:指借助软件漏洞来违反一个显式或隐式的安全策略的软件或技术。漏洞利用代码的存在让安全分析人员感到不安。因此,需要根据利用代码的目的对其进行有效的分析。例如,验证性的漏洞利用代码是为了验证某漏洞确实存在而开发的。
  • 缓解措施:指能够保护或限制对漏洞进行利用的方法、技术、过程、工具或运行库。
1.3 C&C++
  • C存在的问题。C语言的目标是称为一种内存耗用微小的轻量级语言,C的这种特征使得当程序员误以为某些事情会由C自动处理(而实际上并不会)时,就可能导致漏洞的出现。这导致程序员容易犯这样的一些错误:对数组的越界不加保护,不处理整数操作的溢出和截断,以及用错误的实参数目调用函数等。
  • C语言缺乏类型安全性。类型安全包括两方面的含义:保持性和前进性。保持性要求如果变量x的类型为t,那么如果x具有值v,则v的类型也为t。前进性要求对一个表达式的计算不会以非预期的方式进行,要么得到一个值(且计算结束),要么存在某种方式对其进行继续处理。通俗的说,类型安全就是要求对某特定类型的操作,其结果仍然是原来的类型。C语言的例如显式类型转换或隐式转换。这种类型安全的缺乏导致了很大范围的安全缺陷和漏洞。

第二章 字符串

2.1 字符串
2.1.1 字符串数据类型

字符串的一些术语:

  • 界限(Bound):数组中元素的个数;
  • 低位地址(Lo):数组首元素地址;
  • 高位地址(Hi):数组末元素地址;
  • TooFar:数组最远端的元素之后加1位置的元素地址,这个元素正好在Hi元素之后;
  • 目标大小(Tsize):与sizeof(array)相同;
  • 空字符结尾(Null-terminated):在Hi或它之前,存在空终结符;
  • 长度(Length):空终结符之前的字符数量;
    《C安全编码标准》在对计算数组大小这个问题的时候提出了警告:ARR01-C. 在获取一个数组的大小时,不要对一个指针应用sizeof运算符。
  • 基本执行字符集包括拉丁字母表的26个大写字母和26个小写字母、10个十进制数组、29个图形字符、空格字符,以及表示水平制表符、垂直制表符、换页符、警告、退格键、回车和换行符的控制字符。基本字符集的每个成员都适合用单个字节表示。
  • 执行字符集可能包含大量的字符,因此需要多个字节来表示扩展字符集中的而一些单个字符,这就是所谓的多字节字符集。
2.1.2 UTF-8

UTF-8是一个多字节字符集,它可以表示在Unicode字符集中的每个字符,而且与美国7位ASCII字符集向后兼容。每个UTF-8字符由1~4个字节表示。一个具有前导0位的字节是一个单字节码,一个具有多个前导1位的字节是一个多字节序列的首字节,而一个具有前导“10”位模式的字节是一个多字节序列的延续字节。这种字节格式允许检测每个序列的开始,而无需从字符串的开头解码。

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

若要处理大字符集的字符,程序可以将每个字符都表示为一个宽字符,宽字符一般比一个普通字符需要更多的空间。一个宽字符串是一个连续的宽字符序列,它包括并由第一个null宽字符终止。

2.1.4 字符串字面值
  • 一个字符串字面值是一个包围在双引号中的零个或更多字符的序列,比如“xyz”。宽字符串字面值除了以L作为前缀外,其他的表达方式与字符串字面值相同,比如L“xyz”。
    “a”“b”“c”
    “a"L"b”“c”
    L"a"“b"L"c”
    L"a"L"b"L"c"
    以上相邻字符串字面值的标记序列都等同于字符串字面值:L"abc"
  • 在C中,字符串字面值的类型是一个char数组,但在C++中,它是一个const char数组。因此,一个字符串字面值在C中是可修改的,然而,如果程序试图修改这样的一个数组,该行为是未定义的。
    《C安全行为编码标准》:STR 30-C. 不要试图修改字符串字面值。
    制定这条规则的一个原因是,如果这些数组的元素有适当的值,在C标准中没有规定这些数组必须是不同的,修改这样一个字面值可能也会改变其他字面值。另一个原因是,字符串字面值经常存储在只读存储器(ROM)中。
  • C标准允许在声明一个数组变量时,既包括界限索引,又包括一个初始化字面值。初始化字面值也蕴含着一个数组大小,即其中指定的元素数量。对于字符串,一个字符串字面值的大小是字面值中的字符数再加上1(用于终止的空字符)。
  • 数组变量常常由一个字符串字面值进行初始化,并且声明为一个字符串字面值中的字符数目相匹配的显式界限。例如:
const char s[3]="abc";

虽然字符串字面值的大小是4,但数组的大小是3,任何随后将数组作为一个空字节结尾的字符串的使用都会导致漏洞,因为s没有正确地以空字符结尾。一个更好的方法是,对于一个用字符串字面值初始化的字符串,不指定它的界限,因为编译器会自动为整个字符串字面值分配足够的空间,包括终止的空字符:

const char s[]="abc";

《C安全编码标准》:STR 36-C. 不要指定一个用字符串字面值初始化的字符数组的界限。

2.1.5 C++中的字符串
2.1.6 字符类型

char、signed char和unsigned char统称为字符类型。编译器可以自由地定义char,使它与signed char或unsigned char具有相同的范围
表示方式和行为,不管编译器作出的选择是什么,char都是独特的类型。虽然没有在任何地方指出,但C便准选择字符类型遵从如下一致的理念:

  • signed char和unsigned char适用于小整数范围;
  • 普通的char适用于一个字符串字面值的每个元素的类型以及与整数的数据相对的字符数据(其中符号没有意义)。
  • int适用于这种情况:数据可能是EOF(一个负值)或解释为unsigned char的字符,为防止出现符号扩展,于是把它转换为int。
  • unsigned char当正在操作的对象可能是任何类型,而且有必要访问该对象的所有二进制位,比如用fwrite()时,unsigned char类型是有用的。
  • wchar_t宽字符用于自然语言的字符数据。
    《C安全编码标准》:STR00-C. 使用一个适当的类型来表示字符。
2.1.7 计算字符串的大小

为防止缓冲区溢出和其他一些运行时错误,正确地计算字符串大小是必不可少的。使用不正确的字符串大小会导致缓冲溢出,例如,会分配一个大小不充足的缓冲区。
《C安全编码标准》:STR31-C. 保证字符串的存储空间具有容纳字符数据和空终结符的足够空间。
一些概念:

  • 大小(size):分配给数组的字节数(等于sizeof(array))。
  • 计数(count):在数组中的元素数目(等于在Visual Studio 2010z中的_countof(array))。
  • 长度(length):在空终结符之前的字符数。
    混淆这些概念经常会导致C和C++程序中的严重错误。C标准保证,类型为char的对象由单个字节组成。因此,一个字符数组的大小等于一个char数组的计数(这也是数组的界限)。长度是在空终结符之前的字符数,对于一个正确地以null结尾的char类型的字符串,其长度必然是小于或等于其大小减1.
    当宽字符串被误认为是窄字符串或多字节字符串时,可能会不正确地计算其大小。C标准定义的wchar_t是一个整数类型,其值的范围可以代表所支持的语言环境中最大的扩展字符集的所有成员的不同编码。Windows通常会使用UTF-16编码,所以wchar_t的大小通常为两个字节。Linux和OS X(GCC/g++以及Xcode中)使用的是UTF-32字符编码,所以wchar_t的大小通常为4个字节。
2.2 常见的字符串错误操作

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

2.2.1 无界字符串复制

无界字符串复制发生于从源数据复制数据到一个定长的字符数组时(例如,从标准输入读取数据到一个定长的缓冲区中)。
《C安全编码标准》:MSC34-C. 不要使用废弃或过时的函数。
对于程序员而言,从无界数据源(例如stdin)读入数据是一个有趣的问题。由于实现无法得知用户将会输入多少个字符,因此不可能预先分配一个长度足够的数组。常见的解决方案是静态分配一个认为长度远远大于所需的数组。
《C安全编码标准》:STR35-C. 不要从一个无界源复制数据到定长数组。

  • **复制和连接字符串。**此操作也很容易错误,因为执行这个功能的许多标准库调用,如strcpy()、strcat()和sprintf()函数,执行无界复制操作。 下面一个例子可以用来说明复制和连接字符串可能出现的错误:
    **Exp1:**从命令行读入的参数保存在进程内存中,当程序执行时调用的main()函数,程序接受命令行参数时,通常声明为如下格式
int main(int argc, char *argv[]){
	/*...*/
}

当分配空间不足以复制一个程序的输入(比如一个命令行参数)时,就会产生漏洞。虽然按照惯例,argv[0]包含程序名,但攻击者可以控制argv[0]的内容,在如下程序中,提供一个超过128个字节的字符串就会造成一个漏洞。而且攻击者还可以把argc[0]设置为NULL来调用这个程序。

int main(int argc, char *argv[]){
	/*...*/
	char prog_name[128];
	strcpy(prog_name,arg[0]);
	/*...*/
}

这个程序可以在Microsoft Visual C++2012下编译并运行,但在警告级别/W3下会对使用strcy()发出警告。这个程序也能在G++4.7.2下编译并运行,如果定义了_FORTIFY_SOURCE,那么在运行时,如果对strcpy()的调用导致了缓冲区溢出,由于对对象大小检查的结果失败,此程序会中止。
strlen()可以用于确定由argv[0]到argv[argc-1]引用的字符串的长度,以便可动态分配足够的内存。要记得加一个字节,以容纳用于终止字符串的空字符,必须注意避免假设argv数组中的任何元素(包括argv[0])是非空的。

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{
	/*动态分配内存失败,复原*/
	}
}
  • 无界字符串的复制问题不仅存在于C语言中。例如下面的C++程序,如果用户输入多余11个字符,也会导致越界。
    Exp2:
#include<iostream>

int main(void){
	char buf[12];
	std::cin>>buf;
	str::cout<<"echo:"<<buf<<'\n';
}

在微软Visual C++2012中,当警告级别是/W4时,这个程序可以正常编译。在G++4.7.2中,当选项是-Wall-Wextra-pedantic时,它也可以正确编译。通过设置域宽成员可以消除溢出:

#include<iostream>

int main(void){
	char buf[12];
	std::cin.width(12);
	std::cin>>buf;
	str::cout<<"echo:"<<buf<<'\n';
}
2.2.2 差一错误

空字符串结尾的字符串的另一个常见问题是差一错误,差一错误与无界字符串复制有相似之处,即都涉及对数组的越界写问题。下面程序存在很多错误。
Exp1:

#include<string.h>
#include<stdio.h>
#include<stdlib.h>

int main() {
	char s1[] = "012345678";
	char s2[] = "0123456789";
	char* dest;
	int i;

	strcpy_s(s1, sizeof(s2), s2);
	/*sizeof(s2)=11,且s1的大小仍为10,s1[10]='\n'*/
	dest = (char*)malloc(strlen(s1));
	/*strlen(s1)=10,此时dest只有10个字节的内存*/
	for (i = 1; i <= 11; i++) {
		/*数组下标从0开始,但是循环从1开始了*/
		dest[i] = s1[i];
		/*s1会超出索引*/
	}
	dest[i] = '\0';
	printf("dest=%s", dest);
}
2.2.3 空字符结尾错误

一个字符串正确地以空字符结尾,是指在数组最后一个元素或在它之前存在一个空终结符。如果一个字符串没有以空字符结果,程序可能会被欺骗,导致在数组边界之外读取或写入数据。
**《C安全编码标准》:STR 32-C. 按要求提供空字节结尾的字符串。**要注意,该规则并不排除使用字符数组。
空字符串结尾错误也很难检测,它们会潜伏在部署好的代码中,直至遇到一组特别的输入而导致发生错误。编写代码不能依赖于编译器如何分配内存,因为这在编译器的下个版本中很可能发生改变。

2.2.4 字符串截断

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

2.2.5 与函数无关的字符串错误
2.3 字符串漏洞及其利用

之前描述的C或C++中操作字符串常见的错误。当代码对源于外部的不受信任的数据(如命令行参数、环境变量、控制台输入、文本文件和网络连接)进行操作时,这些错误就会变得很严重。

2.3.1 被污染的数据

**Exp1:**这个程序检查用户密码(应考虑被污染的数据),并授权或拒绝访问。

bool IsPasswordOK() {
	char Password[12];

	gets(Password);
	return 0 == strcmp(Password, "goodpass");
}

int main(void) {
	bool PwStatus;
	puts("Enter password:");
	PwStatus = IsPasswordOK();
	if (PwStatus == false) {
		puts("Access denied");
		exit(-1);
	}
}

IsPasswordOK()没有检查gets()的返回状态,这违反了“FIO04-C. 检测和处理输入和输出错误。”当gets()失败时,Password缓冲区的内容是不确定的,所以后续的strcmp()调用的行为是未定义的。在实际的程序中,缓冲区甚至可能包含先前其他用户输入的密码。

2.3.3 缓冲区溢出

C和C++都容易发生缓冲区溢出问题,因为它们具有以下共同之处:

  • 将在字符串定义为以空字符结尾的字符数组;
  • 未进行隐式的边界检查;
  • 提供了未强制性边界检查的标准字符串函数的调用。
    取决于内存的位置以及溢出的规模,缓冲区溢出可能不会被检测到,但它可能会破坏数据,导致程序出现奇怪的行为甚至非正常中止。
    静态分析工具可以帮助我们发现在早期开发过程中的缓冲区溢出问题,不过一但测试数据可以引发一个可侦测的溢出,我们就可以使用动态分析工具来发现缓冲区溢出问题。
    并非所有的缓冲区溢出都会造成软件漏洞,然而如果攻击者能够操纵用户控制的输入来利用安全缺陷,那么缓冲区溢出就会导致漏洞了。例如一些技术可以用于覆写栈帧以执行任意的代码。缓冲区溢出也可以在堆或静态内存区域被利用,它的而做法是通过覆写邻接内存的数据结构。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值