[译]CERT Secure Coding Standard — C语言安全编程规范(3)

Posted by c4pr1c3 on February 6, 2011

08. 内存管理

</tbody> </table>

09. 输入输出

规则/建议条目全称 例外 笔记 /备注/点评
MEM00- C. 在同一个模块、同一个抽象层次上分配和释放内存 不遵循该建议所导致的常见软件错误和漏洞包括:内存泄漏, double-free漏洞, 非法访问已经释放的内存,写入已释放或未分配的内存区域
MEM01- C. 在调用free()之后立刻给相应的指针赋一个新值 MEM01- EX1:如果一个非静态变量在调用free()释放内存后就立刻失去作用域(例如子函数中的一个局部非静态变量), 则没有必要执行重新赋值操作,因为该变量已经不会再被访问到。 一般赋的新值是NULL
MEM02- C. 在调用内存分配函数后立刻对返回的指针进行类型转换 推荐采用CERT的宏替换法来重新封装malloc系列函数(注意区分简单单个对象简单数组对象包含变长数组的单个结构体对象的不同宏封装定义方法)
MEM03- C. 清理存储在可重复使用资源中的敏感信息 例如在free()之前先调用memset_s置全0
MEM04- C. 禁止执行长度为0的内存分配操作 如果试图调用malloc(), calloc(), realloc()系列函数分配长度为0的内存,其行为是具体编译器/操作系统实现相关的,会导致不可预料的结果。
MEM05- C. 避免进行较大的栈内存分配 C99标准中引入了变长数组支持,如果变长数组的长度传入未进行任何检 查和处理则很容易被攻击者利用来实施DoS攻击
MEM06- C. 确保敏感数据没有写入磁盘 导致敏感数据被“意外”写入磁盘的两种常见机制是:虚拟内存管理中 swap机制和操作系统异常处理时的core dump机制
MEM07- C. 确保calloc()的参数进行乘法操作时,乘数的大小可以用size_t表示 标准C库(*nix系统上通常是/usr/include /stdint.h)中定义了SIZE_MAX宏,该值是size_t的最大值。该条建议主要是检查calloc()的第一个参数是否越界,例如以下代 码:</p>
long *buffer;
size_t num_elements;

if (num_elements > SIZE_MAX/sizeof(long)) {
 /* Handle error condition */
}
buffer = (long *)calloc(num_elements, sizeof(long));
if (buffer == NULL) {
 /* Handle error condition */
}
MEM08- C. 仅对动态分配内存的数组调整容量时才使用realloc()函数 根据C语言规范:调用realloc(ptr, size)时会先释放ptr所指向的旧对象的内存再返回一个size字节指向新对象的指针。新对象会”继承“旧对象被释放内存前相应内存位置的值(继承多 少取决于size参数的值,最多只能”继承“旧对象的所有值),超过旧对象长度部分的值是”未确定“(未初始化,等同于malloc效果)的。
MEM09- C. 不要假设在内存分配函数中会初始化内存 </p>
FUNCTION ALLOCATION DEALLOCATION INITIALIZATION
calloc()
free()
malloc()
realloc() partial

</span></span></td> </tr>

MEM10- C. 定义并使用一个指针校验函数 通常,对一个非法指针进行解引用操作会导致程序异常终止。但有时也会出 现对非法指针解引用操作但程序没有异常终止而是继续”正常“运行的情况(因为C语言规范里对于此类行为是”未定义“的)。出现此类问题,没有通用的调试方 法,需要特别小心的检查指针的”合法性“。
MEM11- C. 不要假设堆容量无限
MEM12- C. 当函数因在使用和释放资源出错退出时可以考虑使用一个Goto式处理链 goto语法一个合理的应用场景就在于此。一个很长的程序调用链中的一 个执行环节出错,需要进行错误处理,这是非常常见的一个编程场景。很多时候,我们在进行错误处理时顾此失彼,往往只考虑了某种失败条件下的资源释放,或是 重复调用了资源释放过程。可以参考该建议中给出的2个小例子,都用到了goto语句。其中一个例子直接取自Linux kernel 2.6.29的kernel/fork.c文件中的copy_process函数代码。
MEM30- C. 禁止访问已释放的内存 否则是”未定义“行为
MEM31- C. 动配分配的内存必须且只能释放一次 否则是”未定义“行为
MEM32- C. 检测并处理内存分配错误
MEM33- C. 动态分配和复制包含变长数组成员的结构体 该类结构体不应该</p>
  • 在栈上声 明;                    应该在堆上声明
  • 通过赋值来实现拷贝;       应该使用类似memcpy()的函数来实现拷贝
  • 直接作为参数传递给函数; 应该使用指向 结构体的指针作为参数进行传递
MEM34- C. 仅释放动态分配的内存 否则是”未定义“行为
MEM35- C. 给一个对象分配足够的内存 缓冲区溢出攻击行为的原因
</tbody> </table>

10. 环境

规则/建议条目全称 例外 笔记 /备注/点评
FIO01-C. 小心使用以文件名作为标识符的函数 具体来说指的是C99标准中的这几个函数:</p>
remove()
rename()
fopen()
freopen()
  

尽可能的使用文件描述符或者FILE指针操作文件,以避免访问非预期文件。 </td> </tr>

FIO02-C. 归一化来自不可信输入源的路径名 避免类似“使用相对路径访问任意文件”的漏洞
FIO03-C. 使用fopen和创建文件时不要随意假设 fopen函数用来打开一个已有文件或创建一个新文件,但fopen()并不能区别一个已有文件被打开待写还是创建了一个新文件。因此,可能导致文件错误覆盖或非法访问文件。推荐的方法是使用正确的fopen参数或者使用操作系统提供的更精细化的文件操作API,明确指定是新创建一个文件还是打开一个已有文件。
FIO04-C. 检测并处理输入输出错误
FIO05-C. 使用多重文件属性识别一个文件 例如文件属主,创建时间,文件创建和关闭相关信息等可以联合用于标识一个文件是否是“上次”操作的“那个”文件。
FIO06-C. 创建文件时正确设置文件访问权限 不要仅仅依赖操作系统的umask机制
FIO07-C. 优先使用fseek()于rewind() 使用rewind()后会重置流的错误指示符,而rewind()可以等效的使用:</p>
 (void)fseek(stream, 0L, SEEK_SET)
  

来代替。 </td> </tr>

FIO08-C. 对已打开的文件使用remove()函数要小心 其行为是”具体实现相关”的。如果确实要删除已打开的文件,尽可能使用相应平台上支持该功能的函数。否则,避免删除已打开的文件。
FIO09-C. 跨系统传输二进制数据时要小心 不同系统上的结构体对齐方式、浮点数模型、每字节的bit位数、endian类型(little endian or big endian)等可能会有所差别。
FIO10-C. 小心使用rename()函数 rename函数的原型如下:</p>
int rename(const char *src_file, const char *dest_file);
  

如果dest_file所指向的文件抢先一步调用了rename()函数,则后调用rename()函数的行为结果是“未定义”的。在POSIX系统上,目标文件会被删除。在Windows系统上,后一次rename()调用会失败。 </td> </tr>

FIO11-C. 小心指定fopen()的文件操作模式参数 重复使用或同时使用多种模式字符串其行为是“具体实现相关”的。
FIO12-C. 优先使用setvbuf()于setbuf()
FIO13-C. 一次只“退回”一个字符 该条建议是针对ungetc()和ungetwc()说的,根据C99标准,这两个函数无论重复调用多少次只“确保”退回一个字符。所以,对同一个输入输出流多次调用ungetc()或ungetwc()时必须确保用一个read函数或者文件流定位函数分隔多次调用。
FIO14-C. 理解文件流的文本模式和二进制模式 注意fputs()、fseek()、ungetc()等函数在不同文件流操作模式下的调用结果区别
FIO15-C. 确保文件操作是在一个安全的目录中执行 安全目录的概念指的是除了当前进程的属主用户之外,其他用户无权限修改创建、重命名、删除或改变“安全目录”中的数据。安全配置是系统管理员的职责和任务,但对于程序员来说,需要在程序运行过程中检查目录相关权限、文件、属性等的完整性,避免“误操作”落入攻击者设定的“陷阱”。
FIO16-C. 创建一个受控环境限制文件访问 在*nix系统上,可以通过chroot、setuid/setgid、chidir等API调用,完成进程的文件访问限制在特定目录、特定资源、特定权限范围之内,避免进程被非法控制后“提权”、“越界”访问。
FIO17-C. 在使用fread()函数时不要依赖于null结尾字符 fread的函数原型如下:</p>
size_t fread(void * restrict ptr,
   size_t size, size_t nmemb,
   FILE * restrict stream)
  

fread读取数据的大小是受nmemb和size参数控制,且不会自动添加null字符到ptr。 </td> </tr>

FIO18-C. 绝不要认为fwrite()会在遇到null字符时停止写操作 fwrite的函数原型如下:</p>
size_t fwrite(const void * restrict ptr,
   size_t size, size_t nitems,
   FILE * restrict stream)
  

C99标准中没有要求fwrite()在遇到null字符时停止向文件写入数据,因此在向文件写入NTBS(Null Terminated Byte String)时记得nitems参数值应该是待写入NTBS长度+1(null字符)。</td> </tr>

FIO19-C. 禁止使用fseek()和ftell()来计算文件大小 fseek()在二进制流模式方式访问文件时,SEEK_END宏无法正确标识文件结尾。ftell()在文本模式方式访问文件时,无法返回文件指针当前的偏移量值。
FIO00-C. 小心创建格式化字符串 格式化字符串通常特指以%开头的转换指示符,包括必须的转换格式类型标志和可选的最小宽度、精度和长度修饰符。官方文档中所给出C99兼容格式化字符串搭配表格值得一看。
FIO30-C. 排除用户输入中的格式化字符串
FIO31-C. 禁止打开一个已经处于打开状态的文件 否则会产生一个“实现相关的”行为。
FIO32-C. 禁止对设备(文件)执行只适用于(普通)文件的API 假设一个浏览器程序违反了本条规则,则以下代码就有可能使该浏览器程序在浏览到包含如下代码的网页时导致鼠标死锁。</p>

  

在执行文件操作API前,先检查所打开文件的类型,避免对设备文件的误操作。 </td> </tr>

FIO33-C. 检查并处理输入输出错误避免未定义的行为 I/O函数执行成功与否一般可以通过检查函数的返回值来实现,但不要认为只要不是“错误”状态返回值就一定是执行成功了。部分函数执行成功时的返回值条件相比较出错返回值会更精确、更苛刻。例如snprintf()函数在执行失败时返回负数或>=缓冲区长度的值,在执行成功时返回成功操作的字符个数(非负数并且小于缓冲区长度)。
FIO34-C. 使用int来接受字符IO函数的返回值 FIO34-EX1: 如果一个字符输入/输出函数的返回值不会被和EOF整数常量表达式值进行比较,就没有必要遵循本规则。
FIO35-C. 当sizeof(int) == sizeof(char)时使用feof()和ferror()来检查文件结尾和文件错误 FIO35-EX1: C99中有一些函数不会返回字符但会返回EOF作为状态码。这些函数包括:fclose(), fflush(), fputs(), fscanf(), puts(), scanf(), sscanf(), vfscanf(), vscanf()。直接将这些函数的返回值与EOF进行比较是没有问题的。
FIO35-EX2: 如果能够保证sizeof(char) != sizeof(int),则不需要遵守本规则。
sizeof(char) == sizeof(int)的平台其实并不多见。
FIO36-C. 使用fgets()时不要假设会读取换行符 fgets()有可能读取时出现截断的问题,因此可能读取的内容不包含换行符。fgetws()也是类似。
FIO37-C. 不要假设fgets()在执行成功时一定会返回一个非空字符串 最典型的情况就是以“二进制模式”访问文件时,完全有可能读取到一行的内容的第一个字符就是一个null字符。
FIO38-C. 禁止使用一个FILE对象的拷贝来输入和输出 违反本规则有可能导致编译失败,也有可能引起DoS攻击。
FIO39-C. 禁止对一个流交替输入和输出而不间隔使用flush或者流指针定位函数调用 否则会导致“未定义”行为。
FIO40-C. fgets()调用失败时执行字符串重置操作 FIO40-EX1:如果在fgets()或者fgetws()执行后,相关字符串立刻失去作用域范围或者失去引用,则无需字符串重置。 fgets()在调用失败时会向缓冲区中写入不确定的内容。
FIO41-C. 禁止向getc()或者putc()函数传递有副作用的流参数 getc()和putc()有可能是以宏的形式来实现,因此在向这两个函数传递参数不当(例如传递一个表达式)时可能会造成参数被执行多次的情况。
FIO42-C. 确保文件在不再使用时被正确关闭 否则可能会导致文件描述符耗尽引起的DoS攻击。
FIO43-C. 禁止在共享目录中创建临时文件 FIO43-EX1: 如果所有目标实现代码都是在一个安全目录中执行则TR24731-1 tmpfile_s()可以不受本条规则限制使用。
FIO44-C. 仅使用fgetpos()的返回值作为fsetpos()的参数值 否则会产生一个“未定义”行为。
规则/建议条目全称 例外 笔记 /备注/点评
ENV00-C. 禁止存储getenv()的返回指针 getenv()的返回指针指向值可能会被后续的getenv()调用所覆盖,除此之外,getenv()不是线程安全的。Windows平台提供了两个替代函数_dupenv_s()和_wdupenv_s()用于安全读取系统环境变量值。
ENV01-C. 不要假设一个环境变量的内存占用大小 跨平台代码需要注意,没有跨平台需求的,可以视目标平台实际情况而定。
ENV02-C. 小心多个环境变量有相同的有效名 最典型的例子是Windows平台上环境变量名是不区分大小写的,而*nix平台上环境变量名是区分大小写的,因此跨平台代码要小心环境变量名的大小写问题。
ENV03-C. 调用外部程序时要“清洗”环境变量 此条建议是STR02-C. 在将数据传递给复杂子系统之前做好数据清洗的具体化。
ENV04-C. 如果你不是需要一个命令解释器就不要调用system() 使用system()调用会增加输入数据过滤的复杂度。
ENV30-C. 禁止修改某些函数的返回值所指向的对象 具体来说,这些函数包括:
getenv(), setlocale(), localeconv(), strerror()。不遵守本条规则会导致“未定义”行为。
ENV31-C. 禁止依赖于一个环境变量指针,该指针指向的地址可能会被后续操作改变 系统环境变量随时都可能被系统中其他进程改变,如果直接使用getenv()返回的指针地址,可能会在后续被其他进程所改变。
ENV32-C. 所有atexit()处理函数都应该正常返回 否则会产生一个“未定义”行为。C99定义的exit()函数用于正常的程序终结,如果嵌套调用exit()会导致“未定义”行为。