几乎每天都有人宣布某个关键软件或其他软件存在严重缺陷。软件真的那么糟糕吗?程序员如此无能吗?到底发生了什么,为什么问题越来越严重而不是好转?
软件安全的一个令人不安的方面是,我们似乎根本“不明白”。在我从事安全工作的15年里,我已经数不清多少次看到(和教授)关于“如何编写安全代码”的教程,或者阅读关于这个主题的书籍。对我来说,很明显我们
• 试图教程序员如何编写更安全的代码
• 在这项任务上彻底失败
我们陷入了教育概念的无休止循环。我们至少十年来一直试图教育程序员编写安全代码,但它完全没有奏效。虽然我是第一个同意撞墙显示奉献精神的人,但我开始怀疑我们是否选错了墙。Plan B是什么?
事实上,当我写这篇文章时,我看到微软、英特尔和AMD已经联合宣布建立新的合作伙伴关系,以帮助使用硬件控制来防止缓冲区溢出。换句话说,软件质量问题已经变得如此糟糕,以至于硬件厂商也在试图解决它。先不管许多处理器内存管理单元能够将页面标记为不可执行;但在我看来,我们试图用硬件来解决根本上是软件问题的问题,这似乎是本末倒置。这甚至不是一个通用的软件问题;它是一个特定于特定编程语言的运行时环境问题。
通常,当有人在关于软件质量的文章中提到编程语言时,这就会邀请所有人加入进来,提出有用的意见,例如,“如果我们都用[我最喜欢的被热炒的编程语言]编程,我们就不会有这个问题!”在某些情况下,这可能是真的,但这不是现实。
早在20世纪90年代,我们就尝试通过Ada立法来改变编程语言。还记得Ada吗?那是一场代价高昂的灾难。然后在20世纪90年代后期,我们尝试让每个人都切换到Java的“沙箱”环境,它确实效果更好——但每个人都抱怨想要绕过“沙箱”以获得对本地主机的文件级访问权限。事实上,Java效果非常好,微软用ActiveX回应,它完全绕过了安全性,因为它很容易将授权执行恶意代码的责任归咎于用户。拜托,我们不要再有任何能够解决我们所有问题的替代编程语言了!
Plan B是什么?我认为Plan B很大程度上是在我们的编译器和运行时环境上做更多的工作,重点是使它们嵌入更多对代码质量和错误检查的支持。我们必须将其置于程序员意识的“雷达屏幕之下”,就像我们对编译器优化、目标代码的创建和链接所做的那样。我们在构建编程环境方面做得非常出色,这些环境可以生成快速的可执行文件,而无需程序员的大量手把手指导。事实上,今天大多数程序员都完全认为优化是理所当然的——为什么软件安全分析和运行时安全也一样呢?就此而言,为什么我们仍然将安全视为与代码质量分离的问题?不安全的代码就是有缺陷的代码!
我们为提高软件安全性所做的一切都必须在程序员不必切换语言的情况下工作。我们需要现实地认识到,我们已经陷入了一套不容易发生大规模变化的语言和环境。因此,我们需要考虑如何在我们已有的环境中提高安全性。通过几个简单的步骤可以取得巨大的进步。考虑以下非常小段的糟糕代码
main()
{
char buf[512];
printf("你说了:%s %d\n",gets(buf));
}
C库例程gets()从终端读取一行到为其提供的内存区域以供填充。Gets()曾经是程序员的一个大问题,因为它实际上不检查内存区域的大小——这是灾难的根源。早在1994年,我就教C程序员,“不要使用gets();它很糟糕!”但是,当然,他们一直在这样做——很多教科书都使用gets(),因为它对于编写示例代码很简单。但是,后来,发生了一些变化:一些有安全意识的程序员厌倦了gets(),并将其扼杀在摇篮里(见图1)。
当有人试图使用该函数时,编译器/链接器会生成警告。多么棒(和简单)的主意。在早期尝试杀死gets()时,一些维护Unix C库的程序员修改了gets()函数本身,使其在运行时发出警告。这很有效,直到一些程序员开始发布补丁来删除警告消息。1 如果可以消除消息,为什么要修复损坏的代码呢?
当然,这以前也尝试过——大多数编译器在遇到可疑代码时都会生成各种警告。老式的Unix/C程序员肯定会记得lint(1),这是一个代码检查器,它可以进行跨文件错误检查和参数类型匹配。这些工具已经存在多年,但并不流行。为什么?因为它们会生成大量警告,而且,正如无数软件工程师指出的那样,筛选出虚假警告以寻找真正重要的警告非常耗时。我要告诉他们一个消息:没有不重要的警告。这就是它警告你的原因。任何处理过足够代码的人都会告诉你,一般来说,没有警告编译的软件崩溃的频率较低。就我而言,警告是给懦夫的。像lint(1)和DevStudio这样的工具不应该发出警告:它们应该决定是否发现了错误并停止构建过程,或者它们应该闭嘴并生成代码。
我们为提高软件安全性所做的一切都不能依赖程序员的合作。我并不是暗示程序员(除非在极少数情况下)会试图彻底绕过软件安全工具,但是如果这些工具只是嵌入在开发环境中,那会容易得多。代码已经被推送到预处理器、编译器,然后是优化器——为什么不添加另一个步骤,或者将安全功能合并到现有步骤中呢?
当我20世纪80年代初开始用C语言编程时,我们中的一些人过去常常嘲笑那些进行垃圾回收的编程语言。通常,嘲笑的目标是垃圾回收对性能的影响。今天,垃圾回收是一个相当容易理解的问题,其成本已大大降低。在20世纪90年代,许多学生撰写了关于内存分配器和垃圾收集器的论文,每篇论文都将技术水平向前推进了一步。现在,我们几乎可以将垃圾回收视为“免费”,并认识到程序员的时间太宝贵了,不能花时间担心内存分配。
当然,我们的大部分编程仍然是用C/C++完成的——这些语言迫使程序员手动将free()与每个malloc()配对。我感到惊讶的是,还没有人提出这样的想法:将malloc()和free()变成通用垃圾回收内存分配器的存根调用,并完全取消C内存管理。让想要malloc()的程序员尽情malloc(),但使用与C当前方法完全不同的运行时来管理内存。它可以在无需更改一行代码的情况下工作,内存泄漏将成为过去。我们应该能够对安全性做同样的事情:将其置于“雷达屏幕”之下。
什么可能有效?我认为我们已经掌握了良好软件安全所需的所有基本技术,我们只是没有使用它们。显然,我们了解如何解析源代码并从中生成可执行文件。我们了解如何构建运行时环境,甚至了解编程语言如何使用堆栈。否则,我们编写的所有代码都根本无法工作!为什么我们的编译器默认会接受有问题的代码,而它们可以很容易地默认拒绝它呢?考虑图二中的示例。
这与我在前面的示例中使用的代码完全相同,它在使用make的默认编译器选项时构建时没有报错——但是当我使用-Wall标志打开完整编译器警告时,它会强烈报错。其中一些警告可能是噪音,但它捕获了一个我故意试图在第一个示例中偷偷溜过去的错误:printf()缺少一个备用参数。这个例子让我问的真正问题是,编译器为什么要生成任何代码,因为这段代码显然是坏的?如果我运行它
mjr@lyra-> foo
bar
你说了:bar –541075068
mjr@lyra->
……它愉快地打印回我的输入,然后打印一些来自执行堆栈中我的代码不应该查看的值。我们应该能够做得更好。注意GCC(GNU编译器集合)发出的“烦人”警告,而我忽略了这些警告。编译器在“知道”代码有缺陷时,为什么还要生成代码?
目前,软件安全领域的最新技术是将您的代码通过某种静态源代码分析器,例如ITS4或Fortify,它们会查找危险的做法和已知的错误。这是一个很好的开始,而且,据我的朋友加里·麦格劳(Cigital的首席技术官,也是几本关于软件安全书籍的作者)所说,他与这些东西一起工作,它捕获了大量潜在的安全问题。但是,正如您所看到的,编译器已经知道了很多它需要知道的东西,以便很好地判断哪里出了问题。
一个非常巧妙的概念体现在Perl编程语言中——污点。污点的想法是,解释器跟踪数据的来源,如果危险操作是直接由用户输入调用的,则关闭这些操作。例如,当您在污点模式下运行Perl脚本时,它会在将用户提供的数据传递给某些系统调用之前打开大量错误检查。当您尝试使用污点数据的文件名打开文件进行写入时,它会检查以确保目标目录的目录树所有权正确,并且文件名不包含“../”路径扩展。换句话说,运行时环境不仅跟踪数据的类型和值,还跟踪数据的来源。您可以想象这种功能对于编写服务器端代码或封闭应用程序有多么好。
不幸的是,很少有程序员使用污点,因为它给程序员带来了额外的负担,而且有时很难找到一种安全的方法来完成工作。但是,如果我们将污点类型的功能构建到我们的C/C++运行时环境中呢?一种简单且高价值的方法可能是修改I/O例程(读/写),以确定它们是否连接到来自远程系统的套接字,并对跨套接字传输的数据进行一些基本检查,例如检查堆栈是否在调用某些函数后被更改。
例如,早在20世纪80年代,我就在MIPS Ultrix2上使用了一个名为Pixie的工具,它可以获取已编译的可执行文件,在函数点插入性能收集例程,并编写一个新的可执行文件,该文件已针对性能测量进行了检测。Pixie工作的MIPS指令集是非常复杂的RISC-y东西,所以我认为Pixie在底层是一个非常出色的作品。但重要的是,在我所有在可执行文件上使用Pixie的次数中,它从未创建过无法正常工作或比原始优化代码慢得多的可执行文件。所以这是一个可能性:如果有人编写一个“安全后处理器”,它接受一个普通的可执行文件并发出一个“加固”版本呢?发出应用程序的加固版本可能是可行的,该版本会对内存使用、套接字使用、操作系统调用或文件I/O进行额外的检查。诸如Purify和BoundsChecker之类的工具已经做到了这一点,但主要是作为查找内存泄漏或悬空指针的一种手段。如果有一个后处理器,可以在运行时添加Perl风格的污点,而无需程序员采取任何操作呢?
我使用一个名为CodeCenter(以前称为Saber-C)的C语言开发环境,它是一个C语言解释器。它是一个很棒的调试环境,因为它所做的一件事是在其解释器中模拟运行时环境。因此,当您malloc()一块内存并在其中粘贴一个指向整数的指针时,如果您然后更改指针以指向字符数组或未分配的内存,它会停止执行并生成错误。它所做的部分工作是绕过用于内存分配的普通C库例程,并用执行详尽错误检查的版本替换它们——这是一个好主意。
对于已编译的应用程序,我们可以将“智能”构建到编译器中,以便它传递更多关于静态分配内存的类型和大小的信息,链接器可以利用这些信息来添加运行时检查。当然,这可能会稍微减慢我们的编译-链接周期,或者使我们的目标代码更大一些,但我宁愿拥有运行正确的软件,也不愿拥有小的软件。此外,现在看来,大多数软件既不大也不正确。如果这成为一个问题,那么请给我一个编译器标志,让我可以将某些模块标记为“可以安全处理从用户接受的数据”,然后在我彻底测试这些模块后关闭检查。
我认为我们需要认识到,我们正在使用20世纪的工具为21世纪构建软件。对于许多应用程序而言,C几乎肯定不是最佳语言,但这不太可能改变。因此,让我们尝试稍微升级我们的环境。作为一名长期的Unix系统开发人员,我对Unix和Windows环境之间开发工具的质量差异感到沮丧。与大多数Unix程序员使用的工具:GCC、GDB(GNU调试器)和Make相比,Windows开发人员可用的工具非常出色。
我相信这是开源运动对我们的伤害大于帮助的一个领域:免费、足够的Unix工具的可用性扼杀了商业高质量工具的潜在市场。很少有程序员愿意花费数千美元购买更好的编程环境,因为客户无法通过最终软件的运行情况来区分差异。Windows程序员可以使用完全集成的环境,这些环境管理依赖项、调试器以惊人的细节呈现执行,以及可视化开发引擎,这些引擎消除了用户界面代码的大部分工作(和所有错误)。我们需要将安全性列入构建这些环境的公司的待办事项清单,以便它只是另一个嵌入式功能。当您在集成环境中操作时,添加可以产生重大影响的那种额外分析和交叉检查并不难。
如果所有行业分析师都是正确的,那么软件将成为全球信息经济的关键构建模块之一。我们已经看到,对于某些制成品而言,软件开发成本有时在产品开发预算中占主导地位。这一切都指向一个未来,软件开发的速度越来越快,潜在的失败变得越来越昂贵。当您考虑到外包开发的速率时,对我来说很清楚,软件游戏中的赢家将是那些能够更快地编写更多、更好的代码的人。他们不会是纯粹主义者,他们不会在意是否在垃圾回收和运行时安全性上损失了一些CPU滴答,如果这意味着他们赢得开发合同并按时交付。
我意识到我对软件安全的状态听起来很消极,但我认为我们所看到的只是一场暂时的危机。在未来的软件世界中,只有强者才能生存,我认为安全性越来越被视为一种弱点。这可能会推动我们开发环境中一系列全新功能的发展。您还记得在可视化开发环境出现之前的日子里编写Windows用户界面代码吗?如果您将今天的工具与那时可用的工具进行比较,我认为您可以很好地了解我们已经走了多远,以及我们可能会走多远。
每当编程环境发生重大转变时,总会有一批老派程序员坚持认为新方法过于臃肿、速度太慢或过于复杂。总是存在遗留代码的问题,人们抱怨升级它的成本。但事实是:老派程序员退休或成为经理,遗留代码被替换。对我来说,真正的问题是:我们能否及时将安全性嵌入到工具中?如果我们能做到,也许时间在我们这边。
1. 有关开始发布补丁以删除警告消息的程序员的信息,请访问Grass GIS(地理资源分析支持系统)网站:http://grass.itc.it/pipermail/grassuser/2000-May/003586.html。
2. 数字设备公司版本的Unix。
喜欢它,讨厌它?请告诉我们
[email protected] 或 www.acmqueue.com/forums
马库斯·J·拉努姆,TruSecure的高级科学家,是安全系统设计和实施方面的专家。他设计了许多安全产品,并参与了安全产品业务的各个运营层面,从开发人员到创始人和首席执行官。
© 2004 1542-7730/04/0600 $5.00
最初发表于Queue vol. 2, no. 4—
在数字图书馆中评论本文
Jinnan Guo, Peter Pietzuch, Andrew Paverd, Kapil Vaswani - 使用保密联邦学习的可信赖人工智能
安全性、隐私性、问责制、透明度和公平性的原则是现代人工智能法规的基石。经典FL的设计非常强调安全性和隐私性,但以透明度和问责制为代价。CFL通过将FL与TEE和承诺相结合,弥合了这一差距。此外,CFL还带来了其他理想的安全属性,例如基于代码的访问控制、模型机密性和推理期间模型的保护。保密计算(如保密容器和保密GPU)的最新进展意味着,现有的FL框架可以无缝扩展以支持开销较低的CFL。
Raluca Ada Popa - 保密计算还是密码计算?
通过MPC/同态加密与硬件飞地的安全计算,提出了涉及部署、安全性和性能的权衡。关于性能,您想到的工作负载非常重要。对于简单的求和、低阶多项式或简单的机器学习任务等简单工作负载,这两种方法都可以在实践中随时使用,但对于复杂的计算(如复杂的SQL分析或训练大型机器学习模型),目前只有硬件飞地方法才足够实用,适用于许多实际部署场景。
Matthew A. Johnson, Stavros Volos, Ken Gordon, Sean T. Allen, Christoph M. Wintersteiger, Sylvan Clebsch, John Starks, Manuel Costa - 保密容器组
此处提供的实验表明,Parma(在Azure容器实例上驱动保密容器的架构)增加的额外性能开销不到底层TEE增加的性能开销的百分之一。重要的是,Parma确保了容器组所有可到达状态的安全不变量,该不变量植根于证明报告中。这允许外部第三方与容器安全地通信,从而实现需要机密访问安全数据的各种容器化工作流程。公司获得了在云中运行最机密工作流程的优势,而无需在其安全要求上妥协。
Charles Garcia-Tobin, Mark Knight - 使用Arm CCA提升安全性
保密计算具有通过将监管系统移出TCB来提高通用计算平台安全性的巨大潜力,从而减小了TCB的大小、攻击面以及安全架构师必须考虑的攻击向量。保密计算需要平台硬件和软件方面的创新,但这些创新有可能增强对计算的信任,尤其是在第三方拥有或控制的设备上。保密计算的早期消费者将需要自行决定他们选择信任的平台。