斯坦福大学设计新的铁锈课程:系统编程的安全性

2020-10-08 09:11:34

编写高质量的软件很难。有时,软件会以娱乐的方式中断。然而,当软件运行从Alexa和谷歌主页(Google Home)等个人助理到银行业务再到选举的一切时,一些漏洞可能会更严重。

在过去的一个季度里,Armin Namavari和我试图教授一个关于如何编写差劲一点的软件的课程。我们将重点放在由某些愚蠢(但非常严重)的错误导致的计算机系统中的常见问题上,例如内存安全和线程安全问题。这门课的核心主题是,现在系统编程的常见问题是什么?人们是如何回应这些问题的?这些措施有什么不足之处?我们希望学生意识到困扰该行业数十年的问题,我们想教学生如何使用人们为应对这些问题而开发的工具和心理模型。然而,这些工具并不完美,我们也希望学生体验并理解这些工具的局限性,以便更好地意识到在构建系统时要注意什么。

特别是,我们专注于教授Rust编程语言,以此作为建立更好的习惯和打击基于C和C++的软件中普遍存在的错误的一种方式。在许多方面,Rust需要良好的实践,而且它有一个具有教育意义的编译器,提供有用的错误消息,帮助学生学习。此外,我们还学习了如何应用Rust中的知识来用C++编写更好的代码,我们还向学生讲授了一些工具,这些工具可以用来在常见错误成为问题之前将其检测出来。

与典型的安全课程不同,我们的目标是在课程中构建一个健壮的软件工程方面,给出繁重的代码作业,并试图改进学生的过程,而不仅仅是让学生意识到常见的问题。我们这门课的目标是培养学生成为更好的软件开发人员,不管他们最终使用的是什么编程语言。

我认为课程进行得很好,学生的评价非常积极。甚至在本季度结束之前,学生们就告诉我们,这门课对实现和调试其他班级的作业非常有帮助。我们希望在今年秋天再次教授这门课,并正在就如何改进这门课征求意见。

这篇博客旨在总结我们做了什么,为什么要这么做,以及我们对未来的改变有什么想法。这本书很长,但是写得很好,所以你可以随便找你感兴趣的东西。下面是一个提纲:

主要感谢Armin Namavari是一位出色的合作讲师,Sergio Benitez做了一次出色的客座讲座,Will Crichton在设计课程时提供了反馈和指导,Jerry Cain给了我们机会授课并在整个过程中给予鼓励,Rakesh Chatrath,Jeff Tucker,Vinesh Kannan,John邓和Shiranka Miskin审阅了这篇文章的草稿。

不幸的是,安全是一个模糊的术语,没有一个很好的定义,但为了我们的目的,我们会说安全是关于避免有害的错误。我认为安全是指潜在的严重错误的子集:如果网站上的一个按钮呈现为紫色而不是蓝色,这可能是一个我们不太关心的错误,但如果银行账户软件允许用户多次提取相同的1000美元,或者如果自动驾驶汽车软件在某些情况下可能会失败,那就是一个更令人担忧的问题。

安全性在系统编程中特别重要,因为系统编程很难。系统编程通常涉及突破硬件所能做的极限,并且经常涉及对多线程状态的推理,有时甚至分布在数千台机器上。此外,由于性能和历史原因,大多数系统软件都是用C或C++编写的,这是出了名的难以正确使用。很难对指针和内存进行推理,C和C++也帮不了什么忙。C/C++的弱类型系统和定义不佳的规范意味着它们会乐于接受明显损坏的代码,而不敏感的解释。更糟糕的是,有无数的雷区,语言的糟糕设计只是在乞求错误的发生。像strcpy这样将字符串从内存中的一个位置复制到另一个位置的简单函数非常容易使用,并且已经造成了无数的安全漏洞。引入strncpy函数是为了解决strcpy的弱点,但事实证明strncpy几乎同样糟糕。当以错误的方式调用时,即使printf也可能导致安全漏洞。

此外,由于系统软件提供了其他软件运行的基础,因此正确使用尤为重要。许多现实世界的例子表明了上述问题的严重影响。我最喜欢的例子之一是“汽车表面综合实验分析”。这是一本很棒的书,但总而言之,作者买了一辆很受欢迎的车,并试图找到尽可能多的方法来远程劫持这辆车,而不能进行物理访问。他们检查了无线钥匙扣、蓝牙等载体,甚至轮胎压力监测系统(该系统使用无线信号传输轮胎传感器的信息)。每一种媒介都被发现是可利用的,其中许多都是微不足道的。例如,蓝牙软件“对strcpy的调用超过20次,但没有一次明显是安全的。”作者只查看了strcpy的第一个实例,发现它在处理蓝牙配置命令时会将数据复制到堆栈,而不检查字符串的长度。这会导致很容易被利用的缓冲区溢出,使得配对设备能够在媒体系统中执行任意代码。由于大多数汽车中的子系统缺乏隔离性,一个子系统(如媒体播放器)受损可能会导致整个汽车受损。2015年,研究人员展示了这一点,远程击毙了一辆行驶在高速公路上的吉普车。

这似乎是我们应该讨论的问题。你会递给非化学专业的学生一堆挥发性化学物质,这些化学物质经常在专业实验室爆炸,而没有对安全进行有力的讨论吗?大概不会吧。然而,这实际上就是我们的课程正在做的。我们给学生们提供了一系列专业人士经常用来砸自己脚的工具,我们并没有就我们可以采取的预防措施进行实质性的讨论,以避免潜在的危及生命的错误。

有人可能会争辩说,Strcpy的危险与库存充足的化学实验室的危险完全不同;这里没有学生死在电脑前的危险。(嗯,我们希望如此。)。然而,我认为我们应对危险的规模要大得多。一行代码可以很容易地影响数百万(或数十亿)人,我们代码的影响可能比我们实现的要大得多,即使我们不是在为汽车(我们用它杀死了人)或医疗设备(我们用它也杀死了人)开发软件时也是如此。文件共享服务器的最坏情况下的漏洞似乎只是阻止用户共享文件,但其中一个这样的漏洞导致英国国家医疗服务体系严重中断。非危急紧急情况不得不被拒绝。看起来,网络应用程序库中最严重的案例错误只会导致一些网站瘫痪,但其中一个错误会导致几乎每个有信任史的美国成年人的极其敏感的数据泄露。

预防措施和安全措施确实存在,但人们没有使用它们。部分原因可能是工具不够好或不够容易使用,部分原因可能是没有足够的时间看到大规模采用,但我认为部分原因也可能是缺乏关于这些问题的教育和意识。我们可以教C和C++,并希望学生们能养成良好的习惯,学会如何在工作中使用静态分析器、消毒器、模糊器和更安全的语言,但作为教育工作者,我们没有辜负他们的期望吗?看到软件工程师不断犯有严重影响的基本错误,似乎有些地方不对劲,我们应该努力做更多的事情。

在计划这门课的时候,我们找不到任何其他的编程安全类。这是因为还没有人考虑过这样做,还是因为在讲授更核心的材料的同时,结合上下文来教授安全更好?

特别是因为

其次,尽管编写安全、正确的代码通常涉及避免无数无关的陷阱,但在安全性方面确实存在一些共同的主题。例如,Rust的所有权模型旨在确保内存安全,但Rust的设计者发现它也有助于提高线程安全。

因为安全与安全密切相关,所以安全类与现有的安全类有很大的重叠。斯坦福大学尤其有CS155,这是一门优秀的安全课程,本科生可以接触到,它已经在涵盖内存安全和网络攻击等主题方面做得很好。为什么我们要分开教东西呢?

虽然安全课程通常侧重于在经常损坏的C/C++软件环境中教授攻击性技术和防御性技术,但我们认为,从一开始就关注于正确构建系统的角度进行教学可能是有益的。CS155经常带学生玩过去三十年来一直在玩的猫捉老鼠的游戏:攻击者是如何侵入东西的,我们如何才能堵住他们使用的漏洞?相比之下,我们感兴趣的是探索是否有一种方法可以改变学生处理系统编程的方式,以便他们在一开始就少犯错误和制造更少的漏洞。在第一个CS155作业中,学生们亲眼看到了缓冲区溢出或双重释放如何导致远程代码执行,但他们可能不一定了解如何编写没有缓冲区溢出和双重释放的代码,而且他们肯定没有得到任何实践经验。在CS155的第一个作业中,学生们亲眼看到了缓冲区溢出或双重释放如何导致远程代码执行,但他们可能不会了解如何编写没有缓冲区溢出和双重释放的代码,而且他们肯定没有得到任何实践。我们能不能养成更好的习惯、更强的危机感、更好地使用工具,从而让学生马上就能写出更好的代码呢?

当有这么多不同的陷阱需要注意时,教授安全编程是困难的。我们如何设计一个不仅仅是要避免的错误清单的类呢?

在我们的课堂上,我们想使用Rust编程语言来探索安全性,这是一种相对较新的系统编程语言,已经获得了越来越多的关注。我们希望这样做的具体原因有几个:

默认情况下,Ruust需要许多最佳实践,并防止C和C++允许的常见错误。通常情况下,做事只有一种方式,那就是正确的方式。C++可以做许多与Rust相同的事情,但即使是它的安全特性也充斥着分散注意力和令人困惑的不安全行为(例如,Rust的选项和C++的std::Optional从概念上做同样的事情,但后者有三种方法来提取内值,其中两种是不安全的)。

Rust的编译器错误比其他语言的错误更有教育意义。核心Rust团队花费了大量时间使编译器消息信息丰富且易于阅读,甚至在出现错误时为修复问题提供了有用的建议。在某些情况下,在Rust中进行编程有点像与编译器进行对话,编译器会告诉您为什么您正在做的事情是不安全的。

从Rust编程中学到的经验教训适用于C和C++编程。在许多方面,锈蚀是对数十年来常见错误Inc/C++的回应,在教授为什么语言是这样设计的过程中,我们可以讨论许多微妙损坏的C/C++代码的情况,并检查如何改进。在整个季度里,我们班上的一些学生提到,在做CS110的作业时,他们感觉自己的头脑里有一个小小的Rust编译器,突出了他们C++代码中的所有权错误。

Rust是一种后起之秀的语言,发展势头良好,真正有可能取代C和C++代码。我认为它对学生的未来很有价值。

除了用Rust教授编程之外,我们还想确保讨论如何将Rust课程应用于C++,以及如何使用工具来提高C++代码的质量。在过去的20年里,C++添加了许多有用的安全功能,以帮助防止常见问题。此外,还开发了许多静态和动态分析器工具,如衬垫、消毒器和模糊器,它们有助于识别程序中的错误,我们希望向学生展示如何将这些工具合并到工作流中,以便在开发早期捕获错误。

我不会声称我们找到了教授安全的最好方法,但正如我稍后将讨论的那样,这种方法最终取得了良好的效果。我很想听听关于我们如何处理这件事的任何其他想法!

我们注册了CS 110L作为CS 110的两个单元的可选补充课程,CS 110是我们的第二个核心系统课程,涵盖文件系统、多处理、多线程和网络。(平均本科课程负担是15个单位。两个单元的课程预计需要6个小时/周的时间。)。

虽然将其设置为独立类可能更好,但我们将其注册为补充类有两个原因:

如前所述,它使我们可以更容易地尝试将安全和安保的讨论带到核心课程中。

一到两个单元的补充课程更容易获得批准,并允许在本季度进行更多的即兴表演。

这给我们的班级带来了一些额外的限制,因为我们需要确保材料与目前正在通过CS 110学习的学生相关。平衡相互冲突的利益在一开始是很困难的,因为有很多补充性的CS110材料,谈论起来真的很有趣,但与安全无关,而且也很难安排课程,以便学生在我们谈论它之前已经涵盖了CS110的所有必备材料。

讲座时长50分钟,每周在Zoom上授课两次。在50分钟内覆盖我们的材料是相当有挑战性的,将来我们可能会考虑把这门课变成三个单元的课程,这样讲课就不会感觉那么仓促。而且,一开始在Zoom上教学非常困难,我感觉就像是在对着一个没有生命的摄像机说话,很难感觉到全神贯注,而且我很难测量和衡量学生的理解,因为我不能真正看到学生的脸。然而,随着季度的推移,我们在这方面做得更好了。

你可以在这里找到所有的在线授课材料。以下是我们所介绍内容的高级概述:

Rust依赖所有权模型来防止内存错误,如内存泄漏、双重释放、事后使用释放、迭代器无效等等。学习如何在Rust的模型中操作对于新手程序员来说可能是一个挑战,但Rust只是强制要求优秀的C/C++程序员可以随时使用来编写更安全的代码的习惯。

学生们已经熟悉了异常,所以我们讨论了异常有时会出现问题的原因,我们还讨论了通过返回值处理错误的利弊,以及Rust如何使用Option和Result做到这一点。

CS110花了几周时间介绍叉子、管道和信号。我们花了一个半的时间总结了直接使用这些抽象词的几个常见问题,并认为您应该尽可能地使用更高级别的抽象。如果学生需要直接使用这些原语,我们希望他们知道要注意什么。

我们花了一堂课做了一个案例研究,讲述了即使攻击者在Chrome沙箱中发现漏洞,Google Chrome如何使用多处理来促进选项卡之间的隔离。学生们报告说,这是他们最喜欢的讲座之一,因为他们可以看到我们关于虚拟化和沙盒的讨论在实践中是如何进行的。

我认为Rust的优势在多线程方面表现得最好,Rust的所有权模型(在存在可变引用时禁止多次引用一段数据)已经帮助防止了许多类型的多线程错误。此外,Rust的类型系统使用称为Sync和Sendto的“标记特性”来表示类是否可以在线程之间传递并由多个线程并发访问。使用此类型系统,编译器理解受互斥锁保护的数据可以安全地并发访问,而普通缓冲区则不安全,而且它不会让您编写具有数据竞争的代码。

我们从系统设计的角度简要介绍了安全性:如何保持重要系统运行并防止攻击者泄露数据?我们讨论了负载均衡(使用负载均衡器、DNS和IP选播)和容错,随后让学生实施负载均衡器。我们还举办了一场信息安全讲座,强调保护阻力最小的路径的重要性(认为像Elasticsearch这样的数据库太容易不安全了),以及确保依赖关系保持最新的重要性。

我们花了一周的时间专注于非阻塞I/O和异步/等待编程。这与安全性无关,但有助于本课程的软件工程方面;在期末专题中,我们要求学生比较使用多线程和使用非阻塞I/O的多路复用的性能。虽然这感觉有点分散注意力,但我认为这是一个非常有用的主题,可以让学生接触到这一主题,因为异步/等待使非阻塞I/O的人体工程学足够容易让学生在网络连接的应用程序中实际使用。

我们简要介绍了如何使用C++11、17和20语言特性将类中的材料应用于C++。回想起来,我希望我们花了更多的时间来讨论如何编写好的C++代码,以及如何使用工具来捕获常见错误。正如我们向学生解释的那样,Rust是一种很棒的语言,也是学习更好的编码实践的好方法,但由于Rust相对较新,而且大多数系统代码库仍然是C或C++语言,了解如何在这些语言中有效地工作是很重要的。尽管Rust在概念上很好地映射到较新的C++语言功能,但C++类比设计得很差(在我看来),有很多意外出错的地方(例如,当您使用.value()检索内部值时,std::Optional会进行nullopt检查,但当您使用*或->;运算符执行此操作时,不会进行nullopt检查)。我们匆忙地浏览了这些细节,让学生实际练习使用这些功能会更好。此外,C++生态系统有这么多有用的工具来捕获错误或坏习惯(因为默认情况下编译器不会做太多事情),我们应该分配更多的时间来向学生展示如何使用这些工具。

这堂课有两种类型的作业:每周练习和专题,前者被认为是对我们每周讲课中讨论的材料的轻量级强化,后者更具实质性,会给学生提供构建更复杂软件的练习。我们还让学生们选择用他们感兴趣的博客帖子来代替一周的练习,我们允许。

.