编写安全程序

作者:Cal Erickson

大多数关于安全性的文章主要关注网络安全和物理安全。关于编写安全程序的文章不多。编写安全程序所需的很多东西都是常识,但由于时间限制和设计上的捷径,这些常识很少被使用。任何优秀的程序员都知道这些概念,但通常没有时间去实现它们;产生大量代码并完成项目的压力很大。

在 1970 年代初期,结构化编程的概念盛行。不仅程序是结构化的,整个项目也具有结构;有技术规范、设计规范、详细设计规范、设计走查和代码走查。这使得项目规模更大、周期更长,但完成后,代码很容易调试,并且通常只需少量修改即可工作。其中一些项目需要多年才能完成。然而,当时来自网络、Web 和上市时间的外部影响很小。

如今,许多结构化开发流程实际上已经消失了。但是,安全性始于程序或应用程序的设计,并取决于工作所在组织建立的编码标准。

任何代码都不太可能 100% 安全;从来没有代码是完全安全的。但是,可以做些什么来确保代码是可靠和安全的呢?本文提供了一些关于需要考虑事项的想法,并解释了三个有助于编写安全代码的工具。在设计和实施嵌入式系统时,编码需要更加谨慎。借助“资源”部分中的工具,可以检查出许多编码错误。安全代码的最终评判取决于代码的实施者以及实施者理解什么是安全的能力。

检查错误

每个函数都返回某种类型的状态,可以直接返回,也可以作为 errno 的一部分返回。检查这些应该很简单。在 C++ 中,异常处理功能易于使用,但设置起来可能很复杂。一旦 C++ 标准最终确定,异常处理在过去几年中得到了极大的改进。在实际应用中,应该使用它。以前的做法是忽略错误,因为当时认为传递的数据是有效的。这已被证明是一个糟糕的假设。

数据缓冲区溢出在过去几年中导致了许多安全修复。在为嵌入式系统编写代码时,检查错误返回值非常重要。需要决定错误是否是良性的并且可以忽略。如果错误不是良性的,也许可以纠正它。如果无法纠正,系统是执行软复位还是硬复位?在某些情况下,软复位,导致错误操作重新启动,是所有需要的。这是某些容错系统的基础。根据设备类型,硬复位可能不是一件坏事。在其他时候,某种形式的恢复是必须的。

查找字符串

与其使用 sprintf、strcpy 和 strcat,不如使用 strncpy 和 strncat 等函数。这些函数确保缓冲区不会溢出并丢弃任何多余的内容。读取数据时不要使用 fgets,因为它允许溢出。这些可能看起来是简单的更改,但它们很容易被忘记,字符串处理也是如此,它是程序中最容易被利用的领域之一。自动化测试程序可以很好地检查这些问题,但测试可能会产生误导。字符串函数的某些用法可能会被标记为问题,但在使用的上下文中证明是没问题的。这就是实施者的能力和知识发挥重要作用的地方。需要扫描工具生成的日志,以确定哪些代码已被标记并需要更改。

内存泄漏和缓冲区溢出

内存泄漏本身不一定会造成安全风险。但是,如果内存由多个过程和结构共享,则可能会被利用。

缓冲区溢出是迄今为止最常见的安全问题。如果缓冲区在堆栈上分配,则可能会溢出以清除或更改函数的返回地址。当函数返回时,它会返回到新地址而不是正确的地址。一些缓冲区攻击也可能发生在堆上。这些更难创建,但仍然可以做到。用 C 语言编写的程序最容易受到这些攻击,但任何提供低级内存访问和指针运算的语言都可能存在问题。指针运算是应该进行边界检查的一个领域。

GNU C 编译器有一个可用的扩展,在构建编译器时需要包含该扩展,该扩展实现了边界检查。它用作向程序添加代码的选项。在测试期间,可以打开并使用该代码。在部署期间,该代码将不存在。应该关闭代码的原因是它在边界被违反时会打印消息。如果系统是一个工作站,则可以保留消息,但嵌入式系统通常没有控制台。

这里可能出现一个想法,即所有缓冲区都应该静态分配;这样问题就消失了。事实上,缓冲区长度固定的概念可能会被利用。移动到缓冲区的数据仍然可能比缓冲区长。当移动时,它会溢出,并且会发生同样的问题。为了降低风险,数据移动应仅移动到缓冲区允许的最大值。字符串的动态重新分配允许程序处理任意大小的输入。这样做的问题是程序可能会耗尽内存。在嵌入式系统上,这样的错误是致命的。在工作站上,虚拟内存系统可能会开始抖动并造成性能瓶颈。在 C++ 中,std::string 类具有动态增长方法。如果该类的数据被转换为 char * 指针,则可能会发生缓冲区溢出。其他字符串库可能没有这些问题,但实施者需要了解这些限制。

验证输入

如果程序正在接收它必须操作的数据,则应该进行某种类型的验证,以确保数据正确、不超过最大大小并且没有无效类型。例如,如果数据仅限于从 A 到 Z 的大写字母,则该函数应拒绝任何其他内容。它还应检查以确保数据长度有效。多年前,每个人都认为数据是 80 个字符,即穿孔卡片的大小。今天,数据实际上可以是任何大小;它可以是文本、二进制或加密的。但它仍然有一些类型的限制。这应该进行检查,如果失败,则拒绝它。

不仅应该检查记录或数据段的最大大小,而且在某些情况下,还应检查最小大小。应检查字符串是否具有合法值或合法模式。如果要检查的数据包含需要保持原样的二进制数据,则最好使用通用转义字符来指示数据是二进制的。如果数据是数字,则应进行范围检查。如果它是任何正整数,请检查它是否小于零。如果有最大值,请检查该值。文件 limits.h 定义了大多数值的最大值和最小值,因此很容易检查系统限制。

帮助工具

大多数开发人员陷入的困境是代码已经存在,并且几乎没有时间和人力来检查潜在的安全问题。毕竟,代码没有损坏,为什么要修复它?这种态度在许多组织中盛行。然而,一旦发现代码容易受到攻击,修复它就会成为高度优先事项,就像分配责任一样。

除了代码检查之外,还可以做些什么来发现潜在的问题?我了解到有三种工具能够发现潜在的问题并在报告中标记它们。这些工具可以用于嵌入式系统,但大多数开发都是在交叉托管环境中完成的。在主机工作站上完成繁重的工作,并将微调留给目标。有关在哪里获取工具的信息列在“资源”部分中。

Flawfinder、RATS 和 ITS4 是三个软件包,它们扫描源代码树并显示有关潜在问题的报告。显示内容是一个列表,列出哪里出了问题、在哪个源代码模块以及在哪一行。所有这些信息也根据其漏洞程度进行了加权。列表 1 显示了在示例代码上执行 Flawfinder 的代码片段。严重程度级别从 0 到 5,其中 0 表示风险很小,5 表示高风险。

列表 1. Flawfinder 示例

Flawfinder version 1.21, (C) 2001-2002 David A. Wheeler.
Number of dangerous functions in C/C++ ruleset: 127

Examining ../../example_code/msgqueue/mksem.c
../../example_code/msg_queue/msgtool.c:73  [4] (buffer) strcpy:
  Does not check for buffer overflows when copying to destination.
  Consider using strncpy or strlcpy (warning, strncpy is easily
  misused).
../../example_code/msgqueue/mksem.c:34  [4] (shell) system:
  This causes a new program to execute and is difficult to use safely.
  Try using a library call that implements the same functionality if
  available.
../../example_code/pipes/fifo/fifo_out.c:28  [4] (race) access:
  This usually indicates a security flaw.  If an attacker can change
  anything along the path between the call to access() and the file's
  actual use (e.g., by moving files), the attacker can exploit the race
  condition. Set up the correct permissions (e.g., using setuid()) and
  try to open the file directly.
../../example_code/process_control/proc_mem_info/proc_mem_info.c:139
  [4] (buffer) sscanf:
  The scanf() family's %s operation, without a limit specification,
  permits buffer overflows. Specify a limit to %s, or use a different
  input function.
../../example_code/msg_queue/sender/snd_thread.c:70  [3] (random)
  srand:
  This function is not sufficiently random for security-related
  functions such as key and nonce creation. Use a more secure technique
  for acquiring random values.
../../example_code/dlopen/dltest.c:30  [2] (misc) fopen:
  Check when opening files - can an attacker redirect it (via
  symlinks), force the opening of special file type (e.g., device
  files), move things around to create a race condition, control its
  ancestors, or change its contents?
../../example_code/msg_queue/receiver/rcvr.c:51  [2] (buffer) char:
  Statically-sized arrays can be overflowed. Perform bounds checking,
  use functions that limit length, or ensure that the size is larger
  than the maximum possible length.
../../example_code/dlopen/another_dlopen_test/obj.c:15  [1] (buffer)
  strlen:
  Does not handle strings that are not \0-terminated (it could cause a
  crash if unprotected).

...

Number of hits = 139
Number of Lines Analyzed = 5491 in 2.67 seconds (2527 lines/second)
Not every hit is necessarily a security vulnerability.
There may be other security vulnerabilities; review your code!

即使返回了多条消息,实施者也可以选择修复或忽略潜在的问题。一些开发人员可能会认为这些工具应该更改代码,但最好有选择地更改代码,而不是在没有帮助的情况下进行全面编辑。Flawfinder 程序使用一个名为规则集的内部数据库。此规则集是常见安全漏洞的列表。这些漏洞是一般性问题,可能会对 C/C++ 和许多特定的有问题的运行时函数产生影响。

结论

编写安全代码可以很容易。思考正在编写的内容以及如何利用它必须是设计标准的一部分。应该设计测试方法来检查各种类型的攻击或误用。完全自动化这些测试是一种奢侈,它可以大大有助于为消费者提供优质的产品。此处讨论的技术和工具只是辅助工具。安全程序的开发仍然掌握在开发人员的手中和思想中。

资源

Flawfinder,由 David A. Wheeler 编写和维护:www.dwheeler.com/flawfinder

ITS4,由 John Viega 编写,版权归 Reliable Software Technologies 所有:www.rstcorp.com/its4

RATS (Rough Auditing Tool for Security,安全粗略审计工具),由 Secure Software, Inc. 编写、维护和分发:www.securesoftware.com

Splint Secure Programming Lint(Splint 安全编程检查工具),由弗吉尼亚大学计算机科学系安全编程组维护:www.splint.org

Cal Erickson (cal_erickson@mvista.com) 目前在 MontaVista Software 担任高级 Linux 顾问。在加入 MontaVista 之前,他曾担任 Mentor Graphics 嵌入式软件部门的高级支持工程师。Cal 在计算机行业工作了 30 多年,在计算机制造商和最终用户开发环境方面拥有丰富的经验。

加载 Disqus 评论