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

Posted by c4pr1c3 on February 6, 2011

写在最后的话

规范很精彩,评论更精彩。很多建议和规则虽然本身写的有瑕疵,但是当你看了建议/规则页的评论之后,你会发现你并不是第一个提问的人。很多时候你的疑问和疑惑别人已经给你解答了。在我看来,CERT的这个C语言安全编程规范的最重要的意义不是告诉你怎样使用C语言编写一个“无漏洞”的系统,重要的是通过阅读这个规范,你会知道怎样使用C语言会编写出一个“有漏洞”的系统。和很多安全编程规范一样,《CERT Secure Coding Standard》不是要告诉你”What to do”,而是让你知道”What is bad”。

《CERT Secure Coding Standard》中有很多建议/规则都是面向“可移植”代码需求写的,对于确定性的运行在某一个专有平台上,没有代码可移植性需求的C程序员来说,要特别留意不同平台上的一些比较tricky的情况。虽然代码并不总是跨平台的,但程序员却有很大可能性跨平台的。一旦养成了某个平台上的一些“思维惯性”和“经验假设”,写出来的代码跑到另一个平台上时就会出现“可移植性”代码相关的安全漏洞了。

《CERT Secure Coding Standard》通篇都在引经据典,围绕C99、C1X、IEEE等等标准来提建议,定规则。一方面,可以看出建议和规则的制订者们非常重视科学的态度和权威的影响;另一方面,我在翻译的时候得到的一点小小的体会。看过RFC文档的人都知道,RFC标准里关于标准的需求强烈程度专门有一个RFC——RFC 2119来定义”MUST”, “MUST NOT”, “REQUIRED”, “SHALL”, “SHALL NOT”, “SHOULD”, “SHOULD NOT”, “RECOMMENDED”, “MAY”, 和”OPTIONAL”。通过使用不同的“情态动词”来让标准的阅读者更好的理解哪些是“强制要求”,哪些是“一般性要求”。虽然《CERT Secure Coding Standard》中很多规则/建议都只是使用Do not …来开头,实际上每一条规则/建议都有一个“打分”——Risk Assessment,我在翻译的时候没有把这些“打分”包含进来,主要还是blog排版的问题,谁让我从一开始用的是表格这种形式呢。如果每条规则/建议后面都附上“打分”,可能可读性会更强一些吧。这对我们在制订自己的编程规范时也是一个很好的学习范例,不仅可以通过“情态动词”这种方式,采用量化得分的形式也可以有效的提高标准的“可读性”。

原计划是在春节期间分5天连载完所有的译稿,计划赶不上变化,明天不能上网,索性今天就全部发布完结。我不是英语专业出身,也不通“信、达、雅”,我只是从一个C语言使用者的角度,尽可能用自己的话把建议和规则都说清楚。水平有限,欢迎拍砖。

49. 杂项

</tr> </tbody> </table>

50. POSIX

规则/建议条目全称 例外 笔记 /备注/点评
MSC00-C. 使用高警告等级编译代码 MSC00-EX1: 编译器会提供诊断信息以帮助改正代码。但有时代码确实正确,但编译器仍然产生警告,可以通过编译器支持的格式化注释方法或其他编译器指令消除警告,或通过注释明确指出相关代码的警告信息为何可以被安全的忽略。
MSC01-C. 尽可能实现逻辑完备性 很多软件漏洞都是由于程序员没有考虑所有可能的逻辑状态而引起的,例如缺少缺省处理逻辑或缺省错误处理逻辑等。
MSC02-C. 避免粗心遗漏造成的错误 该条建议已经被以下3条建议代替:</p>
MSC03-C. 避免粗心画蛇添足造成的错误 该条建议已经被以下2条建议代替:</p>
MSC04-C. 注释风格统一且可读性高
MSC05-C. 禁止直接操作time_t类型数据
MSC06-C. 操作敏感数据时小心编译器的优化 优化的结果可能是代码的屏蔽执行、常量化或者缓存等,可以使用编译器支持的指令禁止某段代码的自动优化,或者使用一些不会被优化的API。
MSC07-C. 检测并删除死代码 死代码指的是逻辑上永远不会被执行到的代码。
MSC09-C. 字符集编码——使用ASCII字符的子集以保证安全 屏蔽掉一些可能会引起异常或冲突的“元字符”,例如文件名命名中的一些禁用字符。
MSC10-C. 字符集编码——UTF8相关问题 UTF-8是Unicode的变长编码方式。UTF-8根据要表示的Unicode符号,会使用1~4字节来表示一个字符。在实际使用UTF-8字符集时要注意检查UTF-8字节序列的合法性,特别是大部分的Unicode系统最多只支持到16bit长的字符集范围,而不是ISO 10646标准的31bit完全UTF-8字符集空间。
MSC11-C. 使用断言将诊断测试整合到代码中 注意断言的适用场合,特别是一些运行时(依赖于具体用户输入或运行环境)错误不要使用断言来测试。
MSC12-C. 检测并删除无效代码
MSC13-C. 检测并删除不再使用的值
MSC14-C. 禁止引入不必要的平台依赖性 从代码的可移植性角度,C99定义了4类不可移植代码的行为:</p>
  • 非确定行为
  • 未定义行为
  • 具体实现相关的行为
  • 本地化相关行为
MSC15-C. 禁止依赖“未定义”行为
MSC16-C. 考虑加密函数的调用地址 函数的内存调用地址正常情况下是在编译器确定并存储在二进制程序文件中。如果攻击者能够覆盖(hook)该地址,则攻击者可以执行任意代码。为了减轻这种攻击的效果,可以使用运行时函数调用地址确定的方法,来随机化函数的调用地址。Windows平台提供了两个相关的API来实现该功能:EncodePointer()和DecodePointer()。
MSC17-C. 每一个选择语句分支都使用一个case标签和break MSC17-EX1: 最后一个分支语句可以不使用break,一般情况下最后一个分支语句的标签是default。</p>

MSC17-EX2: 当一个控制流确实需要跨越多个分支语句标签时,可以不用每个分支标签配备一个break。但必须在文档注释中明确标记理由。 </td>

MSC18-C. 在程序代码中要小心操作敏感数据,例如口令
MSC19-C. 函数返回值是数组类型的,优先使用空数组而不是null值作为返回值
MSC20-C. 禁止在switch流程控制语句分支中使用复杂逻辑代码块
MSC21-C. 使用不等号来终结for循环 MSC21-EX1: 如果循环计数器是连续递增/递减,则可以使用相等关系来终结for循环。
MSC22-C. 安全的使用setjmp(), longjmp() 详细建议请移步官方原文,引用内容较多。
MSC30-C. 禁止使用rand()函数产生伪随机数
MSC31-C. 确保函数返回值的比较对象类型匹配
MSC32-C. 确保你的随机数产生器的种子选择恰当
MSC33-C. 禁止传递非法数据给asctime()函数
MSC34-C. 禁止使用废弃或过时的函数
MSC35-C. 禁止在switch语句的第一个case标签之前包含任何可执行语句 编译器会忽略这些可执行语句。如果在switch语句的第一个case标签之前声明了一个变量,并试图在后续的case标签语句中使用该变量,该变量的有效作用域范围局限于swtich语句代码块内,但不会被初始化因而只会包含垃圾数据。
MSC36-C. 调用realloc()函数之前检查内存地址的对齐情况 本条规则仅适用于遵循C1X 标准(即将出版)的编译器。
MSC37-C. 确保非-void函数的控制流程不会到达函数尾部 确保非-void函数的尾部总是有一个默认的返回值语句存在。
MSC38-C. 禁止将那些可能为宏实现的预定义标识当作一个对象 例如:ssert, errno, math_errhandling, setjmp, va_start, va_arg, va_copy, 和va_end。不要把这些宏直接当作函数来重新定义实现,如果需要用作全局变量,必须包含必要的头文件。
MSC39-C. 禁止将va_arg()作用于有未确定值的va_list
MSC40-C. 禁止使用空的无限循环 MSC40-EX1: 在极少的情况下需要使用空的无限循环。例如,在某些平台上不支持用户态sleep()或等价函数。另一种情况是在操作系统内核,一个任务开始于任务调度相关函数从而无法调用sleep()或等价函数。在这些情况下就有必要采取一些其他方法来避免编译器优化导致空无限循环代码被删除。可用的方法包括使用volatile型变量作为无限循环的条件变量或者使用编译器支持的函数级别的关闭编译器优化选项指令。
</tbody> </table>

规则/建议条目全称 例外 笔记 /备注/点评
POS01-C. 操作文件时检查文件链接的存在性

Windows和*nix都支持文件链接,如硬链接,符号(软)链接和虚拟磁盘。在*nix系统中可以使用ln命令创建硬链接,在Windows系统上可以使用CreateHardLink()这个API。*nix上还可以使用ln -s创建符号链接,Windows系统可以在NTFS文件系统上使用目录junction、Linkd.exe(Win2K系统工具集)或者”junciton”这个免费软件来创建符号链接。Windows系统可以使用subst 命令创建虚拟磁盘。

由于文件链接机制的存在,当使用API打开一个文件的时候,有可能实际上操作的是另一个被链接到的文件。如果目标进程是运行在高权限下,则会对系统造成很大的隐患。

通常并不需要检查符号链接的存在性,只需要在打开文件前先执行“降权”操作,将当前进程的权限降低为普通用户,从而避免操作重要系统文件。

当创建一个新文件时,需要先检查目标文件是否已经存在,避免覆盖已有重要文件。

只要极个别情况下,需要检查软/硬链接文件的存在性以确保程序操作的是预期的文件,而不是其他目录下的文件。在这些情况下,在检查符号链接文件存在性时要避免造成竞争条件。

建议遵循FIO05-C. 使用多重文件属性识别一个文件,确定一个文件到底是普通文件,还是某种类型的链接文件、虚拟磁盘文件、设备文件等等。对于符号链接文件,可以在打开文件时,使用O_NOFOLLOW属性参数来避免打开真实目标文件。对于硬链接文件,由于其不能跨不同设备链接。因此,在生产系统上,通常把重要目录和普通目录分别挂载在不同的设备上。

POS02-C. 遵循最小化权限原则
POS03-C. 禁止使用volatile类型的同步原语 关于volatile类型变量用于多线程编程有以下三点误解:</p>
  • 原子性:不可再拆分的内存操作
  • 可见性:(一个线程的)写操作是所有其他线程可见的
  • 有序性:一个线程的实际内存操作顺序可以确保在被其他线程“看到”的顺序完全一致

实际上,volatile类型变量不具备以上三点性质中的任意一点volatile修饰符作用于变量只是为了告诉编译器:该变量可能会以不确定的方式被改变,因此禁止编译器在该变量所在内存区域执行优化操作。使用volatile类型变量只能保证该变量值不会被“意外”的缓存到CPU的寄存器,从而导致一个线程的修改,其他线程再访问的可能是寄存器中的“旧”值。 </td> </tr>

POS04-C. 避免使用PTHREAD_MUTEX_NORMAL类型的互斥锁
POS30-C. 恰当的使用readlink()函数
POS33-C. 禁止使用vfork()
POS34-C. 禁止调用putenv()时传递一个指向自动变量的指针作为参数
POS35-C. 检查符号链接存在性时避免出现竞争条件 例如time-of-check-time-of-use (TOCTOU)竞争条件。
POS36-C. 释放权限时遵循正确的撤销顺序 先setgid(getgid()),再setuid(getuid())。除此之外,还要注意一个用户可能同时属于多个用户组,因此一个进程除了egid之外,还会有多个补充组id(supplementary group ID)。getgroups()会返回一个包含所有补充组id的数组,该数组中也可能会包括egid。POSIX标准中定义了getgroups(),但没有定义setgroups()。正常情况下setuid()相关的API调用不会改变补充组id,除非该进程运行在setuid-root模式下。所以正确释放所有补充组id的顺序应该是先释放所有的补充组id,最后再释放root权限。
POS37-C. 确保权限释放成功
POS38-C. 使用fork和文件描述符时小心竞争条件 使用fork创建子进程时,文件描述符会被拷贝到子进程中因此可能会导致对同一个文件的并发操作。
POS39-C. 跨系统传输数据时注意字节顺序 htonl(), htons(), ntohl(), 和 ntohs()用于网络字节顺序(big endian)和主机字节顺序(little endian)之间的转换。在big endian系统上执行这些函数是没有效果的。
POS41-C. 当一个线程的退出状态不重要时,必须使用pthread_detach()或其他等效函数 当线程退出但没有detach时,线程的栈内存会被释放,但其他资源,包括堆内存和操作系统级别对象,将一直存在直到pthread_join()或pthread_detach()被调用。
POS44-C. 禁止使用信号来终结线程
POS45-C. 为线程专有数据定义显式的销毁器以避免内存泄漏 pthread_key_create()调用时传递一个内存清理函数指针作为第二个参数。
POS47-C. 禁止使用可能会被异步取消的线程